Android动画(四):PathMeasure实现路径动画

本系列将介绍以下内容:
在这里插入图片描述

Android动画

1 PathMeasure概述

PathMeasure是一个单独的类,其全部源码如下(请详细研读注释):

package android.graphics;

public class PathMeasure {
    private Path mPath;
    
    public PathMeasure() {
        mPath = null;
        native_instance = native_create(0, false);
    }
    
    /**
     * @param forceClosed If true, then the path will be considered as "closed"
     *        even if its contour was not explicitly closed.
     *       如果为 "true",则路径将被视为 "封闭" 即使其轮廓没有明确封闭。
     */
    public PathMeasure(Path path, boolean forceClosed) {
        // The native implementation does not copy the path, prevent it from being GC'd
        mPath = path;
        native_instance = native_create(path != null ? path.readOnlyNI() : 0,
                                        forceClosed);
    }

    public void setPath(Path path, boolean forceClosed) {
        mPath = path;
        native_setPath(native_instance,
                       path != null ? path.readOnlyNI() : 0,
                       forceClosed);
    }

	/**
     * Return the total length of the current contour, or 0 if no path is
     * associated with this measure object.
     * 返回当前轮廓的总长度,如果此测量对象没有关联路径,则返回 0。
     */
    public float getLength() {
        return native_getLength(native_instance);
    }

    public boolean getPosTan(float distance, float pos[], float tan[]) {
        if (pos != null && pos.length < 2 ||
            tan != null && tan.length < 2) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return native_getPosTan(native_instance, distance, pos, tan);
    }

    public static final int POSITION_MATRIX_FLAG = 0x01;    // must match flags in SkPathMeasure.h
    public static final int TANGENT_MATRIX_FLAG  = 0x02;    // must match flags in SkPathMeasure.h

    public boolean getMatrix(float distance, Matrix matrix, int flags) {
        return native_getMatrix(native_instance, distance, matrix.ni(), flags);
    }
	
	/**
     * @param dst 将截取的Path添加(不是替换)到dst中。
     * @param startWithMoveTo 起始点是否使用moveTo
     * 
     * 注意:
     * 1、路径截取是以路径的左上角为起始点开始的。
     * 2、路径的截取方向与路径的生成方向相同。
     * 3、截取的Path片段是被添加到路径dst中,而不是替换dst中的内容。
     * 4、如果startWithMoveTo为true,则被截取出来的Path片段保持原状;如果为false,则会将截取出来的Path片段的起始点移动到dst的最后一个点,以保证dst路径的连续性。
     */
    public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
        float length = getLength();
        if (startD < 0) {
            startD = 0;
        }
        if (stopD > length) {
            stopD = length;
        }
        if (startD >= stopD) {
            return false;
        }

        return native_getSegment(native_instance, startD, stopD, dst.mutateNI(), startWithMoveTo);
    }

	/**
     * Return true if the current contour is closed()
     * 如果当前轮廓封闭,则返回 true()
     */
    public boolean isClosed() {
        return native_isClosed(native_instance);
    }

	/**
     * Move to the next contour in the path. Return true if one exists, or
     * false if we're done with the path.
     * 移动到路径中的下一个轮廓。如果存在下一个轮廓,则返回 true;
     * 如果已经完成路径的移动,则返回 false。
     * 
     * 注意:通过该方法得到的曲线的顺序与Path中添加的顺序相同。
     */
    public boolean nextContour() {
        return native_nextContour(native_instance);
    }

    protected void finalize() throws Throwable {
        native_destroy(native_instance);
        native_instance = 0;  // Other finalizers can still call us.
    }

    private static native long native_create(long native_path, boolean forceClosed);
    private static native void native_setPath(long native_instance, long native_path, boolean forceClosed);
    private static native float native_getLength(long native_instance);
    private static native boolean native_getPosTan(long native_instance, float distance, float pos[], float tan[]);
    private static native boolean native_getMatrix(long native_instance, float distance, long native_matrix, int flags);
    private static native boolean native_getSegment(long native_instance, float startD, float stopD, long native_path, boolean startWithMoveTo);
    private static native boolean native_isClosed(long native_instance);
    private static native boolean native_nextContour(long native_instance);
    private static native void native_destroy(long native_instance);

    private long native_instance;
}

PathMeasure的初始化方法是

Path mCirclePath = new Path();

PathMeasure mPathMeasure = new PathMeasure();
mPathMeasure.setPath(mCirclePath, true);

Path mCirclePath = new Path();
PathMeasure mPathMeasure = new PathMeasure(mCirclePath, false);

getLength()、getSegment()都只会针对其中第一条线段进行计算。它们针对的是当前的曲线,而不是整个Path,所以getLength()方法获取到的是当前曲线的长度,而不是整个Path的长度。

2 实现路径加载动画

主要使用PathMeasure的getSegment(x)方法实现动画效果。

直接在布局文件中引用:

<?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.example.myapplication.GetSegmentView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

自定义的GetSegmentView:

package com.example.myapplication;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.*;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;


public class GetSegmentView extends View {
	private Paint mPaint;
	
    private Path mCirclePath, mDstPath;
    private PathMeasure mPathMeasure;
    
