Android高级UI之Canvas深度分析—变换技巧,状态保存

Posted 码农小风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android高级UI之Canvas深度分析—变换技巧,状态保存相关的知识,希望对你有一定的参考价值。

前言

在前面我们把Paint关于UI颜色样式的处理进行了学习, 其实真正高级部分就是三个点,渲染,滤镜,图形组合,而我们图形绘制比较重要的另一个对象Canvas也是需要我们去重点掌握的,那么这次咱们来进行Canvas的深层次的学习,主要了解有两个点

  1. Canvas的变换使用技巧
  2. Canvas的状态,Canvas Layer

1.Canvas基本概念

直面意思是画布,其实是分装的一个工具类(绘制会话,用来和底层沟通最终交给底层绘制),一个Canvas类对象有四大基本要素:

  • 一个是用来保存像素的bitmap
  • 一个Canvas在Bitmap上进行绘制操作
  • 绘制的东西
  • 绘制的画笔Paint

2.Canvas变换操作----坐标系概念

在我们进行canvas操作的时候我们会有一个问题产生,在进行图形的平移,旋转操作时,我们没有去更改原始的坐标,只通过了非常简单的几个api就直接进行了移动,那么中间他的具体到底是发生了什么,通过之前在绘制流程当中draw时我们发现在下面我已经缩减了之后的代码上我门发现, 在绘制之初就产生了一个矩形,并且他通过面板进行了一次初始化

 private void draw(boolean fullRedrawNeeded) 
    Surface surface = mSurface;
   ...

    final Rect dirty = mDirty;
    if (mSurfaceHolder != null) 
        // The app owns the surface, we won't draw.
        dirty.setEmpty();
        if (animating && mScroller != null) 
            mScroller.abortAnimation();
        
        return;
    

    if (fullRedrawNeeded) 
        mAttachInfo.mIgnoreDirtyState = true;
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    

    int xOffset = -mCanvasOffsetX;
    int yOffset = -mCanvasOffsetY + curScrollY;
    final WindowManager.LayoutParams params = mWindowAttributes;
    final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
    if (surfaceInsets != null) 
        xOffset -= surfaceInsets.left;
        yOffset -= surfaceInsets.top;

        // Offset dirty rect for surface insets.
        dirty.offset(surfaceInsets.left, surfaceInsets.right);
    
  ......

            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) 
                return;
            
        
    

    if (animating) 
        mFullRedrawNeeded = true;
        scheduleTraversals();
    

那么在上面的代码当中我门可以看到在绘制开始之初, 在底层就确定了一个绘制区域,确定了canvas绘制位置的坐标,那么这个就是被称之为我门canvas的坐标系,确定我们canvas绘制图形的位置。那么,当我们进行了

    canvas.translate(50, 50);
    canvas.rotate(45);
    canvas.scale(1.5f, 0.5f);
    canvas.skew(1.73f, 0);

等操作的时候,我们的图形绘制会直接发生改变,那么这个时候我门考虑一个问题,下图中绿色的点移动到红色的点,我们刚才所设置的canvas移动了吗

其实很多通过会在这里认为我们canvas的坐标进行了移动,其实不然,在Canvas里面牵扯两种坐标系:Canvas自己的坐标系、绘图坐标系

2.1 Canvas的坐标系

它就在View的左上角,做坐标原点往右是X轴正半轴,往下是Y轴的正半轴,有且只有一个,唯一不变这一个其实就是在我们canvas当中在绘制之初由surface所初始化的那个点

2.2 绘图坐标系

它不是唯一不变的,它与Canvas的Matrix有关系,当Matrix发生改变的时候,绘图坐标系对应的进行改变,他有一个特性就是在这个过程中是不可逆的

那么其实实际就是我们在画图的时候,有一块总面板,总面板不动, 而当我在开始进行绘制图形的时候,有一个时时刻刻在动的面板,而这个面板就是具体去绘制我们图形的画板

那么里层的绘图坐标系他的实际是用一个Matrix矩阵表示的,这个和我门之前的滤镜矩阵表示差不多,只不过,绘图坐标系的矩阵是一个2x2的矩阵传入的值是由我们的canvas进行解析之后将自己想要的数据给底层底层自己计算所得

public void drawRect(@NonNull Rect r, @NonNull Paint paint) 
    throwIfHasHwBitmapInSwMode(paint);
    drawRect(r.left, r.top, r.right, r.bottom, paint);

那么在这里我们可以看到在进入底层native方法之前,实现会根据每一种绘制的不同对底层的数据进行传入, 然后会计算出我门的绘制坐标系(此处底层不看,涉及c,我们这里明白这一点就行)

