摘要
对于自定义View,Canvas是一个很重要的概念,对rotate()和translate()两个方法的理解一直有偏差,特此记录,参考此文章
首先看一个需求,显示一个竖向显示的TextView。
第一反应是让画布旋转。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class RotateTextView extends TextView { public RotateTextView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { canvas.rotate(90); super.onDraw(canvas); } } <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.xiongcen.myapplication.test.RotateTextView android:layout_width="100dp" android:layout_height="200dp" android:text="Hello World!" /> </RelativeLayout>
|
在绘制TextView之前把画布顺时针旋转90°。
打开开发者选项-显示布局边界,可以看到
文字并没有显示出来。
首先要明确一点,Canvas#rotate(float degrees)这个方法的旋转中心是坐标的原点。
该方法从字面上来看是旋转“画布”,但我们最好理解成旋转的是画布的坐标轴。
所以其实真正顺时针旋转90°的情况如下所示:
“画控件的画布的区域”:在xml中添加的控件RotateTextView中设置的layout_widths和layout_height这两个属性组成的矩形。原点的位置要按照控件所在的ViewGroup来决定,比如这个控件在LinearLayout里面,那么就看LinearLayout的orientation是vertical还是horizontal;如果这个控件在RelativeLayout里面,那么就看这个控件他在父容器的左边、右边、等等。
“旋转坐标系之后画控件的实际区域”:旋转后的坐标系的x,y轴正向的交集的区域,不管坐标系怎么转,控件都是画在x,y数值都为正的那个区间里面的。
通过上图所示,可以看出坐标系旋转之后,实际画控件的区域并没有画布,所以也就什么都画不出来。可以看到布局边界显示了,但并没有任何内容。
所以我们应该如何实现一个竖直的TextView。
按照之前的结论,我们要把控件画在画布上,就要让我们的坐标系的x,y正向区域在”画控件的画布区域“上。
这个时候,我们就要用到Canvas#translate(float dx, float dy)方法。这个方法的作用就是移动画图的坐标系的原点。
先说明,translate()方法是在x轴还是y轴移动的距离是按照“旋转坐标系之后画控件的实际区域“所标注的xy轴指向。
所以我们只要让原点的位置向右移动,让“旋转坐标系之后画控件的实际区域”向右移动,移动“画控件的画布的区域”的宽度就可以了。
1 2 3 4 5 6
| @Override protected void onDraw(Canvas canvas) { canvas.rotate(90); canvas.translate(0, -getWidth()); super.onDraw(canvas); }
|
效果如下图:
接下来看Canavs#rotate(float degrees, float px, float py)方法:
1 2 3 4 5 6 7 8 9 10 11 12
| /** * Preconcat the current matrix with the specified rotation. * * @param degrees The amount to rotate, in degrees * @param px The x-coord for the pivot point (unchanged by the rotation) * @param py The y-coord for the pivot point (unchanged by the rotation) */ public final void rotate(float degrees, float px, float py) { translate(px, py); rotate(degrees); translate(-px, -py); }
|
假设我们调用Canvas#rotate(90,250,250);
如图所示:
黄色区域:画布初始位置,原点为(0,0);红色的xy轴;
绿色区域:画布原点(0,0)移动至(250,250)的位置;
蓝色区域:画布顺时针旋转90°的位置;桃红色的x1y1轴;
空白区域:画布原点(0,0)移回至(-250,-250)的位置(使用桃红色箭头指示);形成黑色的新的xy轴;
所以当我们自定义View代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class RotateArrow extends View { private Paint mLinePaint = new Paint(); private Paint mBackgroundPaint = new Paint(); private int width = 500; private int height = 500; public RotateArrow(Context context, AttributeSet attrs) { super(context, attrs); mLinePaint.setStrokeWidth(4); mLinePaint.setColor(Color.RED); mBackgroundPaint.setColor(Color.GRAY); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawRect(0, 0, width, height, mBackgroundPaint); canvas.rotate(90, width / 2, height / 2); canvas.drawLine(width / 2, 0, 0, height / 2, mLinePaint); canvas.drawLine(width / 2, 0, width, height / 2, mLinePaint); canvas.drawLine(width / 2, 0, width / 2, height, mLinePaint); canvas.drawCircle(width - 100, height - 100, 30, mLinePaint); } }
|
drawLine()和drawCircle()方法画出来的线和圆都是基于最新的黑色的xy轴。
所以效果如下:
如果我们修改RotateArrow#onDraw()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawRect(0, 0, width, height, mBackgroundPaint); canvas.save(); canvas.rotate(90, width / 2, height / 2); canvas.drawLine(width / 2, 0, 0, height / 2, mLinePaint); canvas.drawLine(width / 2, 0, width, height / 2, mLinePaint); canvas.drawLine(width / 2, 0, width / 2, height, mLinePaint); canvas.restore(); canvas.drawCircle(width - 100, height - 100, 30, mLinePaint); }
|
drawLine()方法画出来的线是基于最新的黑色的xy轴;
drawCircle()方法画出来的圆是基于save()方法调用之前的红色的xy轴;
效果如下:
所以很明显:
Canvas#save():用来保存Canvas的状态。
Canvas#restore():用来恢复Canvas之前保存的状态(可以想成是保存坐标轴的状态),防止save()方法代码之后对Canvas执行的操作,继续对后续的绘制会产生影响。
为了对save()和translate()两个方法有深刻认识,请看以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Override protected void onDraw(Canvas canvas) { /* * 保存并裁剪画布填充黄色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300); canvas.drawColor(Color.YELLOW); /* * 保存并裁剪画布填充绿色 */ int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); }
|
效果如下图:
save()是依靠stack栈来进行,此时在Canvas内部会有这样的一个Stack栈:
Canvas会默认保存一个底层的控件给我们绘制一些东西,当我们没有调用save方法时所有的绘图操作都在这个Default Stack ID中进行,每当我们调用一次save就会往Stack中存入一个ID,将其后所有的操作都在这个ID所指向的空间进行直到我们调用restore方法还原操作。
如果我们要继续绘制东西,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| /* * 保存并裁剪画布填充黄色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300); canvas.drawColor(Color.YELLOW); /* * 保存并裁剪画布填充绿色 */ int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); mPaint.setColor(Color.CYAN); canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 200, mViewHeight / 2F + 200, mPaint);
|
效果如下图:
我们在saveID3之后又画了一个青色的矩形,很明显,这段代码是在saveID3所标识的空间中绘制的,因此其必然会受到saveID3的约束旋转,并且!!!这个矩形除了被旋转,还被clip了,也就是说saveID1、saveID2也同时对其产生了影响,此时我们再次尝试在saveID2绘制完我们想要的东西后将其还原:
1 2 3 4 5 6 7
| /* * 保存并裁剪画布填充绿色 */ int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); canvas.restore();
|
同时将青色的矩形变大:
1
| canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 400, mViewHeight / 2F + 400, mPaint);
|
效果如下图:
很明显,saveID2已经不再对下面的saveID3起作用了,也就是说当我们调用canvas.restore()后标志着上一个save操作的结束或者说回滚了。
同理,我们再把saveID1也restore:
1 2 3 4 5 6 7
| /* * 保存并裁剪画布填充黄色 */ int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300); canvas.drawColor(Color.YELLOW); canvas.restore();
|
这时saveID3将彻底不再受前面操作的影响:
如果我们在绘制青色的矩形之前将saveID3也还原:
1 2 3 4 5 6 7 8
| /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore();
|
那么这个青色的矩形将会被绘制在Default Stack ID上而不受其他save状态的影响:
每当我们调用restore还原Canvas,对应的save栈空间就会从Stack中弹出去,Canvas提供了getSaveCount()方法来为我们提供查询当前栈中有多少save的空间。
还有一点需要注意,save(@Saveflags int saveFlags)方法所标注的类型来保证画布保存的值,如果save一个flag为MATRIX_SAVE_FLAG,画布做的却是clipRect操作,那么就算调用restore,依然无法回滚。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| /* * 保存并裁剪画布填充黄色 */ int saveID1 = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300); canvas.drawColor(Color.YELLOW); canvas.restore(); /* * 保存并裁剪画布填充绿色 */ int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200); canvas.drawColor(Color.GREEN); canvas.restore(); /* * 保存画布并旋转后绘制一个蓝色的矩形 */ int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.rotate(5); mPaint.setColor(Color.BLUE); canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint); canvas.restore(); mPaint.setColor(Color.CYAN); canvas.drawRect(mViewWidth / 2F, mViewHeight / 2F, mViewWidth / 2F + 400, mViewHeight / 2F + 400, mPaint);
|
saveID1依然对青色矩形起作用:
参考
Android Canvas编程:对rotate()和translate()两个方法的研究
自定义控件