Canvas常用方法解析第二篇

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方法调用时的状态。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 140,856评论 1 295
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 60,475评论 1 254
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 92,639评论 0 208
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,628评论 0 171
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,311评论 1 250
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,533评论 1 167
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,235评论 2 266
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,017评论 0 161
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,738评论 6 225
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,358评论 0 211
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,115评论 2 211
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,444评论 1 222
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,138评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 26,965评论 2 209
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,349评论 3 200
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,574评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,901评论 0 163
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,314评论 2 227
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,433评论 2 228