android高级UI之Canvas(坐标系、Layer)
在上一次https://www.cnblogs.com/webor2006/p/12664585.html已经对于Paint的三大高级技法有所了解了,接下来对于View的绘制还差另一个非常重要的对象----Canvas,俗称画布,关于它其实主要要学习两个内容:坐标系、Canvas状态保存,接下来则来攻克它们。
坐标系:
前言:
关于这块的学习还是参考博主:https://www.jianshu.com/p/cddb9dccb9ac。对于Canvas它的初始化其实是由Surface来完成的,关于这块在之前https://www.cnblogs.com/webor2006/p/12178704.html已经详细分析过了,回忆一下:
而其中可以看到用了一个dirty矩形区域来限定了Canvas绘制位置的坐标了,其实这个就被称之为Canvas的坐标系,而我们在使用Canvas的时候有时候会有这种移动旋转的操作:
那拿其中translate()操作来举例:
那对于图中绿色的矩形移动到了红色的矩形区域时,其中肯定会用到了Canvas的translate()方法了,那此时咱们的Canvas的坐标系发生变化了么?其实关于这块在早些年https://www.cnblogs.com/webor2006/p/3596728.html有学习过,其实Canvas它是牵扯到两种坐标系类型的:Canvas自己的坐标系、Canvas的绘图坐标系。所以说下面来梳理一下这俩个概念。
Canvas坐标系:
啥是Canvas自身的坐标系呢? 其实就是这块代码所确定的坐标系:
可以理解成是最外层的画板,一经确定其坐标系是会再改变的。所以很明显在绘图时经过Canvas.translate()平移操作肯定移动的不是Canvas坐标,而是下面要来阐述的绘图坐标系,总之把握“Canvas坐标系一经确定是不能再改变了”这一原则既可。具体坐标点是它:
有且只有一个!!!
绘图坐标系:
理论说明:
好,接下来再来理解一下这个绘图坐标系,很明显这个坐标系肯定不是唯一不变的,像Canvas.translate()就会让Canvas的绘图坐标系进行改变,这里查看一下这个api的说明,发现有一个提到了matrix矩阵的关键词:
其实Canvas的绘图坐标系跟这个Matrix是有关系的:当Matrix发生改变的时候,绘图坐标系对应的也会进行改变的 ,关于Canvas自身的坐标系和绘图坐标系之间的关系可以看一下博主的这张图所示:
关于矩阵这块就有一个深层次的知识点了,不要问为什么,先了解既可,有了这些理论作为基础对于坐标系的理解就会比较扎实,对于Canvas的绘图坐标系其实是一个2x2的矩阵的值传入给Canvas进行解析之后再将自己想要的数据传给NDK底层计算所得,这里看一下Canvas.drawRect()绘制矩形的源代码也能看到底层的身影:
当然具体的细节得要看NDK的C代码了,不过这里不用这么深,感受一下既可,重点是理解其机制。
好继续再深挖,那对于Canvas的移动、缩放、旋转等操作它又是如何与Matrix发生关系的呢?此时第二个矩阵又诞生了,这是一个3x3,其矩阵参数信息如下:
cosX -sinX translateX
sinX cosX translateY
0 0 scale其中sinX和cosX,代表的是旋转角度的sin和cos值。注意旋转的正方向是顺时针方向。
translateX和translateY代表的是平移的X和Y。
scale代表的是缩放的大小。
这些操作在改变绘图坐标系的时候会依据这个3x3的矩阵进行计算得到位置信息,而这个3x3的矩阵可以通过Canvas.getMatrix()来查看,确实是从NDK来从底层获取的:
实践:
咱们可以做一个实验直观的来感受一下这个3X3的矩阵:
package com.paintgradient.test.canvas; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.View; public class MyView extends View { private Paint paint; public MyView(Context context) { this(context, null); } public MyView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); paint = new Paint(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); 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.e("cexo", "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.e("cexo", "fs2:" + fs2[i]); } paint.setColor(Color.BLUE); canvas.drawRect(r, paint); } }
就是做了一个Canvas的平移操作,看一下运行效果:
这个显示不是重点,重点是来看一下日志输出:
很明显在做了translate()之后,其矩阵信息也发生变化了,很能够说明这些像平移操作是跟这个3x3的矩阵是做了一些计算的。
但是!!!有一个点就得注意了,此时的translate()造成了Canvas的绘图坐标系进行了改变之后,这种操作是“不可逆”的!!!啥叫不可逆呢,也就是说之后所有的Canvas的绘制的坐标点就变成:
不信的话,咱们再来绘制一个矩形验证一下:
运行:
看到木有,这次咱们木有进行平移了,其绘制出来的黄色矩形的左上角是不是就是从平移之后的那个位置开始绘制了呢?这就是所谓的不可逆,绘图坐标系一被改变就没法还原了!!!我不信这个邪,下面尝试将其的左上角回到平移之前的那个坐标系上来,也就是:
也就是将矩形的left和top传一个负值是不是就能达到回到平移之前的效果了,试试:
效果:
嗯,确实是回到平移操作之前的坐标系位置了,但是!!注意此时的绘图坐标系还是:
只是图拉到到了平移之前绘图坐标系的位置而已,对于绘图坐标系被更改操作是不可逆的理论还是依然满足!!!那Android没有提供还原绘图坐标系的办法么?很显然不可能不提供,此时另一个在自定义View大量会动用到的两个Canvas的API就出现了:Canvas.save()、Canvas.restore(),说实话这块真的一直搞不太明白,所以借此机会来彻底对它的机制进行梳理,下面先不管它原理,先来使用看一下效果:
运行看下效果:
看到木有,此时再绘制黄图时,坐标就回到平移之前的状态了:
下面再来修改一个代码,我们多次save(),看restore()最终会用哪一个:
结果:
很明显以restore最近的save()那次状态为准,那为啥会这样呢?此时就得了解Canvas的状态栈和layer栈了,知识真是一环扣一环。
状态保存:
状态栈
而要解释上面Canvas.save()和Canvas.retore()的原理,就得来理解这个状态栈的概念了,它主要是用来保存绘图坐标系的, 其实每一次的Canvas.save()会将save()时的坐标系给保存到一个栈当中,该栈就被称之为状态栈,而restore则是一个出栈的过程,那如果栈中保存了很多绘图坐标系,比如:
假如保存了8个绘图坐标系,那有没有一种机制能restore到指定save()时的绘图坐标系继续再进行绘制, 有的,在Canvas中还有这样一个方法:
好,咱们来写一个程序来看一下这块的效果:
package com.paintgradient.test.canvas; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.util.Log; import android.view.View; public class MyView1 extends View { private static final String TAG = "cexo"; private Paint mPaint = null; public MyView1(Context context) { this(context, null); } public MyView1(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint = new Paint(); mPaint.setColor(Color.RED); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(10); } @Override protected void onDraw(Canvas canvas) { //第1次保存,并通过canvas.getSaveCount的到当前状态栈容量 canvas.save(); Log.e(TAG, "Current SaveCount1 = " + canvas.getSaveCount()); canvas.translate(400, 400); RectF rectF = new RectF(0, 0, 600, 600); canvas.drawRect(rectF, mPaint); //第2次保存,并通过canvas.getSaveCount的到当前状态栈容量 canvas.save(); Log.e(TAG, "Current SaveCount2 = " + canvas.getSaveCount()); canvas.rotate(45); mPaint.setColor(Color.GREEN); canvas.drawRect(rectF, mPaint); //第3次保存,并通过canvas.getSaveCount的到当前状态栈容量 canvas.save(); Log.e(TAG, "Current SaveCount3 = " + canvas.getSaveCount()); canvas.rotate(45); mPaint.setColor(Color.BLUE); canvas.drawRect(rectF, mPaint); //第4次保存,并通过canvas.getSaveCount的到当前状态栈容量 canvas.save(); Log.e(TAG, "Current SaveCount4 = " + canvas.getSaveCount()); //通过canvas.restoreToCount出栈到第三层状态 canvas.restoreToCount(3);//出栈到指定层 Log.e(TAG, "restoreToCount--Current SaveCount = " + canvas.getSaveCount()); canvas.translate(0, 200); mPaint.setColor(Color.BLACK); canvas.drawRect(rectF, mPaint); //通过canvas.restoreToCount出栈到第1层(最原始的那一层)状态 canvas.restoreToCount(1); Log.e(TAG, "restoreToCount--Current SaveCount = " + canvas.getSaveCount()); mPaint.setColor(Color.YELLOW); canvas.drawRect(rectF, mPaint); } }
运行看一下效果:
看一下日志:
其中为啥第一次保存之后打印保存的次数为啥是2呢?
这是因为默认状态栈中就保存了一个0.0的坐标系的,所以这就是为啥第一次显示调Canvas.save(),看到的保存次数为2的原因所在,咱们打印一下日志验证一下:
运行:
嗯,确实有个默认的,那。。这个默认的坐标系能不能restore呢?咱们来试一下:
哦,等于默认的这个状态栈中的坐标系咱们是没法restore进行使用的。
接下来再来理解一下代码关于restore的相关逻辑:
看一下这个黑矩形的位置:
刚好就回到了绘制绿色矩形的状态点来绘制了,接下来再看一下第二个restore:
此时当然就回到了原点的状态了:
其中还有一个注意事项,就是:
所以:
layer栈
关于Canvas还有最后一个比较重要的知识点,就是关于layer图层,这里可以先来看一下它的相关API:
其实在上一次https://www.cnblogs.com/webor2006/p/12664585.htmlPaint的Xfermode的示例中都用到过的,这里回忆其中一个非常经典的16个模式图的代码:
package com.paintgradient.test.xfermode; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Xfermode; import android.os.Build; import android.util.AttributeSet; import android.view.View; 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, attrs); 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, 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, oldh); mItemSize = w / 4.5f; mItemHorizontalOffset = mItemSize / 6; mItemVerticalOffset = mItemSize * 0.426f; mCircleRadius = mItemSize / 3; mRectSize = mItemSize * 0.6f; } }
其中看到标红的代码没有?这里为啥要搞一个图层呢?所以这里来阐述其原因,先将这句代码注释直观的感受一下效果区别,先来给其增加一个背景以更观察效果:
而此时我们将图层代码去掉,再来看效果:
@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); } } }
为了便于观察效果,这里给其设置了一个背景,运行:
看出区别木有,很明显木有增加图层在清除源图时直接将其背景都给清掉了。。这里来个特写:
要想搞明白为什么,就得理解Canvas的图层概念【其实类似于PS中图层的概念】,saveLayer的api主要做用就是用来新建一个图层,后续的绘图操作都在新建的layer上面进行,当我们调用restore 或者 restoreToCount 时更新到对应的图层和画布上。Canvas图层的切面示意图如下:
结合咱们的代码来理解一下:
而对于图层最终也会像之前的Canvas绘图坐标系保存一样,最终都会保存在栈中的,只是Canvas.save()保存的是绘图坐标系,而Canvas.saveLayer()保存的是图层信息,而且它们都是存在同一个栈中的,那回到咱们的代码上来看:
咱们试一下:
运行发现效果跟加了save()一模一样,那这是为啥呢?这是因为saveLayer()有一个参数来决定的:
参数了解:
这里就系统的对saveLayer()参数进行了解:
这四个参数则为构建的图层所在的区域;Paint就不用多说了;重点是理解最后一个参数:
它的作用是告诉Canvas用来保存哪些信息,有6种类型:
- 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
读一下官网的说明:
实验论证:
下面来看一个具体的实例,代码如下:
package com.paintgradient.test.canvas; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.view.View; 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.drawColor(Color.BLUE); paint.setColor(Color.YELLOW); canvas.drawRect(rectF, paint); RectF rectF1 = new RectF(10, 10, 300, 400); paint.setColor(Color.RED); canvas.drawRect(rectF1, paint); } }
运行:
可以发现,咱们这里的Canvas都是在一个图层进行绘制的所以最初的背景被后面设置的覆盖了:
好,接下来增加图层再来看下效果:
再看效果:
看到图层的效果木有,此时的绿矩形就出来了,而有一个细节需要注意:
所以看到的效果就是:
那如果想最后的一个红矩形又跳出新建的图层,则可以这样做:
也能很清晰的感受到save()和saveLayer()最终保存的是同一个状态栈,至此,关于Canvas的核心用法就已经学完了,收获颇多,终于搞清楚了状态栈的概念,最后关于这次的学习借用博主的总结进行收尾: