攻略大全
1. 粘贴攻略
1.1 Canvas
1.1.1 简介
- 定义:画布,是一种绘制时的规则
是安卓平台2D图形绘制的基础
- 作用:规定绘制内容时的规则 & 内容
- 记住:绘制内容是根据画布的规定绘制在屏幕上的
- 理解为:画布只是绘制时的规则,但内容实际上是绘制在屏幕上的
1.1.2 本质
请务必记住:
-
绘制内容是根据画布(Canvas)的规定绘制在屏幕上的
-
画布(Canvas)只是绘制时的规则,但内容实际上是绘制在屏幕上的
-
代码实例
// 画一个矩形(蓝色)
canvas.drawRect(100,100,150,150,mPaint1);
// 将画布的原点移动到(400,,500)
canvas.translate(400,500);
// 再画一个矩形(红色)
canvas.drawRect(100,100,150,150,mPaint2);
- 效果图
- 效果流程图
1.1.3 对象的创建与获取
// 方法1
// 利用空构造方法直接创建对象
Canvas canvas = new Canvas();
// 方法2
// 通过传入装载画布Bitmap对象创建Canvas对象
// CBitmap上存储所有绘制在Canvas的信息
Canvas canvas = new Canvas(bitmap);
// 方法3
// 通过重写View.onDraw()创建Canvas对象
// 在该方法里可以获得这个View对应的Canvas对象
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//在这里获取Canvas对象
}
// 方法4
// 在SurfaceView里画图时创建Canvas对象
SurfaceView surfaceView = new SurfaceView(this);
// 从SurfaceView的surfaceHolder里锁定获取Canvas
SurfaceHolder surfaceHolder = surfaceView.getHolder();
//获取Canvas
Canvas c = surfaceHolder.lockCanvas();
// ...(进行Canvas操作)
// Canvas操作结束之后解锁并执行Canvas
surfaceHolder.unlockCanvasAndPost(c);
官方推荐方法4来创建并获取Canvas,原因:
- SurfaceView里有一条线程是专门用于画图,所以方法4的画图性能最好,并适用于高质量的、刷新频率高的图形
- 而方法3刷新频率低于方法4,但系统花销小,节省资源
1.1.4 绘制方法
1.2 Paint
- 定义:画笔
- 作用:确定绘制内容的具体效果(如颜色、大小等等)
在绘制内容时需要画笔Paint
/**
* 根据需求设置画笔的各种属性
*/
private void initPaint() {
// 设置最基本的属性
// 设置画笔颜色
// 可直接引入Color类,如Color.red等
mPaint.setColor(Color.RED);
// 设置画笔模式
// Style有3种类型:
// 类型1:Paint.Style.FILL_AND_STROKE(描边+填充)
// 类型2:Paint.Style.FILL(只填充不描边)
// 类型3:Paint.Style.STROKE(只描边不填充)
// 具体差别请看下图:
// 特别注意:前两种就相差一条边
// 若边细是看不出分别的;边粗就相当于加粗
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
// 设置画笔的粗细
mPaint.setStrokeWidth(14);
// 如设置画笔宽度为10px
mPaint.setStrokeWidth(10f);
// 不常设置的属性
// 得到画笔的颜色
mPaint.getColor();
// 设置Shader
// 即着色器,定义了图形的着色、外观
// 可以绘制出多彩的图形
mPaint.setShader(new Shader());
//设置画笔的a,r,p,g值
mPaint.setARGB(0, 1, 1, 1);
//设置透明度
mPaint.setAlpha(0);
//得到画笔的Alpha值
mPaint.getAlpha();
// 对字体进行设置(大小、颜色)
//设置字体大小
mPaint.setTextSize(14f);
// 设置对齐方式
// LEFT:左对齐
// CENTER:居中对齐
// RIGHT:右对齐
mPaint.setTextAlign(Paint.Align.CENTER);
//设置文本的下划线
mPaint.setUnderlineText(false);
//设置文本的删除线
mPaint.setStrikeThruText(false);
//设置文本粗体
mPaint.setFakeBoldText(false);
// 设置斜体
mPaint.setTextSkewX(-0.5f);
// 设置文字阴影
mPaint.setShadowLayer(5, 5, 5, Color.YELLOW);
}
1.3 Path
- 定义:路径,即无数个点连起来的线
- 作用:设置绘制的顺序 & 区域
Path只用于描述顺序 & 区域,单使用Path无法产生效果
- 应用场景:绘制复杂图形(如心形、五角星等等)
Path类封装了由直线和曲线(2、3次贝塞尔曲线)构成的几何路径。
1.3.1 开放路径与闭合路径的区别
1.3.2 如何判断点在图形内还是图形外
-
判断方法分为奇偶规则 & 非零环绕规则,具体介绍如下:
-
举例说明1:(奇偶规则)
由上图知:
-
p1发出的射线与图形相交1个点,即奇数点,所以P1点在图形内
-
p2发出的射线与图形相交2个点,即偶数点,所以P2点在图形内
-
举例说明2:(非零环绕数规则)
从上面方法分析到,任何图形都是由点连成线组成的,是具备方向的,看下图:(矩形是顺时针)
-
p1发出的射线与图形相交1个点,矩形的右侧线从左边射到右边,环绕数-1,最终环绕数为-1,故p1在图形内部。
-
p2发出的射线与图形相交2个点:矩形的右侧边从左边射到右边
环绕数-1;矩形的下侧边从右边射到左边,环绕数+1,最终环绕数为0.故p2在图形外部
1.4 自定义Veiw分类
1.4.1 继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
1.4.2 继承ViewGroup派生特殊的Layout
这种方法主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
1.4.3 继承特定的View(比如TextView)
这种方法比较常见,一般是用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持wrap_content和padding等。
1.4.4 继承特定的ViewGroup(比如LinearLayout)
这种方法也比较常见,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。
1.5 自定义View须知
1.5.1 让View支持wrap_content
这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。
1.5.2 如果有必要,让你的View支持padding
这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。
1.5.3 尽量不要在View中使用HandIer,没必要
这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,除非你很明确地要使用Handler来发送消息。
1.5.4 View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
1.5.5 View带有滑动嵌套情形时,需要处理好滑动冲突
如果有滑动冲突的话,那么要合适地处理滑动冲突,否则将会严重影响View的效果。
2. 造火箭攻略
3. 拧螺丝攻略
3.1 Canvas
3.1.1 绘制方法
3.1.1.1 绘制颜色
将颜色填充整个画布,常用于绘制底色。
// 传入一个Color类的常量参数来设置画布颜色
// 绘制蓝色
canvas.drawColor(Color.BLUE);
3.1.1.2 绘制基本图形
3.1.1.2.1 绘制点(drawPoint)
- 原理:在某个坐标处绘制点
可画一个点或一组点(多个点)
// 特别注意:需要用到画笔Paint
// 所以之前记得创建画笔
// 为了区分,这里使用了两个不同颜色的画笔
// 描绘一个点
// 在坐标(200,200)处
canvas.drawPoint(300, 300, mPaint1);
// 绘制一组点,坐标位置由float数组指定
// 此处画了3个点,位置分别是:(600,500)、(600,600)、(600,700)
canvas.drawPoints(new float[]{600,500,600,600,600,700},mPaint2);
3.1.1.2.2 绘制直线(drawLine)
- 原理:两点(初始点 & 结束点)确定一条直线
// 画一条直线
// 在坐标(100,200),(700,200)之间绘制一条直线
canvas.drawLine(100,200,700,200,mPaint1);
// 绘制一组线
// 在坐标(400,500),(500,500)之间绘制直线1
// 在坐标(400,600),(500,600)之间绘制直线2
canvas.drawLines(new float[]{400,500,500,500,400,600,500,600},mPaint2);
3.1.1.2.3 绘制矩形(drawRect)
- 原理:矩形的对角线顶点确定一个矩形
一般是采用左上角和右下角的两个点的坐标。
// 关于绘制矩形,Canvas提供了三种重载方法
// 方法1:直接传入两个顶点的坐标
// 两个顶点坐标分别是:(100,100),(800,400)
canvas.drawRect(100,100,800,400,mPaint);
// 方法2:将两个顶点坐标封装为Rect
Rect rect = new Rect(100,100,800,400);
canvas.drawRect(rect,mPaint);
// 方法3:将两个顶点坐标封装为RectF
RectF rectF = new RectF(100,100,800,400);
canvas.drawRect(rectF,mPaint);
// 特别注意:Rect类和RectF类的区别
// 精度不同:Rect = int & RectF = float
// 三种方法画出来的效果是一样的。
3.1.1.2.4 绘制圆角矩形(drawRoundRect)
- 原理:矩形的对角线顶点确定一个矩形
类似于绘制矩形
// 方法1:直接传入两个顶点的坐标
// API21时才可使用
// 第5、6个参数:rx、ry是圆角的参数
canvas.drawRoundRect(100,100,800,400,30,30,mPaint);
// 方法2:使用RectF类
RectF rectF = new RectF(100,100,800,400);
canvas.drawRoundRect(rectF,30,30,mPaint);
-
与矩形相比,圆角矩形多了两个参数rx 和 ry
-
圆角矩形的角是椭圆的圆弧,rx 和 ry实际上是椭圆的两个半径,如下图:
-
特别注意:当 rx大于宽度的一半, ry大于高度一半 时,画出来的为椭圆
实际上,在rx为宽度的一半,ry为高度的一半时,刚好是一个椭圆;但由于当rx大于宽度一半,ry大于高度一半时,无法计算出圆弧,所以drawRoundRect对大于该数值的参数进行了修正,凡是大于一半的参数均按照一半来处理
3.1.1.2.5 绘制椭圆(drawOval)
- 原理:矩形的对角线顶点确定矩形,根据传入矩形的长宽作为长轴和短轴画椭圆
- 椭圆传入的参数和矩形是一样的;
- 绘制椭圆实际上是绘制一个矩形的内切图形。
// 方法1:使用RectF类
RectF rectF = new RectF(100,100,800,400);
canvas.drawOval(rectF,mPaint);
// 方法2:直接传入与矩形相关的参数
canvas.drawOval(100,100,800,400,mPaint);
3.1.1.2.6 绘制圆(drawCircle)
- 原理:圆心坐标+半径决定圆
// 参数说明:
// 1、2:圆心坐标
// 3:半径
// 4:画笔
// 绘制一个圆心坐标在(500,500),半径为400 的圆。
canvas.drawCircle(500,500,400,mPaint);
3.1.1.2.7 绘制圆弧(drawArc)
- 原理:通过圆弧角度的起始位置和扫过的角度确定圆弧
// 绘制圆弧共有两个方法
// 相比于绘制椭圆,绘制圆弧多了三个参数:
startAngle (确定角度的起始位置)
sweepAngle (确定扫过的角度)
useCenter (是否使用中心)
// 方法1
public void drawArc(@NonNull RectF oval, float startAngle,
float sweepAngle, boolean useCenter,
@NonNull Paint paint){
}
// 方法2
public void drawArc(float left, float top,
float right, float bottom,
float startAngle,float sweepAngle,
boolean useCenter, @NonNull Paint paint) {
}
- 不使用中心点:圆弧的形状 = (起、止点连线+圆弧)构成的面积
- 使用中心点:圆弧面积 = (起点、圆心连线 + 止点、圆心连线+圆弧)构成的面积
类似扇形
3.1.1.3 绘制文字
绘制文字分为三种应用场景:
- 情况1:指定文本开始的位置
- 即指定文本基线位置
- 基线x默认在字符串左侧,基线y默认在字符串下方
- 情况2:指定每个文字的位置
- 情况3:指定路径,并根据路径绘制文字
3.1.1.3.1 情况1:指定文本开始的位置
// 参数text:要绘制的文本
// 参数x,y:指定文本开始的位置(坐标)
// 参数paint:设置的画笔属性
public void drawText (String text, float x, float y, Paint paint)
// 实例
canvas.drawText("abcdefg",300,400,mPaint1);
// 仅绘制文本的一部分
// 参数start,end:指定绘制文本的位置
// 位置以下标标识,由0开始
public void drawText (String text, int start, int end, float x, float y, Paint paint)
public void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
// 对于字符数组char[]
// 截取文本使用起始位置(index)和长度(count)
public void drawText (char[] text, int index, int count, float x, float y, Paint paint)
// 实例:绘制从位置1-3的文本
canvas.drawText("abcdefg",1,4,300,400,mPaint1);
// 字符数组情况
// 字符数组(要绘制的内容)
char[] chars = "abcdefg".toCharArray();
// 参数为 (字符数组 起始坐标 截取长度 基线x 基线y 画笔)
canvas.drawText(chars,1,3,200,500,textPaint);
3.1.1.3.2 情况2:分别指定文本的位置
// 参数text:绘制的文本
// 参数pos:数组类型,存放每个字符的位置(坐标)
// 注意:必须指定所有字符位置
public void drawPosText (String text, float[] pos, Paint paint)
// 对于字符数组char[],可以截取部分文本进行绘制
// 截取文本使用起始位置(index)和长度(count)
public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)
// 特别注意:
// 1. 在字符数量较多时,使用会导致卡顿
// 2. 不支持emoji等特殊字符,不支持字形组合与分解
// 实例
canvas.drawPosText("abcde", new float[]{
100, 100, // 第一个字符位置
200, 200, // 第二个字符位置
300, 300, // ...
400, 400,
500, 500
},
mPaint1);
// 数组情况(绘制部分文本)
char[] chars = "abcdefg".toCharArray();
canvas.drawPosText(chars, 1, 3, new float[]{
300, 300, // 指定的第一个字符位置
400, 400, // 指定的第二个字符位置
500, 500, // 指定的第三个字符位置
},
mPaint1);
3.1.1.3.3 情况3:指定路径,并根据路径绘制文字
// 1.创建路径对象
Path path = new Path();
// 2. 设置路径轨迹
path.cubicTo(540, 750, 640, 450, 840, 600);
// 3. 画路径
canvas.drawPath(path, mPaint);
// 4. 画出在路径上的字
// 可设置文字沿路径绘制的起始位置,并定位文本的路径上方 (-) 或下方 (+) 的距离
canvas.drawTextOnPath("情况3:指定路径,并根据路径绘制文字", path, 0, -50, mPaint);
3.1.1.4 绘制图片
绘制图片分为:绘制矢量图(drawPicture)和 绘制位图(drawBitmap)
3.1.1.4.1 绘制矢量图(drawPicture)
- 作用:绘制矢量图的内容,即绘制存储在矢量图里某个时刻Canvas绘制内容的操作
矢量图(Picture)的作用:存储(录制)某个时刻Canvas绘制内容的操作
- 应用场景:绘制之前绘制过的内容
- 相比于再次调用各种绘图API,使用Picture能节省操作 & 时间
- 如果不手动调用,录制的内容不会显示在屏幕上,只是存储起来
特别注意:使用绘制矢量图时前请关闭硬件加速,以免引起不必要的问题!
Picture mPicture = new Picture();
// 获取宽度
mPicture.getWidth();
// 获取高度
mPicture.getHeight ();
// 开始录制
// 将Canvas中所有的绘制内容存储到Picture中
// 返回一个Canvas
Canvas pictureCanvas = mPicture.beginRecording(500,500);
// 一系列操作...
// 结束录制
mPicture.endRecording();
// 将Picture里的内容绘制到Canvas中
mPicture.draw(canvas);
// 还有两种方法可以将Picture里的内容绘制到Canvas中
// 方法2:Canvas.drawPicture()
// 方法3:将Picture包装成为PictureDrawable,使用PictureDrawable的draw方法绘制。
3.1.1.4.2 绘制位图(drawBitmap)
- 作用:将已有的图片转换为位图(Bitmap),最后再绘制到Canvas上
位图,即平时我们使用的图片资源
获取Bitmap对象的方式
要绘制Bitmap,就要先获取一个Bitmap对象,具体获取方式如下:
特别注意:绘制位图(Bitmap)是读取已有的图片转换为Bitmap,最后再绘制到Canvas。
所以:
- 对于第1种方式:排除
- 对于第2种方式:虽然满足需求,但一般不推荐使用
具体请自行了解关于Drawble的内容
- 对于第3种方式:满足需求,下面会着重讲解
通过BitmapFactory获取Bitmap (从不同位置获取):
// 共3个位置:资源文件、内存卡、网络
Bitmap bitmap = null;
// 位置1:资源文件(drawable/mipmap/raw)
bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.amount_unit);
// 位置2:资源文件(assets)
try {
InputStream is = mContext.getAssets().open("bitmap.png");
bitmap = BitmapFactory.decodeStream(is);
is.close();
} catch (IOException e) {
e.printStackTrace();
}
// 位置3:内存卡文件
bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");
// 位置4:网络文件:
// 省略了获取网络输入流的代码
bitmap = BitmapFactory.decodeStream(is);
is.close();
绘制Bitmap
// 方法1
// 后两个参数matrix, paint是在绘制时对图片进行一些改变
// 后面会专门说matrix
// 如果只是将图片内容绘制出来只需将传入新建的matrix, paint对象即可
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
// 方法2
// 参数 left、top指定了图片左上角的坐标(距离坐标原点的距离)
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
// 方法3
// 参数(src,dst) = 两个矩形区域
// Rect src:指定需要绘制图片的区域(即要绘制图片的哪一部分)
// Rect dst 或RectF dst:指定图片在屏幕上显示(绘制)的区域
// 特别注意的是:如果src规定绘制图片的区域大于dst指定显示的区域的话,
// 那么图片的大小会被缩放
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
绘制Bitmap方法3的应用场景:
-
便于素材管理
当我需要画很多个图时,如果1张图=1个素材的话,那么管理起来很不方便;如果素材都放在一个图,那么按需绘制会便于管理。
-
实现动态效果
动态效果 = 逐渐绘制图形部分,如下:
在绘制时,只需要一个资源文件,然后逐渐描绘就可以:
3.1.1.5 绘制路径
// 通过传入具体路径Path对象 & 画笔
canvas.drawPath(mPath, mPaint)
3.1.2 画布操作
- 作用:改变画布的性质
改变之后,任何的后续操作都会受到影响
3.1.2.1 画布变化
3.1.2.1.1 平移(translate)
- 作用:移动画布(实际上是移动坐标系,如下图)
- 具体使用
// 将画布原点向右移200px,向下移100px
canvas.translate(200, 100)
// 注:位移是基于当前位置移动,而不是每次都是基于屏幕左上角的(0,0)点移动
3.1.2.1.2 缩放(scale)
- 作用:放大 / 缩小 画布的倍数
- 具体使用:
// 共有两个方法
// 方法1
// 在x方向缩放sx倍,在y方向缩放sy倍
// 缩放中心默认为(0,0)
public final void scale(float sx, float sy)
// 方法2
// 比方法1多了两个参数(px,py),用于控制缩放中心位置
// 缩放中心为(px,py)
public final void scale (float sx, float sy, float px, float py)
缩放的本质是:把形状先画到画布,然后再缩小/放大。所以当放大倍数很大时,会有明显锯齿。
当缩放倍数为负数时,会先进行缩放,然后根据不同情况进行图形翻转:
(设缩放倍数为(a,b),旋转中心为(px,py)):
- a<0,b>0:以px为轴翻转
- a>0,b<0:以py为轴翻转
- a<0,b<0:以旋转中心翻转
具体如下图:(缩放倍数为1.5,旋转中心为(0,0)为例)
3.1.2.1.3 旋转(rotate)
注意:角度增加方向为顺时针(区别于数学坐标系)
// 方法1
// 以原点(0,0)为中心旋转 degrees 度
public final void rotate(float degrees)
// 方法2
// 以(px,py)点为中心旋转degrees度
public final void rotate(float degrees, float px, float py)
3.1.2.1.4 错切(skew)
- 作用:将画布在x方向倾斜a角度、在y方向倾斜b角度
- 具体使用:
// 实例
// 为了方便观察,我将坐标系移到屏幕中央
canvas.translate(300, 500);
// 初始矩形
canvas.drawRect(20, 20, 400, 200, mPaint2);
// 向X正方向倾斜45度
canvas.skew(1f, 0);
canvas.drawRect(20, 20, 400, 200, mPaint1);
//向X负方向倾斜45度
canvas.skew(-1f, 0);
canvas.drawRect(20, 20, 400, 200, mPaint1);
// 向Y正方向倾斜45度
canvas.skew(0, 1f);
canvas.drawRect(20, 20, 400, 200, mPaint1);
// 向Y负方向倾斜45度
canvas.skew(0, -1f);
canvas.drawRect(20, 20, 400, 200, mPaint1);
3.1.2.2 画布裁剪
即从画布上裁剪一块区域,之后仅能编辑该区域
特别注意:其余的区域只是不能编辑,但是并没有消失,如下图
// 裁剪共分为:裁剪路径、裁剪矩形、裁剪区域
// 裁剪路径
// 方法1
public boolean clipPath(@NonNull Path path)
// 方法2
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)
// 裁剪矩形
// 方法1
public boolean clipRect(int left, int top, int right, int bottom)
// 方法2
public boolean clipRect(float left, float top, float right, float bottom)
// 方法3
public boolean clipRect(float left, float top, float right, float bottom,
@NonNull Region.Op op)
// 裁剪区域
// 方法1
public boolean clipRegion(@NonNull Region region)
// 方法2
public boolean clipRegion(@NonNull Region region, @NonNull Region.Op op)
这里特别说明一下参数Region.Op op
作用:在剪下多个区域下来的情况,当这些区域有重叠的时候,这个参数决定重叠部分该如何处理,多次裁剪之后究竟获得了哪个区域,有以下几种参数:
3.1.2.3 画布快照
- 画布状态:当前画布经过的一系列操作
- 状态栈:存放画布状态和图层的栈(后进先出)
- 画布的构成:由多个图层构成,如下图
- 在画布上操作 = 在图层上操作
- 如无设置,绘制操作和画布操作是默认在默认图层上进行
- 在通常情况下,使用默认图层就可满足需求;若需要绘制复杂的内容(如地图),则需使用更多的图层
- 最终显示的结果 = 所有图层叠在一起的效果
3.1.2.3.1 保存当前画布状态(save)
- 作用:保存画布状态(即保存画布的一系列操作)
- 应用场景:画布的操作是不可逆的,而且会影响后续的步骤,假如需要回到之前画布的状态去进行下一次操作,就需要对画布的状态进行保存和回滚
// 方法1:
// 保存全部状态
public int save ()
// 方法2:
// 根据saveFlags参数保存一部分状态
// 使用该参数可以只保存一部分状态,更加灵活
public int save (int saveFlags)
// saveFlags参数说明:
// 1.ALL_SAVE_FLAG(默认):保存全部状态
// 2. CLIP_SAVE_FLAG:保存剪辑区
// 3. CLIP_TO_LAYER_SAVE_FLAG:剪裁区作为图层保存
// 4. FULL_COLOR_LAYER_SAVE_FLAG:保存图层的全部色彩通道
// 5. HAS_ALPHA_LAYER_SAVE_FLAG:保存图层的alpha(不透明度)通道
// 6. MATRIX_SAVE_FLAG:保存Matrix信息(translate, rotate, scale, skew)
// 每调用一次save(),都会在栈顶添加一条状态信息(入栈)
3.1.2.3.2 保存某个图层状态(saveLayer)
- 作用:新建一个图层,并放入特定的栈中
- 具体使用
使用起来非常复杂,因为图层之间叠加会导致计算量成倍增长,营尽量避免使用。
// 无图层alpha(不透明度)通道
public int saveLayer (RectF bounds, Paint paint)
public int saveLayer (RectF bounds, Paint paint, int saveFlags)
public int saveLayer (float left, float top, float right, float bottom, Paint paint)
public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)
// 有图层alpha(不透明度)通道
public int saveLayerAlpha (RectF bounds, int alpha)
public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)
3.1.2.3.3 回滚上一次保存的状态(restore)
- 作用:恢复上一次保存的画布状态
- 具体使用
// 采取状态栈的形式。即从栈顶取出一个状态进行恢复
canvas.restore();
3.1.2.3.4 回滚指定保存的状态(restoreToCount)
- 作用:恢复指定状态;将指定位置以及以上所有状态出栈
- 具体使用:
canvas.restoreToCount(3);
// 弹出 3、4、5的状态,并恢复第3次保存的画布状态
3.1.2.3.5 获取保存的次数(getSaveCount)
- 作用:获取保存过图层的次数
即获取状态栈中保存状态的数量
canvas.getSaveCount();
// 以上面栈为例,则返回5
// 注:即使弹出所有的状态,返回值依旧为1,代表默认状态。(返回值最小为1)
3.1.2.3.6 总结
对于画布状态的保存和回滚的套路,一般如下:
// 步骤1:保存当前状态
// 把Canvas的当前状态信息入栈
save();
// 步骤2:对画布进行各种操作(旋转、平移Blabla)
...
// 步骤3:回滚到之前的画布状态
// 把栈里面的信息出栈,取代当前的Canvas信息
restore();
3.2 Path
// 使用Path首先要new一个Path对象
// Path的起点默认为坐标为(0,0)
Path path = new Path();
// 特别注意:建全局Path对象,在onDraw()按需修改;尽量不要在onDraw()方法里new对象
// 原因:若View频繁刷新,就会频繁创建对象,拖慢刷新速度。
3.2.1 设置路径
// 设置当前点位置
// 后面的路径会从该点开始画
moveTo(float x, float y);
// 当前点(上次操作结束的点)会连接该点
// 如果没有进行过操作则默认点为坐标原点。
lineTo(float x, float y);
// 闭合路径,即将当前点和起点连在一起
// 注:如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么也不做
close();
- 可使用setLastPoint()设置当前点位置(代替moveTo())
- 二者区别:
3.2.2 关于重置路径
- 重置Path有两个方法:reset()和rewind()
- 两者区别在于:
方法 | 是否保留FillType设置 | 是否保留原有数据结构 |
---|---|---|
Path.reset() | 是 | 否 |
Path.rewind() | 否 | 是 |
- FillType影响显示效果;数据结构影响重建速度
- 所以一般选择Path.reset()
3.2.3 添加路径
3.2.3.1 添加基本图形
- 作用:在Path路径中添加基本图形
如圆形路径、圆弧路径等等
- 具体使用
// 添加圆弧
// 方法1
public void addArc (RectF oval, float startAngle, float sweepAngle)
// startAngle:确定角度的起始位置
// sweepAngle : 确定扫过的角度
// 方法2
// 与上面方法唯一不同的是:如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点
public void arcTo (RectF oval, float startAngle, float sweepAngle)
// 方法3
// 参数forceMoveTo:是否将之前路径的结束点设置为圆弧起点
// true:在新的起点画圆弧,不连接最后一个点与圆弧起点,即与之前路径没有交集(同addArc())
// false:在新的起点画圆弧,但会连接之前路径的结束点与圆弧起点,即与之前路径有交集(同arcTo(3参数))
public void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
// 下面会详细说明
// 加入圆形路径
// 起点:x轴正方向的0度
// 其中参数dir:指定绘制时是顺时针还是逆时针:CW为顺时针, CCW为逆时针
// 路径起点变为圆在X轴正方向最大的点
addCircle(float x, float y, float radius, Path.Direction dir)
// 加入椭圆形路径
// 其中,参数oval作为椭圆的外切矩形区域
addOval(RectF oval, Path.Direction dir)
// 加入矩形路径
// 路径起点变为矩形的左上角顶点
addRect(RectF rect, Path.Direction dir)
//加入圆角矩形路径
addRoundRect(RectF rect, float rx, float ry, Path.Direction dir)
// 注:添加图形路径后会改变路径的起点
主要说一下dir这个参数:
dir = Direction = 图形的方向,为枚举类型:
- CW:clockwise,顺时针
- CCW:counter-clockwise,逆时针
图形的方向影响的是:
- 添加图形时确定闭合顺序(各个点的记录顺序)
- 图形的渲染结果(是判断图形渲染的重要条件)
图形绘制的本质:先画点,再将点连接起来。所以,点与点之间是存在一个先后顺序的;顺时针和逆时针用于确定这些点的顺序。
3.2.3.2 添加路径
- 作用:合并路径
即将路径1加到路径2里
- 具体使用
// 方法1
public void addPath (Path src)
// 方法2
// 先将src进行(x,y)位移之后再添加到当前path
public void addPath (Path src, float dx, float dy)
// 方法3
// 先将src进行Matrix变换再添加到当前path
public void addPath (Path src, Matrix matrix)
// 实例:合并矩形路径和圆形路径
// 为了方便观察,平移坐标系
canvas.translate(350, 500);
// 创建路径的对象
Path pathRect = new Path();
Path pathCircle = new Path();
// 画一个矩形路径
pathRect.addRect(-200, -200, 200, 200, Path.Direction.CW);
// 画一个圆形路径
pathCircle.addCircle(0, 0, 100, Path.Direction.CW);
// 将圆形路径移动(0,200),再添加到矩形路径里
pathRect.addPath(pathCircle, 0, 200);
// 绘制合并后的路径
canvas.drawPath(pathRect,mPaint1);
3.2.3.3 判断路径属性
// 判断path中是否包含内容
public boolean isEmpty ()
// 例子:
Path path = new Path();
path.isEmpty(); //返回false
path.lineTo(100,100); // 返回true
// 判断path是否是一个矩形
// 如果是一个矩形的话,会将矩形的信息存放进参数rect中。
public boolean isRect (RectF rect)
// 实例
path.lineTo(0,400);
path.lineTo(400,400);
path.lineTo(400,0);
path.lineTo(0,0);
RectF rect = new RectF();
boolean b = path.isRect(rect); // b返回ture,
// rect存放矩形参数,具体如下:
// rect.left = 0
// rect.top = 0
// rect.right = 400
// rect.bottom = 400
// 将新的路径替代现有路径
public void set (Path src)
// 实例
// 设置一矩形路径
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
// 设置一圆形路径
Path src = new Path();
src.addCircle(0,0,100, Path.Direction.CW);
// 将圆形路径代替矩形路径
path.set(src);
// 绘制图形
canvas.drawPath(path,mPaint);
// 平移路径
// 与Canvas.translate ()平移画布类似
// 方法1
// 参数x,y:平移位置
public void offset (float dx, float dy)
// 方法2
// 参数dst:存储平移后的路径状态,但不影响当前path
// 可通过dst参数绘制存储的路径
public void offset (float dx, float dy, Path dst)
// 为了方便观察,平移坐标系
canvas.translate(350, 500);
// path中添加一个圆形(圆心在坐标原点)
path = new Path();
path.addCircle(0, 0, 100, Path.Direction.CW);
// 平移路径并存储平移后的状态
Path dst = new Path();
path.offset(400, 0, dst); // 平移
canvas.drawPath(path, mPaint1); // 绘制path
// 通过dst绘制平移后的图形(红色)
mPaint1.setColor(Color.RED);
canvas.drawPath(dst,mPaint1);
3.2.3.4 设置路径填充颜色
- 在Android中,有四种填充模式,具体如下
均封装在Path类中
填充模式 | 介绍 |
---|---|
EVEN_ODD | 奇偶规则 |
INVERSE_EVEN_ODD | 反奇偶规则 |
WINDING | 非零环绕数规则 |
INVERSE_WINDING | 反非零环绕数规则 |
// 设置填充规则
path.setFillType()
// 可填规则
// 1. EVEN_ODD:奇偶规则
// 2. INVERSE_EVEN_ODD:反奇偶规则
// 3. WINDING :非零环绕数规则
// 4. INVERSE_WINDING:反非零环绕数规则
// 理解奇偶规则和反奇偶规则:填充效果相反
// 举例:对于一个矩形而言,使用奇偶规则会填充矩形内部,而使用反奇偶规则会填充矩形外部(下面会举例说明)
// 获取当前填充规则
path.getFillType()
// 判断是否是反向(INVERSE)规则
path.isInverseFillType()
// 切换填充规则(即原有规则与反向规则之间相互切换)
path.toggleInverseFillType()
实例1:(奇偶规则)
// 为了方便观察,平移坐标系
canvas.translate(350, 500);
// 在Path中添加一个矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
// 设置Path填充模式为 奇偶规则
path.setFillType(Path.FillType.EVEN_ODD);
// 反奇偶规则
// path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
// 画出路径
canvas.drawPath(path, mPaint1);
3.2.3.5 布尔操作
- 作用:两个路径Path之间的运算
- 应用场景:用简单的图形通过特定规则合成相对复杂的图形。
- 具体使用
boolean op (Path path, Path.Op op)
// 举例
// 对 path1 和 path2 执行布尔运算,运算方式由第二个参数指定`
// 运算结果存入到path1中。
path1.op(path2, Path.Op.DIFFERENCE);
// 方法2
boolean op (Path path1, Path path2, Path.Op op)
// 举例
// 对 path1 和 path2 执行布尔运算,运算方式由第三个参数指定
// 运算结果存入到path3中。
path3.op(path1, path2, Path.Op.DIFFERENCE)
之间的运算方式(即Path.Op参数)如下