End

自定义 View 绘制基础 HenCoder-1 [MD]

博文地址

我的GitHub 我的博客 我的微信 我的邮箱
baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

目录

扔物线自定义 View 系列教程总结-1

全文整理自 扔物线(HenCoder)自定义 View 系列文章

重新整理的目标:

  • 内容压缩:去除活跃气氛的段子、图片,去除无意义的解释、代码,去除不刚兴趣的内容,压缩比至少 50%
  • 排版优化:更清晰的结构,更精简的标题,更规范的缩进、标点符号、代码格式,好的结构才能更好的吸收
  • MarkDown:以标准的 MarkDown 格式重新编排,纯文本更易迭代维护

扔物线自定义 View 系列教程分绘制布局触摸反馈三部分内容。

绘制基础

自定义绘制概述

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

自定义绘制知识的四个级别

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

Canvas.drawXXX() 是自定义绘制最基本的操作。掌握了这些方法,你才知道怎么绘制内容,例如怎么画圆、怎么画方、怎么画图像和文字。组合绘制这些内容,再配合上 Paint 的一些常见方法来对绘制内容的颜色和风格进行简单的配置,就能够应付大部分的绘制需求了。

2、Paint 的完全攻略

Paint 可以做的事,不只是设置颜色,也不只是我在视频里讲的实心空心、线条粗细、有没有阴影,它可以做的风格设置真的是非常多、非常细。例如:拐角要什么形状?开不开双线性过滤?加不加特效?

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

范围裁切和几何变换都是用于辅助的,大多数时候,它们并不会被用到,但一旦用到,通常都是很炫酷的效果。

4、使用不同的绘制方法来控制绘制顺序

控制绘制顺序解决的并不是「做不到」的问题,而是性能问题。同样的一种效果,你不用绘制顺序的控制往往也能做到,但需要用多个 View 甚至是多层 View 才能拼凑出来,因此代价是 UI 的性能;而使用绘制顺序的控制的话,一个 View 就全部搞定了。

一切的开始:onDraw()

自定义绘制的上手非常容易:提前创建好 Paint 对象,重写 onDraw(),把绘制代码写在 onDraw() 里面,就是自定义绘制最基本的实现。大概就像这样:

Paint paint = new Paint();
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawCircle(300, 300, 200, paint); // 绘制一个圆
}

Canvas.drawXXX() 和 Paint 基础

drawXXX() 系列方法和 Paint 的基础掌握了,就能够应付简单的绘制需求。它们主要包括:

  • Canvas 类下的所有 draw- 打头的方法,例如 drawCircle() drawBitmap()
  • Paint 类的几个最常用的方法:
    • Paint.setStyle(Style style) 设置绘制模式
    • Paint.setColor(int color) 设置颜色
    • Paint.setStrokeWidth(float width) 设置线条宽度
    • Paint.setTextSize(float textSize) 设置文字大小
    • Paint.setAntiAlias(boolean aa) 设置抗锯齿开关

Google 的官方文档:CanvasPaint

drawColor 颜色填充

drawColor(@ColorInt int color)
drawRGB(int r, int g, int b)
drawARGB(int a, int r, int g, int b)

