好记性不如烂博客,古人诚不欺我。
看本篇文章前,请先阅读 专治花里胡哨(三)征服自定义 View,画笔 Paint 的各种详细全面的使用方法。
作为 花里胡哨系列的第四篇,今天就来聊一聊 Canvas 的各种操作,看完可能会让你眼前一亮,原来画布还要有这么多骚操作。最简单的各种基本图形的绘制就不多说了,如果不知道的请看专治花里胡哨(二)征服自定义View,各种最基本的drawXXX()方法你都会了吗?
下面就来说一说其它的画布的操作,主要就是 path路径 、 画布的裁剪 和 几何变换 这三类。
一、path 路径
canvas 的各种 drawXXX() 方法,只能绘制一些基本的图形,例如矩形、圆等,但是对于复杂的图形,例如五角星这个就绘制不出来,所以需要通过 path 这个神器去完成特殊图形的绘制。
1.1 直接描述路径
1.1.1 添加子图形
通过一系列的 mPath.addXXX() 方法绘制最基本的几何图形,例如圆,椭圆,矩形等,例如下面的代码:
// 圆形
public void addCircle (float x, float y, float radius, Path.Direction dir)
// 椭圆
public void addOval (RectF oval, Path.Direction dir)
// 矩形
public void addRect (float left, float top, float right, float bottom, Path.Direction dir)
public void addRect (RectF rect, Path.Direction dir)
// 圆角矩形
public void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
public void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)
从代码中可以看出与原本的 canvas.addXXX() 方法的使用基本一样,唯一的区别就是多了一个 Path.Direction dir 的参数。这个参数的意思是你要顺时针绘制,还是逆时针绘制,Path.Direction.CW 是顺时针绘制,Path.Direction.CCW 是逆时针绘制。
1.1.2 画线
还是老规矩,先来一张整体的图,体验一下:

下面贴出上面绘图给出的代码,代码中都给了详细的注释,应该都能看的懂:
public class PathView extends View {
private Paint mPaint;
public PathView(Context context) {
this(context, null);
}
public PathView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Path mPath = new Path();
//移动到(100,100)
mPath.moveTo(100,100);
//从(100,100)画线到(200,200),未做过移动操作,默认坐标原点
mPath.lineTo(200,200);
canvas.drawPath(mPath,mPaint);
//设置最后一个点为(400,100),即连接(100,100)和(400,100)
mPath.setLastPoint(400,100);
//连接(400,100)和(300,200)
mPath.lineTo(300,200);
//以(300,200)点为原点,即相对该点,画线到(300,300)
mPath.rLineTo(0,300);
/**
* close的作用是封闭路径,与连接当前最后一个点和第一个点并不等价。
* 如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么也不做。
*/
//mPath.close();
canvas.drawPath(mPath,mPaint);
/**
* addArc 表示添加一个圆弧到path
*/
Path mPath1 = new Path();
mPath1.moveTo(200,1200);
mPath1.lineTo(200,900);
RectF rectF = new RectF(100,600,400,800);
mPath1.addArc(rectF,-180,240); //和下面一句话等价
//mPath1.arcTo(oval,-180,240,true);
canvas.drawPath(mPath1,mPaint);
/**
* arcTo 添加一个圆弧到path
* 最后一个参数forceMoveTo
* 如果为true,表示将最后一个点移动到圆弧起点,即不连接最后一个点与圆弧起点
* 如果为false,不移动,而是连接最后一个点与圆弧起点
*/
Path mPath2 = new Path();
mPath2.moveTo(200,1800);
mPath2.lineTo(200,1600);
RectF rectF1 = new RectF(100,1300,400,1600);
mPath2.arcTo(rectF1,40,-240);//与下面一句话等价
//mPath2.arcTo(rectF1,40,-240,false);
canvas.drawPath(mPath2,mPaint);
}
}
1.2 相交图形填充模式
Path.setFillType(fillType) 是用来设置图形自相交时的填充算法的方法,一般实际场景中使用较少,有个印象即可,给张图来说一让你了解它大概是用来做什么的:

FillType 的取值有四个,先来看前两个:
EVEN_ODD(奇偶原则):对于平面中的任意一点,向任意方向射出一条射线,这条射线和图形相交的次数(相交才算,相切不算哦)如果是奇数,则这个点被认为在图形内部,是要被涂色的区域;如果是偶数,则这个点被认为在图形外部,是不被涂色的区域。还以左右相交的双圆为例,给个图:

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

完整版的EVEN_ODD和WINDING的效果应该是这样的:

二、画布裁剪
画布裁剪很简单,就两个方法,一正一反,一个正向裁剪,一个反向裁剪。
//切割
canvas.clipRect(200,200,700,700);//画布被裁剪
canvas.drawCircle(100,100,100,mPaint);//坐标超出裁剪区域
canvas.drawCircle(300,300,100,mPaint);//坐标区域在裁剪范围内,绘制成功
//此方法需要API >=26
canvas.clipOutRect(200,200,700,700);
canvas.drawCircle(100,100,100,mPaint);//坐标区域在裁剪范围内,绘制成功
canvas.drawCircle(300,300,100,mPaint);//坐标超出裁剪区域,无法绘制
除了这两个方法,其实也可以根据路径 Path 去切割,也就是 clipPath() 以及 clipOutPath() 这两个方法,在此就不详细介绍了。
三、几何变换
canvas 的几何变换大致分为三类:
- 使用
Canvas来做常见的二维变换。 - 使用
Matrix来做常见和不常见的二维变换。 - 使用
Camera来做三维变换。
3.1 Canvas 做常见的二维变换
先来介绍第一类,也是最简单的一类,这一类有 4 种变换:平移、旋转、缩放、错切。
1.1 平移(Translate)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0,0, 400, 400, mPaint);
canvas.translate(50, 50);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0, 400, 400, mPaint);
canvas.drawLine(0, 0, 600,600, mPaint);
}
效果图:

