21

I'm currently working with graphics and paths, and I can successufully display whatever I want.

But instead of drawing a line directly on my SurfaceView, I'd like to draw it progressively in an animation.

What I've done so far is to create a Path and then to use PathMeasure to retrieve the coordinates progressively along the path. Here is basically what I've done so far

PathMeasure pm = new PathMeasure(myPath, false);

    float position = 0;
    float end = pm.getLength();
    float[] coord = {0,0,0,0,0,0,0,0,0};

    while (position < end){
        Matrix m = new Matrix();
        // put the current path position coordinates into the matrix
        pm.getMatrix(position, m, PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
        // put the matrix data into the coord array (coord[2] = x and coord[5] = y)
        m.getValues(coord);
        ????
        position += 1;

    }

The question marks is where I'm stuck. I want to draw the path progressively and see it animated on the screen. I couldn't find much info about it on the internet, so any clue would be much appreciated if you have already come across the same situation. The final effect I want to create is like a pencil drawing progressively a text automatically.

Sasuke
  • 219
  • 1
  • 2
  • 4

4 Answers4

19

Instead of creating a for loop, you can use the ObjectAnimator class to callback to one of your class's methods every time you'd like to draw a bit more of the path.

import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.View;
import android.util.Log;

public class PathView extends View
{
    Path path;
    Paint paint;
    float length;

    public PathView(Context context)
    {
        super(context);
    }

    public PathView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    public PathView(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
    }

    public void init()
    {
        paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(10);
        paint.setStyle(Paint.Style.STROKE);

        path = new Path();
        path.moveTo(50, 50);
        path.lineTo(50, 500);
        path.lineTo(200, 500);
        path.lineTo(200, 300);
        path.lineTo(350, 300);

        // Measure the path
        PathMeasure measure = new PathMeasure(path, false);
        length = measure.getLength();

        float[] intervals = new float[]{length, length};

        ObjectAnimator animator = ObjectAnimator.ofFloat(PathView.this, "phase", 1.0f, 0.0f);
        animator.setDuration(3000);
        animator.start();
    }

    //is called by animtor object
    public void setPhase(float phase)
    {
        Log.d("pathview","setPhase called with:" + String.valueOf(phase));
        paint.setPathEffect(createPathEffect(length, phase, 0.0f));
        invalidate();//will calll onDraw
    }

    private static PathEffect createPathEffect(float pathLength, float phase, float offset)
    {
        return new DashPathEffect(new float[] { pathLength, pathLength },
            Math.max(phase * pathLength, offset));
    }

    @Override
    public void onDraw(Canvas c)
    {
        super.onDraw(c);
        c.drawPath(path, paint);
    }
}

Then, just call init() to begin the animation, like this (or if you'd like it to start as soon as the view is inflated, put the init() call inside the constructors):

PathView path_view = (PathView) root_view.findViewById(R.id.path);
path_view.init();

Also see this question here, and this example, which I've based my code on.

Community
  • 1
  • 1
skunkwerk
  • 2,920
  • 2
  • 37
  • 55
  • Hey! Your code works great for me. I want one addition for my use case though. I need to draw circles at the start and end of current path, with diameter more that stroke width, which means stroke cap won't work. Start circle remains at its position but end circle moves along the path. I have the starting coordinates so I can call canvas.drawCircle. But I can't figure out how do I get coordinates of current end of path to draw the circle. Got any ideas for me? – gitter Apr 18 '16 at 10:03
  • @skunkwerk, Thanks for this answer. When I tried to call init from Activity, the app crashes as it says path is null in onDraw. Also, how would i set animation listener in this answer? – Rakesh May 20 '17 at 11:15
  • i wish there was a way to not keep constantly calling "new" to create the patheffect. like maybe a pool of path effects. it slowing down the animation and im wondering if its due to the "new" keyword. – j2emanue Jan 14 '19 at 05:59
  • 2
    I can't undestand: wherea are u using intervals? Why were u coding it???? – Georgiy Chebotarev Sep 05 '19 at 19:06
  • This is great thanks, took me a little while to figure out how this worked, as a bit of colour about the DashPathEffect: This gives us a neat way to provide an offset, so that we can start small (big offset) and work back over the "on" dash length making it longer and longer thereby drawing more and more of the actual path. – hmac Apr 28 '21 at 12:59
5

I just have resolve this problem, here what I do:

private float[] mIntervals = { 0f, 0f };
private float drawSpeed = 2f;
private int currentPath = -1;
private PathMeasure mPathMeasure = new PathMeasure();
private ArrayList<Path> mListPath = new ArrayList<Path>(this.pathCount);


@Override
protected void onDraw(Canvas canvas) {
   if (mIntervals[1] <= 0f && currentPath < (pathCount - 1)) {
     // Set the current path to draw
     // getPath(int num) a function to return a path.
     Path newPath = this.getPath(mListPath.size());
     this.mListPath.add(newPath);
     this.mPathMeasure.setPath(newPath, false);
     mIntervals[0] = 0;
     mIntervals[1] = this.mPathMeasure.getLength();
   }

  if (mIntervals[1] > 0) {
     // draw the previous path
     int last = this.mListPath.size();
     for (int i = 0; i < last; i++) {
        canvas.drawPath(this.mListPath.get(i), mPaint);
     }
     // partially draw the last path
     this.mPaint.setPathEffect(new DashPathEffect(mIntervals, 0f));

     canvas.drawPath(this.mListPath.get(last), mPaint);

     // Update the path effects values, to draw a little more
     // on the path.
     mIntervals[0] += drawSpeed;
     mIntervals[1] -= drawSpeed;

     super.invalidate();
  } else {
     // The drawing have been done, draw it entirely
     for (int i = 0; i < this.mListPath.size(); i++) {
        canvas.drawPath(this.mListPath.get(i), mPaint);
     }
  }
}

This example, is an adaptation of what I've done (to simplify the example). Hope you will understand it. Since I've just made this function working, It lacks of optimizations and things like that.

Hope it will help ;-)

vono
  • 200
  • 2
  • 6
  • 1
    Hey thanks for wonderful post. that real help me. but still one problem is that How to define Path array. i try my best to resolve but i can't. can you help please...! – Ashok Domadiya Jul 24 '12 at 07:26
1

here is an alternative solution that worked for me

 package com.sample;
 /**
* Created by Sumit
 */
public class PathView extends View {

Paint mPaint;
Path mPath;
int mStrokeColor;
float mStrokeWidth;

float mProgress = 0.0f;
float mLength = 0f;
float mTotal;

public PathView(Context context) {
    this(context, null);
    init();
}

public PathView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    init();
}

public PathView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);

    mStrokeColor = Color.RED;
    mStrokeWidth = 8.0f;
    init();
}