canvas.drawColor(Color.BLACK);
canvas.drawColor(Color.parse("#88880000");
canvas.drawRGB(100, 200, 100);
canvas.drawARGB(100, 100, 200, 100);

这类颜色填充方法一般用于在绘制之前设置底色,或者在绘制之后为界面设置半透明蒙版

drawCircle 画圆

drawCircle(float centerX, float centerY, float radius, Paint paint) //圆心坐标、半径

canvas.drawCircle(300, 300, 200, paint);

View 的坐标系

在 Android 里,每个 View 都有一个自己的坐标系,彼此之间是不影响的。这个坐标系的原点是 View 左上角的那个点;水平方向是 x 轴,右正左负;竖直方向是 y 轴,下正上负。

所以一个 View 的坐标 (x, y) 处,指的就是相对它的左上角那个点的水平方向 x 像素、竖直方向 y 像素的点。也就是说,canvas.drawCircle(300, 300, 200, paint) 这行代码绘制出的圆,在 View 中的位置和尺寸应该是这样的:

Paint.setColor 画笔颜色

Paint.setColor(int color)Paint 最常用的方法之一,用来设置绘制内容的颜色。

Paint.setStyle 画笔样式

而如果你想画的不是实心圆,而是空心圆(或者叫环形),也可以使用 paint.setStyle(Paint.Style.STROKE) 来把绘制模式改为画线模式。

setStyle(Style style) 这个方法设置的是绘制的 Style,具体来说有三种:

  • FILL 填充模式,默认值
  • STROKE 画线模式(即勾边模式)
  • FILL_AND_STROKE 两种模式一并使用:既画线又填充

Paint.setStrokeWidth 线条宽度

STROKEFILL_AND_STROKE 下,还可以使用 paint.setStrokeWidth(float width) 来设置线条的宽度。

Paint.setAntiAlias 抗锯齿

在绘制的时候,往往需要开启抗锯齿来让图形和文字的边缘更加平滑。

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); //方式一
paint.setAntiAlias(boolean aa); //方式二

抗锯齿并不一定适合所有场景。

drawRect 画矩形

drawRect(float left, float top, float right, float bottom, Paint paint) //四条边的坐标
drawRect(RectF rect, Paint paint)
drawRect(Rect rect, Paint paint)

paint.setStyle(Style.STROKE);
canvas.drawRect(700, 100, 1100, 500, paint);

drawPoint 画点

点的大小可以通过 paint.setStrokeWidth(width) 来设置,点的形状可以通过 paint.setStrokeCap(cap) 来设置。

注: Paint.setStrokeCap(cap) 可以设置点的形状,但这个方法并不是专门用来设置点的形状的,而是一个设置线条端点形状的方法。端点有圆头(ROUND)、平头(BUTT) 和方头(SQUARE) 三种。

drawPoint(float x, float y, Paint paint) //坐标

paint.setStrokeWidth(20);
paint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawPoint(50, 50, paint);

drawPoints 批量画点

它和 drawPoint() 的区别是可以画多个点。

drawPoints(float[] pts, Paint paint) //pts 表示坐标的数组,每两个成一对
drawPoints(float[] pts, int offset, int count, Paint paint) //跳过数组的前 offset 个,一共要绘制 count 个点

float[] points = {0, 0, 50, 50, 50, 100, 100, 50, 100, 100, 150, 50, 150, 100};
canvas.drawPoints(points, 2, 8, paint);
//跳过2个数,一共绘制 8 个数(4 个点):(50, 50) (50, 100) (100, 50) (100, 100)

drawOval 画椭圆

只能绘制横着的或者竖着的椭圆,不能绘制斜的。

drawOval(float left, float top, float right, float bottom, Paint paint) //左上右下四个边界点的坐标
drawOval(RectF rect, Paint paint)

canvas.drawOval(50, 50, 350, 200, paint);

drawLine 画线

由于直线不是封闭图形,所以 setStyle(style) 对直线没有影响。

drawLine(float startX, float startY, float stopX, float stopY, Paint paint) //起点和终点坐标

canvas.drawLine(200, 200, 800, 500, paint);

drawLines 批量画线

drawLines(float[] pts, int offset, int count, Paint paint)
drawLines(float[] pts, Paint paint)

float[] points = {20, 20, 120, 20, 70, 20, 70, 120, 20, 120, 120, 120, 150, 20, 250, 20, 150, 20, 150, 120, 250, 20, 250, 120, 150, 120, 250, 120};
canvas.drawLines(points, paint);

drawRoundRect 画圆角矩形

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) //坐标、横纵半径
drawRoundRect(RectF rect, float rx, float ry, Paint paint)

canvas.drawRoundRect(100, 100, 500, 300, 50, 50, paint);