1.2 缩放(Scale)
canvas.drawRect(200,200, 700,700, mPaint);
canvas.scale(0.5f, 0.5f);
mPaint.setColor(Color.GRAY);
canvas.drawRect(200,200, 700,700, mPaint);
效果图如下:

1.3 旋转(Rotate)
canvas.translate(50,50);
canvas.drawRect(0,0, 700,700, mPaint);
canvas.rotate(45);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0, 700,700, mPaint);
效果如图所示:

围绕某一个点旋转:
canvas.drawRect(400, 400, 900, 900, mPaint);
canvas.rotate(45, 650, 650); //px, py表示旋转中心的坐标
mPaint.setColor(Color.GRAY);
canvas.drawRect(400, 400, 900, 900, mPaint);
效果图:

1.4 错切(Skew)
canvas.drawRect(0,0, 400, 400, mPaint);
canvas.skew(0, 1); //在y方向倾斜45度, X轴顺时针旋转45
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, 400, 400, mPaint);
效果图:

canvas.drawRect(0,0, 400, 400, mPaint);
canvas.skew(1, 0); //在X方向倾斜45度,Y轴逆时针旋转45
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, 400, 400, mPaint);
效果图:

3.2 Matrix 做常见的二维变换
这部分其实和 Canvas 使用效果是差不多的,直接就上代码了:
public class MatrixView extends View {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Bitmap bitmap;
Point point1 = new Point(200, 200);
Point point2 = new Point(600, 200);
Matrix matrix = new Matrix();
public MatrixView(Context context) {
super(context);
}
public MatrixView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MatrixView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
{
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.maps);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//平移
canvas.save();
matrix.reset();
matrix.postTranslate(-100, -100);
canvas.concat(matrix);
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
//缩放
canvas.save();
matrix.reset();
matrix.postScale(1.3f, 1.3f, point1.x + bitmapWidth / 2, point1.y + bitmapHeight / 2);
canvas.concat(matrix);
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
//旋转
canvas.save();
matrix.reset();
matrix.postRotate(180, point1.x + bitmapWidth / 2, point1.y + bitmapHeight / 2);
canvas.concat(matrix);
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
//斜切
canvas.save();
matrix.reset();
matrix.postSkew(0, 0.5f, point1.x + bitmapWidth / 2, point1.y + bitmapHeight / 2);
canvas.concat(matrix);
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
}
}
3.3 Camera 做常见的三维变换
3.1 Camera.rotate*() 三维旋转
public class Sample11CameraRotateView extends View {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Bitmap bitmap;
Point point1 = new Point(200, 100);
Point point2 = new Point(600, 200);
Camera camera = new Camera();
Matrix matrix = new Matrix();
public Sample11CameraRotateView(Context context) {
super(context);
}
public Sample11CameraRotateView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public Sample11CameraRotateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
{
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.maps);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//旋转X轴
canvas.save();
camera.save();//保存Camera状态
camera.rotateX(30);//旋转Camera的三维空间
camera.applyToCanvas(canvas);//把旋转投影到 Canvas
camera.restore();// 恢复 Camera 的状态
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
//旋转轴
canvas.save();
camera.save();
camera.rotateY(30);
camera.applyToCanvas(canvas);
camera.restore();
canvas.drawBitmap(bitmap, point2.x, point2.y, paint);
canvas.restore();
//旋转轴
canvas.save();
camera.save();
camera.rotateZ(30);
camera.applyToCanvas(canvas);
camera.restore();
canvas.drawBitmap(bitmap, point2.x, point2.y, paint);
canvas.restore();
//旋转X,Y,Z轴
canvas.save();
camera.save();
camera.rotate(30,30,30);
camera.applyToCanvas(canvas);
camera.restore();
canvas.drawBitmap(bitmap, point2.x, point2.y, paint);
canvas.restore();
//如果你需要图形左右对称,需要配合上 Canvas.translate(),
//在三维旋转之前把绘制内容的中心点移动到原点,即旋转的轴心,
//然后在三维旋转后再把投影移动回来
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
int center1X = point1.x + bitmapWidth / 2;
int center1Y = point1.y + bitmapHeight / 2;
camera.save();
matrix.reset();
camera.rotateX(30);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-center1X, -center1Y);
matrix.postTranslate(center1X, center1Y);
canvas.save();
canvas.concat(matrix);
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
}
}
3.2 设置相机虚拟位置
在 Camera 中,相机的默认位置是 (0, 0, -8)(英寸)。8 x 72 = 576,所以它的默认位置是 (0, 0, -576)(像素)。
如果绘制的内容过大,当它翻转起来的时候,就有可能出现图像投影过大的「糊脸」效果。而且由于换算单位被写死成了 72 像素,而不是和设备 dpi 相关的,所以在像素越大的手机上,这种「糊脸」效果会越明显。
而使用 setLocation() 方法来把相机往后移动,就可以修复这种问题。
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
float newZ = - displayMetrics.density * 6;
camera.setLocation(0, 0, newZ);
最后
在接下来的文章中,我会继续以简单通俗的方式给你带来自定义 View 的方方面面,毕竟,我们的目的是在产品和设计面前,腰杆子挺直,面对花里胡哨的效果,有种云淡风轻(zhuang bi)的状态。
4666

被折叠的 条评论
为什么被折叠?



