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看起来像倒下去一样:(这里只做了解,已超出我们本系列的范畴)
"1.0" encoding="utf-8"?>
"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>
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