drawArc 画弧形或扇形

drawArc() 是使用一个椭圆来描述弧形的

  • left, top, right, bottom 描述的是这个弧形所在的椭圆
  • startAngle 是弧形的起始角度,注意:即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度
  • sweepAngle 是弧形划过的角度
  • useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)

paint.setStyle(Paint.Style.FILL); // 填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint); // 绘制扇形
canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint); // 绘制弧形

paint.setStyle(Paint.Style.STROKE); // 画线模式
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint); // 绘制不封口的弧形

drawPath 画自定义图形

前面的这些方法,都是绘制某个给定的图形,而 drawPath() 可以绘制自定义图形。当你要绘制的图形比较特殊,使用前面的那些方法做不到的时候,就可以使用 drawPath() 来绘制。

drawPath(path) 这个方法是通过描述路径的方式来绘制图形的,它的 path 参数就是用来描述图形路径的对象。

drawPath(Path path, Paint paint)

path.addArc(200, 200, 400, 400, -225, 225);
path.arcTo(400, 200, 600, 400, -180, 225, false);
path.lineTo(400, 542);
canvas.drawPath(path, paint); // 绘制出 path 描述的图形(心形)

drawBitmap 画 Bitmap

绘制 Bitmap 对象,也就是把这个 Bitmap 中的像素内容贴过来。

drawBitmap(Bitmap bitmap, float left, float top, Paint paint) //源 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)

drawBitmap(bitmap, 200, 100, paint);

它还有一个兄弟方法 drawBitmapMesh(),可以绘制具有网格拉伸效果的 Bitmap,使用场景较少,不多介绍。

drawText 绘制文字

界面里所有的显示内容,都是绘制出来的,包括文字。

drawText(String text, float x, float y, Paint paint) //起点坐标

canvas.drawText(text, 200, 100, paint);

Paint.setTextSize 文字大小

通过 Paint.setTextSize(textSize),可以设置文字的大小。

文字的绘制具有极高的定制性,不过由于它的定制性实在太高了,所以会在后面专门用一期来讲文字的绘制。

Path 描述图形路径

Path 可以描述直线、二次曲线、三次曲线、圆、椭圆、弧形、矩形、圆角矩形。把这些图形结合起来,就可以描述出很多复杂的图形。

Path 的方法可以分为两类:

  • 第一类是 直接描述路径 的方法,这类方法还可以细分为两组:
    • 添加子图形 的方法:addXxx()
    • 添加线条 的方法:xxxTo()
  • 第二类是 辅助的设置或计算 的方法,这类方法的使用场景比较少,只需了解一下 setFillType 即可

Path.setFillType 设置填充方式

Path.setFillType(Path.FillType ft)

它是用来设置图形自相交时的填充算法的,FillType 的取值有四个:

  • EVEN_ODD:全填充
  • WINDING:交叉填充,默认值
  • INVERSE_EVEN_ODD:EVEN_ODD 的反色版本
  • INVERSE_WINDING:WINDING 的反色版本

EVEN_ODDWINDING 的原理有点复杂,用的也不多,不过多介绍了。

添加子图形 addXxx

  • addPath 添加另一个 Path
  • addCircle 添加圆
  • addOval 添加椭圆
  • addRect 添加矩形
  • addRoundRect 添加圆角矩形
addPath(Path path)
addCircle(float x, float y, float radius, Direction dir)
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.addCircle(300, 300, 200, Path.Direction.CW); //新增一个圆
canvas.drawPath(path, paint); //和直接使用 canvas.drawCircle 的效果是一样的

Direction 路径方向

Direction 用于指定路径的方向:顺时针(CW clockwise) 和逆时针(CCW counter-clockwise)。

对于普通情况,这个参数填 CW 还是填 CCW 没有影响,它只是在 需要填充图形 (Paint.StyleFILLFILL_AND_STROKE) ,并且 图形出现自相交 时,用于 判断填充范围 的。