    private Float mCurAnimValue;

    public GetSegmentView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mPaint.setColor(Color.BLACK);

        mDstPath = new Path();
        mCirclePath = new Path();
        mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);

        mPathMeasure = new PathMeasure(mCirclePath, true);

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(2000);
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float length = mPathMeasure.getLength();
        float stop = length * mCurAnimValue;
        float start = (float) (stop - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));

		// 清空之前生成的路径
        mDstPath.reset();
        canvas.drawColor(Color.WHITE);

        mPathMeasure.getSegment(0, stop, mDstPath, true);
//        mPathMeasure.getSegment(start, stop, mDstPath, true);

        canvas.drawPath(mDstPath, mPaint);
    }

}

效果图:
在这里插入图片描述
上述动画效果的起始位置是从0开始的,将onDraw(x)中的代码切换,改变动画的起始位置:

//        mPathMeasure.getSegment(0, stop, mDstPath, true);
        mPathMeasure.getSegment(start, stop, mDstPath, true);

效果图:
在这里插入图片描述

3 实现箭头加载动画

利用PathMeasure的getPosTan(x)方法实现箭头加载动画。

箭头资源图片:
在这里插入图片描述
布局文件引用:

<?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.example.myapplication.GetPosTanView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

自定义的GetPosTanView:

package com.example.myapplication;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;


public class GetPosTanView extends View {
    private Paint mPaint;

    private Path mCirclePath, mDstPath;
    private PathMeasure mPathMeasure;

    private Float mCurAnimValue;
    private Bitmap mArrawBmp;

    private float[] pos = new float[2];
    private float[] tan = new float[2];

    public GetPosTanView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        // 缩小箭头图片
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = 6;
        mArrawBmp = BitmapFactory.decodeResource(getResources(), R.drawable.arraw, options);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mPaint.setColor(Color.BLACK);

        mDstPath = new Path();
        mCirclePath = new Path();
        mCirclePath.addCircle(200, 200, 50, Path.Direction.CW);

        mPathMeasure = new PathMeasure(mCirclePath, true);

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(2000);
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);

        float length = mPathMeasure.getLength();
        float stop = length * mCurAnimValue;

        mDstPath.reset();
        mPathMeasure.getSegment(0, stop, mDstPath, true);
        canvas.drawPath(mDstPath, mPaint);

        // 箭头旋转、位移实现方式一,通过getPosTan(x)实现
        mPathMeasure.getPosTan(stop, pos, tan);
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
        Matrix matrix = new Matrix();
        matrix.postRotate(degrees, mArrawBmp.getWidth() / 2, mArrawBmp.getHeight() / 2);
        matrix.postTranslate(pos[0] - mArrawBmp.getWidth() / 2, pos[1] - mArrawBmp.getHeight() / 2);

        // 箭头旋转、位移实现方式二,通过getMatrix(x)实现
        /*Matrix matrix = new Matrix();
        mPathMeasure.getMatrix(
                stop,
                matrix,
                PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG
        );
        matrix.preTranslate(-mArrawBmp.getWidth() / 2, -mArrawBmp.getHeight() / 2);*/

        canvas.drawBitmap(mArrawBmp, matrix, mPaint);
    }

}

效果图:
在这里插入图片描述

4 实现操作成功动画

需要用到PathMeasure的nextContour()方法。

布局文件:

<?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.example.myapplication.OperationView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

自定义OperationView:

package com.example.myapplication;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;


public class OperationView extends View {
    private Paint mPaint;

    private Path mCirclePath, mDstPath;
    private PathMeasure mPathMeasure;

    private Float mCurAnimValue;

    private int mCentX = 200;
    private int mCentY = 200;
    private int mRadius = 50;

    boolean mNext = false;

    public OperationView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mPaint.setColor(Color.BLACK);

        mDstPath = new Path();
        mCirclePath = new Path();

        mCirclePath.addCircle(mCentX, mCentY, mRadius, Path.Direction.CW);
        mCirclePath.moveTo(mCentX - mRadius / 2, mCentY);
        mCirclePath.lineTo(mCentX, mCentY + mRadius / 2);
        mCirclePath.lineTo(mCentX + mRadius / 2, mCentY - mRadius / 3);

        mPathMeasure = new PathMeasure(mCirclePath, false);

		// 0~1之间画第一条路径,1~2之间画第二条路径
        ValueAnimator animator = ValueAnimator.ofFloat(0, 2);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(4000);
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);

        if (mCurAnimValue < 1) {
            float stop = mPathMeasure.getLength() * mCurAnimValue;
            mPathMeasure.getSegment(0, stop, mDstPath, true);
        } else {
            if (!mNext) {
                mNext = true;
                mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
                mPathMeasure.nextContour();
            }
            float stop = mPathMeasure.getLength() * (mCurAnimValue - 1);
            mPathMeasure.getSegment(0, stop, mDstPath, true);
        }

        canvas.drawPath(mDstPath, mPaint);
    }

}

效果图:
在这里插入图片描述

参考文献:
[1] UML中的类图及类图之间的关系
[2] 启舰.Android自定义控件开发入门与实战[M].北京:电子工业出版社,2018

微信公众号:TechU
在这里插入图片描述

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值