自定义View--(4)Path/Canvas

基础

1. Paint

确定绘制内容的具体效果(如颜色、大小等等)。
步骤:

  1. 创建一个画笔对象;
  2. 画笔设置,即设置绘制内容的具体效果(如颜色、大小等等);
  3. 初始化画笔(尽量选择在View的构造函数)。
        // 可直接引入Color类,如Color.red等
        mPaint.setColor(int color); 
        // 设置画笔模式
        // Style有3种类型:
        // 类型1:Paint.Style.FILLANDSTROKE(描边+填充)
        // 类型2:Paint.Style.FILL(只填充不描边)
        // 类型3:Paint.Style.STROKE(只描边不填充)
         mPaint.setStyle(Style style); 
        //设置画笔的粗细
        mPaint.setStrokeWidth(float width) 
        // 设置Shader
        // 即着色器,定义了图形的着色、外观
        // 可以绘制出多彩的图形
        // 具体请参考文章:http://blog.csdn.net/iispring/article/details/50500106
        mPaint.setShader(Shader shader)  
		// 设置斜体
        mPaint.setTextSkewX(-0.5f);
        // 设置文字阴影
        mPaint.setShadowLayer(5,5,5,Color.YELLOW);

2. 关闭硬件加速

在Android4.0的设备上,在打开硬件加速的情况下,使用自定义View可能会出现问题。

// 在AndroidMenifest.xml的application节点添加
android:hardwareAccelerated="false"

Path

Path类的最全面详解 - 自定义View应用系列

1. 设置路径

采用moveTo()setLastPoint()lineTo()close()组合。
默认起点为(0, 0)。

// Path
// 设置当前点位置
// 后面的路径会从该点开始画
moveTo(float x, float y)// 当前点(上次操作结束的点)会连接该点
// 如果没有进行过操作则默认点为坐标原点。
lineTo(float x, float y)// 闭合路径,即将当前点和起点连在一起
// 注:如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么也不做
close()

可使用 #setLastPoint 设置当前点位置(代替 #moveTo),两者区别:

类型是否影响起点是否影响之前操作
moveTo()
setLastPoint()

重置路径
重置Path有两个方法:reset()rewind(),两者区别在于:

方法说明是否保留FillType设置是否保留原有数据结构
Path.reset()清空path对象数据和轨迹,保留FillType
Path.rewind()清空所绘制的轨迹,但保留对象内部数据,方便下次快速复用,不保留FillType

注:FillType影响显示效果;数据结构影响重建速度。所以一般选择Path.reset()。

2. 添加路径

采用addXxx()arcTo()组合。

Direction = 画图的方向,为枚举类型:
CW:clockwise,顺时针;
CCW:counter-clockwise,逆时针。

// 为了方便观察,平移坐标系
canvas.translate(350, 500);
// 顺时针
path.addRect(0, 0, 400, 400, Path.Direction.CW);
// 逆时针
// path.addRect(0,0,400,400, Path.Direction.CCW);
canvas.drawPath(path,mPaint1);

加入图形路径后会影响路径的起点,起点改变原则:新画图形在x轴正方向的最后一个坐标。

这里着重说明:添加圆弧路径(addArc/arcTo)。

// Path
 // addArc
// 直接添加一个圆弧到path中
//  startAngle:确定角度的起始位置
//  sweepAngle : 确定扫过的角度
    public void addArc (RectF oval, float startAngle, float sweepAngle)

    // arcTo
    // 方法1
    // 同样是添加一个圆弧到path
    // 与上面方法唯一不同的是:如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点
    public void arcTo (RectF oval, float startAngle, float sweepAngle)
   
   // 方法2
   // 参数forceMoveTo:If true, always begin a new contour with the arc
   // 是否将圆弧起点设置为path的新起点(可以使用path.close()来验证起点)
   // true:圆弧起点作为path的新起点,不连接最后一个点与圆弧起点,即与之前路径没有交集(同addArc())
  // false:圆弧起点不作为path的新起点,且会连接之前路径的结束点与圆弧起点,即与之前路径有交集(同arcTo(3参数))
    public void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)

3. 判断路径属性

采用isEmpty()isRect()isConvex()set()offset()组合。

// Path
// 判断path中是否包含内容
 public boolean isEmpty ()
 // 判断path是否是一个矩形
// 如果是一个矩形的话,会将矩形的信息存放进参数rect中。
public boolean isRect (RectF rect)
// 将新的路径替代现有路径
 public void set (Path src)

// 平移路径,与Canvas.translate() 平移画布类似
//  平移方法1
// 参数dx,dy:平移距离
public void offset (float dx, float dy)

//  平移方法2
// 参数dst:存储平移后的路径状态,但不影响当前path
// 可通过dst参数绘制存储的路径
public void offset (float dx, float dy, Path dst)

4. 设置路径填充颜色

在Android中,有四种填充模式(均封装在Path类中),具体如下:

填充模式介绍
EVEN_ODD奇偶规则
INVERSE_EVEN_ODD反奇偶规则
WINDING非零环绕数规则
INVERSE_WINDING反非零环绕数规则

图形是存在方向的(画图 = 连接点成的线 = 有连接顺序)。

5. 布尔操作

作用:两个路径Path之间的运算。
应用场景:用简单的图形通过特定规则合成相对复杂的图形。

// 方法1
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参数)如下:
在这里插入图片描述

6. 贝赛尔曲线

一句话概括贝塞尔曲线:将任意一条曲线转化为精确的数学公式。
贝塞尔曲线开发的艺术

// 绘制二阶贝塞尔曲线
//  (x1,y1)为控制点,(x2,y2)为终点
quadTo(float x1, float y1, float x2, float y2)
//  (x1,y1)为控制点距离起点的偏移量,(x2,y2)为终点距离起点的偏移量
rQuadTo(float x1, float y1, float x2, float y2)

// 绘制三阶贝塞尔曲线
// (x1,y1),(x2,y2)为控制点,(x3,y3)为终点
cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
// (x1,y1),(x2,y2)为控制点距离起点的偏移量,(x3,y3)为终点距离起点的偏移量
rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

Canvas

1. 简介

  • 定义:画布,是安卓平台2D图形绘制的基础,是一种绘制时的规则。
  • 作用:规定绘制内容时的规则 & 内容。
  1. 记住: 绘制内容是根据画布的规定绘制在屏幕上的
  2. 理解为:画布只是绘制时的规则,但内容实际上是绘制在屏幕上的

2.本质

示例:

// 画一个矩形(蓝色)
canvas.drawRect(100, 100, 150, 150, mPaint1);
// 将画布的原点移动到(400,500)
canvas.translate(400,500);
// 再画一个矩形(红色)
canvas.drawRect(100, 100, 150, 150, mPaint2);

canvas
总结:

  1. 内容实际上是绘制在屏幕上;
  2. 画布,即Canvas,只是规定了绘制内容时的规则;
  3. 内容的位置由坐标决定,而坐标是相对于画布而言的。

关于对画布的操作(缩放、旋转和错切)原理都是相同的。

3. 使用

Canvas对象的获取,4中方法:

// 方法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);
}

// 方法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刷新频率低于方法3,但系统花销小,节省资源。

canvas绘制方法

3.1 绘制圆角矩形

// 方法1:直接传入两个顶点的坐标
// API21时才可使用
// 第5、6个参数:rx、ry是圆角的参数
// 圆角矩形的角是椭圆的圆弧,rx 和 ry实际上是椭圆的两个半径
public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint)

// 方法2:使用RectF类
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)

特别注意:当 rx大于宽度的一半, ry大于高度一半 时,画出来的为椭圆。
实际上,在rx为宽度的一半,ry为高度的一半时,刚好是一个椭圆;但由于当rx大于宽度一半,ry大于高度一半时,无法计算出圆弧,所以drawRoundRect对大于该数值的参数进行了修正,凡是大于一半的参数均按照一半来处理。

3.2 绘制圆弧

// 绘制圆弧共有两个方法
// 相比于绘制椭圆,绘制圆弧多了三个参数:
startAngle  // 确定角度的起始位置
sweepAngle // 确定扫过的角度
useCenter   // 是否使用中心:true,连接矩形中心及弧;false,(Paint.Style.FILL时)连接弧的起点终点

// 方法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.3 绘制文字

// 参数x,y:指定文本开始的位置(坐标),baseline的左下角坐标
public void drawText (String text, float x, float y, Paint paint)

// 参数pos:数组类型,存放每个字符的位置(坐标)
// 注意:必须指定所有字符位置
public void drawPosText (String text, float[] pos, Paint paint)

// 指定路径,并根据路径绘制文字
public void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset, float vOffset, @NonNull Paint paint)

3.4 绘制图片

绘制图片分为:绘制矢量图(drawPicture)和 绘制位图(drawBitmap)。

3.4.1 绘制矢量图(drawPicture)

绘制矢量图的内容,即绘制存储在矢量图里某个时刻Canvas绘制内容的操作。
矢量图(Picture)的作用:存储(录制)某个时刻Canvas绘制内容的操作。

应用场景:绘制之前绘制过的内容
相比于再次调用各种绘图API,使用Picture能节省操作 & 时间;
如果不手动调用,录制的内容不会显示在屏幕上,只是存储起来。

// 步骤1:创建Picture对象
Picture mPicture = new Picture();

// 步骤2:开始录制 
mPicture.beginRecording(int width, int height);

// 步骤3:绘制内容 or 操作Canvas
canvas.drawCircle(500,500,400,mPaint);
// ...(一系列操作)

// 步骤4:结束录制
mPicture.endRecording();

// 步骤5:某个时刻将存储在Picture的绘制内容绘制出来
// 有三种方法绘制picture
mPicture.draw(Canvas canvas);

