Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

android.graphics draw a line from one View pointing to another View

I know android.graphics is old, but i am having trouble doing a simple stuff.

I want to draw a line animation where one View points an arrow/line into another View

First Button-------------------------------->Second Button

I have tried creating a custom View class and overriding the onDraw(Canvas c) method and then using the drawLine(startX, startY, stopX, stopY, paint) method from the Canvas Object. But i don't know which coordinates to get in order to point one View to the other View

I don't want to create a static View in the XML layout with a slim height because the View can be added dynamically by the user, which i think drawing the line dynamically is the best way.

Please help me out. Thank you!

like image 295
GGWP Avatar asked Aug 01 '18 02:08

GGWP


2 Answers

For drawing lines between views better if all of it lays on same parent layout. For the conditions of the question (Second Button is exactly to the right of First Button) you can use custom layout like that:

public class ArrowLayout extends RelativeLayout {

    public static final String PROPERTY_X = "PROPERTY_X";
    public static final String PROPERTY_Y = "PROPERTY_Y";

    private final static double ARROW_ANGLE = Math.PI / 6;
    private final static double ARROW_SIZE = 50;

    private Paint mPaint;

    private boolean mDrawArrow = false;
    private Point mPointFrom = new Point();   // current (during animation) arrow start point
    private Point mPointTo = new Point();     // current (during animation)  arrow end point

    public ArrowLayout(Context context) {
        super(context);
        init();
    }

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

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

    public ArrowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        setWillNotDraw(false);
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.BLUE);
        mPaint.setStrokeWidth(5);
    }

    @Override
    public void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.save();
        if (mDrawArrow) {
            drawArrowLines(mPointFrom, mPointTo, canvas);
        }
        canvas.restore();
    }

    private Point calcPointFrom(Rect fromViewBounds, Rect toViewBounds) {
        Point pointFrom = new Point();

        pointFrom.x = fromViewBounds.right;
        pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;

        return pointFrom;
    }


    private Point calcPointTo(Rect fromViewBounds, Rect toViewBounds) {
        Point pointTo = new Point();

        pointTo.x = toViewBounds.left;
        pointTo.y = toViewBounds.top + (toViewBounds.bottom - toViewBounds.top) / 2;

        return pointTo;
    }


    private void drawArrowLines(Point pointFrom, Point pointTo, Canvas canvas) {
        canvas.drawLine(pointFrom.x, pointFrom.y, pointTo.x, pointTo.y, mPaint);

        double angle = Math.atan2(pointTo.y - pointFrom.y, pointTo.x - pointFrom.x);

        int arrowX, arrowY;

        arrowX = (int) (pointTo.x - ARROW_SIZE * Math.cos(angle + ARROW_ANGLE));
        arrowY = (int) (pointTo.y - ARROW_SIZE * Math.sin(angle + ARROW_ANGLE));
        canvas.drawLine(pointTo.x, pointTo.y, arrowX, arrowY, mPaint);

        arrowX = (int) (pointTo.x - ARROW_SIZE * Math.cos(angle - ARROW_ANGLE));
        arrowY = (int) (pointTo.y - ARROW_SIZE * Math.sin(angle - ARROW_ANGLE));
        canvas.drawLine(pointTo.x, pointTo.y, arrowX, arrowY, mPaint);
    }

    public void animateArrows(int duration) {
        mDrawArrow = true;

        View fromView = getChildAt(0);
        View toView = getChildAt(1);

        // find from and to views bounds
        Rect fromViewBounds = new Rect();
        fromView.getDrawingRect(fromViewBounds);
        offsetDescendantRectToMyCoords(fromView, fromViewBounds);

        Rect toViewBounds = new Rect();
        toView.getDrawingRect(toViewBounds);
        offsetDescendantRectToMyCoords(toView, toViewBounds);

        // calculate arrow sbegin and end points
        Point pointFrom = calcPointFrom(fromViewBounds, toViewBounds);
        Point pointTo = calcPointTo(fromViewBounds, toViewBounds);

        ValueAnimator arrowAnimator = createArrowAnimator(pointFrom, pointTo, duration);
        arrowAnimator.start();
    }

    private ValueAnimator createArrowAnimator(Point pointFrom, Point pointTo, int duration) {

        final double angle = Math.atan2(pointTo.y - pointFrom.y, pointTo.x - pointFrom.x);

        mPointFrom.x = pointFrom.x;
        mPointFrom.y = pointFrom.y;

        int firstX = (int) (pointFrom.x + ARROW_SIZE * Math.cos(angle));
        int firstY = (int) (pointFrom.y + ARROW_SIZE * Math.sin(angle));

        PropertyValuesHolder propertyX = PropertyValuesHolder.ofInt(PROPERTY_X, firstX, pointTo.x);
        PropertyValuesHolder propertyY = PropertyValuesHolder.ofInt(PROPERTY_Y, firstY, pointTo.y);

        ValueAnimator animator = new ValueAnimator();
        animator.setValues(propertyX, propertyY);
        animator.setDuration(duration);
        // set other interpolator (if needed) here:
        animator.setInterpolator(new AccelerateDecelerateInterpolator());

        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mPointTo.x = (int) valueAnimator.getAnimatedValue(PROPERTY_X);
                mPointTo.y = (int) valueAnimator.getAnimatedValue(PROPERTY_Y);

                invalidate();
            }
        });

        return animator;
    }
}

