From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige
自定义控件其实很简单4:
在讲ColorMatrix的时候说过其是一个4x5的颜色矩阵,而同样,我们的Matrix也是一个矩阵,只不过不是4*5而是3*3的位置坐标矩阵:
变换变换,既然说到变换那么必定涉及最基本的旋转啊、缩放啊、平移之类,而在Matrix中除了该三者还多了一种:错切,什么叫错切呢?所谓错切数学中也称之为剪切变换,原理呢就是将图形上所有点的X/Y坐标保持不变而按比例平移Y/X坐标,并且平移的大小和某点到X/Y轴的垂直距离成正比,变换前后图形的面积不变。其实对于Matrix可以这样说:图形的变换实质上就是图形上点的变换,而我们的Matrix的计算也正是基于此,比如点P(x0,y0)经过上面的矩阵变换后会去到P(x,y)的位置:
注:除了平移外,缩放、旋转、错切、透视都是需要一个中心点作为参照的,如果没有平移,默认为图形的[0,0]点,平移只需要指定移动的距离即可,平移操作会改变中心点的位置!非常重要!记牢了!
有一点需要注意的是,矩阵的乘法运算是不符合交换律的,因此矩阵B*A和A*B是两个截然不同的结果,前者表示A右乘B,是列变换;后者表示A左乘B,是行变换。如果有心的童鞋会发现Matrix类中会有三大类方法:setXXX、preXXX和postXXX,而preXXX和postXXX就是分别表示矩阵的左右乘,也有前后乘的说法,对于不懂矩阵的人来说都一样 = = ……但是要注意一点!!!大家在理解Matrix的时候要把它想象成一个容器,什么容器呢?存放我们变换信息的容器,Matrix的所有方法都是针对其自身的!!!!当我们把所有的变换操作做完后再“一次性地”把它注入我们想要的地方,比如上面我们为shader注入了一个Matrix。还有一点要注意,一定要注意:ColorMatrix和Matrix在应用给其他对象时都是左乘的,而其自身内部是可以左右乘的!千万别搞混了!
上图的公式中,GHI都表示的是透视参数,一般情况下我们不会去处理,三维的透视我更乐意使用Camare,所以很多时候G和H的值都为0而I的值恒为1
所有的Matrix变换中最好理解的其实是缩放变换,因为缩放的本质其实就是图形上所有的点X/Y轴沿着中心点放大一定的倍数,比如:
这么一个矩阵变换实质就是x = x0 * a、y = y0 * b,难度系数:0
X/Y轴分别放大a\b倍 相对来说平移稍难但是也好理解:
同理x = x0 + a、y = y0 + b,难度系数:0
旋转就很复杂了……分为两种:一种是直接绕默认中点[0,0]旋转,另一种是指定中点,也就是将中点[0,0]平移后在旋转: 直接绕[0,0]顺时针转:
唉、这个先看图吧:
根据三角函数的关系我们可以得出p(x,y)的坐标:
同样根据三角函数的关系我们也可以得出p(x0,y0)的坐标:
上述两公式结合我们则可以得出简化后的p(x,y)的坐标:
这是什么公式呢?是不是就是上面矩阵的乘积呢?囧……
绕点p(a,b)顺时针转: 其实绕某个点旋转没有想象中的那么复杂,相对于绕中心点来说就多了两步:先将坐标原点移到我们的p(a,b)处然后执行旋转最后再把坐标圆点移回去:
对了……开头忘说了……矩阵的乘法是从右边开始的,额,其实也只有上面这算式才有多个矩阵相乘 = = 冏,也就是说最右边的两个会先乘,大家看看最右边的两个乘积是什么……是不是就是我们把原点移动到P(a,b)后[x0,y0]的新坐标啊?然后继续往左乘,旋转一定得角度这跟上面[0,0]旋转是一样的,最后往左乘把坐标还原
Android给我们封装的方法:setXXX会重置数据
matrix.preScale(0.5f, 1); matrix.setScale(1, 0.6f); matrix.postScale(0.7f, 1); matrix.preTranslate(15, 0);那么Matrix的计算过程即为:translate (15, 0) -> scale (1, 0.6f) -> scale (0.7f, 1),我们说过set会重置数据,所以最开始的
matrix.preScale(0.5f, 1);也就GG了 同样地,对于类似的变换:
matrix.preScale(0.5f, 1); matrix.preTranslate(10, 0); matrix.postScale(0.7f, 1); matrix.postTranslate(15, 0);其计算过程为:translate (10, 0) -> scale (0.5f, 1) -> scale (0.7f, 1) -> translate (15, 0)
那么对于上图的结果真的是一样的吗?这里我教给大家一个方法自己去验证,Matrix有一个getValues方法可以获取当前Matrix的变换浮点数组,也就是我们之前说的矩阵:
/* * 新建一个9个单位长度的浮点数组 * 因为我们的Matrix矩阵是9个单位长的对吧 */ float[] fs = new float[9]; // 将从matrix中获取到的浮点数组装载进我们的fs里 matrix.getValues(fs); Log.d("Aige", Arrays.toString(fs));// 输出看看呗!大家觉得好奇的都可以去验证,这三类方法我就不多说了,Matrix中还有其他很多实用的方法,以后我们用到的时候在讲,因为Matrix太常用了
上面我们说到Matrix矩阵最后的3个数是用来设置透视变换的,为什么最后一个值恒为1?因为其表示的是在Z轴向的透视缩放,这三个值都可以被设置,前两个值跟右手坐标系的XY轴有关,大家可以尝试去发现它们之间的规律,我就不多说了。这里多扯一点,大家一定要学会如何透过现象看本质,即便看到的本质不一定就是实质,但是离实质已经不远了,不要一来就去追求什么底层源码啊、逻辑什么的,就像上面的矩阵变换一样,矩阵的9个数作用其实很多人都说不清,与其听别人胡扯还不如自己动手试试你说是吧,不然苦逼的只是你自己。 在实际应用中我们极少会使用到Matrix的尾三数做透视变换,更多的是使用Camare摄像机,比如我们使用Camare让ListView看起来像倒下去一样:(这里只做了解,已超出我们本系列的范畴)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.xiey94.view.view.ag.view4.AnimListView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="wrap_content"></com.xiey94.view.view.ag.view4.AnimListView> </LinearLayout> public class AnimListView extends ListView { //相机 private Camera mCamera; //矩阵 private Matrix mMatrix; public AnimListView(Context context, AttributeSet attrs) { super(context, attrs); mCamera = new Camera(); mMatrix = new Matrix(); } @Override protected void onDraw(Canvas canvas) { //初始保存当前绘制 mCamera.save(); //X轴位面旋转30度,三维 mCamera.rotate(30, 0, 0); //获取矩阵 mCamera.getMatrix(mMatrix); //设置变换矩阵 mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2); mMatrix.postTranslate(getWidth() / 2, getHeight() / 2); //设置变换矩阵:先旋转,变换矩阵,然后平移后矩阵乘以旋转矩阵,最后再乘以平移矩阵,不是两个矩阵的无用操作,而是三个矩阵的操作 canvas.concat(mMatrix); super.onDraw(canvas); //还原画布,保留绘制 mCamera.restore(); } }矩阵这一块确实比较头疼,之前的矩阵没学好,所以还得在这里跟着推理计算。
最后绘制的那个图,开始是不想敲的,但是反过来想象,给了自己一巴掌,敲!
public class MultiCricleView extends View { /** * 描边宽度占比 */ public static final float STROKE_WIDTH = 1F / 256F, /** * 大圆小圆线段两端间隔占比 */ SPACE = 1F / 64F, /** * 线段长度占比<连接线> */ LINE_LENGTH = 3F / 32F, /** * 大圆半径占比 */ CRICLE_LARGER_RADIU = 3F / 32F, /** * 小圆半径 */ CRICLE_SMALL_RADIU = 5F / 64F, /** * 弧半径 */ ARC_RADIU = 1F / 8F, /** * 弧围绕文字半径 */ ARC_TEXT_RADIU = 5F / 32F; /** * 描边画笔、文字画笔、圆弧画笔 */ private Paint strokePaint, textPaint, arcPaint; /** * 控件边长 */ private int size; /** * 描边宽度 */ private float strokeWidth; /** * 中心圆圆心坐标 */ private float ccX, ccY; /** * 大圆半径 */ private float largeCircleRadiu; /** * 线段长宽 */ private float lineLength; /** * 大圆小圆线段两端间隔 */ private float space; /** * 小圆半径 */ private float smallCircleRadiu; /** * 文字的Y轴偏移量 */ private float textOffsetY; private enum Type { LARGER, SAMLL } //-----------------------------------------属性分割线--------------------------------------------- //------------------------------------------构造函数---------------------------------------------- /** * 构造函数 */ public MultiCricleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); //初始化画笔 initPaint(context); } //-----------------------------------------初始化画笔--------------------------------------------- /** * 初始化画笔 */ private void initPaint(Context context) { /** * 初始化描边画笔 */ //抗锯齿、抗抖动 strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); //风格:描边 strokePaint.setStyle(Paint.Style.STROKE); //画笔颜色:白色 strokePaint.setColor(Color.WHITE); //画笔圆润 strokePaint.setStrokeCap(Paint.Cap.ROUND); /** * 初始化文字画笔 */ textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.SUBPIXEL_TEXT_FLAG); textPaint.setColor(Color.WHITE); textPaint.setTextSize(30); //绘制到中间区域 textPaint.setTextAlign(Paint.Align.CENTER); //计算文字画笔Y轴偏移量 textOffsetY = (textPaint.descent() + textPaint.ascent()) / 2; /** * 圆弧画笔初始化 */ arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); //风格:描边 arcPaint.setStyle(Paint.Style.STROKE); //画笔颜色:白色 arcPaint.setColor(Color.WHITE); //画笔圆润 arcPaint.setStrokeCap(Paint.Cap.ROUND); } //--------------------------------------------测量----------------------------------------------- /** * 测量 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //强制宽高一致 super.onMeasure(widthMeasureSpec, widthMeasureSpec); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { //控件宽度 size = w; //参数计算 calculation(); } //------------------------------------------参数计算---------------------------------------------- /** * 参数计算 */ private void calculation() { //计算描边宽度 strokeWidth = STROKE_WIDTH * size; //计算大圆半径 largeCircleRadiu = size * CRICLE_LARGER_RADIU; //计算线段长度 lineLength = size * LINE_LENGTH; //计算大圆小圆线段两端间隔 space = size * SPACE; //计算小圆半径 smallCircleRadiu = size * CRICLE_SMALL_RADIU; //计算中心圆圆心坐标 ccX = size / 2; ccY = size / 2 + size * CRICLE_LARGER_RADIU; //设置参数 setPara(); } //------------------------------------------设置参数---------------------------------------------- /** * 设置参数 */ private void setPara() { //设置描边宽度 strokePaint.setStrokeWidth(strokeWidth); arcPaint.setStrokeWidth(strokeWidth); } //--------------------------------------------绘制----------------------------------------------- /** * 绘制 */ @Override protected void onDraw(Canvas canvas) { //绘制背景 canvas.drawColor(0xFFF29B76); //绘制中心圆 canvas.drawCircle(ccX, ccY, largeCircleRadiu, strokePaint); //绘制中心圆文字 canvas.drawText("Studio", ccX, ccY - textOffsetY, textPaint); //绘制左上方圆形 drawTopLeft(canvas); //绘制右上方图形 drawTopRight(canvas); //绘制左下方图形 drawBottomLeft(canvas); //绘制下方图形 drawBottom(canvas); //绘制右下方图形 drawBottomRight(canvas); } //----------------------------------------绘制左上方圆形------------------------------------------- /** * 绘制左上方圆形 */ private void drawTopLeft(Canvas canvas) { //锁定画布 canvas.save(); //平移和旋转画布 canvas.translate(ccX, ccY); canvas.rotate(-30); //依次画:线、圈、线、圈 canvas.drawLine(0, -largeCircleRadiu, 0, -lineLength * 2, strokePaint); canvas.drawCircle(0, -lineLength * 3, largeCircleRadiu, strokePaint); canvas.drawText("Apple", 0, -lineLength * 3 - textOffsetY, textPaint); canvas.drawLine(0, -largeCircleRadiu * 4, 0, -lineLength * 5, strokePaint); canvas.drawCircle(0, -lineLength * 6, largeCircleRadiu, strokePaint); canvas.drawText("Orange", 0, -lineLength * 6 - textOffsetY, textPaint); //释放画布 canvas.restore(); } //----------------------------------------绘制右上方圆形------------------------------------------- /** * 绘制右上方图形 */ private void drawTopRight(Canvas canvas) { //锁定画布 canvas.save(); //平移和旋转画布 canvas.translate(ccX, ccY); canvas.rotate(30); //依次画:线、圆 canvas.drawLine(0, -largeCircleRadiu, 0, -lineLength * 2, strokePaint); canvas.drawCircle(0, -lineLength * 3, largeCircleRadiu, strokePaint); canvas.drawText("Tropical", 0, -lineLength * 3 - textOffsetY, textPaint); drawTopRightArc(canvas, -lineLength * 3); //释放画布 canvas.restore(); } //----------------------------------------绘制左下方圆形------------------------------------------- /** * 绘制左下方图形 */ private void drawBottomLeft(Canvas canvas) { //锁定画布 canvas.save(); //平移和旋转画布 canvas.translate(ccX, ccY); canvas.rotate(-100); //依次画:线、圆 canvas.drawLine(0, -largeCircleRadiu - space, 0, -lineLength * 2 - space, strokePaint); canvas.drawCircle(0, -lineLength * 2 - smallCircleRadiu - space * 2, smallCircleRadiu, strokePaint); canvas.save(); canvas.translate(0, -lineLength * 2 - smallCircleRadiu - space * 2); canvas.rotate(100); canvas.drawText("Duck", 0, -textOffsetY, textPaint); canvas.restore(); //释放画布 canvas.restore(); } //----------------------------------------绘制正下方圆形------------------------------------------- /** * 绘制正下方图形 */ private void drawBottom(Canvas canvas) { // 锁定画布 canvas.save(); // 平移和旋转画布 canvas.translate(ccX, ccY); canvas.rotate(180); // 依次画:(间隔)线(间隔)-圈 canvas.drawLine(0, -largeCircleRadiu - space, 0, -lineLength * 2 - space, strokePaint); canvas.drawCircle(0, -lineLength * 2 - smallCircleRadiu - space * 2, smallCircleRadiu, strokePaint); canvas.save(); canvas.translate(0, -lineLength * 2 - smallCircleRadiu - space * 2); canvas.rotate(180); canvas.drawText("Cat", 0, -textOffsetY, textPaint); canvas.restore(); // 释放画布 canvas.restore(); } //----------------------------------------绘制右下方圆形------------------------------------------- /** * 绘制右下方图形 */ private void drawBottomRight(Canvas canvas) { // 锁定画布 canvas.save(); // 平移和旋转画布 canvas.translate(ccX, ccY); canvas.rotate(100); // 依次画:(间隔)线(间隔)-圈 canvas.drawLine(0, -largeCircleRadiu - space, 0, -lineLength * 2 - space, strokePaint); canvas.drawCircle(0, -lineLength * 2 - smallCircleRadiu - space * 2, smallCircleRadiu, strokePaint); canvas.save(); canvas.translate(0, -lineLength * 2 - smallCircleRadiu - space * 2); canvas.rotate(-100); canvas.drawText("Dog", 0, -textOffsetY, textPaint); canvas.restore(); // 释放画布 canvas.restore(); } /** * 绘制右上角弧形 */ private void drawTopRightArc(Canvas canvas, float circleY) { canvas.save(); canvas.translate(0, circleY); canvas.rotate(-30); float arcRadiu = size * ARC_RADIU; RectF oval = new RectF(-arcRadiu, -arcRadiu, arcRadiu, arcRadiu); arcPaint.setStyle(Paint.Style.FILL); arcPaint.setColor(0x55EC6941); canvas.drawArc(oval, -22.5F, -135, true, arcPaint); arcPaint.setStyle(Paint.Style.STROKE); arcPaint.setColor(Color.WHITE); canvas.drawArc(oval, -22.5F, -135, false, arcPaint); float arcTextRadiu = size * ARC_TEXT_RADIU; canvas.save(); // 把画布旋转到扇形左端的方向 canvas.rotate(-135F / 2F); /* * 每隔33.75度角画一次文本 */ for (float i = 0; i < 5 * 33.75F; i += 33.75F) { canvas.save(); canvas.rotate(i); canvas.drawText("Aige", 0, -arcTextRadiu, textPaint); canvas.restore(); } canvas.restore(); canvas.restore(); } }再看一眼,还好没放弃!!!
根据爱哥留下的问题,并在看过评论区和自己思考之后,修改了一下。
这个画布的旋转差点让我疯了,开始想着到底是怎么转的,然后跟着爱哥的思路走,但是也只能跟着别人的思路走,自己大部分还是转不动的,然后在评论中看到那个转文字之后,自己尝试了一下,然后抠破了一张纸,自己就用这两张纸转来转去,终于转出点思路来: 当save的时候,保留了canvas的位置信息(只说位置); 平移:先把要绘制的图形起始位置拽到原点,便于计算; 旋转:光平移还不行,可能相应的角度并不是针对的xy坐标系的正(负)方向,毕竟在xy轴上的数字才比较好计算,不然还得去用三角函数计算位置,把方向也转过来就方便多了。 restore,这个就是相当于画布就是一个弹簧,之前我们又是拽又是转的,目的达到之后,当然得让他弹回去,便于下一次的操作。
难一点的就是那个字体的旋转和平移:
就说左下方的那个小圆中的文字; save:记录位置(原始位置); 平移:拽到原点,便于计算; 旋转:摆正方向,便于计算; 画线、画圆 画字:字的位置应该在线的延长线上看起来才算是合理、美观; 但是,这时候,线是竖着的,难道字要竖着写? 何必呢,老规矩嘛: 要画字是吧!更简单一点,一样可以把画布在此基础上再拉扯拉扯嘛!
save:记录当前位置信息(第二个位置); 平移:先把画布拽下来,拽到原点; 旋转,要想达到那种效果,最后我们反着角度转回去,(就当前的位置而言,和我们最终的位置尤其是那个线应该是平行的,这样才能达到效果,所以反着转当初转过来的角度) 然后开始画字 restore:画完得弹到第二个位置上; …… 其他操作 …… restore:全部画完了弹到起始位置。
个人见解,不对求教!
参考1–Android canvas.drawArc() 画圆弧 参考2–安卓开发——详解camera.rotate(x,y,z);的旋转方向 参考3–Android Graphics专题(1)— Canvas基础 参考4–canvas.draw(bitmap,matrix,paint)有什么区别呢? 参考5–Canvas 中 concat 与 setMatrix