View自定义学习摘要笔记(二)

学习资料: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

此类主要是用来测量Path的类,主要有以下方法:
这里写图片描述
这里写图片描述

无参构造函数 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 等方法是针对当前的曲线(其它方法请自行验证)。

getPosTan
这里写图片描述
这里写图片描述
这里写图片描述

上图中箭头在沿着 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(后的意思)的因为是左乘,所以它会放在最左边,那么就会最后执行

Path & SVG

SVG 是一种矢量图,内部用的是 xml 格式化存储方式存储这操作和数据,你完全可以将 SVG 看作是 Path 的各项操作简化书写后的存储格式。
详细介绍及下载:
https://bigbadaboom.github.io/androidsvg/

Matrix详解

这里写图片描述
创建一个全新的Matrix,使用格式如下:

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);
    }
}

效果图:这里写图片描述

setRectToRect
这里写图片描述

需要学习的博客:
打造个性的图片预览与多点触控
Android FoldingLayout 折叠布局 原理及实现(一)
Android FoldingLayout 折叠布局 原理及实现(二)
从零开始打造一个Android 3D立体旋转容器
效果:
这里写图片描述

Matrix Camera

这里写图片描述
这里写图片描述
2D和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)移动到原来的位置

官方示例-Rotate3dAnimation

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() );
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值