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方法調用時的狀態。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。