我们通过简单设置translate、rotate、scale、skew来改变我们绘制图形的位置时他的计算时依赖与另外一个矩阵来对绘图坐标系进行改变,这是一个3x3的矩阵,它里面的九个参数

cosX -sinX translateX
sinX cosX translateY
0 0 scale

其中,sinX和cosX,代表的是旋转角度的sin和cos值。注意旋转的正方向是顺时针方向。translateX和translateY代表的是平移的X和Y。scale代表的是缩放的大小。

我们可以通过getMatrix()的到这个矩阵,而通过看到底层源码,这里我能清晰的看到我们是直接调用底层的矩阵

@Deprecated
public void getMatrix(@NonNull Matrix ctm) 
    nGetMatrix(mNativeCanvasWrapper, ctm.native_instance);

那么这里我做了一组测试

 RectF r = new RectF(0, 0, 400, 500);
    paint.setColor(Color.GREEN);
    canvas.drawRect(r, paint);
    float[] fs = new float[10];
            canvas.getMatrix().getValues(fs);
    for (int i = 0;i < fs.length;i++)
        Log.i("barry","fs:"+fs[i]);
    
    //平移
    canvas.translate(50, 50);
    float[] fs2 = new float[10];
    canvas.getMatrix().getValues(fs2);
    for (int i = 0;i < fs2.length;i++)
        Log.i("barry","fs2:"+fs2[i]);
    

    paint.setColor(Color.BLUE);
    canvas.drawRect(r, paint);

可以很明显看到,矩阵进行平移之后这个矩阵信息的变化。那么注意,绘图矩阵的坐标系移动是一个不可逆转的状态也就是说,一旦矩阵移动完成之后,那么他不能回到之前的位置,具体效果如下

但是在我们的Canvas当中提供了save和restore方法来保存和还原变化操作,

    RectF r = new RectF(0, 0, 400, 500);
    paint.setColor(Color.GREEN);
    //画完之后,绘图坐标系定位在此处
    canvas.drawRect(r, paint);
    //save保存当前坐标
    canvas.save();

    //平移之后,坐标系发生改变
    canvas.translate(50, 50);
    
    paint.setColor(Color.BLUE);
    canvas.drawRect(r, paint);
    //通过restore进行还原到save保存时的坐标系
    canvas.restore();

但是想要知道这两个方法是怎么进行操作的才能让我们更加深入的去熟悉Canvas的使用技巧,那么我门必须去了解Canvas的状态栈、Layer栈

3. Canvas的状态保存—状态栈、Layer栈

3.1 状态栈

在前面我们提到坐标系的转换是一个不可逆转的,而我们可以通过save来进行保存restore进行恢复,其实我们在进行save操作时在canvas当中会将我门save下来的坐标系进行保存到一个栈当中,并且可以通过restore或者是restoreToCount进行操作下面通过一段测试代码我门印证下

public class MyView extends View 
private static final String TAG = "BARRY";

private Paint mPaint = null;
private Bitmap mBitmap = null;

public MyView(Context context) 
    this(context, null);


public MyView(Context context, AttributeSet attrs) 
    super(context, attires
    mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lsj);
    init();


public MyView(Context context, AttributeSet attrs, int defStyleAttr) 
    super(context, attrs, defStyleAttr);


private void init() 
    mPaint = new Paint();
    mPaint.setColor(Color.RED);
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setStrokeWidth(10);


@Override
protected void onDraw(Canvas canvas) 
    //第1次保存,并通过canvas.getSaveCount的到当前状态栈容量
    canvas.save();
    Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());

    canvas.translate(400, 400);
    RectF rectF = new RectF(0,0,600,600);
        canvas.drawBitmap(mBitmap, null, rectF, mPaint);
    //第2次保存,并通过canvas.getSaveCount的到当前状态栈容量
    canvas.save();
    Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());

    canvas.rotate(45);

    canvas.drawBitmap(mBitmap, null, rectF, mPaint);
    //第3次保存,并通过canvas.getSaveCount的到当前状态栈容量
    canvas.save();
    Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());

    canvas.rotate(45);

    canvas.drawBitmap(mBitmap, null, rectF, mPaint);
    //第4次保存,并通过canvas.getSaveCount的到当前状态栈容量
    canvas.save();
    Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());
    //通过canvas.restoreToCount出栈到第三层状态
    canvas.restoreToCount(3);
    Log.i(TAG, "restoreToCount--Current SaveCount = " + canvas.getSaveCount());

    canvas.translate(0, 200);

    //rectF = new RectF(0,0,600,600);
    canvas.drawBitmap(mBitmap, null, rectF, mPaint);
    //通过canvas.restoreToCount出栈到第1层(最原始的那一层)状态
    canvas.restoreToCount(1);
    Log.i(TAG, "restoreToCount--Current SaveCount = " + canvas.getSaveCount());
    canvas.drawBitmap(mBitmap, null, rectF, mPaint);
   
    