private void init() {
    mPaint = new Paint();
    mPaint.setColor(mStrokeColor);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(mStrokeWidth);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    mPaint.setAntiAlias(true);
    mPaint.setDither(true);

    setPath(new Path());
    // setPath2(new Path());
}

public void setPath(Path p) {
    mPath = p;
    PathMeasure measure = new PathMeasure(mPath, false);
    mPathLength = measure.getLength();
}


public void setPath(List<float[][]> list) {
    Log.d("Path", "size " + list.size());
    Path p = new Path();
    p.moveTo(list.get(0)[0][0], list.get(1)[0][1]);

    for (int i = 1; i < list.size(); i++) {
        p.lineTo(list.get(i)[0][0], list.get(i)[0][1]);
        //if (i > 100)
            //p.moveTo(list.get(i)[0][0], list.get(i)[0][1]);
    }
    //p.setFillType(FillType.WINDING);
    setPath(p);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mTotal = (mLength - mLength * mProgress);
    PathEffect pathEffect = new DashPathEffect(new float[] { mLength, mLength }, mTotal);
    Log.d("Path Tag", "length =" + mLength + ", totla=" + mTotal);

    mPaint.setPathEffect(pathEffect);

    canvas.save();
    // canvas.translate(getPaddingLeft(), getPaddingTop());
    canvas.drawPath(mPath, mPaint);
    canvas.restore();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(widthMeasureSpec);

    int measuredWidth, measuredHeight;

    if (widthMode == MeasureSpec.AT_MOST)
        throw new IllegalStateException("Use MATCH_PARENT");
    else
        measuredWidth = widthSize;

    if (heightMode == MeasureSpec.AT_MOST)
        throw new IllegalStateException("Use MATCH_PARENT");
    else
        measuredHeight = heightSize;

    setMeasuredDimension(measuredWidth, measuredHeight);
    setPath();
}

void setPath() {
    int cX = getWidth() / 2;
    int cY = getHeight() / 2;
    cY += 50;
    cX -= 50;
    List<float[][]> list = new ArrayList<float[][]>();

    for (int i = 0; i < 50; i++) {
        list.add(new float[][] { { cX--, cY++ } });
    }

    for (int i = 0; i < 100; i++) {
        list.add(new float[][] { { cX--, cY-- } });
    }

    for (int i = 0; i < 100; i++) {
        list.add(new float[][] { { cX++, cY-- } });
    }

    for (int i = 0; i < 200; i++) {
        list.add(new float[][] { { cX++, cY++ } });
    }

    for (int i = 0; i < 100; i++) {
        list.add(new float[][] { { cX++, cY-- } });
    }
    for (int i = 0; i < 100; i++) {
        list.add(new float[][] { { cX--, cY-- } });
    }

    for (int i = 0; i < 100; i++) {
        list.add(new float[][] { { cX--, cY++ } });
    }

    setPath(list);
}

}

and use

 final PathView pathView = (PathView) findViewById(R.id.path_view);
 pathView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            ObjectAnimator anim = ObjectAnimator.ofFloat(view, "percentage", 0.0f, 1.0f);
            anim.setDuration(2000);
            anim.setInterpolator(new LinearInterpolator());
            anim.setRepeatCount(Animation.INFINITE);
            anim.start();
        }
    });
Sumit
  • 1,022
  • 13
  • 19
1

