View基础练习

参考:https://hencoder.com/ui-1-1/
练习github地址:https://github.com/fandazeng/ViewDemo

简单总结一下自定义View中的第一阶段(绘制)的关键点:

  • 自定义绘制的方式是重写绘制方法,其中最常用的是 onDraw()
  • 绘制的关键是 Canvas 的使用
    • Canvas 的绘制类方法: drawXXX() (关键参数:Paint)
    • Canvas 的辅助类方法: 范围裁切和几何变换
  • 可以重写不同的绘制方法来控制遮盖关系

自定义绘制知识分为四个级别

1. Canvas 的 drawXXX() 系列方法及Paint 最常见的使用

Canvas.drawXXX() 是自定义绘制最基本的操作,比如画圆、画矩形、画文本、画线等。组合绘制这些内容,再配合上 Paint 的一些常见方法来对内容的颜色和风格进行简单的设置,就能完成基本的绘制需求了。

2. Paint 完全使用

全面熟悉 Paint 的所有方法使用,能够绘制出更加丰富的内容,比如设置实心空心,线条粗细,特效、阴影等等。

3. Canvas 对绘制的辅助——范围裁切和几何变换

可以对绘制的范围进行任意的裁切以及进行几何的变换,来实现一些比较炫酷的绘制。

4. 重写不同的绘制方法来控制遮盖关系

不同的绘制方法,绘制的顺序是不一样,后面的绘制会覆盖前面的绘制内容。

下面将讲述四个级别中的第一个级别

Canvas 的 drawXXX() 系列方法及Paint 最常见的使用

1. 画背景,即颜色填充,在整个绘制区域涂上指定颜色,一般用来绘制底色或半透明蒙版

canvas.drawColor(Color.parseColor("#FFFF00"));
canvas.drawRGB(255,255,0);
canvas.drawARGB(255,255,255,0);

2. 画圆,前两个参数分别是圆心的X和Y坐标,坐标原点是左上角,往右和下是正数,第三个参数是半径(单位像素),第四个参数是画笔对象。

下述 Paint 的常用方法先忽略,后面会讲。

drawCircle(float centerX, float centerY, float radius, Paint paint)

mPaint.setAntiAlias(true);
//注意这行代码,如果不加,fragment切换的时候,绘画会混乱
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(450, 250, 230, mPaint);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(450, 790, 230, mPaint);

mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
canvas.drawCircle(1050, 250, 230, mPaint);

3. 画矩形,分别是左上右下对应的坐标,即左上角和右下角的坐标点。

drawRect(float left, float top, float right, float bottom, Paint paint)

RectF mRectF = new RectF();
Rect mRect = new Rect();
mRectF.set(400, 200, 1000, 800);
mRect.set(400, 200, 1000, 800);

canvas.drawRect(400, 200, 1000, 800, mPaint);
canvas.drawRect(mRectF, mPaint);
canvas.drawRect(mRect, mPaint);

4. 画点,前两个参数分别是点的X和Y坐标。

drawPoint(float x, float y, Paint paint)

//设置点的大小
mPaint.setStrokeWidth(150);

//设置点的形状,方头
mPaint.setStrokeCap(Paint.Cap.SQUARE);
canvas.drawPoint(1000, 400, mPaint);

//设置端点的形状,圆头
mPaint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawPoint(500, 400, mPaint);

5. 批量画点。

方式一
drawPoints(float[] pts, Paint paint)

方式二,offset是跳过的个数,count是总共要绘制的个数,注意
这个个数不是坐标个数(即不是每两个数组成一个),是X和Y坐标分别算一个数
drawPoints(float[] pts, int offset, int count, Paint paint)

float[] points = {0, 0, 50, 50, 50, 100, 100, 50, 100, 100, 150, 50, 150, 100};

//这将会画出7个点,每两个数是一个点的XY坐标
canvas.drawPoints(points,mPaint);

//跳过0,0这两个点,绘制(50, 50, 50, 100, 100, 50, 100, 100)这8个点
canvas.drawPoints(points, 2, 8, mPaint);

6. 画椭圆,只能绘制水平或竖直方向的椭圆,不能绘制斜的

left, top, right, bottom 是这个椭圆的左、上、右、下四个边界点的坐标
drawOval(float left, float top, float right, float bottom, Paint paint)

//即在矩形内绘制椭圆
RectF mRectF = new RectF();
mRectF.set(500, 400, 1000, 700);
canvas.drawOval(mRectF, mPaint);

//这个方法需要API21
canvas.drawOval(500, 400, 1000, 700, mPaint);

7. 画线,分别是线的起点和终点坐标

drawLine(float startX, float startY, float stopX, float stopY, Paint paint)

