Android 绘制动画(波浪动画/轨迹动画/PathMeasure)

Android 绘制动画(波浪动画/轨迹动画/PathMeasure)


本文由 Luzhuo 编写,转发请保留该信息.
原文: https://blog.csdn.net/rozol/article/details/79730582 java


绘制动画, 由Android的绘画功能 + 属性动画 组成的一种动画web

主要方法

  1. valueAnimator.addUpdateListener(AnimatorUpdateListener) // 监听动画数值更新
  2. 估值器canvas

    ValueAnimator.ofObject(new TypeEvaluator<PointF>() {
        @Override
        public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
            // fraction(时间因子[0,1]), startValue(开始值), endValue(结束值)
            return null; // 返回计算结果值
        }
    });

波浪动画

  • 原理: 经过动画计算的数值, 不断将波浪右移.
    ide

  • 代码svg

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath.moveTo(-waveLength + offset, centerY);
    
        // 绘制2个贝塞尔曲线
        for (int i = 0; i < waveCount; i++){
            mPath.quadTo( - waveLength * 3 / 4 + i * waveLength + offset, centerY + 60, - waveLength / 2 + i * waveLength + offset, centerY);
            mPath.quadTo( - waveLength / 4 + i * waveLength + offset, centerY - 60, i * waveLength + offset, centerY);
        }
    
        // 封闭波浪
        mPath.lineTo(screenWidth, screenHeight);
        mPath.lineTo(0, screenHeight);
        mPath.close();
    
        canvas.drawPath(mPath, mPaintBezier);
    }
    
    public void onClick(View v) {
        // 设置属性动画
        valueAnimator = valueAnimator.ofInt(0, waveLength);
        valueAnimator.setDuration(1000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                offset = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();
    }
  • 效果

轨迹动画

  • 原理: 使用估值器计算出动画执行时间点的坐标结果, 而后将黑色小球按该坐标结果绘制post

  • 代码动画

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(movePointX, movePointY, 20, paintCircle);
    }
    
    public void onClick(View v) {
        // 运动轨迹
        BezierEvaluator evaluator = new BezierEvaluator(new PointF(flagPointX, flagPointY));
        ValueAnimator animator = ValueAnimator.ofObject(evaluator,
                new PointF(startPointX, startPointY), new PointF(endPointX, endPointY));
        animator.setDuration(600);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF) animation.getAnimatedValue();
                movePointX = (int) pointF.x;
                movePointY = (int) pointF.y;
                invalidate();
            }
        });
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.start();
    }
    
    public class BezierEvaluator implements TypeEvaluator<PointF> {
    
        private PointF flagPoint;
    
        public BezierEvaluator(PointF flagPoint){
            this.flagPoint = flagPoint;
        }
    
        @Override
        public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
            // fraction(时间因子[0,1]), startValue(开始值), endValue(结束值)
            return BezierUtil.getCalculateBezierPointForQuadratic(fraction, startValue, flagPoint, endValue);
        }
    }
    
    /** * 获取二阶贝塞尔曲线点的坐标 * B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1] * * @param t 曲线长度比例 * @param p0 起始点 * @param p1 控制点 * @param p2 终止点 * @return t对应的点 */
    public static PointF getCalculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) {
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
        point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
        return point;
    }
  • 效果

PathMeasure (Path测量类)

基本使用

public void onclick(View view){
    Bitmap copybitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888); // 白纸
    Canvas canvas = new Canvas(copybitmap); // 画布
    Paint paint = new Paint(); // 画笔
    paint.setStyle(Paint.Style.STROKE);


    Path path = new Path();
    Path dst = new Path();
    path.addCircle(300, 300, 200, Path.Direction.CW);

    PathMeasure pm;
    // 测量Path的类
    pm= new PathMeasure();
    // pm = new PathMeasure(path, true);

    pm.setPath(path, true); // 设置path
    float length = pm.getLength(); // 获取Path长度
    // 截取片断 (参数:起始截取位置, 结束截取位置, 截取的path添加到dst, 是否从起点开始截) 返回:true存入dst中
    boolean segment = pm.getSegment(0 + 100, length - 100, dst, true);
    // 选择下个path
    boolean next = pm.nextContour();
    // 获取指定位置的 坐标 和 该坐标的正切值 (参数:位置, 坐标, 正切值) 返回:true将数据存入 pos 和 tan 中,
    float[] pos = new float[2], tan = new float[2];
    boolean postan = pm.getPosTan(300, pos, tan);
    // 获取指定位置的 坐标 和 该坐标的矩阵 (参数:位置, 矩阵, 将什么存入矩阵[POSITION_MATRIX_FLAG(坐标) / TANGENT_MATRIX_FLAG(正切)]) 返回:true将数据存入 matrix 中
    Matrix matrix = new Matrix();
    boolean m = pm.getMatrix(300, matrix, PathMeasure.TANGENT_MATRIX_FLAG);

    // 若是清除dst里的内容, 修改后加上该代码, 用以解决 Android 4.4- 开启硬件加速的bug
    // dst.reset();
    // dst.lineTo(0, 0);

    // 记算路径上某点的切线的角度(Math.atan2(纵坐标, 横坐标))
    float degree = (float) (Math.atan2(tan[1], tan[0]) * 180 / Math.PI);

    canvas.drawPath(dst, paint);
    iv.setImageBitmap(copybitmap);
}

案例: 缓冲圆圈

  • 实现this

    public PathView(Context context, AttributeSet attrs) {
        super(context, attrs);
    
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    
        path = new Path();
        dst = new Path();
    
        // 画一个圆, 关联pathMeasure
        path.addCircle(400, 400, 100, Path.Direction.CW);
        pathMeasure = new PathMeasure(); // 测量Path的类
        pathMeasure.setPath(path, true);
    
        length = pathMeasure.getLength(); // 路径长度
    
        // 建立属性动画
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setDuration(1000);
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                animValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }
    
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        dst.reset();
        dst.lineTo(0, 0); // 解决硬件加速的bug
    
        float stop = length * animValue;
        float start = (float) (stop - ((0.5 - Math.abs(animValue - 0.5)) * length));
        // 从起点开始截取, 路径将愈来愈长
        pathMeasure.getSegment(start, stop, dst, true); // 截取整个path的任何片断(开始长度 / 结束长度 / 保存截取的路径 / 是否从起点开始截取)
        canvas.drawPath(dst, paint);
    }
  • 上述主要使用pathMeasure.getSegment(start, stop, dst, true);对path进行截取, 而后进行重绘; 除此以外还能够使用Path虚线类, 改变起始偏移量实现相似截取的效果, 因为是经过起始偏移量实现的, 因此只能从头部开始.lua

    public void onAnimationUpdate(ValueAnimator valueAnimator) {
         animValue = (float) valueAnimator.getAnimatedValue();
         // 初始化路径风格(float intervals[]:实现的长度, float phase:起始偏移量)
         effect = new DashPathEffect(new float[]{length, length}, length * animValue);
         paint.setPathEffect(effect);
         invalidate();
     }
  • 效果