三种绘制picture的方法

方法是否对Canvas状态(clip,matrix)有影响对绘制结果可控程度
Picture.draw()
Canvas.drawPicture()
PictureDrawable.draw()
// PictureDrawable绘制picture
// 将Picture包装成为Drawable
PictureDrawable drawable = new PictureDrawable(mPicture);

// 设置在画布上的绘制区域(类似drawPicture (Picture picture, Rect dst)的Rect dst参数)
// 每次都从Picture的左上角开始绘制
// 并非根据该区域进行缩放,也不是剪裁Picture。

// 实例1:将录制的内容显示(区域刚好布满图形)
drawable.setBounds(0, 0,mPicture.getWidth(), mPicture.getHeight());
drawable.draw(canvas);

// 实例2:将录制的内容显示在当前画布上(区域小于图形)
// 从Picture的左上角开始绘制, 并非根据该区域进行缩放,也不是剪裁Picture
drawable.setBounds(0, 0,250, mPicture.getHeight());
3.4.2 绘制位图(drawBitmap)

作用:将已有的图片转换为位图(Bitmap),最后再绘制到Canvas上。
位图,即平时我们使用的图片资源。

public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)
// 参数 left、top指定了图片左上角的坐标(距离坐标原点的距离)
public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
// 参数(src,dst) = 两个矩形区域
// Rect src:指定需要绘制图片的区域(即要绘制图片的哪一部分)
// Rect dst 或RectF dst:指定图片在屏幕上显示(绘制)的区域
public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)

3.5 画布操作

3.5.1 画布变换
// 1. 平移
// 注:位移是基于当前位置移动,而不是每次都是基于屏幕左上角的(0,0)点移动
public void translate(float dx, float dy)

// 2. 缩放
// 缩放的本质:把形状先画到画布,然后再缩小/放大。所以当放大倍数很大时,会有明显锯齿。
// 以(0,0)为中心,在x方向缩放sx倍,在y方向缩放sy倍
public final void scale(float sx, float sy)     
// 以(px,py)为中心,在x方向缩放sx倍,在y方向缩放sy倍
 public final void scale (float sx, float sy, float px, float py)

// 3. 旋转
// 以原点(0,0)为中心旋转 degrees 度
public final void rotate(float degrees)  
// 以(px,py)点为中心旋转degrees度
public final void rotate(float degrees, float px, float py)  

// 4.  错切
// 参数 sx = tan a ,sx>0时表示向X正方向倾斜(即向左)
// 参数 sy = tan b ,sy>0时表示向Y正方向倾斜(即向下)
public void skew(float sx, float sy)   
3.5.2 画布裁剪

从画布上裁剪一块区域,之后仅能编辑该区域。
特别注意:其余的区域只是不能编辑,但是并没有消失。

// 裁剪共分为:裁剪路径、裁剪矩形、裁剪区域
// 裁剪路径
public boolean clipPath(@NonNull Path path)
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)

// 裁剪矩形
public boolean clipRect(int left, int top, int right, int bottom)
public boolean clipRect(float left, float top, float right, float bottom)
public boolean clipRect(float left, float top, float right, float bottom,
            @NonNull Region.Op op) 

// 裁剪区域
public boolean clipRegion(@NonNull Region region)
public boolean clipRegion(@NonNull Region region, @NonNull Region.Op op)
3.5.3 画布快照

画布状态:当前画布经过的一系列操作。
状态栈:存放画布状态和图层的栈(后进先出)。
画布的构成:由多个图层构成。
canvas栈

关于画布/图层:

1. 在画布上操作 = 在图层上操作;
2. 如无设置,绘制操作和画布操作是默认在默认图层上进行;
3. 在通常情况下,使用默认图层就可满足需求;若需要绘制复杂的内容(如地图),则需使用更多的图层;
4. 最终显示的结果 = 所有图层叠在一起的效果。

保存当前画布状态(save)
作用:保存画布状态(即保存画布的一系列操作)。
应用场景:画布的操作是不可逆的,而且会影响后续的步骤,假如需要回到之前画布的状态去进行下一次操作,就需要对画布的状态进行保存和回滚。

保存某个图层状态(saveLayer)
作用:新建一个图层,并放入特定的栈中。
使用起来非常复杂,因为图层之间叠加会导致计算量成倍增长,尽量避免使用。

回滚上一次保存的状态(restore)
作用:恢复上一次保存的画布状态。

3.5.4 总结

画布状态的保存和回滚的套路,一般如下:

// 步骤1:保存当前状态
//  把Canvas的当前状态信息入栈
save();     

// 步骤2:对画布进行各种操作(旋转、平移Blabla)
...      

// 步骤3:回滚到之前的画布状态
// 把栈里面的信息出栈,取代当前的Canvas信息
restore();  

参考:
Canvas类的最全面详解 - 自定义View应用系列

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值