//设置线宽
mPaint.setStrokeWidth(15);
canvas.drawLine(500, 400, 1000, 800, mPaint);

8. 批量画线。

方式一
drawLines(float[] pts, Paint paint)

方式二,offset是跳过的个数,count是总共要绘制的个数,注意
这个个数不是坐标个数(即不是每两个数组成一个),是X和Y坐标分别算一个数
drawLines(float[] pts, int offset, int count, Paint paint)

float[] points = {0, 0, 50, 50, 50, 100, 100, 50, 100, 100, 150, 50, 150, 100};

//这将会画出7条线,每两个数是一个点的XY坐标,每四个点是一条线
canvas.drawLines(points,mPaint);

//跳过0,0这两个点,绘制(50, 50, 50, 100, 100, 50, 100, 100)这8个点,即两条线
canvas.drawLines(points, 2, 8, mPaint);

9. 画圆角矩形,rx和ry 是圆角的横向半径和纵向半径

drawRoundRect(float left, float top, float right, float bottom,
float rx, float ry, Paint paint)

RectF mRectF = new RectF();
mRectF.set(400, 200, 1000, 600);
canvas.drawRoundRect(mRectF,50,50,mPaint);

//这个方法需要API21
canvas.drawRoundRect(400, 200, 1000, 600, 50, 50, mPaint);

10. 画弧形或扇形

startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;
顺时针为正角度,逆时针为负角度),sweepAngle 是弧形划过的角度;useCenter
表示是否连接到圆心,如果不连接到圆心,就是弧形,
如果连接到圆心,就是扇形。

drawArc(float left, float top, float right, float bottom,
float startAngle, float sweepAngle, boolean useCenter, Paint paint)

mPaint.setStyle(Paint.Style.STROKE);

mRectF.set(500, 550, 800, 750);
canvas.drawArc(mRectF, -180, 90, false, mPaint);
canvas.drawArc(500, 550, 800, 750, -180, 90, false, mPaint);

mPaint.setStyle(Paint.Style.FILL);
mRectF.set(500, 600, 1000, 800);
canvas.drawArc(mRectF, 0, 180, false, mPaint);
canvas.drawArc(500, 600, 1000, 800, 0, 180, false, mPaint);

mRectF.set(700, 550, 1000, 750);
canvas.drawArc(mRectF, -125, 100, true, mPaint);
canvas.drawArc(700, 550, 1000, 750, -125, 100, true, mPaint);

11. Paint 常见用法

上面的示例代码有用到 Paint 的一些常见用法,这里先说明一下

// 设置画笔颜色
Paint.setColor(int color)

// 设置画笔风格,有3种风格,STROKE,FILL,FILL_AND_STROKE
Paint.setStyle(Paint.Style style)

// 设置线条的宽度,FILL风格无效
Paint.setStrokeWidth(float width)    

// 设置抗锯齿,如果不开启,会有毛边现象
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Paint.setAntiAlias(boolean aa)

// 设置文字的大小
Paint.setTextSize(textSize)


下面是示例代码:

//空心,即画线风格、勾边风格

mPaint.setStyle(Paint.Style.STROKE);

//实心,填充风格STROKE
mPaint.setStyle(Paint.Style.FILL);

//上面两种风格的结合
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);

12. 绘制 Bitmap 对象

drawBitmap(Bitmap bitmap, float left, float top, Paint paint)

绘制 Bitmap 对象,也就是把这个 Bitmap 中的像素内容贴过来。其中 left 和 top
是要把 bitmap 绘制到的位置坐标。

它的重载方法:

// 第一个Rect 代表要绘制的bitmap 区域,第二个 Rect 代表的是要将bitmap 绘制在屏幕的什么地方
drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) /
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) /
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)

// 示例代码
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
canvas.drawBitmap(bitmap, 200, 100, mPaint);

13. 绘制文字,text 是用来绘制的字符串,x 和 y 是绘制的起点坐标

drawText(String text, float x, float y, Paint paint)

canvas.drawText("海兰小天使",200,200,mPaint);    

14. 画自定义图形,path 参数是用来描述图形路径的对象

drawPath(Path path, Paint paint)

Path 方法第一类:直接描述路径

这一类方法还可以细分为两组:添加子图形和画线(直线或曲线)

第一组:addXxx() ——添加子图形

添加圆,参数 dir 是画圆的路径的方向
addCircle(float x, float y, float radius, Direction dir)

路径方向有两种:顺时针 (CW clockwise) 和逆时针 (CCW counter-clockwise) 。
对于普通情况,这个参数填 CW 还是填 CCW 没有影响。它只是在需要填充图形
(Paint.Style 为 FILL 或 FILL_AND_STROKE) ,并且图形出现自相交时,用于判断填充范围的。

