文章概览
- 1 PathMeasure概述
- 2 实现路径加载动画
- 3 实现箭头加载动画
- 4 实现操作成功动画
本系列将介绍以下内容:
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