Canvas常用方法解析第二篇

作者: 小芸论 | 来源:发表于2019-09-29 19:46 被阅读0次

    Canvas常用方法解析第一篇中分析了Canvas的drawBitmapMesh方法和drawText方法,接下来我们继续分析其他的常用方法。

    1 预备知识

    1.1 Canvas的坐标系和View的坐标系

    当View没有通过scrollBy或者scrollTo滑动时,View的坐标系和其对应的Canvas的坐标系是相同的,即View的左上角为坐标系原点、向下为Y轴正方向、向右为X轴正方向;
    当View没有通过scrollBy(dx, dy)或者scrollTo(getScrollX() + dx, getScrollY() + dy)滑动时,Canvas的坐标系原点就会在横向平移dy(dy > 0时向左平移,否者向右平移)、纵向平移dx(dx > 0时向上平移,否者向下平移);
    注意: 无论View有没有滑动(即Canvas坐标系如何变化),MotionEvent的getX()方法获取的值永远是触摸点到View左边的距离,getY()方法获取的值永远是触摸点到View上边的距离,做过图片编辑功能的小伙伴应该对此深有体会
    举个例子验证我的理论:

    public class TestCanvasView extends View {
        private GestureDetector mGDetector;
    
        private Path path;
        private Paint paint;
    
        private Bitmap bitmap;
        private Rect srcRect;
        private Rect destRect;
    
        {
            mGDetector = new GestureDetector(getContext(), new MoveAdapter());
    
            path = new Path();
            paint = new Paint();
            paint.setAntiAlias(true);
            paint.setDither(true);
            paint.setStrokeWidth(6);
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.STROKE);
    
            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
            srcRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
            destRect = new Rect(100, 200, bitmap.getWidth() + 100, bitmap.getHeight() + 200);
        }
    
        public TestCanvasView(Context context) {
            super(context);
        }
    
        public TestCanvasView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public TestCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return mGDetector.onTouchEvent(event);
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            // 坐标系
            path.moveTo(0, 0);
            path.lineTo(w, 0);
            path.moveTo(0, 0);
            path.lineTo(0, h);
            path.moveTo(100, 0);
            path.lineTo(100, 20);
            path.moveTo(200, 0);
            path.lineTo(200, 20);
            path.moveTo(300, 0);
            path.lineTo(300, 20);
            path.moveTo(400, 0);
            path.lineTo(400, 20);
            path.moveTo(500, 0);
            path.lineTo(500, 20);
            path.moveTo(600, 0);
            path.lineTo(600, 20);
            path.moveTo(700, 0);
            path.lineTo(700, 20);
            path.moveTo(0, 100);
            path.lineTo(20, 100);
            path.moveTo(0, 200);
            path.lineTo(20, 200);
            path.moveTo(0, 300);
            path.lineTo(20, 300);
            path.moveTo(0, 400);
            path.lineTo(20, 400);
            path.moveTo(0, 500);
            path.lineTo(20, 500);
            path.moveTo(0, 600);
            path.lineTo(20, 600);
            path.moveTo(0, 700);
            path.lineTo(20, 700);
            path.moveTo(0, 800);
            path.lineTo(20, 800);
            path.moveTo(0, 900);
            path.lineTo(20, 900);
            path.moveTo(0, 1000);
            path.lineTo(20, 1000);
            path.moveTo(0, 1100);
            path.lineTo(20, 1100);
            path.moveTo(0, 1200);
            path.lineTo(20, 1200);
            path.moveTo(0, 1300);
            path.lineTo(20, 1300);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.drawPath(path, paint);
            canvas.drawBitmap(bitmap, srcRect, destRect, null);
        }
    
        private class MoveAdapter extends GestureDetector.SimpleOnGestureListener {
    
            @Override
            public boolean onDown(MotionEvent e) {
                return true;
            }
    
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                scrollTo(getScrollX() + Math.round(distanceX), getScrollY() + Math.round(distanceY));
                return true;
            }
    
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                return super.onFling(e1, e2, velocityX, velocityY);
            }
        }
    }
    
    布局文件:
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <com.cytmxk.demo.canvas.TestCanvasView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="60dp"
            android:background="@android:color/holo_blue_light"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </android.support.constraint.ConstraintLayout>
    

    运行效果图如下:


    可以看到当TestCanvasView的内容滑动时,Canvas的原点也跟着滑动了,从而证明了上面的说法。

    1.2 Matrix的使用

    在Android中Matrix是一个3*3的矩阵:

    Matrix中对矩阵中9个值得定义:
    public static final int MSCALE_X = 0;
    public static final int MSKEW_X  = 1;
    public static final int MTRANS_X = 2;
    public static final int MSKEW_Y  = 3;
    public static final int MSCALE_Y = 4;
    public static final int MTRANS_Y = 5;
    public static final int MPERSP_0 = 6;
    public static final int MPERSP_1 = 7;
    public static final int MPERSP_2 = 8;
    
    对应的3*3矩阵为:
    [MSCALE_X  MSKEW_X  MTRANS_X
     MSKEW_Y   MSCALE_Y MTRANS_Y
     MPERSP_0  MPERSP_1 MPERSP_2]
    
    顾名思义MSCALE_X和MSCALE_Y用于缩放,MTRANS_X和MTRANS_Y用于平移,MSKEW_X和MSKEW_Y用于倾斜,
    除了这些Matrix还支持旋转;最后一行的三个值不常用,这里就不在解释了,有兴趣的同学可以自己去研究一下。
    
    为了方便Matrix对缩放、平移、倾斜和旋转操作提供了如下方法:
    public void setScale(float sx, float sy, float px, float py)
    public boolean preScale(float sx, float sy, float px, float py)
    public boolean postScale(float sx, float sy, float px, float py)
    其中sx为横向的缩放比例(大于1放大、小于1缩小、等于1不变),sy为纵向的缩放比例(大于1放大、小于1缩小、等于1不变),
    缩放的中心点为(px, py)
    
    public void setTranslate(float dx, float dy)
    public boolean preTranslate(float dx, float dy)
    public boolean postTranslate(float dx, float dy)
    其中dx为横向的平移距离(大于0向右平移、小于0向左平移、等于1不变),
    dy为横向的平移距离(大于0向下平移、小于0向上平移、等于1不变)
    
    public void setSkew(float kx, float ky, float px, float py)
    public boolean preSkew(float kx, float ky, float px, float py)
    public boolean postSkew(float kx, float ky, float px, float py)
    其中kx为横向的错切距离(大于0向右错切、小于0向左错切、等于0不变),
    其中ky为横向的错切距离(大于0向下错切、小于0向上错切、等于0不变),
    错切的中心点为(px, py)
    对于错切其实可以这样理解,比如一个矩形在横向错切,就相当于你拎着矩形的左上角和右下角横向向相反的方向拉拽使其变形。
    
    public void setRotate(float degrees, float px, float py)
    public boolean preRotate(float degrees, float px, float py)
    public boolean postRotate(float degrees, float px, float py)
    其中degrees为旋转的角度(大于0顺时针旋转、小于0逆时针旋转、等于0不变),旋转中心点为(px, py)
    
    大家应该注意到了每一种操作都提供了三个方法set、pre和post,那么它们有什么区别呢,
    对于每一种操作用到的都是矩阵乘法,对于矩阵乘法是不支持交换律的,因此就有了左乘和右乘,
    那么pre代表的是左乘、post代表的是右乘,应用到上面的四种操作,左乘就是先进行该操作,右乘就是后进行该操作;
    set方法会将前面的操作清除,只有set对应的操作。
    

    上面矩阵的四种操作可以应用到Path上,大家可以参考我的博客Drawable绘制过程源码分析和自定义Drawable实现动画中的2.3节。

    2 Canvas进行缩放、平移、倾斜和旋转操作

    下面就用Matrix来实现Canvas的缩放、平移、倾斜和旋转操作

    2.1 Canvas的缩放

    接着上面的例子,以图片左上角为缩放中心点缩小50%:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.save();
        M.setScale(0.5f, 0.5f, 100, 200);
        canvas.concat(M);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.restore();
    }
    

    接下来看一下运行结果:


    可以看到Canvas缩放的过程中,Canvas坐标系原点也会向缩放中心点靠拢,学习自定义View的绘制最关键的就是理解Canvas坐标系的变化

    2.2 Canvas的平移

    横向平移200, 纵向平移200,先上代码:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.save();
        M.setTranslate(200,200);
        canvas.concat(M);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.restore();
    }
    

    看一下运行结果:



    可以看到Canvas平移的过程中,Canvas坐标系原点也会跟着平移。

    2.3 Canvas的错切

    以图片左上角为中心点进行横向错切45度,先上代码:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.save();
        M.setSkew(1,0, 100, 200);
        canvas.concat(M);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.restore();
    }
    

    运行结果如下:



    可以看到Canvas错切的过程中,Canvas坐标系会发生改变,具体怎么改变相信大家看到上图应该会理解的。

    2.4 Canvas的旋转

    以图片的左上角为中心顺时针旋转45度,先上代码:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.save();
        M.setRotate(45,100, 200);
        canvas.concat(M);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.restore();
    }
    

    运行效果如下:



    可以看到Canvas旋转的过程中,Canvas坐标系会发生改变,具体怎么变化就不用赘叙了。

    注意:为了方便,Canvas还提供如下方法:

    public final void scale(float sx, float sy, float px, float py)
    public void translate(float dx, float dy)
    public void skew(float sx, float sy)
    public final void rotate(float degrees, float px, float py)
    这些方法最终使用的还是矩阵,因此和矩阵的实现的效果一样
    

    3 Canvas的setMatrix和concat方法

    在上面的例子中用的都是concat方法,如果换成setMatrix方法会有什么效果呢,接下来就用上面平移的例子实验一下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.save();
        M.setTranslate(200, 200);
        canvas.setMatrix(M);
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, srcRect, destRect, null);
        canvas.restore();
    }
    

    运行结果如下:


    看到结果是不是很奇怪,偏移的部分怎么没有跟着拖动,其实这个就说明了setMatrix和concat之间的差别,其实scrollTo方法作用就是平移Canvas,最终修改的还是Canvas中的Matrix对象,通过setMatrix会将原来的平移清除掉,Canvas的坐标系原点就回到了View的左上角,接着再偏移(dx:200, dy:200), Canvas坐标系原点就会就会一直保持为(200, 200),因此偏移的部分不会被拖动;而concat是在原Matrix对象的基础进行偏移(通过矩阵乘法实现),因此偏移的部分会被拖动

    4 Canvas的save、restore

    /**
     * Saves the current matrix and clip onto a private stack.
     * <p>
     * Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
     * clipPath will all operate as usual, but when the balancing call to
     * restore() is made, those calls will be forgotten, and the settings that
     * existed before the save() will be reinstated.
     *
     * @return The value to pass to restoreToCount() to balance this save()
     */
    public int save()
    
    /**
     * This call balances a previous call to save(), and is used to remove all
     * modifications to the matrix/clip state since the last save call. It is
     * an error to call restore() more times than save() was called.
     */
    public void restore()
    

    上面是源码中对于save和restore方法的解释,大致意思是:

    1 Canvas有两种类型的操作:Matrix操作(即translate,scale,rotate,skew和concat)和Clip操作(clipRect和clipPath)
    2 save方法调用时会保存此刻之前的Matrix操作和Clip操作,之后的Matrix操作和Clip操作正常执行,
    最后调用rotate方法平衡之前的save方法,此时save方法调用之后的Matrix操作和Clip操作会被撤销,
    还原到save方法调用时的状态。
    

    相关文章

      网友评论

        本文标题:Canvas常用方法解析第二篇

        本文链接:https://www.haomeiwen.com/subject/yvsnyctx.html