学习资料:1、Canvas之translate、scale、rotate、skew方法讲解!
2、安卓自定义View教程
3、《android群英传》徐宜生
4、Android Canvas的save(),saveLayer()和restore()浅谈
5、Approximate a circle with cubic Bézier curves
无参构造函数 PathMeasure ()
创建一个PathMeasure(),注意:使用时需要用setPath(…)指定一个需要测量的Path。
有参构造函数:PathMeasure (Path path, boolean forceClosed)
注意:如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。
Note:
不论 forceClosed 设置为何种状态(true 或者 false), 都不会影响原有Path的状态,即 Path 与 PathMeasure 关联之后,之前的的 Path 不会有任何改变。
forceClosed 的设置状态可能会影响测量结果,如果 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,获取到到是该 Path 闭合时的状态。forceClosed 为 false 测量的是当前 Path 状态的长度, forceClosed 为 true,则不论Path是否闭合测量的都是 Path 的闭合长度。
getSegment
如果 startWithMoveTo 为 true, 则被截取出来到Path片段保持原状,如果 startWithMoveTo 为 false,则会将截取出来的 Path 片段的起始点移动到 dst 的最后一个点,以保证 dst 的连续性。
nextContour
我们知道 Path 可以由多条曲线构成,但不论是 getLength , getgetSegment 或者是其它方法,都只会在其中第一条线段上运行,而这个 nextContour 就是用于跳转到下一条曲线到方法,如果跳转成功,则返回 true, 如果跳转失败,则返回 false。
1.曲线的顺序与 Path 中添加的顺序有关。
2.getLength 获取到到是当前一条曲线的长度,而不是整个 Path 的长度。
3.getLength 等方法是针对当前的曲线(其它方法请自行验证)。
上图中箭头在沿着 Path 运动时,方向始终与 Path 走向保持一致,保持方向主要就是依靠 tan
源码:
public class Demo4 extends View {
private float[] mPos;
private float[] mTan;
private Bitmap mArrows;
private float mCurrentValues;
private Matrix mMatrix;
public Demo4(Context context) {
super(context);
init(context);
}
public Demo4(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public Demo4(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@TargetApi(21)
public Demo4(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.BLACK);
paint.setStrokeWidth(5);
canvas.translate(getWidth() / 2, getHeight() / 2);
Path path = new Path();
path.addCircle(0, 0, 200, Path.Direction.CW);
PathMeasure measure = new PathMeasure(path, false);
mCurrentValues += 0.005;// 计算当前的位置在总长度上的比例[0,1]
if (mCurrentValues >= 1) {
mCurrentValues = 0;
}
measure.getPosTan(measure.getLength() * mCurrentValues, mPos, mTan);// 获取当前位置的坐标以及趋势
mMatrix.reset();
// 计算图片旋转角度
float degrees = (float) (Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);
// 旋转图片
mMatrix.postRotate(degrees, mArrows.getWidth() / 2, mArrows.getHeight() / 2);
// 将图片绘制中心调整到与当前点重合
mMatrix.postTranslate(mPos[0] - mArrows.getWidth() / 2, mPos[1] - mArrows.getHeight() / 2);
canvas.drawPath(path, paint);
canvas.drawBitmap(mArrows, mMatrix, paint);
invalidate();
}
public void init(Context context) {
mTan = new float[2];
mPos = new float[2];
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 6;
mArrows = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrows, options);
mMatrix = new Matrix();
}
}
getMatrix
我们可以将上面都例子中 getPosTan 替换为 getMatrix, 看看是不是会显得简单很多:
具体绘制:
Path path = new Path(); // 创建 Path
path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一个圆形
PathMeasure measure = new PathMeasure(path, false); // 创建 PathMeasure
currentValue += 0.005; // 计算当前的位置在总长度上的比例[0,1]
if (currentValue >= 1) {
currentValue = 0;
}
// 获取当前位置的坐标以及趋势的矩阵
measure.getMatrix(measure.getLength() * currentValue, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2); // 将图片绘制中心调整到与当前点重合。(注意:此处是前乘pre)
canvas.drawPath(path, mDeafultPaint); // 绘制 Path
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint); // 绘制箭头
invalidate(); // 重绘页面
上述两种方法的对比:
measure.getPosTan(measure.getLength() * mCurrentValues, mPos, mTan);// 获取当前位置的坐标以及趋势
//计算图片旋转角度
float degrees = (float) (Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);
//旋转图片
mMatrix.postRotate(degrees, mArrows.getWidth() / 2, mArrows.getHeight() / 2);
//将图片绘制中心调整到与当前点重合
mMatrix.postTranslate(mPos[0] - mArrows.getWidth() / 2, mPos[1] - mArrows.getHeight() / 2);
measure.getMatrix(measure.getLength() * mCurrentValues, mMatrix,
PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
//将图片绘制中心调整到与当前点重合(注意:此处是前乘pre)
mMatrix.preTranslate(-mArrows.getWidth() / 2, -mArrows.getHeight() / 2);
两种方法都有的部分是:将图片绘制中心调整到与当前点重合
和 获取当前位置的坐标以及趋势
。
不同的是前一种有旋转,而后一种并没有,这是为什么?这就要观察两种方式在获取当前位置的坐标以及趋势
的区别了。第一种是将获取的位置及趋势放在了两个数组中,然后再调用matrix的roate方法将其封装的矩阵中,而第二种是直接封装到了matrix中,实际上已经包含了位置和趋势,所以不用再调用roate方法了。
NOTE:
1.对 matrix 的操作必须要在 getMatrix 之后进行,否则会被 getMatrix 重置而导致无效。
2.矩阵对旋转角度默认为图片的左上角,我们此处需要使用 preTranslate 调整为图片中心。
3.pre(矩阵前乘) 与 post(矩阵后乘) 的区别:
pre是拿当前的矩阵乘以T,而post是拿T来乘以当前的矩阵,(矩阵的乘法是不满足交换率的,所以这两种乘法的结果是不一样的).在图形学中,矩阵M右乘A,表示的是 A * M,而矩阵 M 左乘 A,则表示的是 M * A,可以形象地理解为右乘就是从右边乘进来,左乘就是从左边乘进来。pre其实执行的就是右乘的操作,而post执行的就是左乘的操作。在图像处理中,越靠近右边的矩阵越先执行,所以pre(也就是先的意思)所设置的矩阵就会先执行,而post(后的意思)的因为是左乘,所以它会放在最左边,那么就会最后执行。
SVG 是一种矢量图,内部用的是 xml 格式化存储方式存储这操作和数据,你完全可以将 SVG 看作是 Path 的各项操作简化书写后的存储格式。
详细介绍及下载:
https://bigbadaboom.github.io/androidsvg/
Matrix matrix = new Matrix();
通过这种方式创建出来的并不是一个数值全部为空的矩阵,而是一个单位矩阵,如下:
Matrix 相关的重要知识:
1.一开始从Canvas中获取到到Matrix并不是初始矩阵,而是经过偏移后到矩阵,且偏移距离就是距离屏幕左上角的位置。
这个可以用于判定View在屏幕上的绝对位置,View可以根据所处位置做出调整。
2.构造Matrix时使用的是矩阵乘法,前乘(pre)与后乘(post)结果差别很大。
3.受矩阵乘法影响,后面的执行的操作可能会影响到之前的操作。使用时需要注意构造顺序。
特殊方法
setPolyToPoly:
boolean setPolyToPoly (
float[] src, // 原始数组 src [x,y],存储内容为一组点
int srcIndex, // 原始数组开始位置
float[] dst, // 目标数组 dst [x,y],存储内容为一组点
int dstIndex, // 目标数组开始位置
int pointCount) // 测控点的数量 取值范围是: 0到4
从参数我们可以了解到setPolyToPoly最多可以支持4个点,这四个点通常为图形的四个角,可以通过这四个角将视图从矩形变换成其他形状。
示例:
public class MatrixSetPolyToPolyTest extends View {
private Bitmap mBitmap; // 要绘制的图片
private Matrix mPolyMatrix; // 测试setPolyToPoly用的Matrix
public MatrixSetPolyToPolyTest(Context context) {
super(context);
initBitmapAndMatrix();
}
private void initBitmapAndMatrix() {
mBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.poly_test);
mPolyMatrix = new Matrix();
float[] src = {0, 0, // 左上
mBitmap.getWidth(), 0, // 右上
mBitmap.getWidth(), mBitmap.getHeight(), // 右下
0, mBitmap.getHeight()}; // 左下
float[] dst = {0, 0, // 左上
mBitmap.getWidth(), 400, // 右上
mBitmap.getWidth(), mBitmap.getHeight() - 200, // 右下
0, mBitmap.getHeight()}; // 左下
// 核心要点
mPolyMatrix.setPolyToPoly(src, 0, dst, 0, src.length >> 1); // src.length >> 1 为位移运算 相当于处以2
// 此处为了更好的显示对图片进行了等比缩放和平移(图片本身有点大)
mPolyMatrix.postScale(0.26f, 0.26f);
mPolyMatrix.postTranslate(0,200);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 根据Matrix绘制一个变换后的图片
canvas.drawBitmap(mBitmap, mPolyMatrix, null);
}
}
需要学习的博客:
打造个性的图片预览与多点触控
Android FoldingLayout 折叠布局 原理及实现(一)
Android FoldingLayout 折叠布局 原理及实现(二)
从零开始打造一个Android 3D立体旋转容器
效果:
常用方法
这两个方法是Camera中最基础也是最常用的方法。
getMatrix
void getMatrix (Matrix matrix)
计算当前状态下矩阵对应的状态,并将计算后的矩阵赋值给参数matrix。
applyToCanvas
void applyToCanvas (Canvas canvas)
计算当前状态下单矩阵对应的状态,并将计算后的矩阵应用到指定的canvas上。
void translate (float x, float y, float z)
沿x轴平移
camera.translate(x, 0, 0);
matrix.postTranslate(x, 0);
两者x轴同向,所以 Camera 和 Matrix 在沿x轴平移上是一致的。
一致是指平移方向和平移距离一致,在默认情况下,上面两种均可以让坐标系向右移动x个单位。
沿y轴平移
这个就有点意思了,两个坐标系相互关联,但是两者的y轴方向是相反的,很容易把人搞迷糊。你可以这么玩:
Camera camera = new Camera();
camera.translate(0, 100, 0); // camera - 沿y轴正方向(向上)平移100像素
Matrix matrix = new Matrix();
camera.getMatrix(matrix);
matrix.postTranslate(0,100); // matrix - 沿y轴正方向(向下)平移100像素
在上面这种写法,虽然用了5行代码,但是效果却和 Matrix matrix = new Matrix(); 一样,结果都是单位矩阵。而且看起来貌似没有啥问题,毕竟两次平移都是正向100。
Matrix: [1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]
结论:
由于两者y轴相反,所以 camera.translate(0, -y, 0); 与 matrix.postTranslate(0, y);平移方向和距离一致,在默认情况下,这两种方法均可以让坐标系向下移动y个单位。
// (API 12) 可以控制View同时绕x,y,z轴旋转,可以由下面几种方法复合而来。
void rotate (float x, float y, float z);
// 控制View绕单个坐标轴旋转
void rotateX (float deg);
void rotateY (float deg);
void rotateZ (float deg);
旋转中心
旋转中心默认是坐标原点,对于图片来说就是左上角位置。
我们都知道,在2D中,不论是旋转,错切还是缩放都是能够指定操作中心点位置的,但是在3D中却没有默认的方法,如果我们想要让图片围绕中心点旋转怎么办? 这就要使用到我们在Matrix原理提到过的方法:
Matrix temp = new Matrix(); // 临时Matrix变量
this.getMatrix(temp); // 获取Matrix
temp.preTranslate(-centerX, -centerY); // 使用pre将旋转中心移动到和Camera位置相同。
temp.postTranslate(centerX, centerY); // 使用post将图片(View)移动到原来的位置
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
/** * 创建一个绕y轴旋转的3D动画效果,旋转过程中具有深度调节,可以指定旋转中心。 * * @param fromDegrees 起始时角度 * @param toDegrees 结束时角度 * @param centerX 旋转中心x坐标 * @param centerY 旋转中心y坐标 * @param depthZ 最远到达的z轴坐标 * @param reverse true 表示由从0到depthZ,false相反 */
public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
// 调节深度
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
// 绕y轴旋转
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
// 调节中心点
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
实现效果:
可以看到,有失真现象。
解决方法:只要修改两个数值就可以了,这两个数值就是在Matrix中一直被众多开发者忽略的 MPERSP_0 和 MPERSP_1
修改后的代码:
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
float scale = 1; // <------- 像素密度
/** * 创建一个绕y轴旋转的3D动画效果,旋转过程中具有深度调节,可以指定旋转中心。 * @param context <------- 添加上下文,为获取像素密度准备 * @param fromDegrees 起始时角度 * @param toDegrees 结束时角度 * @param centerX 旋转中心x坐标 * @param centerY 旋转中心y坐标 * @param depthZ 最远到达的z轴坐标 * @param reverse true 表示由从0到depthZ,false相反 */
public Rotate3dAnimation(Context context, float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
// 获取手机像素密度 (即dp与px的比例)
scale = context.getResources().getDisplayMetrics().density;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
// 调节深度
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
// 绕y轴旋转
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
// 修正失真,主要修改 MPERSP_0 和 MPERSP_1
float[] mValues = new float[9];
matrix.getValues(mValues); //获取数值
mValues[6] = mValues[6]/scale; //数值修正
mValues[7] = mValues[7]/scale; //数值修正
matrix.setValues(mValues); //重新赋值
// 调节中心点
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
修改后效果图:
摄像机右移等于View左移
View的状态只取决于View和摄像机之间的相对位置,不过由于单位不同,摄像机平移一个单位等于View平移72个像素。下面两段代码是等价的:
Camera camera = new Camera();
camera.setLocation(1,0,-8); // 摄像机默认位置是(0, 0, -8)
Matrix matrix = new Matrix();
camera.getMatrix(matrix);
Log.e(TAG, "location: "+matrix.toShortString() );
Camera camera2 = new Camera();
camera2.translate(-72,0,0);
Matrix matrix2 = new Matrix();
camera2.getMatrix(matrix2);
Log.e(TAG, "translate: "+matrix2.toShortString() );