那么其实我们这样可以直接明白, 每一次的save其实实际上是用了一个栈保存了我的绘图坐标系,这个栈被我们称之为状态栈起来, 而我们的restore就是一个出栈的过程。save、 restore方法来保存和还原变换操作Matrix以及Clip剪裁

3.2 Layer栈

在我们的canvas当中,提供了一个saveLayer的api主要做用是用来新建一个图层,后续的绘图操作都在新建的layer上面进行,当我们调用restore 或者 restoreToCount 时 更新到对应的图层和画布上

下面通过这段测试代码的效果我们来验证当前的结论

public class MyView extends View 

Paint mPaint;
float mItemSize = 0;
float mItemHorizontalOffset = 0;
float mItemVerticalOffset = 0;
float mCircleRadius = 0;
float mRectSize = 0;
int mCircleColor = 0xffffcc44;//黄色
int mRectColor = 0xff66aaff;//蓝色
float mTextSize = 25;

private static final Xfermode[] sModes = 
        new PorterDuffXfermode(PorterDuff.Mode.CLEAR),
        new PorterDuffXfermode(PorterDuff.Mode.SRC),
        new PorterDuffXfermode(PorterDuff.Mode.DST),
        new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER),
        new PorterDuffXfermode(PorterDuff.Mode.DST_OVER),
        new PorterDuffXfermode(PorterDuff.Mode.SRC_IN),
        new PorterDuffXfermode(PorterDuff.Mode.DST_IN),
        new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT),
        new PorterDuffXfermode(PorterDuff.Mode.DST_OUT),
        new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP),
        new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP),
        new PorterDuffXfermode(PorterDuff.Mode.XOR),
        new PorterDuffXfermode(PorterDuff.Mode.DARKEN),
        new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
        new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY),
        new PorterDuffXfermode(PorterDuff.Mode.SCREEN)
;

private static final String[] sLabels = 
        "Clear", "Src", "Dst", "SrcOver",
        "DstOver", "SrcIn", "DstIn", "SrcOut",
        "DstOut", "SrcATop", "DstATop", "Xor",
        "Darken", "Lighten", "Multiply", "Screen"
;

public MyView(Context context) 
    super(context);
    init(null, 0);


public MyView(Context context, AttributeSet attrs) 
    super(context, attires
    init(attrs, 0);


public MyView(Context context, AttributeSet attrs, int defStyle) 
    super(context, attrs, defStyle);
    init(attrs, defStyle);


private void init(AttributeSet attrs, int defStyle) 
    if(Build.VERSION.SDK_INT >= 11)
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setTextSize(mTextSize);
    mPaint.setTextAlign(Paint.Align.CENTER);
    mPaint.setStrokeWidth(2);


@Override
protected void onDraw(Canvas canvas) 
    super.onDraw(canvas);
    //设置背景色
    canvas.drawARGB(255, 139, 197, 186);

    int canvasWidth = canvas.getWidth();
    int canvasHeight = canvas.getHeight();

    for(int row = 0; row < 4; row++)
        for(int column = 0; column < 4; column++)
            canvas.save();
            //此处是建立新的图层
            int layer = canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);
            mPaint.setXfermode(null);
            int index = row * 4 + column;
            float translateX = (mItemSize + mItemHorizontalOffset) * column;
            float translateY = (mItemSize + mItemVerticalOffset) * row;
            canvas.translate(translateX, translateY);
            //画文字
            String text = sLabels[index];
            mPaint.setColor(Color.BLACK);
            float textXOffset = mItemSize / 2;
            float textYOffset = mTextSize + (mItemVerticalOffset - mTextSize) / 2;
            canvas.drawText(text, textXOffset, textYOffset, mPaint);
            canvas.translate(0, mItemVerticalOffset);
            //画边框
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setColor(0xff000000);
            canvas.drawRect(2, 2, 

以上是关于Android高级UI之Canvas深度分析—变换技巧,状态保存的主要内容,如果未能解决你的问题,请参考以下文章

Android 高级UI解密 :Canvas裁剪 与 二维三维Camera几何变换(图层Layer原理)

Android绘图之Canvas变换(6)

Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)

Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)

Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)

Android 高级UI解密 :Paint滤镜 与 颜色过滤(矩阵变换)