专治花里胡哨(四)征服自定义View,Canvas 各种详细全面的使用方法

好记性不如烂博客,古人诚不欺我。


看本篇文章前,请先阅读 专治花里胡哨(三)征服自定义 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_ODDWINDING 的效果应该是这样的:
    在这里插入图片描述



二、画布裁剪

画布裁剪很简单,就两个方法,一正一反,一个正向裁剪,一个反向裁剪。

//切割
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)的状态。

本人水平有限,如有错误,请在下方评论中指出。


请帮顶 / 评论点赞!因为你们的赞同/鼓励是我写作的最大动力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值