// 这样就能画出一个圆了,跟直接用 drawCircle() 的效果是一样的,
一般是在绘制组合图形时才会用到的。
mPath.addCircle(300,300,200,Path.Direction.CW)
canvas.drawPath(mPath,mPaint);

其他的 Path.add-() 方法和这类似,例如:

// 添加椭圆
addOval(float left, float top, float right, float bottom, Direction dir) / addOval(RectF oval, Direction dir) 

// 添加矩形
addRect(float left, float top, float right, float bottom, Direction dir) / addRect(RectF rect, Direction dir) 

// 添加圆角矩形
addRoundRect(RectF rect, float rx, float ry, Direction dir) / 
addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir) /
addRoundRect(RectF rect, float[] radii, Direction dir) / addRoundRect(float left, float top,
float right, float bottom, float[] radii, Direction dir) 

// 添加另一个 Path
addPath(Path path) 
第二组:xxxTo() ——画线(直线或曲线)

画直线
lineTo(float x, float y) / rLineTo(float x, float y)

从当前位置向目标位置画一条直线, x 和 y 是目标位置的坐标。这两个方法的区别是,
lineTo(x, y) 的参数是绝对坐标,而 rLineTo(x, y) 的参数是相对当前位置的相对坐标
(前缀 r 指的就是 relatively 「相对地」)。

当前位置:所谓当前位置,即最后一次调用画 Path 的方法的终点位置。初始值为原点 (0, 0)

path.lineTo(100, 100); // 由当前位置 (0, 0) 向 (100, 100) 画一条直线  
path.rLineTo(100, 0); // 由当前位置 (100, 100) 向正右方 100 像素的位置画一条直线 

画二次贝塞尔曲线
quadTo(float x1, float y1, float x2, float y2) /
rQuadTo(float dx1, float dy1, float dx2, float dy2)

这条二次贝塞尔曲线的起点就是当前位置,而参数中的 x1, y1 和 x2, y2
则分别是控制点和终点的坐标。和 rLineTo(x, y) 同理,rQuadTo(dx1, dy1, dx2, dy2)
的参数也是相对坐标

画三次贝塞尔曲线
cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) /
rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

// 移动到目标位置,辅助来改变起点位置
moveTo(float x, float y) / rMoveTo(float x, float y) 

画弧形
arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) /
arcTo(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean forceMoveTo) / arcTo(RectF oval, float startAngle, float sweepAngle)

forceMoveTo 参数的意思是,绘制是要「抬一下笔移动过去」,还是「直接拖着笔过去」,区别在于是否留下移动的痕迹
(即画的圆弧起点跟当前起点不一致,是否强制直接从圆弧起点画,还是从当前起点先画线到圆弧起点,再画圆弧)

画弧形,其实就是 arcTo方法的简化版,forceMoveTo 默认为 true
addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) /
addArc(RectF oval, float startAngle, float sweepAngle)

//封闭当前子图形
close() 

它的作用是把当前的子图形封闭,即由当前位置向当前子图形的起点绘制一条直线
等价于从一开始的起点向最终的终点画了一条画,把图形封闭起来,Paint.Style 为 FILL 或 FILL_AND_STROKE),
Path 会自动封闭子图形

Path 方法第二类:辅助的设置或计算

// 设置填充方式,是用来设置图形自相交时的填充算法的
Path.setFillType(Path.FillType ft) 

FillType 的取值有四个:

  • EVEN_ODD
  • WINDING (默认值)
  • INVERSE_EVEN_ODD
  • INVERSE_WINDING
EVEN_ODD 原理

奇偶原则:对于平面中的任意一点,向任意方向射出一条射线,这条射线和图形相交的次数
(相交才算,相切不算哦)如果是奇数,则这个点被认为在图形内部,是要被涂色的区域;
如果是偶数,则这个点被认为在图形外部,是不被涂色的区域。

WINDING 原理

即 non-zero winding rule (非零环绕数原则),首先,它需要你图形中的所有线条都是有绘制方向的
然后,同样是从平面中的点向任意方向射出一条射线,但计算规则不一样:以 0 为初始值,对于射线和图形
的所有交点,遇到每个顺时针的交点(图形从射线的左边向右穿过)把结果加 1,遇到每个逆时针的交点
(图形从射线的右边向左穿过)把结果减 1,最终把所有的交点都算上,得到的结果如果不是 0,则认为这个
点在图形内部,是要被涂色的区域;如果是 0,则认为这个点在图形外部,是不被涂色的区域。

注意:如果你所有的图形都用相同的方向来绘制,那么 WINDING 确实是一个「全填充」的规则;
但如果使用不同的方向来绘制图形,结果就不一样了。

练习演示效果