自定义控件其实很简单5/12

来源:互联网 发布:轻淘客cms是什么 编辑:程序博客网 时间:2024/05/16 08:49

转载自:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!

最近龙体欠安,很多任务都堆着,虽说如此,依然没有停下学习的步伐,虽然偶尔还会有点头痛,但是孤依旧在学习……自赞一个~
在1/3中我们结束了全部的Paint方法学习还略带地说了下Matri的简单用法,这两节呢,我们将甩掉第二个陌生又熟悉的情妇:Canvas。Canvas从我们该系列教程的第一节起就嘚啵嘚啵个没完没了,几乎每个View都扯到了它,就像我之前说的那样,自定义控件的关键一步就是如何去绘制控件,绘制说白了就是画,既然要画那么笔和纸是必须的,Canvas就是Android给我们的纸,弥足轻重,它决定了我们能画什么:

这里写图片描述

上面所罗列出来的各种drawXXX方法就是Canvas中定义好的能画什么的方法(drawPaint除外),除了各种基本型比如矩形圆形椭圆直曲线外Canvas也能直接让我们绘制各种图片以及颜色等等,但是Canvas真正屌的我觉得不是它能画些什么,而是对画布的各种活用,上一节最后的一个例子大家已经粗略见识了变换Canvas配合save和restore方法给我们绘制图形带来的极大便利,事实上Canvas的活用远不止此,在讲Canvas之前,我想先给大家说说Canvas中非常屌毛而且很有个性的一个方法:

drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)  

drawBitmapMesh是个很屌毛的方法,为什么这样说呢?因为它可以对Bitmap做几乎任何改变,是的,你没听错,是任何,几乎无所不能,这个屌毛方法我曾一度怀疑谷歌那些逗比为何将它屈尊在Canvas下,因为它对Bitmap的处理实在在强大了。上一节我们在讲到Matrix的时候说过Matrix可以对我们的图像做多种变换,实际上drawBitmapMesh也可以,只不过需要一点计算,比如我们可以使用drawBitmapMesh来模拟错切skew的效果:

这里写图片描述

实现过程也非常非常简单:

public class BitmapMeshView extends View {      private static final int WIDTH = 19;// 横向分割成的网格数量      private static final int HEIGHT = 19;// 纵向分割成的网格数量      private static final int COUNT = (WIDTH + 1) * (HEIGHT + 1);// 横纵向网格交织产生的点数量      private Bitmap mBitmap;// 位图资源      private float[] verts;// 交点的坐标数组      public BitmapMeshView(Context context, AttributeSet attrs) {          super(context, attrs);          // 获取位图资源          mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.gril);          // 实例化数组          verts = new float[COUNT * 2];          /*          * 生成各个交点坐标          */          int index = 0;          float multiple = mBitmap.getWidth();          for (int y = 0; y <= HEIGHT; y++) {              float fy = mBitmap.getHeight() * y / HEIGHT;              for (int x = 0; x <= WIDTH; x++) {                  float fx = mBitmap.getWidth() * x / WIDTH + ((HEIGHT - y) * 1.0F / HEIGHT * multiple);                  setXY(fx, fy, index);                  index += 1;              }          }      }      /**      * 将计算后的交点坐标存入数组      *       * @param fx      *            x坐标      * @param fy      *            y坐标      * @param index      *            标识值      */      private void setXY(float fx, float fy, int index) {          verts[index * 2 + 0] = fx;          verts[index * 2 + 1] = fy;      }      @Override      protected void onDraw(Canvas canvas) {          // 绘制网格位图          canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);      }  }  

其他的我就不说了,关键代码就一段:

/*  * 生成各个交点坐标  */  int index = 0;  float multiple = mBitmap.getWidth();  for (int y = 0; y <= HEIGHT; y++) {      float fy = mBitmap.getHeight() * y / HEIGHT;      for (int x = 0; x <= WIDTH; x++) {          float fx = mBitmap.getWidth() * x / WIDTH + ((HEIGHT - y) * 1.0F / HEIGHT * multiple);          setXY(fx, fy, index);          index += 1;      }  }  

这段代码生成了200个点的坐标数据全部存入verts数组,verts数组中,偶数位表示x轴坐标,奇数位表示y轴坐标,最终verts数组中的元素构成为:[x,y,x,y,x,y,x,y,x,y,x,y,x,y………………]共200 * 2=400个元素,为什么是400个?如果你不是蠢13的话一定能计算过来。那么现在我们一定很好奇,drawBitmapMesh到底是个什么个意思呢?,其实drawBitmapMesh的原理灰常简单,它按照meshWidth和meshHeight这两个参数的值将我们的图片划分成一定数量的网格,比如上面我们传入的meshWidth和meshHeight均为19,意思就是把整个图片横纵向分成19份:

这里写图片描述

横纵向19个网格那么意味着横纵向分别有20条分割线对吧,这20条分割线交织又构成了20 * 20个交织点
每个点又有x、y两个坐标……而drawBitmapMesh的verts参数就是存储这些坐标值的,不过是图像变化后的坐标值,什么意思?说起来有点抽象,借用国外大神的两幅图来理解:

这里写图片描述

如上图,黄色的点是使用mesh分割图像后分割线的交点之一,而drawBitmapMesh的原理就是通过移动这些点来改变图像:

这里写图片描述

如上图,移动黄色的点后,图像被扭曲改变,你能想象在一幅刚画好的油画上有手指尖一抹的感觉么?油画未干,手指抹过的地方必将被抹得一塌糊涂,drawBitmapMesh的原理就与之类似,只不过我们不常只改变一点,而是改变大量的点来达到效果,而参数verts则存储了改变后的坐标,drawBitmapMesh依据这些坐标来改变图像,如果上面的代码中我们不将每行的x轴坐标进行平移而是单纯地计算了一下均分后的各点坐标:

/*  * 生成各个交点坐标  */  int index = 0;  //      float multiple = mBitmap.getWidth();  for (int y = 0; y <= HEIGHT; y++) {      float fy = mBitmap.getHeight() * y / HEIGHT;      for (int x = 0; x <= WIDTH; x++) {          float fx = mBitmap.getWidth() * x / WIDTH;  //              float fx = mBitmap.getWidth() * x / WIDTH + ((HEIGHT - y) * 1.0F / HEIGHT * multiple);          setXY(fx, fy, index);          index += 1;      }  }  

你会发现图像没有任何改变,为什么呢?因为上面我们说过,verts表示了图像变化后各点的坐标,而点坐标的变化是参照最原始均分后的坐标点,也就是图:

这里写图片描述

中的各个交织点,在此基础上形成变化,比如我们最开始的错切效果,原理很简单,我们这里把图像分成了横竖20条分割线(实际上错切变换只需要四个顶点即可,这里我只作点稍复杂的演示),我们只需将第一行的点x轴向上移动一定距离,而第二行的点移动的距离则比第一行点稍短,依次类推即可,每行点移动的距离我们通过

(HEIGHT - y) * 1.0F / HEIGHT * multiple  

来计算,最终形成错切的效果
drawBitmapMesh不能存储计算后点的值,每次调用drawBitmapMesh方法改变图像都是以基准点坐标为参考的,也就是说,不管你执行drawBitmapMesh方法几次,只要参数没改变,效果不累加。
drawBitmapMesh可以做出很多很多的效果,比如类似放大镜的:

/*  * 生成各个交点坐标  */  int index = 0;  float multipleY = mBitmap.getHeight() / HEIGHT;  float multipleX = mBitmap.getWidth() / WIDTH;  for (int y = 0; y <= HEIGHT; y++) {      float fy = multipleY * y;      for (int x = 0; x <= WIDTH; x++) {          float fx = multipleX * x;          setXY(fx, fy, index);          if (5 == y) {              if (8 == x) {                  setXY(fx - multipleX, fy - multipleY, index);              }              if (9 == x) {                  setXY(fx + multipleX, fy - multipleY, index);              }          }          if (6 == y) {              if (8 == x) {                  setXY(fx - multipleX, fy + multipleY, index);              }              if (9 == x) {                  setXY(fx + multipleX, fy + multipleY, index);              }          }          index += 1;      }  }  

这时我们将图片眼睛附近的四个点外移到临近的四个点上,图像该区域就会被像放大一样:

这里写图片描述

太恶心了……我们借助另外一个例子来更好地理解drawBitmapMesh,这个例子与API DEMO类似,我只是参考了国外大神的效果给他加上了一些标志点和位移线段来更好地展示drawBitmapMesh做了什么:

public class BitmapMeshView2 extends View {      private static final int WIDTH = 9, HEIGHT = 9;// 分割数      private static final int COUNT = (WIDTH + 1) * (HEIGHT + 1);// 交点数      private Bitmap mBitmap;// 位图对象      private float[] matrixOriganal = new float[COUNT * 2];// 基准点坐标数组      private float[] matrixMoved = new float[COUNT * 2];// 变换后点坐标数组      private float clickX, clickY;// 触摸屏幕时手指的xy坐标      private Paint origPaint, movePaint, linePaint;// 基准点、变换点和线段的绘制Paint      public BitmapMeshView2(Context context, AttributeSet set) {          super(context, set);          setFocusable(true);          // 实例画笔并设置颜色          origPaint = new Paint(Paint.ANTI_ALIAS_FLAG);          origPaint.setColor(0x660000FF);          movePaint = new Paint(Paint.ANTI_ALIAS_FLAG);          movePaint.setColor(0x99FF0000);          linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);          linePaint.setColor(0xFFFFFB00);          // 获取位图资源          mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bt);          // 初始化坐标数组          int index = 0;          for (int y = 0; y <= HEIGHT; y++) {              float fy = mBitmap.getHeight() * y / HEIGHT;              for (int x = 0; x <= WIDTH; x++) {                  float fx = mBitmap.getWidth() * x / WIDTH;                  setXY(matrixMoved, index, fx, fy);                  setXY(matrixOriganal, index, fx, fy);                  index += 1;              }          }      }      /**      * 设置坐标数组      *       * @param array      *            坐标数组      * @param index      *            标识值      * @param x      *            x坐标      * @param y      *            y坐标      */      private void setXY(float[] array, int index, float x, float y) {          array[index * 2 + 0] = x;          array[index * 2 + 1] = y;      }      @Override      protected void onDraw(Canvas canvas) {          // 绘制网格位图          canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, matrixMoved, 0, null, 0, null);          // 绘制参考元素          drawGuide(canvas);      }      /**      * 绘制参考元素      *       * @param canvas      *            画布      */      private void drawGuide(Canvas canvas) {          for (int i = 0; i < COUNT * 2; i += 2) {              float x = matrixOriganal[i + 0];              float y = matrixOriganal[i + 1];              canvas.drawCircle(x, y, 4, origPaint);              float x1 = matrixOriganal[i + 0];              float y1 = matrixOriganal[i + 1];              float x2 = matrixMoved[i + 0];              float y2 = matrixMoved[i + 1];              canvas.drawLine(x1, y1, x2, y2, origPaint);          }          for (int i = 0; i < COUNT * 2; i += 2) {              float x = matrixMoved[i + 0];              float y = matrixMoved[i + 1];              canvas.drawCircle(x, y, 4, movePaint);          }          canvas.drawCircle(clickX, clickY, 6, linePaint);      }      /**      * 计算变换数组坐标      */      private void smudge() {          for (int i = 0; i < COUNT * 2; i += 2) {              float xOriginal = matrixOriganal[i + 0];              float yOriginal = matrixOriganal[i + 1];              float dist_click_to_origin_x = clickX - xOriginal;              float dist_click_to_origin_y = clickY - yOriginal;              float kv_kat = dist_click_to_origin_x * dist_click_to_origin_x + dist_click_to_origin_y * dist_click_to_origin_y;              float pull = (float) (1000000 / kv_kat / Math.sqrt(kv_kat));              if (pull >= 1) {                  matrixMoved[i + 0] = clickX;                  matrixMoved[i + 1] = clickY;              } else {                  matrixMoved[i + 0] = xOriginal + dist_click_to_origin_x * pull;                  matrixMoved[i + 1] = yOriginal + dist_click_to_origin_y * pull;              }          }      }      @Override      public boolean onTouchEvent(MotionEvent event) {          clickX = event.getX();          clickY = event.getY();          smudge();          invalidate();          return true;      }  }  

运行后的效果如下:

这里写图片描述

大波妹子图上我们绘制了很多蓝色和红色的点,默认状态下,蓝色和红色的点是重合在一起的,两者间通过一线段连接,当我们手指在图片上移动时,会出现一个黄色的点,黄色的点代表我们当前的触摸点,而红色的点代表变换后的坐标点,蓝色的点代表基准坐标点:

这里写图片描述

可以看到越靠近触摸点的红点越向触摸点坍塌,红点表示当前变换后的点坐标,蓝点表示基准点的坐标,所有的变化都是参照蓝点进行的,这个例子可以很容易地理解drawBitmapMesh:

这里写图片描述

drawBitmapMesh参数中有个vertOffset,该参数是verts数组的偏移值,意为从第一个元素开始才对位图就行变化,这些大家自己去尝试下吧,还有colors和colorOffset,类似。
drawBitmapMesh说实话真心很屌,但是计算复杂确是个鸡肋,这么屌的一个方法被埋没其实是由原因可循的,高不成低不就,如上所示,有些变换我们可以使用Matrix等其他方法简单实现,但是drawBitmapMesh就要通过一些列计算,太复杂。那真要做复杂的图形效果呢,考虑到效率我们又会首选OpenGL……这真是一个悲伤的故事……无论怎样,请记住这位烈士一样的方法…………总有用处的
好了,真的要开始搞Canvas,开始搞了哦~~谁先上?
要学懂Canvas就要知道Canvas的本质是什么,那有盆友就会说了,麻痹你不是扯过无数次Canvas是画布么,难道又不是了?是,Canvas是画布,但是我们真的是在Canvas上画东西么?在前几节的一些例子中我们曾这样使用过Canvas:

Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);  Canvas canvas = new Canvas(bitmap);  canvas.drawColor(Color.RED);  

也就是说将Bitmap注入到Canvas中,尔后Canvas所有的操作都会在这个Bitmap上进行,如果,此时我们的界面中有一个ImageView,那么我们可以直接将绘制后的Bitmap显示出来:

public class MainActivity extends Activity {      private ImageView ivMain;      @Override      public void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          ivMain = (ImageView) findViewById(R.id.main_iv);          Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);          Canvas canvas = new Canvas(bitmap);          canvas.drawColor(Color.RED);          ivMain.setImageBitmap(bitmap);      }  }  

