自定义View(二-番外5-canvas-上)

来源:互联网 发布:苏州聚合数据招聘 编辑:程序博客网 时间:2024/06/05 20:24

From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige

跟着爱哥打天下

自定义控件其实很简单5:

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); 

这里写图片描述

clipRect(int left, int top, int right, int bottom)  
clipRect(float left, float top, float right, float bottom)  
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);      }  }  

这里写图片描述

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, @Nullable AttributeSet attrs) {        super(context, attrs);        //实例化画笔并设置属性        mPaint = new Paint(Paint.ANTI_ALIAS_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();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);

这里写图片描述

Path中除了上面介绍的几个XXXTo方法外还有一套rXXXTo方法:

rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)  rLineTo(float dx, float dy)  rMoveTo(float dx, float dy)  rQuadTo(float dx1, float dy1, float dx2, float dy2) 

这一系列rXXXTo方法其实跟上面的那些XXXTo差不多的,唯一的不同是rXXXTo方法的参考坐标是相对的而XXXTo方法的参考坐标始终是参照画布原点坐标,什么意思呢?举个简单的例子:

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);          mPaint.setStrokeWidth(5);          // 实例化路径          mPath = new Path();          // 移动点至[100,100]          mPath.moveTo(100, 100);          // 连接路径到点          mPath.lineTo(200, 200);      }      @Override      protected void onDraw(Canvas canvas) {          // 绘制路径          canvas.drawPath(mPath, mPaint);      }  }  

上述代码我们从点[100,100]开始连接点[200,200]构成了一条线段:

这里写图片描述

这个点[200,200]是相对于画布圆点坐标[0,0]而言的,这点大家应该好理解,如果我们换成

mPath.rLineTo(200, 200); 

那么它的意思就是将会以[100,100]作为原点坐标,连接以其为原点坐标的坐标点[200,200],如果换算成一画布原点的话,实际上现在的[200,200]就是[300,300]了:

这里写图片描述

懂了么?而这个前缀r也就是relative(相对)的简写,so easy是么!头脑简单!
XXXTo方法可以连接Path中的曲线而Path提供的另一系列addXXX方法则可以让我们直接往Path中添加一些曲线,比如

addArc(RectF oval, float startAngle, float sweepAngle) 

方法允许我们将一段弧形添加至Path,注意这里我用到了“添加”这个词汇,也就是说,通过addXXX方法添加到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);          mPaint.setStrokeWidth(5);          // 实例化路径          mPath = new Path();          // 移动点至[100,100]          mPath.moveTo(100, 100);          // 连接路径到点          mPath.lineTo(200, 200);          // 添加一条弧线到Path中          RectF oval = new RectF(100, 100, 300, 400);          mPath.addArc(oval, 0, 90);      }      @Override      protected void onDraw(Canvas canvas) {          // 绘制路径          canvas.drawPath(mPath, mPaint);      }  }  

这里写图片描述

这里是个椭圆

如图和代码所示,虽然我们先绘制了由[100,100]到[200,200]的线段,但是在我们往Path中添加了一条弧线后该弧线并没与线段连接。除了addArc,Path还提供了一系列的add方法

addCircle(float x, float y, float radius, Path.Direction dir)  addOval(float left, float top, float right, float bottom, Path.Direction dir)  addRect(float left, float top, float right, float bottom, Path.Direction dir)  addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir)  

这些方法和addArc有很明显的区别,就是多了一个Path.Direction参数,其他呢都大同小异,除此之外不知道大家还发现没有,addArc是往Path中添加一段弧,说白了就是一条开放的曲线,而上述几种方法都是一个具体的图形,或者说是一条闭合的曲线,Path.Direction的意思就是标识这些闭合曲线的闭合方向。那什么叫闭合方向呢?光说大家一定会蒙,有学习激情的童鞋看到后肯定会马上敲代码试验一下两者的区别,可是不管你如何改,单独地在一条闭合曲线上你是看不出所谓闭合方向的区别的,这时我们可以借助Canvas的另一个方法来简单地说明一下

drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)  

这个方法呢很简单沿着Path绘制一段文字,参数也是一看就该懂得了不多说。Path.Direction只有两个常量值CCW和CW分别表示逆时针方向闭合和顺时针方向闭合,我们来看一段代码

public class PathView extends View {      private Path mPath;// 路径对象      private Paint mPaint;// 路径画笔对象      private TextPaint mTextPaint;// 文本画笔对象      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);          mPaint.setStrokeWidth(5);          mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);          mTextPaint.setColor(Color.DKGRAY);          mTextPaint.setTextSize(20);          // 实例化路径          mPath = new Path();          // 添加一条弧线到Path中          RectF oval = new RectF(100, 100, 300, 400);          mPath.addOval(oval, Path.Direction.CW);      }      @Override      protected void onDraw(Canvas canvas) {          // 绘制路径          canvas.drawPath(mPath, mPaint);          // 绘制路径上的文字          canvas.drawTextOnPath("ad撒发射点发放士大夫斯蒂芬斯蒂芬森啊打扫打扫打扫达发达省份撒旦发射的", mPath, 0, 0, mTextPaint);      }  }  

我们往Path中添加了一条闭合方向为CW椭圆形的闭合曲线并将其绘制在Canvas上,同时呢我们沿着该曲线绘制了一段文本,效果如下:

这里写图片描述

如果我们把闭合方向改为CCW那么会发生什么呢?

mPath.addOval(oval, Path.Direction.CCW);  

这里写图片描述

沿着Path的文字全都在闭合曲线的“内部”了,Path.Direction闭合方向大概就是这么个意思。对于我们平时开发来说,掌握Path的以上一些方法已经是足够了,当然Path的方法还有很多,但是因为平时开发涉及的少,我也就不累赘了,毕竟用得少或者根本不会用到的东西说了也是浪费口水,对吧。
Path用的也相当广泛,在之前的章节中我们也讲过一个PathEffect类,两者结合可以得到很多很酷的效果。在众多的用途中,使用Path做折线图算是最最最常见的了,仅仅使用以上我们讲到的一些Path的方法可以完成很多的折线图效果。
在上一节最后的一个例子中我们绘制了一个自定义的圈圈View,当时我跟大家说过在你想去自定义一个控件的时候一定要把自己看作一个designer而不是coder,你要用设计的眼光去看待一个控件,那么我们在做一个折线图的控件之前就应该要分析一个折线图应该是怎样的,下面我google一些简单折线图的例子:

这里写图片描述

这种比较简单

这里写图片描述

这种呢有文字标注稍难

这里写图片描述

这种就复杂了点
不管是哪种折线图,我们都可以发现其必有一个横坐标和一个纵坐标且其上都有刻度,一般情况下来说横纵坐标上的刻度数量是一样的。对于平面折线图来说,分析到上面一点就差不多了,而我们要做的折线图控件我在PS里简单地做了一个design:

这里写图片描述

设计地很简单,当中有一些辅助参数什么的,实际上整个控件就几个元素:

这里写图片描述

如上图所示,两个带刻度的轴和一个网格还有两个轴文字标识和一条曲线,very simple!图好像很简单~~但是真要code起来就不是件容易的事了,首先我们要考虑到不同的数据、其次是屏幕的适配,说到适配,上一节我们曾讲过,因为屏幕的多元化,我们必定不能写死一个参数,so~我们在上一节画圈圈的时候是使用控件的边长来作为所有数值的基准参考,这次也一样。

这里写图片描述

public class PolylineView extends View {    private static final float LEFT = 1 / 16F, TOP = 1 / 16F, RIGHT = 15 / 16F, BOTTOM = 7 / 8F;// 网格区域相对位置    private static final float TIME_X = 3 / 32F, TIME_Y = 1 / 16F, MONEY_X = 31 / 32F, MONEY_Y = 15 / 16F;// 文字坐标相对位置    private static final float TEXT_SIGN = 1 / 32F;// 文字相对大小    private static final float THICK_LINE_WIDTH = 1 / 128F, THIN_LINE_WIDTH = 1 / 512F;// 粗线和细线相对大小    private TextPaint mTextPaint;// 文字画笔    private Paint linePaint, pointPaint;// 线条画笔和点画笔    private Path mPath;// 路径对象    private Bitmap mBitmap;// 绘制曲线的Btimap对象    private Canvas mCanvas;// 装载mBitmap的Canvas对象    private List<PointF> pointFs;// 数据列表    private float[] rulerX, rulerY;// xy轴向刻度    private String signX, signY;// 设置X和Y坐标分别表示什么的文字    private float textY_X, textY_Y, textX_X, textX_Y;// 文字坐标    private float textSignSzie;// xy坐标标识文本字体大小    private float thickLineWidth, thinLineWidth;// 粗线和细线宽度    private float left, top, right, bottom;// 网格区域左上右下两点坐标    private int viewSize;// 控件尺寸    private float maxX, maxY;// 横纵轴向最大刻度    private float spaceX, spaceY;// 刻度间隔    /**     * 构造方法     */    public PolylineView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        setLayerType(LAYER_TYPE_SOFTWARE, null);        // 实例化文本置参数        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);        mTextPaint.setColor(Color.WHITE);        // 实例化线条画笔并设置参数        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);        linePaint.setStyle(Paint.Style.STROKE);        linePaint.setColor(Color.WHITE);        // 实例化点画笔并设置参数        pointPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);        pointPaint.setStyle(Paint.Style.FILL);        pointPaint.setColor(Color.WHITE);        // 实例化Path对象        mPath = new Path();        // 实例化Canvas对象        mCanvas = new Canvas();        // 初始化数据        initData();    }    /**     * 初始化数据支撑     * View初始化时可以考虑给予一个模拟数据     * 当然我们可以通过setData方法设置自己的数据     */    private void initData() {        Random random = new Random();        pointFs = new ArrayList<PointF>();        for (int i = 0; i < 20; i++) {            PointF pointF = new PointF();            pointF.x = (float) (random.nextInt(100) * i);            pointF.y = (float) (random.nextInt(100) * i);            pointFs.add(pointF);        }    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        // 在我们没学习测量控件之前强制宽高一致        super.onMeasure(widthMeasureSpec, widthMeasureSpec);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        // 获取控件尺寸        viewSize = w;        // 计算纵轴标识文本坐标        textY_X = viewSize * TIME_X;        textY_Y = viewSize * TIME_Y;        // 计算横轴标识文本坐标        textX_X = viewSize * MONEY_X;        textX_Y = viewSize * MONEY_Y;        // 计算xy轴标识文本大小        textSignSzie = viewSize * TEXT_SIGN;        // 计算网格左上右下两点坐标        left = viewSize * LEFT;        top = viewSize * TOP;        right = viewSize * RIGHT;        bottom = viewSize * BOTTOM;        // 计算粗线宽度        thickLineWidth = viewSize * THICK_LINE_WIDTH;        // 计算细线宽度        thinLineWidth = viewSize * THIN_LINE_WIDTH;    }    @Override    protected void onDraw(Canvas canvas) {        // 填充背景        canvas.drawColor(0xFF9596C4);        // 绘制标识元素        drawSign(canvas);        // 绘制网格        drawGrid(canvas);        // 绘制曲线        drawPolyline(canvas);    }    /**     * 绘制曲线     * 这里我使用一个新的Bitmap对象结合新的Canvas对象来绘制曲线     * 当然你可以直接在原来的canvas(onDraw传来的那个)中直接绘制如果你还没被坐标搞晕的话……     *     * @param canvas 画布     */    private void drawPolyline(Canvas canvas) {        // 生成一个Bitmap对象大小和我们的网格大小一致        mBitmap = Bitmap.createBitmap((int) (viewSize * (RIGHT - LEFT) - spaceX), (int) (viewSize * (BOTTOM - TOP) - spaceY), Bitmap.Config.ARGB_8888);        // 将Bitmap注入Canvas        mCanvas.setBitmap(mBitmap);        // 为画布填充一个半透明的红色        mCanvas.drawARGB(75, 255, 0, 0);        // 重置曲线        mPath.reset();        /*         * 生成Path和绘制Point         */        for (int i = 0; i < pointFs.size(); i++) {            // 计算x坐标            float x = mCanvas.getWidth() / maxX * pointFs.get(i).x;            // 计算y坐标            float y = mCanvas.getHeight() / maxY * pointFs.get(i).y;            y = mCanvas.getHeight() - y;            // 绘制小点点            mCanvas.drawCircle(x, y, thickLineWidth, pointPaint);            /*             * 如果是第一个点则将其设置为Path的起点             */            if (i == 0) {                mPath.moveTo(x, y);            }            // 连接各点            mPath.lineTo(x, y);        }        // 设置PathEffect        // linePaint.setPathEffect(new CornerPathEffect(200));        // 重置线条宽度        linePaint.setStrokeWidth(thickLineWidth);        // 将Path绘制到我们自定的Canvas上        mCanvas.drawPath(mPath, linePaint);        // 将mBitmap绘制到原来的canvas        canvas.drawBitmap(mBitmap, left, top + spaceY, null);    }    /**     * 绘制网格     *     * @param canvas 画布     */    private void drawGrid(Canvas canvas) {        // 锁定画布        canvas.save();        // 设置线条画笔宽度        linePaint.setStrokeWidth(thickLineWidth);        // 计算xy轴Path        mPath.moveTo(left, top);        mPath.lineTo(left, bottom);        mPath.lineTo(right, bottom);        // 绘制xy轴        canvas.drawPath(mPath, linePaint);        // 绘制线条        drawLines(canvas);        // 释放画布        canvas.restore();    }    /**     * 绘制网格     *     * @param canvas 画布     */    private void drawLines(Canvas canvas) {        // 计算刻度文字尺寸        float textRulerSize = textSignSzie / 2F;        // 重置文字画笔文字尺寸        mTextPaint.setTextSize(textRulerSize);        // 重置线条画笔描边宽度        linePaint.setStrokeWidth(thinLineWidth);        // 获取数据长度        int count = pointFs.size();        // 计算除数的值为数据长度减一        int divisor = count - 1;        // 计算横轴数据最大值        maxX = 0;        for (int i = 0; i < count; i++) {            if (maxX < pointFs.get(i).x) {                maxX = pointFs.get(i).x;            }        }        // 计算横轴最近的能被count整除的值        int remainderX = ((int) maxX) % divisor;        maxX = remainderX == 0 ? ((int) maxX) : divisor - remainderX + ((int) maxX);        // 计算纵轴数据最大值        maxY = 0;        for (int i = 0; i < count; i++) {            if (maxY < pointFs.get(i).y) {                maxY = pointFs.get(i).y;            }        }        // 计算纵轴最近的能被count整除的值        int remainderY = ((int) maxY) % divisor;        maxY = remainderY == 0 ? ((int) maxY) : divisor - remainderY + ((int) maxY);        // 生成横轴刻度值        rulerX = new float[count + 1];        for (int i = 0; i < count + 1; i++) {            rulerX[i] = maxX / divisor * i;        }        // 生成纵轴刻度值        rulerY = new float[count];        for (int i = 0; i < count; i++) {            rulerY[i] = maxY / divisor * i;        }        // 计算横纵坐标刻度间隔        spaceY = viewSize * (BOTTOM - TOP) / count;        spaceX = viewSize * (RIGHT - LEFT) / count;        // 锁定画布并设置画布透明度为75%        int sc = canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(), 75, Canvas.ALL_SAVE_FLAG);        // 绘制纵线段        for (float x = viewSize * LEFT; x < viewSize * RIGHT; x += spaceX) {            canvas.drawLine(x, viewSize * BOTTOM, x, viewSize * TOP + spaceY, linePaint);        }        // 绘制横线段        for (float y = viewSize * BOTTOM - spaceY; y > viewSize * TOP; y -= spaceY) {            canvas.drawLine(viewSize * LEFT, y, viewSize * RIGHT - spaceX, y, linePaint);        }        // 还原画布        canvas.restoreToCount(sc);        // 绘制横纵轴向刻度值        int index_x = 0, index_y = 1;        for (float y = viewSize * BOTTOM - spaceY; y > viewSize * TOP; y -= spaceY) {            for (float x = viewSize * LEFT; x < viewSize * RIGHT; x += spaceX) {                /*                 * 绘制横轴刻度数值                 */                if (y == viewSize * BOTTOM - spaceY) {                    canvas.drawText(String.valueOf(rulerX[index_x]), x, y + textSignSzie + spaceY, mTextPaint);                }                /*                 * 绘制纵轴刻度数值                 */                if (x == viewSize * LEFT) {                    canvas.drawText(String.valueOf(rulerY[index_y]), x - thickLineWidth, y + textRulerSize, mTextPaint);                }                index_x++;            }            index_y++;        }    }    /**     * 绘制标识元素     *     * @param canvas 画布     */    private void drawSign(Canvas canvas) {        // 锁定画布        canvas.save();        // 设置文本画笔文字尺寸        mTextPaint.setTextSize(textSignSzie);        // 绘制纵轴标识文字        mTextPaint.setTextAlign(Paint.Align.LEFT);        canvas.drawText(null == signY ? "y" : signY, textY_X, textY_Y, mTextPaint);        // 绘制横轴标识文字        mTextPaint.setTextAlign(Paint.Align.RIGHT);        canvas.drawText(null == signX ? "x" : signX, textX_X, textX_Y, mTextPaint);        // 释放画布        canvas.restore();    }    /**     * 设置数据     *     * @param pointFs 点集合     */    public synchronized void setData(List<PointF> pointFs, String signX, String signY) {        /*         * 数据为空直接GG         */        if (null == pointFs || pointFs.size() == 0) {            throw new IllegalArgumentException("No data to display !");        }        /*         * 控制数据长度不超过10个         * 对于折线图来说数据太多就没必要用折线图表示了而是使用散点图         */        if (pointFs.size() > 10) {            throw new IllegalArgumentException("The data is too long to display !");        }        // 设置数据并重绘视图        this.pointFs = pointFs;        this.signX = signX;        this.signY = signY;        invalidate();    }}

ps:修改了绘制网格的地方,修改了x轴坐标数组的size

简单地介绍了Path之后回到我们的Canvas中,关于裁剪的方法:

clipPath(Path path)  

是不是变得透彻起来呢?
我们可以利用该方法从Canvas中“挖”取一块不规则的画布:

public class CanvasView extends View {
private Path mPath;

public CanvasView(Context context, AttributeSet attrs) {      super(context, attrs);      mPath = new Path();      mPath.moveTo(50, 50);      mPath.lineTo(75, 23);      mPath.lineTo(150, 100);      mPath.lineTo(80, 110);      mPath.close();  }  @Override  protected void onDraw(Canvas canvas) {      canvas.drawColor(Color.BLUE);      canvas.clipPath(mPath);      canvas.drawColor(Color.RED);  }  

}

这里写图片描述

回顾Canvas中有关裁剪的方法,你会发现有一大堆带有Region.Op参数的重载方法:

clipPath(Path path, Region.Op op)  clipRect(Rect rect, Region.Op op)  clipRect(RectF rect, Region.Op op)  clipRect(float left, float top, float right, float bottom, Region.Op op)  clipRegion(Region region, Region.Op op)  

要明白这些方法的Region.Op参数那么首先要了解Region为何物。Region的意思是“区域”,在Android里呢它同样表示的是一块封闭的区域,Region中的方法都非常的简单,我们重点来瞧瞧Region.Op,Op是Region的一个枚举类,里面呢有六个枚举常量:

这里写图片描述

那么Region.Op究竟有什么用呢?其实它就是个组合模式,在1/6中我们曾学过一个叫图形混合模式的,而在本节开头我们也曾讲过Rect也有类似的组合方法,Region.Op灰常简单,如果你看过1/6的图形混合模式的话。这里我就给出一段测试代码,大家可以尝试去改变不同的组合模式看看效果

public class CanvasView extends View {      private Region mRegionA, mRegionB;// 区域A和区域B对象      private Paint mPaint;// 绘制边框的Paint      public CanvasView(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.WHITE);          mPaint.setStrokeWidth(2);          // 实例化区域A和区域B          mRegionA = new Region(100, 100, 300, 300);          mRegionB = new Region(200, 200, 400, 400);      }      @Override      protected void onDraw(Canvas canvas) {          // 填充颜色          canvas.drawColor(Color.BLUE);          canvas.save();          // 裁剪区域A          canvas.clipRegion(mRegionA);          // 再通过组合方式裁剪区域B          canvas.clipRegion(mRegionB, Region.Op.DIFFERENCE);          // 填充颜色          canvas.drawColor(Color.RED);          canvas.restore();          // 绘制框框帮助我们观察          canvas.drawRect(100, 100, 300, 300, mPaint);          canvas.drawRect(200, 200, 400, 400, mPaint);      }  }  

以下是各种组合模式的效果
DIFFERENCE

这里写图片描述

最终区域为第一个区域与第二个区域不同的区域。
INTERSECT

这里写图片描述

最终区域为第一个区域与第二个区域相交的区域。
REPLACE

这里写图片描述

最终区域为第二个区域。
REVERSE_DIFFERENCE

这里写图片描述

最终区域为第二个区域与第一个区域不同的区域。
UNION
这里写图片描述

最终区域为第一个区域加第二个区域。
XOR
这里写图片描述

最终区域为第一个区域加第二个区域并减去两者相交的区域。

Region.Op就是这样,它和我们之前讲到的图形混合模式几乎一模一样换汤不换药……我在做示例的时候仅仅是使用了一个Region,实际上Rect、Cricle、Ovel等封闭的曲线都可以使用Region.Op,介于篇幅,而且也不难以理解就不多说了。
有些童鞋会问那么Region和Rect有什么区别呢?首先最重要的一点,Region表示的是一个区域,而Rect表示的是一个矩形,这是最根本的区别之一,其次,Region有个很特别的地方是它不受Canvas的变换影响,Canvas的local不会直接影响到Region自身,什么意思呢?我们来看一个simple你就会明白:

public class CanvasView extends View {      private Region mRegion;// 区域对象      private Rect mRect;// 矩形对象      private Paint mPaint;// 绘制边框的Paint      public CanvasView(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.DKGRAY);          mPaint.setStrokeWidth(2);          // 实例化矩形对象          mRect = new Rect(0, 0, 200, 200);          // 实例化区域对象          mRegion = new Region(200, 200, 400, 400);      }      @Override      protected void onDraw(Canvas canvas) {          canvas.save();          // 裁剪矩形          canvas.clipRect(mRect);          canvas.drawColor(Color.RED);          canvas.restore();          canvas.save();          // 裁剪区域          canvas.clipRegion(mRegion);          canvas.drawColor(Color.RED);          canvas.restore();          // 为画布绘制一个边框便于观察          canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);      }  }  

大家看到,我在[0, 0, 200, 200]和[200, 200, 400, 400]的位置分别绘制了Rect和Region,它们两个所占大小是一样的:

这里写图片描述

画布因为和屏幕一样大,so~~我们看不出描边的效果,这时,我们将Canvas缩放至75%大小,看看会发生什么:

@Override  protected void onDraw(Canvas canvas) {      // 缩放画布      canvas.scale(0.75F, 0.75F);      canvas.save();      // 裁剪矩形      canvas.clipRect(mRect);      canvas.drawColor(Color.RED);      canvas.restore();      canvas.save();      // 裁剪区域      canvas.clipRegion(mRegion);      canvas.drawColor(Color.RED);      canvas.restore();      // 为画布绘制一个边框便于观察      canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);  }  

这时我们会看到,Rect随着Canvas的缩放一起缩放了,但是Region依旧泰山不动地淡定:

这里写图片描述


原创粉丝点击