PathMeasure Tool Class for Simple Use

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.

Keywords: Fragment Attribute Android Mobile

Added by marijn on Tue, 14 May 2019 19:32:44 +0300