android.graphics 从一个 View 指向另一个 View 画一条线

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

我知道 android.graphics 已经老了,但我在做一些简单的事情时遇到了麻烦。

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

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

我尝试创建自定义 View class 并覆盖 onDraw(Canvas c) 方法,然后使用 Canvas 对象中的 drawLine(startX, startY, stopX, stopY, paint) 方法。但是我不知道要获得哪个坐标才能将一个 View 指向另一个 View

我不想在 XML 布局中创建高度较小的静态 View,因为 View 可以由用户动态添加,我认为这是绘图动态线路是最好的方式。

请帮帮我。谢谢!

使用 Path 和 Pathmeasure 绘制动画线。我已经制作并测试了它。

创建自定义视图并将视图坐标点数组传递给它,

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);
    }
}

如果所有视图都位于同一父布局上,则可以更好地在视图之间绘制线条。对于问题的条件(Second Button 恰好在 First Button 的右侧),您可以使用这样的自定义布局:

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;
    }
}

.xml 布局如:

<?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>

MainActivity.java喜欢:

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);
            }
        });
    }
}

你有类似的东西(在 First Button 点击):

对于其他情况(Second Button正好在First Button的左侧(或上方或下方)或更复杂的above-right/below-left等),您应该修改计算箭头的部分起点和终点:

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;
}