with .xml layout like:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/layout_main"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

    <{YOUR_PACKAGE_NAME}.ArrowLayout
            android:id="@+id/arrow_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <Button
            android:id="@+id/first_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:text="First Button"/>

        <Button
            android:id="@+id/second_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="Second Button"/>

    </{YOUR_PACKAGE_NAME}.ArrowLayout>

</RelativeLayout>

and MainActivity.java like:

public class MainActivity extends AppCompatActivity {

    private ArrowLayout mArrowLayout;
    private Button mFirstButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mArrowLayout = (ArrowLayout) findViewById(R.id.arrow_layout);

        mFirstButton = (Button) findViewById(R.id.first_button);
        mFirstButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mArrowLayout.animateArrows(1000);
            }
        });
    }
}

you got something like that (on First Button click):

[Arrow to View animated

For other cases ( Second Button is exactly to the left (or above, or below) or more complex above-right/below-left etc. of First Button) you should modify part for calculating arrow begin and end points:

private Point calcPointFrom(Rect fromViewBounds, Rect toViewBounds) {
    Point pointFrom = new Point();

    //                                Second Button above
    //                                ----------+----------
    //                               |                     |
    //  Second Button tho the left   +     First Button    + Second Button tho the right
    //                               |                     |
    //                                ----------+----------
    //                                  Second Button below
    //
    //   + - is arrow start point position

    if (toViewBounds to the right of fromViewBounds){
        pointFrom.x = fromViewBounds.right;
        pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;
    } else if (toViewBounds to the left of fromViewBounds) {
        pointFrom.x = fromViewBounds.left;
        pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;
    } else if () {
        ...
    }

    return pointFrom;
}
like image 182
Andrii Omelchenko Avatar answered Oct 06 '22 00:10

Andrii Omelchenko


Use Path and Pathmeasure for Drawing Animated Line. I have Made and test it.

Make Custom View and pass view coordinates points array to it,

public class AnimatedLine extends View {
    private final Paint mPaint;
    public Canvas mCanvas;
    AnimationListener animationListener;

    Path path;
    private static long animSpeedInMs = 2000;
    private static final long animMsBetweenStrokes = 100;
    private long animLastUpdate;
    private boolean animRunning = true;
    private int animCurrentCountour;
    private float animCurrentPos;
    private Path animPath;
    private PathMeasure animPathMeasure;

    float pathLength;


    float distance = 0;
    float[] pos;
    float[] tan;
    Matrix matrix;
    Bitmap bm;


    public AnimatedLine(Context context) {
        this(context, null);
        mCanvas = new Canvas();
    }

    public AnimatedLine(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(15);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setColor(context.getResources().getColor(R.color.materialcolorpicker__red));


        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
            setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
        }
        bm = BitmapFactory.decodeResource(getResources(), R.drawable.hand1);
        bm = Bitmap.createScaledBitmap(bm, 20,20, false);
        distance = 0;
        pos = new float[2];
        tan = new float[2];

        matrix = new Matrix();
    }

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

        mCanvas = canvas;

        if (path != null) {

            if (animRunning) {
                drawAnimation(mCanvas);
            } else {
                drawStatic(mCanvas);
            }

        }

    }


    /**
     * draw Path With Animation
     *
     * @param time in milliseconds
     */
    public void drawWithAnimation(ArrayList<PointF> points, long time,AnimationListener animationListener) {
        animRunning = true;
        animPathMeasure = null;
        animSpeedInMs = time;
        setPath(points);
        setAnimationListener(animationListener);
        invalidate();
    }

    public void setPath(ArrayList<PointF> points) {
        if (points.size() < 2) {
            throw new IllegalStateException("Pass atleast two points.");
        }
        path = new Path();
        path.moveTo(points.get(0).x, points.get(0).y);
        path.lineTo(points.get(1).x, points.get(1).y);
    }

    private void drawAnimation(Canvas canvas) {
        if (animPathMeasure == null) {
            // Start of animation. Set it up.
            animationListener.onAnimationStarted();
            animPathMeasure = new PathMeasure(path, false);
            animPathMeasure.nextContour();
            animPath = new Path();
            animLastUpdate = System.currentTimeMillis();
            animCurrentCountour = 0;
            animCurrentPos = 0.0f;

            pathLength = animPathMeasure.getLength();


        } else {
            // Get time since last frame
            long now = System.currentTimeMillis();
            long timeSinceLast = now - animLastUpdate;

            if (animCurrentPos == 0.0f) {
                timeSinceLast -= animMsBetweenStrokes;
            }

            if (timeSinceLast > 0) {
                // Get next segment of path
                float newPos = (float) (timeSinceLast) / (animSpeedInMs / pathLength) + animCurrentPos;
                boolean moveTo = (animCurrentPos == 0.0f);
                animPathMeasure.getSegment(animCurrentPos, newPos, animPath, moveTo);
                animCurrentPos = newPos;
                animLastUpdate = now;

                 //start draw bitmap along path
                animPathMeasure.getPosTan(newPos, pos, tan);
                matrix.reset();
                matrix.postTranslate(pos[0], pos[1]);
                canvas.drawBitmap(bm, matrix, null);
                //end drawing bitmap



                //take current position
                animationListener.onAnimationUpdate(pos);

                // If this stroke is done, move on to next
                if (newPos > pathLength) {
                    animCurrentPos = 0.0f;
                    animCurrentCountour++;
                    boolean more = animPathMeasure.nextContour();
                    // Check if finished
                    if (!more) {
                        animationListener.onAnimationEnd();
                        animRunning = false;
                    }
                }
            }

            // Draw path
            canvas.drawPath(animPath, mPaint);

        }

        invalidate();
    }

    private void drawStatic(Canvas canvas) {
        canvas.drawPath(path, mPaint);
        canvas.drawBitmap(bm, matrix, null);
    }


    public void setAnimationListener(AnimationListener animationListener) {
        this.animationListener = animationListener;
    }



    public interface AnimationListener {
        void onAnimationStarted();

        void onAnimationEnd();

        void onAnimationUpdate(float[] pos);
    }
}
like image 35
Abhay Koradiya Avatar answered Oct 06 '22 01:10

Abhay Koradiya