想用哪种方式来填充,都可以由你来决定。具体怎么做,下面在讲 Path.setFillType() 的时候会详细介绍,而在这里你可以先忽略 dir 这个参数。

添加线 xxxTo

这一组和第一组 addXxx() 方法的区别在于:第一组是添加完整封闭的子图形(除了addPath()),而这一组添加的只是一条线

lineTo 画直线

lineTo(float x, float y) //从当前位置向目标位置(绝对坐标)画一条直线
rLineTo(float x, float y) //从当前位置向目标位置(相对当前位置的相对坐标)画一条直线

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

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

quadTo 画二次贝塞尔曲线

quadTo(float x1, float y1, float x2, float y2) //控制点和终点的坐标
rQuadTo(float dx1, float dy1, float dx2, float dy2)

贝塞尔曲线:贝塞尔曲线是几何上的一种曲线。它通过起点、控制点和终点来描述一条曲线,主要用于计算机图形学。概念总是说着容易听着难,总之使用它可以绘制很多圆润又好看的图形,但要把它熟练掌握、灵活使用却是不容易的。不过还好的是,一般情况下,贝塞尔曲线并没有什么用处,只在少数场景下绘制一些特殊图形的时候才会用到。

cubicTo 画三次贝塞尔曲线

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 移动到目标位置

不论是直线还是贝塞尔曲线,都是以当前位置作为起点,而不能指定起点。但你可以通过 moveTo(x, y)rMoveTo() 来改变当前位置,从而间接地设置这些方法的起点。

moveTo(float x, float y)
rMoveTo(float x, float y)

paint.setStyle(Style.STROKE);
path.lineTo(100, 100); // 画斜线
path.moveTo(200, 100); // 移动
path.lineTo(200, 0); // 画竖线

moveTo(x, y) 虽然不添加图形,但它会设置图形的起点,所以它是非常重要的一个辅助方法。

arcTo 画弧形

这个方法和 Canvas.drawArc() 比起来:

  • 少了一个参数 useCenter:因为 arcTo() 只用来画弧形而不画扇形,所以不再需要 useCenter 参数
  • 多了一个参数 forceMoveTo:参数的意思是,绘制是要「抬一下笔移动过去」,还是「直接拖着笔过去」,区别在于是否留下移动的痕迹

arcTo()addArc() 并不使用当前位置作为弧线的起点

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

paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, true); //true 或 false 的效果如下

true:强制移动到弧形起点(无痕迹)

false:直接连线连到弧形起点(有痕迹)

addArc 画弧形

addArc() 只是一个直接使用了 forceMoveTo = true 的简化版 arcTo()

addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)
addArc(RectF oval, float startAngle, float sweepAngle)

close 封闭当前子图形

它的作用是把当前的子图形封闭,即由当前位置当前子图形的起点绘制一条直线。它其实等价于 path.lineTo(当前子图形的起点),所以也将其归为第二组的画线 xxxTo()方法。

paint.setStyle(Style.STROKE);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
path.close(); // 使用 close() 封闭子图形,等价于 path.lineTo(100, 100)

子图形 contour

官方文档里叫做 contour「轮廓」。

前面说到,第一组方法是「添加子图形」,所谓「子图形」,指的就是一次不间断的连线。一个 Path 可以包含多个子图形。

当使用第一组方法的时候,每一次方法调用都是新增了一个独立的子图形;而如果使用第二组方法的时候,则是每一次断线(即每一次「抬笔」),都标志着一个子图形的结束,以及一个新的子图形的开始。

另外,不是所有的子图形都需要使用 close() 来封闭。当 需要填充图形时(即 Paint.StyleFILLFILL_AND_STROKEPath 会自动封闭子图形。

paint.setStyle(Style.FILL);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
// 这里只绘制了两条边,但由于 Style 是 FILL ,所以绘制时会自动封口

2021-4-22

posted @ 2021-04-22 00:57  白乾涛  阅读(295)  评论(1编辑  收藏  举报