高级UI-Canvas(变换技巧,状态保存)
创始人
2024-01-28 04:46:24

前言

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

1.Canvas基本概念

直面意思是画布,其实是分装的一个工具类(绘制会话,用来和底层沟通最终交给底层绘制)
一个Canvas类对象有四大基本要素
1、一个是用来保存像素的bitmap
2、一个Canvas在Bitmap上进行绘制操作
3、绘制的东西
4、绘制的画笔Paint

1.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自己的坐标系、绘图坐标系

Canvas的坐标系,

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

绘图坐标系

它不是唯一不变的,它与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栈

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


在前面我们提到坐标系的转换是一个不可逆转的,而我们可以通过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, attiresmBitmap = 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剪裁


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, attiresinit(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, mItemSize - 2, mItemSize - 2, mPaint);mPaint.setStyle(Paint.Style.FILL);//画圆mPaint.setColor(mCircleColor);float left = mCircleRadius + 3;float top = mCircleRadius + 3;canvas.drawCircle(left, top, mCircleRadius, mPaint);mPaint.setXfermode(sModes[index]);//画矩形mPaint.setColor(mRectColor);float rectRight = mCircleRadius + mRectSize;float rectBottom = mCircleRadius + mRectSize;canvas.drawRect(left, top, rectRight, rectBottom, mPaint);mPaint.setXfermode(null);//canvas.restore();canvas.restoreToCount(layer);}}
}@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, old);)mItemSize = w / 4.5f;mItemHorizontalOffset = mItemSize / 6;mItemVerticalOffset = mItemSize * 0.426f;mCircleRadius = mItemSize / 3;mRectSize = mItemSize * 0.6f;
}
}

这段代码我门可以看到,其实实际上就是我们上次课当中,xfermode的演示代码,而在这段代码当中我才用了saveLayer进行操作,通过上面两个结果,一个是我加了saveLayer的,一个是没加的, 那么从中我门可以明显看到在没有加的时候,xfermode的像素输出效果直接将外层背景色也给清空了,而加入之后没有,那么其实我门可以很明显的明白如果用了layer那么其实实际上我们是在当前这个canvas图形上面新建了一个图层当我们调用restore 或者 restoreToCount 时 我们的绘制会更新到当前图层

那么这个时候我门来详细分析saveLayer的参数

canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);
/*** Helper version of saveLayer() that takes 4 values rather than a RectF.** @deprecated Use {@link #saveLayer(float, float, float, float, Paint)} instead.*/
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,@Saveflags int saveFlags) {return nSaveLayer(mNativeCanvasWrapper, left, top, right, bottom,paint != null ? paint.getNativeInstance() : 0,saveFlags);
}

通过上诉方法的注释,以及代码我门明显知道,前面四个参数,为上下左右四个点构成一个图层区,Paint画笔也可以继承过来,而最后一个参数表示的是我们当前的保存形式,总共下面6种,这六个模式其实实际上讲的就是告诉canvas当前保存那些信息

MATRIX_SAVE_FLAG:只保存图层的matrix矩阵 save,saveLayer
CLIP_SAVE_FLAG:只保存大小信息 save,saveLayer
HAS_ALPHA_LAYER_SAVE_FLAG:表明该图层有透明度,和下面的标识冲突,都设置时以下面的标志为准 saveLayer
FULL_COLOR_LAYER_SAVE_FLAG:完全保留该图层颜色(和上一图层合并时,清空上一图层的重叠区域,保留该图层的颜色) saveLayer
CLIP_TO_LAYER_SAVE_:创建图层时,会把canvas(所有图层)裁剪到参数指定的范围,如果省略这个flag将导致图层开销巨大(实际上图层没有裁剪,与原图层一样大)
ALL_SAVE_FLAG:保存所有信息 save,saveLayer

从源码当中我发现其他几种模式在高版本当中已经剔除,只保留了一种。就是我门的all_save_flag

  /** @hide */
@IntDef(flag = true,value = {ALL_SAVE_FLAG})
@Retention(RetentionPolicy.SOURCE)
public @interface Saveflags {}

那么这个时候我们来测试一下

 public class MyView3 extends View {public MyView3(Context context) {super(context);
}@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);RectF rectF = new RectF(0,0,400,500);Paint paint = new Paint();paint.setStyle(Paint.Style.STROKE);paint.setStrokeWidth(10);paint.setColor(Color.GREEN);canvas.drawRect(rectF, paint);canvas.translate(50,50);canvas.saveLayer(0,0,canvas.getWidth(),canvas.getHeight(),null,Canvas.ALL_SAVE_FLAG);//canvas.save();canvas.drawColor(Color.BLUE);// 通过drawColor可以发现saveLayer是新建了一个图层,// 同时结合Lsn5的16种Xfermode叠加形式Demo可以验证是新建的透明图层paint.setColor(Color.YELLOW);canvas.drawRect(rectF,paint);//canvas.restore();canvas.restore();RectF rectF1 = new RectF(10,10,300,400);paint.setColor(Color.RED);canvas.drawRect(rectF1,paint);}

那么这段代码也验证了我门上诉的理论,在加了saveLayer之后,背景色被绘制到了另外一个图层导致前面有一节空白的,同时也得出了一个有趣的结论,貌似,平移操作也被继承了,其实这里我们的出一个结论saveLayer会将之前的一些Canvas状态操作延续过来。这里是通过之前的最后一个参数设置成ALL_SAVE_FLAG完成。他在新建图层的时候完成了保留当前所有信息状态的操作.

总结

Canvas里面牵扯两种坐标系:Canvas自己的坐标系、绘图坐标系
Canvas的坐标系它就在View的左上角,做坐标原点往右是X轴正半轴,往下是Y轴的正半轴,有且只有一个,唯一不变
绘图坐标系它不是唯一不变的,它与Canvas的Matrix有关系,当Matrix发生改变的时候,绘图坐标系对应的进行改变,同时这个过程是不可逆的(save和restore方法来保存和还原变化操作)Matrix又是通过我们设置translate、rotate、scale、skew来进行改变的Canvas的状态保存---状态栈、Layer栈
状态栈--save、 restore方法来保存和还原变换操作Matrix以及Clip剪裁也可以通过restoretoCount直接还原到对应栈的保存状态
Layer栈--- saveLayer的时候都会新建一个透明的图层(离屏Bitmap-离屏缓冲),并且会将saveLayer之前的一些Canvas操作延续过来后续的绘图操作都在新建的layer上面进行当我们调用restore 或者 restoreToCount 时 更新到对应的图层和画布上

更多Android 知识点可参考

Android 性能调优系列https://0a.fit/dNHYY

Android 车载学习指南https://0a.fit/jdVoy

Android Framework核心知识点笔记https://0a.fit/acnLL

Android 八大知识体系https://qr21.cn/CaZQLo?BIZ=ECOMMERCE

Android 中高级面试题锦https://0a.fit/YXwVq

相关内容

热门资讯

万的繁体字怎么写 繁体字怎么写... 一、繁体字大全简化偏旁讠[訁] 饣[飠] [昜] 纟[糹] [臤] 只[戠] 钅[釒] 呙[咼]A爱...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
桂林市属于哪个省 桂林为什么划... 桂林 [guì lín]广西壮族自治区下辖市桂林,简称桂,是世界著名风景游览城市、全国重要高新技术产...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...