Preface
Path animation is a very useful way to achieve animation, using SVG can easily achieve the effect of path animation, but the version required by SVG is relatively high, many applications can not fully support it at present. The PathMeasure tool class provided in Android system can calculate the length of the path, get a part of the path according to the path interval provided, and also get the position and tangent angle of any point in the path. Here is a simple example to learn this powerful tool.
Realization effect
PathMeasure interface
Method name | Significance |
---|---|
PathMeasure() | Create an empty PathMeasure |
PathMeasure(Path path, boolean forceClosed) | To create a PathMeasure and associate it with a specified Path, it is important to note that the Path must be initialized, otherwise only empty Path objects will be measured. Whether forceClose closes the specified Path will only affect PathMeasure calculation and will not have any effect on the original Path. |
setPath(Path path, boolean forceClosed) | Associating a Path, whether forceClose closes the specified Path only affects the PathMeasure calculation and does not have any effect on the original Path |
isClosed() | Is Path Closed |
getLength() | Gets the length of the Path, and the return value is a float type number |
nextContour() | If Path contains multiple unrelated paths and jumps to the next contour, you can traverse all independent paths in Path by this method. |
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) | Segment intercept |
getPosTan(float distance, float[] pos, float[] tan) | Gets the position coordinate of the specified length and the tangent value of the point |
getMatrix(float distance, Matrix matrix, int flags) | Gets the position coordinate of the specified length and the Matrix of the point |
Single Path Animation
CheckBox is a common user control. Every time it is selected, it will display the''figure in the front space, and this figure is drawn by itself. Now, we use PathMeasure to simulate and realize the animation effect of this logo drawing.
public class PathView extends AppCompatImageView {
private Path mRight;
private Path mTmpPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private ValueAnimator mPathAnimator;
public PathView(Context context) {
this(context, null);
}
public PathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PathView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setColor(getResources().getColor(R.color.colorAccent));
mPaint.setStrokeWidth(5);
mPaint.setStyle(Paint.Style.STROKE);
mRight = new Path();
mTmpPath = new Path();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRight.moveTo(0, h / 2);
mRight.lineTo(w / 4, h);
mRight.lineTo(w, 0);
mPathMeasure = new PathMeasure(mRight, false);
float length = mPathMeasure.getLength();
mPathAnimator = ValueAnimator.ofFloat(0f, length);
mPathAnimator.setDuration(1000);
mPathAnimator.setInterpolator(new LinearInterpolator());
mPathAnimator.setRepeatCount(ValueAnimator.INFINITE);
mPathAnimator.setRepeatMode(ValueAnimator.RESTART);
mPathAnimator.addUpdateListener(animation -> {
float current = (float) animation.getAnimatedValue();
mTmpPath.reset();
mPathMeasure.getSegment(0f, current, mTmpPath, true);
invalidate();
});
mPathAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setStrokeWidth(5);
// Draw an external border
canvas.drawRect(4, 4, getMeasuredWidth() - 4, getMeasuredHeight() - 4, mPaint);
mPaint.setStrokeWidth(10);
// Draw the inner''
canvas.drawPath(mTmpPath, mPaint);
}
}
In the onSizeChanged method, the length of the logo is obtained, and the attribute animation from 0 to the length of the logo is created. In the process of animation execution, the path fragment from 0 to current is obtained by getSegment. Finally, the control is refreshed to draw the intercepted logo fragment, which achieves the effect of logo drawing.
Multiple Path Rendering
In addition to the box type CheckBox, there is a circular CheckBox control. At this time, not only the internal alignment needs to be drawn, but also the external roundpath needs to be drawn. In a Path, there are two non-interconnected routes directly using PathMeasure operation is the first path, here is the circular path. In order to be able to continue drawing the second alignment path, nextContour jump is called. Go to the next path.
public class CirclePathView extends AppCompatImageView {
private Path mPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private ValueAnimator mPathAnimator;
private ValueAnimator mRightAnimator;
private Path mTmpPath;
private Path mRightPath;
public CirclePathView(Context context) {
this(context, null);
}
public CirclePathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CirclePathView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setColor(getResources().getColor(R.color.colorAccent));
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mTmpPath = new Path();
mRightPath = new Path();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
float padding = CommonUtils.dp2px(4);
float rightPadding = padding + CommonUtils.dp2px(2);
// Add Round Path
mPath.addCircle(w / 2, h / 2, Math.min(w / 2, h / 2) - padding, Path.Direction.CCW);
// Add a logo path
mPath.moveTo(rightPadding, h / 2);
mPath.lineTo(w / 3, h - rightPadding);
mPath.lineTo(w - rightPadding, rightPadding);
mPathMeasure = new PathMeasure(mPath, false);
float length = mPathMeasure.getLength();
mPathAnimator = ValueAnimator.ofFloat(0f, length);
mPathAnimator.setDuration(1000);
mPathAnimator.setInterpolator(new LinearInterpolator());
mPathAnimator.addUpdateListener(animation -> {
float current = (float) animation.getAnimatedValue();
mTmpPath.reset();
// Get the path fragment of the circle and redraw it
mPathMeasure.getSegment(0f, current, mTmpPath, true);
invalidate();
});
mPathAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// When the circle is drawn, jump to the next path, which is the logo path.
mPathMeasure.nextContour();
float length = mPathMeasure.getLength();
mRightAnimator = ValueAnimator.ofFloat(0f, length);
mRightAnimator.setDuration(1000);
mRightAnimator.setInterpolator(new LinearInterpolator());
mRightAnimator.addUpdateListener(anim -> {
float current = (float) anim.getAnimatedValue();
mRightPath.reset();
// Get the logarithmic path
mPathMeasure.getSegment(0f, current, mRightPath, true);
invalidate();
});
mRightAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Need to clear the logo path
mRightPath.reset();
// Re-associate mPath with PathMeasure, otherwise the previous path will be used.
// Because all the paths have been drawn, the blank space will be displayed.
mPathMeasure.setPath(mPath, false);
// Re-start drawing
mPathAnimator.start();
}
});
mRightAnimator.start();
}
});
mPathAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw fragments
canvas.drawPath(mTmpPath, mPaint);
canvas.drawPath(mRightPath, mPaint);
}
}
The above multi-path rendering is to use nextContour to jump to the next path and continue to perform single-path rendering. However, in restarting the rendering, PathMeasure and Path need to be rebounded, otherwise PathMeasure will remain in the previous nextContour state and cannot be redrawn.
Extended Path Motion
PathMeasure's getPosTan method can get the position and tangent angle at any length, get the position and angle of the current length in the attribute animation, and then adjust the position and angle of the image object, as if the image object is moving along the path.
public class FlightPathView extends AppCompatImageView {
private Path mPath;
private Path mTmpPath;
private Paint mPaint;
private PathMeasure mPathMeasure;
private ValueAnimator mPathAnimator;
private Bitmap mFlight;
private float[] mPos;
private float[] mTan;
private Matrix mMatrix;
public FlightPathView(Context context) {
this(context, null);
}
public FlightPathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlightPathView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setColor(getResources().getColor(R.color.colorAccent));
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mTmpPath = new Path();
mFlight = BitmapFactory.decodeResource(getResources(), R.drawable.flight_ic);
mPos = new float[2];
mTan = new float[2];
mMatrix = new Matrix();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Draw a circular path
mPath.addCircle(w / 2, h / 2, Math.min(w / 2, h / 2) - CommonUtils.dp2px(20), Path.Direction.CCW);
mPathMeasure = new PathMeasure(mPath, false);
float length = mPathMeasure.getLength();
mPathAnimator = ValueAnimator.ofFloat(0f, length);
mPathAnimator.setDuration(2000);
mPathAnimator.setInterpolator(new LinearInterpolator());
mPathAnimator.setRepeatCount(ValueAnimator.INFINITE);
mPathAnimator.setRepeatMode(ValueAnimator.RESTART);
mPathAnimator.addUpdateListener(animation -> {
float current = (float) animation.getAnimatedValue();
mTmpPath.reset();
// Get the current length of the fragment
mPathMeasure.getSegment(0f, current, mTmpPath, true);
// Get the position and angle of the current length
mPathMeasure.getPosTan(current, mPos, mTan);
mMatrix.reset(); // Reset Matrix
float degrees = (float) (Math.atan2(mTan[1], mTan[0]) * 180.0 / Math.PI); // Calculate the rotation angle of the picture
mMatrix.postRotate((degrees + 45f), mFlight.getWidth() / 2, mFlight.getHeight() / 2); // Rotating picture
mMatrix.postTranslate(mPos[0] - mFlight.getWidth() / 2, mPos[1] - mFlight.getHeight() / 2); // Mobile pictures
invalidate();
});
mPathAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(mTmpPath, mPaint);
canvas.drawBitmap(mFlight, mMatrix, mPaint);
}
}
In onSizeChanged method, the position and angle of the current length are obtained by getPosTan method. The position can be used directly relative to the coordinate system of the control. mTan needs to calculate the radian angle through the inverse function and then convert it to the angle value. Because the plane picture rotates 45 degrees, it needs to be adjusted by adding 45 degrees later.