You will have to add this view to the layout, setting height to 1 and width to match parent. The line will be animated from left to right. The later line will be placed over the first one.

                public class AnimatorLineView extends RelativeLayout {

                    private View animatorLineView;
                    private View simpleLineView;
                    View animatorLine;
                    private int colorBeforeAnimation;
                    private int colorAfterAnimation;
                    private int colorForErrorLine;

                    public AnimatorLineView(Context context) {
                        super(context);
                        init();
                        startAnimation();
                    }

                    public AnimatorLineView(Context context, AttributeSet attrs) {
                        super(context, attrs);
                        init();
                        initAttributes(context, attrs);
                        setColors();
                        startAnimation();
                    }

                    public AnimatorLineView(Context context, AttributeSet attrs, int defStyleAttr) {
                        super(context, attrs, defStyleAttr);
                        init();
                        initAttributes(context, attrs);
                        setColors();
                        startAnimation();
                    }


                    private void setColors() {
                        simpleLineView.setBackgroundColor(colorBeforeAnimation);
                        animatorLine.setBackgroundColor(colorAfterAnimation);
                    }

                    public void init() {
                        animatorLineView = inflate(getContext(), R.layout.ainimator_line_view, this);
                        animatorLine = findViewById(R.id.simple_line);
                        simpleLineView = findViewById(R.id.animator_line);

                    }

                    public void setColor(int color) {
                        animatorLine.setBackgroundColor(color);
                    }

                    public void startAnimation() {
                        animatorLine.setVisibility(View.VISIBLE);
                        Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.enter_animation_underline);
                        animatorLine.startAnimation(animation);
                    }

                    public void showErrorLine(){
                        animatorLine.setBackgroundColor(colorForErrorLine);
                        animatorLine.setVisibility(View.VISIBLE);
                        Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.enter_animation_underline);
                        animatorLine.startAnimation(animation);
                    }

                    public void hideErrorLine(){
                        animatorLine.setBackgroundColor(colorAfterAnimation);
                        animatorLine.setVisibility(View.VISIBLE);
                        Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.enter_animation_underline);
                        animatorLine.startAnimation(animation);
                    }

                    private void initAttributes(Context context, AttributeSet attributeSet) {
                        TypedArray attr = getTypedArray(context, attributeSet, R.styleable.ProgressButton);
                        if (attr == null) {
                            return;
                        }
                        try {
                            colorBeforeAnimation = attr.getColor(R.styleable.AnimatorLineView_al_color_after_animation,ContextCompat.getColor(getContext(), R.color.animation_line_text_color));
                            colorAfterAnimation = attr.getColor(R.styleable.ProgressButton_pb_text_color, ContextCompat.getColor(getContext(), R.color.black_color));
                            colorForErrorLine = attr.getColor(R.styleable.ProgressButton_pb_text_color, ContextCompat.getColor(getContext(), R.color.error_msgs_text_color));
                        } finally {
                            attr.recycle();
                        }
                    }

                    protected TypedArray getTypedArray(Context context, AttributeSet attributeSet, int[] attr) {
                        return context.obtainStyledAttributes(attributeSet, attr, 0, 0);
                    }

                    public void resetColor(){
                        animatorLine.setBackgroundColor(colorAfterAnimation);
                        animatorLine.setVisibility(View.GONE);
                    }
                }

            <animator_line_view>

            <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <View
                    android:id="@+id/simple_line"
                    android:layout_width="match_parent"
                    android:layout_height="1.5dp"
                    android:background="#E0E0E0" />

                <View
                    android:id="@+id/animator_line"
                    android:layout_width="match_parent"
                    android:layout_height="1.5dp"
                    android:background="#000000"
                    android:visibility="gone" />

            </FrameLayout>

        <enter_animation_underline>

        <?xml version="1.0" encoding="utf-8"?>
        <set xmlns:android="http://schemas.android.com/apk/res/android"
            android:shareInterpolator="false">
            <translate
                android:fromXDelta="-100%" android:toXDelta="0%"
                android:fromYDelta="0%" android:toYDelta="0%"
                android:duration="@integer/animator_line_duration" />
        </set>
    ---- styles------
      <style name="animator_line">
            <item name="android:layout_width">match_parent</item>
            <item name="android:layout_height">wrap_content</item>
            <item name="al_color_before_animation">#E0E0E0</item>
            <item name="al_color_after_animation">#0000000</item>
            <item name="al_error_line_color">#FF3352</item>
        </style>

        <declare-styleable name="AnimatorLineView">
            <attr name="al_color_before_animation" format="color" />
            <attr name="al_color_after_animation" format="color" />
            <attr name="al_error_line_color" format="color" />
        </declare-styleable>

-------- to be include in the xml
 <com.careem.acma.widget.AnimatorLineView
        android:id="@+id/animator_line"
        style="@style/animator_line" />
Athena
  • 3,200
  • 3
  • 27
  • 35
Qumber Abbas
  • 560
  • 1
  • 4
  • 11
  • Can you add a bit explaining what this does? – Athena Jul 21 '17 at 13:04
  • You will have to add this view to the layout, setting height to 1 and width to match parent. The line will be animated from left to right. The later line will be placed over the first one. – Qumber Abbas Jul 21 '17 at 14:04