运行效果如图所示:

这里写图片描述

我们只是简单地填充了一块红色色块,色块的大小由bitmap决定,更确切地说,这个Canvas的大小是由bitmap决定的,类似的方法我们在前几节的例子中也不少用到,这里就不多说了。除了我们自己去new一个Canvas外,我们更常获得Canvas对象的地方是在View的:

@Override  protected void onDraw(Canvas canvas) {      super.onDraw(canvas);  }  

在这里通过onDraw方法的参数传递我们可以获取一个Canvas对象,好奇的同学一定很想知道这个Canvas对象是如何来的,跟我们自己new的有何区别。事实上两者区别不大,最终都是new过来的,只是onDraw方法传过来的Canvas对象拥有一些绘制的上下文关联。这一过程涉及到太多的源码,这里我只简单地提一下。在framework中,Activty被创建时(更准确地说是在addView的时候)会同时创建一个叫做ViewRootImpl的对象,ViewRootImpl是个很碉堡的类,它负责很多GUI的东西,包括我们常见的窗口显示、用户的输入输出等等,同时,它也负责Window跟WMS通信(Window你可以想象是一个容器,里面包含着我们的一个Activity,而AMS呢全称为Activity Manager Service,顾名思义很好理解它的作用),当ViewRootImpl跟WMS建立通信注册了Window后就会发出第一次渲染View Hierachy的请求,涉及到的方法均在ViewRootImpl下:setView、requestLayout、scheduleTraversals等,大家有兴趣可以自己去搜罗看看,在performTraversals方法中ViewRootImpl就会去创建Surface,而此后的渲染则可以通过Surface的lockCanvas方法获取Surface的Canvas来进行,然后遍历View Hierachy把需要绘制的View通过Canvas(View.onDraw(Canvas canvas))绘制到Surface上,绘制完成后解锁(Surface.unlockCanvasAndPost)让SurfaceFlinger将Surface绘制到屏幕上。我们onDraw(Canvas canvas)方法中传入的Canvas对象大致就是这么来的,说起简单,其实中间还有大量的过程被我省略了………………还是不扯为好,扯了讲通宵都讲不完。
上面我们概述了下onDraw参数列表中的Canvas对象是怎么来的,那么Canvas的实质是什么呢?我们通过追踪Canvas的两个构造方法可以发现两者的实现过程:
无参构造方法:

/**  * Construct an empty raster canvas. Use setBitmap() to specify a bitmap to  * draw into.  The initial target density is {@link Bitmap#DENSITY_NONE};  * this will typically be replaced when a target bitmap is set for the  * canvas.  */  public Canvas() {      if (!isHardwareAccelerated()) {          // 0 means no native bitmap          mNativeCanvas = initRaster(0);          mFinalizer = new CanvasFinalizer(mNativeCanvas);      } else {          mFinalizer = null;      }  }  

含Bitmap对象作为参数的构造方法:

/**  * Construct a canvas with the specified bitmap to draw into. The bitmap  * must be mutable.  *   * <p>The initial target density of the canvas is the same as the given  * bitmap's density.  *  * @param bitmap Specifies a mutable bitmap for the canvas to draw into.  */  public Canvas(Bitmap bitmap) {      if (!bitmap.isMutable()) {          throw new IllegalStateException("Immutable bitmap passed to Canvas constructor");      }      throwIfCannotDraw(bitmap);      mNativeCanvas = initRaster(bitmap.ni());      mFinalizer = new CanvasFinalizer(mNativeCanvas);      mBitmap = bitmap;      mDensity = bitmap.mDensity;  }  

大家看到这两个构造方法我都把它的注释给COPY出来了,目的就是想告诉大家,虽然说无参的构造方法并没有传入Bitmap对象,但是Android依然建议(苛刻地说是要求)我们使用Canvas的setBitmap()方法去为Canvas指定一个Bitmap对象!为什么Canvas非要一样Bitmap对象呢?原因很简单,Canvas需要一个Bitmap对象来保存像素。Canvas有大量的代码被封装并通过jni调用,事实上Android涉及图形图像处理的大量方法都是通过jni调用的,比如上面两个构造方法都调用了一个initRaster方法,这个方法的实现灰常简单:

static SkCanvas* initRaster(JNIEnv* env, jobject, SkBitmap* bitmap) {      if (bitmap) {          return new SkCanvas(*bitmap);      } else {          // Create an empty bitmap device to prevent callers from crashing          // if they attempt to draw into this canvas.          SkBitmap emptyBitmap;          return new SkCanvas(emptyBitmap);      }  }  

可以看到bitmap又被封装成了一个SkCanvas对象。上面我们曾说过,onDraw中传来的Cnavas对象来自于ViewRootImpl的Surface,当调用Surface.lockCanvas时会从图像缓存队列中取出一个可用缓存,把当前Posted Buffer的内容COPY到新缓存中然后加锁该缓存区域并设置为Locked Buffer。此时会根据新缓存的内存地址构建一个SkBitmap并将该SkBitmap设置到SkCanvas中并返回与之对应Canvas。而当调用Surface.unlockCanvasAndPost时则会清空SkCanvas并将SkBitmap设置为空,此时Locked Buffer将会被解锁并重新扔回图像缓存队列中,同时将Poated Buffer设置为Locked Buffer,旧的Posted Buffer就可以被下次取出来使用,设置Locked Buffer为空,当SF下次进行screen composite的时候就会把当前Poated Buffer绘制到屏幕上,这算是Canvas到屏幕绘制的一个小过程,当然事实比我说的复杂得多,这又是我的一个删减版本而已,懂得就听,不懂的权当废话不用管,我们不会涉及到这么深,像什么HardwareCanvas、GL之类的太过深入没必要去学,这里只阐述一个小原理而已。
对我们普通开发者来说,要记住的的是,一个Canvas需要一个Bitmap来保存像素信息,你说不要行不行?当然可以,画得东西没法保存而已,既然没法保存那我画来还有何意义呢?isn’t it?
Canvas所提供的各种方法根据功能来看大致可以分为几类,第一是以drawXXX为主的绘制方法,第二是以clipXXX为主的裁剪方法,第三是以scale、skew、translate和rotate组成的Canvas变换方法,最后一类则是以saveXXX和restoreXXX构成的画布锁定和还原,还有一些渣渣方法就不归类了。
绘制图形、变换锁定还原画布我们都在前面的一些code中使用过,那么什么叫裁剪画布呢?我们来看一段code:

public class CanvasView extends View {      public CanvasView(Context context, AttributeSet attrs) {          super(context, attrs);      }      @Override      protected void onDraw(Canvas canvas) {          canvas.drawColor(Color.BLUE);          canvas.clipRect(0, 0, 500, 500);          canvas.drawColor(Color.RED);      }  }  

这段代码灰常简单,我们在onDraw中将整个画布绘制成蓝色,然后我们在当前画布上从[0,0]为左端点开始裁剪出一块500x500大小的矩形,再次将画布绘制成红色,你会发现只有被裁剪的区域才能被绘制成红色:

这里写图片描述

是不是有点懂裁剪的意思了?不懂?没事,我们再画一个圆加深理解:

public class CanvasView extends View {      private Paint mPaint;      public CanvasView(Context context, AttributeSet attrs) {          super(context, attrs);          mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);          mPaint.setStyle(Paint.Style.FILL);          mPaint.setColor(Color.GREEN);      }      @Override      protected void onDraw(Canvas canvas) {          canvas.drawColor(Color.BLUE);          canvas.clipRect(0, 0, 500, 500);          canvas.drawColor(Color.RED);          canvas.drawCircle(500, 600, 100, mPaint);      }  }  

如代码所示,我们在以[500,600]为圆心绘制一个半径为100px的绿色圆,按道理来说,这个圆应该刚好与红色区域下方相切对吧,但是事实上呢我们见不到任何效果,为什么?因为如上所说,当前画布被“裁剪”了,只有500x500也就是上图中红色区域的大小了,如果我们所绘制的东西在该区域外部,即便绘制了你也看不到,这时我们稍增大圆的半径:

canvas.drawCircle(500, 600, 150, mPaint);  

这里写图片描述

终于看到我们的圆“露”出来了~~现在你能稍微明白裁剪的作用了么?上面的代码中我们使用到了Canvas的

clipRect(int left, int top, int right, int bottom)  

方法,与之类似的还有

clipRect(float left, float top, float right, float bottom)  

方法,一个int一个float,不扯了。除此之外还有两个与之对应的方法

clipRect(Rect rect)  clipRect(RectF rect)  

,Rect和RectF是类似的,只不过RectF中涉及计算的时候数值类型均为float型,两者均表示一块规则矩形,何以见得呢?我们以Rect为例来Test一下:

public class CanvasView extends View {      private Rect mRect;      public CanvasView(Context context, AttributeSet attrs) {          super(context, attrs);          mRect = new Rect(0, 0, 500, 500);      }      @Override      protected void onDraw(Canvas canvas) {          canvas.drawColor(Color.BLUE);          canvas.clipRect(mRect);          canvas.drawColor(Color.RED);      }  }  

如代码所示这样我们得到的结果跟上面的结果并无二致,蓝色的底,500x500大小的红色矩形,但是Rect的意义远不止于此,鉴于Rect类并不复杂,我就讲两个其比较重要的方法,我们稍微更改下我们的代码:

public class CanvasView extends View {      private Rect mRect;      public CanvasView(Context context, AttributeSet attrs) {          super(context, attrs);          mRect = new Rect(0, 0, 500, 500);          mRect.intersect(250, 250, 750, 750);      }      @Override      protected void onDraw(Canvas canvas) {          canvas.drawColor(Color.BLUE);          canvas.clipRect(mRect);          canvas.drawColor(Color.RED);      }  }  

大家看到我在实例化了一个Rect后调用了intersect方法,这个方法的作用是什么?来看看效果先:

这里写图片描述

PS:黄色线框为后期加上的辅助线非程序生成
可以看到原先的红色区域变小了,这是怎么回事呢?其实intersect的作用跟我们之前学到的图形混合模式有点类似,它会取两个区域的相交区域作为最终区域,上面我们的第一个区域是在实例化Rect时确定的(0, 0, 500, 500),第二个区域是调用intersect方法时指定的(250, 250, 750, 750),这两个区域对应上图的两个黄色线框,两者相交的地方则为最终的红色区域,而intersect方法的计算方式是相当有趣的,它不是单纯地计算相交而是去计算相交区域最近的左上端点和最近的右下端点,不知道大家是否明白这个意思,我们来看Rect中的另一个union方法你就会懂,union方法与intersect相反,取的是相交区域最远的左上端点作为新区域的左上端点,而取最远的右下端点作为新区域的右下端点,比如:

mRect.union(250, 250, 750, 750);  

运行后我们会看到如下结果:

这里写图片描述

是不是觉得不是我们想象中的那样单纯地两个区域相加?没事,好好体会,后面还有类似的。类似的方法Rect和RectF都有很多,效果都是显而易见的就不多说了,有兴趣大家可以自己去try。
说到这里会有很多童鞋会问,裁剪只是个矩形区域,如果我想要更多不规则的裁剪区域怎么办呢?别担心,Android必然也考虑到这样的情况,其提供了一个

clipPath(Path path) 

方法给我们以Path的方式创建更多不规则的裁剪区域,在1/4讲PathEffect的时候我们曾对Path有所接触,但是依旧不了解
Path是android中用来封装几何学路径的一个类,因为Path在图形绘制上占的比重还是相当大的,这里我们先来学习一下这个Path,来看看其一些具体的用法:

public class PathView extends View {      private Path mPath;// 路径对象      private Paint mPaint;// 画笔对象      public PathView(Context context, AttributeSet attrs) {          super(context, attrs);          /*          * 实例化画笔并设置属性          */          mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);          mPaint.setStyle(Paint.Style.STROKE);          mPaint.setColor(Color.CYAN);          // 实例化路径          mPath = new Path();          // 连接路径到点[100,100]          mPath.lineTo(100, 100);      }      @Override      protected void onDraw(Canvas canvas) {          // 绘制路径          canvas.drawPath(mPath, mPaint);      }  }  

这里我们用到了Path的一个方法

lineTo(float x, float y)  

该方法很简单咯,顾名思义将路径连接至某个坐标点,事实也是如此:

这里写图片描述

注意,当我们没有移动Path的点时,其默认的起点为画布的[0,0]点,当然我们可以通过

moveTo(float x, float y)  

方法来改变这个起始点的位置:

// 实例化路径  mPath = new Path();  //移动点至[300,300]  mPath.moveTo(300, 300);  // 连接路径到点[100,100]  mPath.lineTo(100, 100);  

效果如下:

这里写图片描述

当然我们可以考虑多次调用lineTo方法来绘制更复杂的图形:

// 实例化路径  mPath = new Path();  // 移动点至[300,300]  mPath.moveTo(100, 100);  // 连接路径到点  mPath.lineTo(300, 100);  mPath.lineTo(400, 200);  mPath.lineTo(200, 200);  

一个没有封闭的类似平行四边形的线条:

这里写图片描述

如果此时我们想闭合该曲线让它变成一个形状该怎么做呢?聪明的你一定想到

mPath.lineTo(100, 100) 

然而Path给我提供了更便捷的方法

close()  

去闭合曲线:

// 实例化路径  mPath = new Path();  // 移动点至[300,300]  mPath.moveTo(100, 100);  // 连接路径到点  mPath.lineTo(300, 100);  mPath.lineTo(400, 200);  mPath.lineTo(200, 200);  // 闭合曲线  mPath.close();  

这里写图片描述

那么有些朋友会问Path就只能光绘制这些单调的线段么?肯定不是!Path在绘制的方法中提供了许多XXXTo的方法来帮助我们绘制各类直线、曲线,例如,方法

quadTo(float x1, float y1, float x2, float y2)  

可以让我们绘制二阶贝赛尔曲线,什么叫贝赛尔曲线?其实很简单,使用三个或多个点来确定的一条曲线,贝塞尔曲线在图形图像学中有相当重要的地位,Path中也提供了一些方法来给我们模拟低阶贝赛尔曲线。
贝塞尔曲线的定义也比较简单,你只需要一个起点、一个终点和至少零个控制点则可定义一个贝赛尔曲线,当控制点为零时,只有起点和终点,此时的曲线说白了就是一条线段,我们称之为
PS:以下图片和公式均来自维基百科和互联网
一阶贝赛尔曲线

这里写图片描述

其公式可概括为:

其中B(t)为时间为t时点的坐标,P0为起点、Pn为终点
贝塞尔曲线于1962年由法国数学家Pierre Bézier第一次研究使用并给出了详细的计算公式,So该曲线也是由其名字命名。Path中给出的quadTo方法属于
二阶贝赛尔曲线

这里写图片描述

二阶贝赛尔曲线的一个明显特征是其拥有一个控制点,大家可以这样想想贝赛尔曲线,在一根两端固定橡皮筋上有一块磁铁,现在我们拿另一块磁铁去吸引橡皮筋上的磁铁,因为引力,橡皮筋会随着我们手上磁铁的移动而改变形状,又因为橡皮筋的张力让束缚在橡皮筋上的磁铁不会轻易吸附到我们手上的磁铁,这时橡皮筋的状态就可以看成是一条贝塞尔曲线,而我们手中的磁铁就是一个控制点,通过这个控制点我们“拉扯”橡皮筋的曲度。
二阶贝赛尔曲线的公式为:

同样的,Path中也提供了三阶贝塞尔曲线的方法cubicTo,按照上面我们的推论,三阶应该是有两个控制点才对对吧

三阶贝赛尔曲线

这里写图片描述

公式:

高阶贝赛尔曲线在Path中没有对应的方法,对我们来说三阶也足够了,不过大家可以了解下,难得我在墙外找到如此动感的贝赛尔曲线高清无码动图
高阶贝塞尔曲线

四阶:

这里写图片描述

五阶:

这里写图片描述

贝塞尔曲线通用公式:

这里写图片描述

回到我们Path的quadTo方法,我们可以使用它来绘制一条曲线:

// 实例化路径  mPath = new Path();  // 移动点至[100,100]  mPath.moveTo(100, 100);  // 连接路径到点  mPath.quadTo(200, 200, 300, 100);  

看图说话:

这里写图片描述

其中quadTo的前两个参数为控制点的坐标,后两个参数为终点坐标,至于起点嘛……这么二的问题就别问了……是不是很简单?如果你这么认为那就太小看贝塞尔曲线了。在我们对Path有一定的了解后会使用Path和裁剪做个有趣的东西,接着看Path的三阶贝赛尔曲线:

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)  

与quadTo类似,前四个参数表示两个控制点,最后两个参数表示终点:

// 实例化路径  mPath = new Path();  // 移动点至[100,100]  mPath.moveTo(100, 100);  // 连接路径到点  mPath.cubicTo(200, 200, 300, 0, 400, 100);  

很好理解:

这里写图片描述

贝塞尔曲线是图形图像学中相当重要的一个概念,活用它可以得到很多很有意思的效果,比如,我在界面中简单模拟一下杯子中水消匿的效果:

这里写图片描述

当然你也可以反过来让模拟往杯子里倒水的效果~实现过程非常简单,说白了就是不断移动二阶曲线的控制点同时不断更改顶部各点的Y坐标,然后不断重绘:

public class WaveView extends View {      private Path mPath;// 路径对象      private Paint mPaint;// 画笔对象      private int vWidth, vHeight;// 控件宽高      private float ctrX, ctrY;// 控制点的xy坐标      private float waveY;// 整个Wave顶部两端点的Y坐标,该坐标与控制点的Y坐标增减幅一致      private boolean isInc;// 判断控制点是该右移还是左移      public WaveView(Context context, AttributeSet attrs) {          super(context, attrs);          // 实例化画笔并设置参数          mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);          mPaint.setColor(0xFFA2D6AE);          // 实例化路径对象          mPath = new Path();      }      @Override      protected void onSizeChanged(int w, int h, int oldw, int oldh) {          // 获取控件宽高          vWidth = w;          vHeight = h;          // 计算控制点Y坐标          waveY = 1 / 8F * vHeight;          // 计算端点Y坐标          ctrY = -1 / 16F * vHeight;      }      @Override      protected void onDraw(Canvas canvas) {          /*          * 设置Path起点          * 注意我将Path的起点设置在了控件的外部看不到的区域          * 如果我们将起点设置在控件左端x=0的位置会使得贝塞尔曲线变得生硬          * 至于为什么刚才我已经说了          * 所以我们稍微让起点往“外”走点          */          mPath.moveTo(-1 / 4F * vWidth, waveY);          /*          * 以二阶曲线的方式通过控制点连接位于控件右边的终点          * 终点的位置也是在控件外部          * 我们只需不断让ctrX的大小变化即可实现“浪”的效果          */          mPath.quadTo(ctrX, ctrY, vWidth + 1 / 4F * vWidth, waveY);          // 围绕控件闭合曲线          mPath.lineTo(vWidth + 1 / 4F * vWidth, vHeight);          mPath.lineTo(-1 / 4F * vWidth, vHeight);          mPath.close();          canvas.drawPath(mPath, mPaint);          /*          * 当控制点的x坐标大于或等于终点x坐标时更改标识值          */          if (ctrX >= vWidth + 1 / 4F * vWidth) {              isInc = false;          }          /*          * 当控制点的x坐标小于或等于起点x坐标时更改标识值          */          else if (ctrX <= -1 / 4F * vWidth) {              isInc = true;          }          // 根据标识值判断当前的控制点x坐标是该加还是减          ctrX = isInc ? ctrX + 20 : ctrX - 20;          /*          * 让“水”不断减少          */          if (ctrY <= vHeight) {              ctrY += 2;              waveY += 2;          }          mPath.reset();          // 重绘          invalidate();      }  }  

除了上面的几个XXXTo外,Path还提供了一个

arcTo (RectF oval, float startAngle, float sweepAngle)  

方法用来生成弧线,其实说白了就是从圆或椭圆上截取一部分而已 = =

// 实例化路径  mPath = new Path();  // 移动点至[100,100]  mPath.moveTo(100, 100);  // 连接路径到点  RectF oval = new RectF(100, 100, 200, 200);  mPath.arcTo(oval, 0, 90);  

效果如下图:

这里写图片描述

这里要注意哦,使用Path生成的路径必定都是连贯的,虽然我们使用arcTo绘制的是一段弧但其最终都会与我们的起始点[100,100]连接起来,如果你不想连怎么办?简单,强制让arcTo绘制的起点作为Path的起点不就是了?Path也提供了另一个重载方法:

arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)  

该方法只是多了一个布尔值,值为true时将会把弧的起点作为Path的起点:

mPath.arcTo(oval, 0, 90, true);  

如果需要了解更多知识,请点击这里

原创粉丝点击