How do I draw an arrowhead (in Android)?
Asked Answered
C

9

25

I'm fairly new to Android and have been toying around with Canvas. I'm attempting to draw an arrow but I'm only having luck with drawing the shaft, none of the arrowhead is working.

I have searched a bit and found a Java example, but Android doesn't have GeneralPath or AffineTransform.

Right now my code looks like the following (the arrowhead looks nothing like an arrowhead):

public class DrawableView extends View {
    Context mContext;
    private int centerX;
    private int centerY;
    private int radius;
    private double arrLength;
    private double arrHeading;
    private int margin = 10;

    public DrawableView(Context context) {
        super(context);
        mContext = context;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        //Paint Background
        Paint background = new Paint();
        background.setColor(getResources().getColor(R.color.background);
        canvas.drawRect(0, 0, getWidth(), getHeight(), background);

        //Set vars for Arrow Paint
        Paint paint = new Paint();
        paint.setColor(getResources().getColor(R.color.arrowColor);
        centerX = getWidth() / 2;
        centerY = getHeight() / 2;
        arrLength = radius - 10;

        if(centerX < centerY)
            radius = centerX - margin;
        else 
            radius = centerY - margin;

        //Draw Shaft
        int[] xy = findArrowPos(arrLength, arrHeading);
        canvas.drawLine(centerX, centerY, xy[0], xy[1], paint);

        //Draw ArrowHead
            //This is where I'm confused

    }

    private int[] findArrowPos(double length, double angle) {
        int[] points = new int[2];
        double theta = Math.toRadians(angle);
        points[0] = centerX + (int) (length * Math.cos(theta));
        points[1] = centerY + (int) (length * Math.sin(theta));
        return points;
    }
}

I have taken a look at the following threads for guidance:
* http://www.java-forums.org/awt-swing/6241-how-u-rotate-arrow-mark-line-moves-accordingly.html
* How to draw a directed arrow line in Java?

Commonly answered 15/7, 2011 at 22:18 Comment(0)
H
20

How about using "Path myPath = new Path();" where you would give the x and y positions to create a triangle using lines and filling it. You can read about it, here is an example I took from somewhere.

// create and draw triangles
// use a Path object to store the 3 line segments
// use .offset to draw in many locations
// note: this triangle is not centered at 0,0
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.RED);
Path path = new Path();
path.moveTo(0, -10);
path.lineTo(5, 0);
path.lineTo(-5, 0);
path.close();
path.offset(10, 40);
canvas.drawPath(path, paint);
path.offset(50, 100);
canvas.drawPath(path, paint);
// offset is cumlative
// next draw displaces 50,100 from previous
path.offset(50, 100);
canvas.drawPath(path, paint);
Hueyhuff answered 15/7, 2011 at 22:33 Comment(1)
Aha! (on my attempt that I didn't post) I forgot to use the moveTo method to let it know where to start drawing from! I assume it create such a weird looking object because it must have drawn from (0,0). Thank you for the help!Commonly
C
13

My Arrow Drawing code, maybe it can be of some use for somebody:

    /**
 * Draw an arrow
 * change internal radius and angle to change appearance
 * - angle : angle in degrees of the arrows legs
 * - radius : length of the arrows legs
 * @author Steven Roelants 2017
 *
 * @param paint
 * @param canvas
 * @param from_x
 * @param from_y
 * @param to_x
 * @param to_y
 */
private void drawArrow(Paint paint, Canvas canvas, float from_x, float from_y, float to_x, float to_y)
{
    float angle,anglerad, radius, lineangle;

    //values to change for other appearance *CHANGE THESE FOR OTHER SIZE ARROWHEADS*
    radius=10;
    angle=15;

    //some angle calculations
    anglerad= (float) (PI*angle/180.0f);
    lineangle= (float) (atan2(to_y-from_y,to_x-from_x));

    //tha line
    canvas.drawLine(from_x,from_y,to_x,to_y,paint);

    //tha triangle
    Path path = new Path();
    path.setFillType(Path.FillType.EVEN_ODD);
    path.moveTo(to_x, to_y);
    path.lineTo((float)(to_x-radius*cos(lineangle - (anglerad / 2.0))),
            (float)(to_y-radius*sin(lineangle - (anglerad / 2.0))));
    path.lineTo((float)(to_x-radius*cos(lineangle + (anglerad / 2.0))),
            (float)(to_y-radius*sin(lineangle + (anglerad / 2.0))));
    path.close();

    canvas.drawPath(path, paint);
}
Corcoran answered 19/1, 2017 at 6:4 Comment(3)
buddy sorry to say but your code aint generating arrowsBenjaminbenji
@AyushBansal, it works. Just change the radius and angle to radius = 30f angle = 35fFactual
this is very good answerVidovik
G
6

I've been having the same problem, I need an arrow to point in a certain direction. After playing around with drawing algorithms I decided the simplest method is to use a bitmap & simply use a Matrix to rotate it, e.g.

ImageView image = (ImageView) findViewById(R.id.bitmap_image);
Bitmap bMap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
Matrix mat = new Matrix();
mat.postRotate(90);
Bitmap bMapRotate = Bitmap.createBitmap(bMap, 0, 0, bMap.getWidth(), bMap.getHeight(), mat, true);
image.setImageBitmap(bMapRotate);

then your bitmap can be any fancy looking arrow you like.

Gainor answered 15/7, 2011 at 22:50 Comment(1)
thank you for the reply and the insight! I have been trying to avoid throwing an image in there (as I found examples on the net how to do it with an image). I might, however, need to use an image. If I do need to use the image, I'll come back here and give your code a shot. +1 for the example :)Commonly
T
6

I try this code it has been working perfectly:

switch (event.getAction())
{
   case MotionEvent.ACTION_DOWN:
        mPath.reset();
        mPath.moveTo(x, y);
        mX = x;
        mY = y;
        startPoint = new PointF(event.getX(), event.getY());
        endPoint = new PointF();
        invalidate();
        break;
    case MotionEvent.ACTION_MOVE:
            float dx = Math.abs(x - mX);
        System.out.println("action move");
        float dy = Math.abs(y - mY);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE)
        {
        //  currentDrawingPath.path.quadTo(mX,mY,(x + mX)/2, (y + mY)/2);
        }
        mX = x;
        mY = y;
          endPoint.x = event.getX();
          endPoint.y = event.getY();
          isDrawing = true;
          invalidate();
        break;
    case MotionEvent.ACTION_UP:
           mPath.lineTo(mX, mY);
           float deltaX =   endPoint.x-startPoint.x;
           float deltaY =   endPoint.y-startPoint.y;
           float frac = (float) 0.1;
     float point_x_1 = startPoint.x + (float) ((1 - frac) * deltaX + frac * deltaY);
     float point_y_1 = startPoint.y + (float) ((1 - frac) * deltaY - frac * deltaX);
           float point_x_2 = endPoint.x;
           float point_y_2 = endPoint.y;
     float point_x_3 = startPoint.x + (float) ((1 - frac) * deltaX - frac * deltaY);
     float point_y_3 = startPoint.y + (float) ((1 - frac) * deltaY + frac * deltaX);
           mPath.moveTo(point_x_1, point_y_1);
           mPath.lineTo(point_x_2, point_y_2);
           mPath.lineTo(point_x_3, point_y_3);
           mPath.lineTo(point_x_1, point_y_1);
           mPath.lineTo(point_x_1, point_y_1);
            mCanvas.drawPath(mPath, ppaint);
            endPoint.x = event.getX();
            endPoint.y = event.getY();
            isDrawing = false;
            invalidate();
        break;
    default:
        break;
}       
Twiggy answered 30/11, 2012 at 7:58 Comment(1)
Very nice solution. Thanks. I modified it to deliver a fixed size arrow head independent of zoom level/line length. All that's needed is to replace "float frac = (float) 0.1;" with int ARROWHEAD_LENGTH=20; float sideZ= (float) Math.sqrt(deltaX deltaX + deltaYdeltaY); //Z=hypotenuse float frac = ARROWHEAD_LENGTH < sideZ ? ARROWHEAD_LENGTH/sideZ : 1.0f;"Steric
B
3

If you are looking for the solution to draw thousands of arrows under a second, with fixed length head lines, try this function (draws only arrow heads):

private void fillArrow(Paint paint, Canvas canvas, float x0, float y0, float x1, float y1) {
    paint.setStyle(Paint.Style.STROKE);

    int arrowHeadLenght = 10;
    int arrowHeadAngle = 45;
    float[] linePts = new float[] {x1 - arrowHeadLenght, y1, x1, y1};
    float[] linePts2 = new float[] {x1, y1, x1, y1 + arrowHeadLenght};
    Matrix rotateMat = new Matrix();

    //get the center of the line
    float centerX = x1;
    float centerY = y1;

    //set the angle
    double angle = Math.atan2(y1 - y0, x1 - x0) * 180 / Math.PI + arrowHeadAngle;

    //rotate the matrix around the center
    rotateMat.setRotate((float) angle, centerX, centerY);
    rotateMat.mapPoints(linePts);
    rotateMat.mapPoints(linePts2);

    canvas.drawLine(linePts [0], linePts [1], linePts [2], linePts [3], paint);
    canvas.drawLine(linePts2 [0], linePts2 [1], linePts2 [2], linePts2 [3], paint);
}

Based on https://gamedev.stackexchange.com/questions/44456/drawing-lines-on-android-with-matrix

Blumenfeld answered 30/4, 2015 at 14:57 Comment(0)
P
3

Here is code working perfect for me draw arrow head while drawing line on canvas

package com.example.canvasexample;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;

import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;

public class DrawerViewArrow extends View {
    private ArrayList<Path> drawingLinePath;
    private ArrayList<Path> drawingArrowPath;
    private ArrayList<Paint> drawingLinePaint;
    private int pathIndex = 0;
    private float startX = -1, startY = -1;
    private float mX = -1, mY = -1;

    public int arrowLength = 80;
    public int arrowWidth = 45;
    public int strokeWidth = 10;

    public DrawerViewArrow(Context context) {
        super(context);
        initPath();
    }

    public DrawerViewArrow(Context context, @NonNull AttributeSet attrs) {
        super(context, attrs);
        initPath();
    }

    public DrawerViewArrow(Context context, @NonNull AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPath();
    }

    private Paint initPaint() {
        Paint mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(strokeWidth);
        return mPaint;
    }

    private void initPath() {
        drawingLinePath = new ArrayList<>();
        drawingArrowPath = new ArrayList<>();
        drawingLinePath.add(new Path());
        drawingArrowPath.add(new Path());
        drawingLinePaint = new ArrayList<>();
        drawingLinePaint.add(initPaint());
        pathIndex++;
    }

    private Path createPath(MotionEvent event) {
        Path path = new Path();
        path.moveTo(event.getX(), event.getY());
        return path;
    }

    private void updateIndex(MotionEvent event) {
        if (pathIndex == drawingLinePath.size()) {
            drawingLinePath.add(createPath(event));
            drawingArrowPath.add(createPath(event));
            drawingLinePaint.add(initPaint());
            pathIndex++;
        }
    }


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

        if (startX > -1 && mX > -1) {
            canvas.drawLine(startX, startY, mX, mY, initPaint());
            drawArrow(canvas);
        }

        for (int index = 0; index < pathIndex; index++) {
            Path path = drawingLinePath.get(index);
            Path arrow_path = drawingArrowPath.get(index);
            Paint paint = drawingLinePaint.get(index);
            canvas.drawPath(path, paint);
            canvas.drawPath(arrow_path, paint);
        }
    }

    private void drawArrow(Canvas canvas) {
        double angle = calculateAngle(startX, startY, mX, mY);

        float final_angle = (float) (180 - angle);

        Path arrow_path = new Path();

        Matrix arrow_matrix = new Matrix();

        arrow_matrix.postRotate(final_angle, mX, mY);

        arrow_path.moveTo(mX, mY);
        arrow_path.lineTo(mX - arrowWidth, mY + arrowLength);
        arrow_path.moveTo(mX, mY);
        arrow_path.lineTo(mX + arrowWidth, mY + arrowLength);
        arrow_path.lineTo(mX - (arrowWidth), mY + arrowLength);
        arrow_path.transform(arrow_matrix);

        canvas.drawPath(arrow_path, initPaint());
    }

    private void saveArrow() {
        if (mX == -1 || mY == -1) {
            return;
        }

        double angle = calculateAngle(startX, startY, mX, mY);

        float final_angle = (float) (180 - angle);

        Path arrow_path = drawingArrowPath.get(pathIndex - 1);

        Matrix arrow_matrix = new Matrix();

        arrow_matrix.postRotate(final_angle, mX, mY);

        arrow_path.moveTo(mX, mY);
        arrow_path.lineTo(mX - arrowWidth, mY + arrowLength);
        arrow_path.moveTo(mX, mY);
        arrow_path.lineTo(mX + arrowWidth, mY + arrowLength);
        arrow_path.lineTo(mX - (arrowWidth), mY + arrowLength);
        arrow_path.transform(arrow_matrix);
    }

    public double calculateAngle(double x1, double y1, double x2, double y2) {
        double angle = Math.toDegrees(Math.atan2(x2 - x1, y2 - y1));

        angle = angle + Math.ceil(-angle / 360) * 360; //Keep angle between 0 and 360

        return angle;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case ACTION_UP:
                actionUp(event);
                break;
            case ACTION_MOVE:
                actionMove(event);
                break;
            case ACTION_DOWN:
                actionDown(event);
                break;
        }
        invalidate();
        return true;
    }

    private void actionDown(MotionEvent event) {
        updateIndex(event);
        startX = event.getX();
        startY = event.getY();
    }

    private void actionMove(MotionEvent event) {
        mX = event.getX();
        mY = event.getY();
    }

    private void actionUp(MotionEvent event) {
        drawingLinePath.get(pathIndex - 1).lineTo(event.getX(), event.getY());
        saveArrow();
        startX = -1;
        startY = -1;
        mX = -1;
        mY = -1;
    }
}
Prithee answered 23/8, 2019 at 12:55 Comment(0)
D
2

Use a Path as below and adjust the co-ordinates accordingly:

// Construct a wedge-shaped path
Path mPath = new Path();
mPath.moveTo(0, -50);
mPath.lineTo(-20, 60);
mPath.lineTo(0, 50);
mPath.lineTo(20, 60);
mPath.close();
Dream answered 15/7, 2011 at 22:34 Comment(0)
O
0

Copypast from this answer https://mcmap.net/q/538488/-how-to-draw-an-arrow-using-android-graphic-class

private void fillArrow(Canvas canvas, float x0, float y0, float x1, float y1) {
    paint.setStyle(Paint.Style.FILL);

    float deltaX = x1 - x0;
    float deltaY = y1 - y0;
    double distance = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY));
    float frac = (float) (1 / (distance / 30));

    float point_x_1 = x0 + (float) ((1 - frac) * deltaX + frac * deltaY);
    float point_y_1 = y0 + (float) ((1 - frac) * deltaY - frac * deltaX);

    float point_x_2 = x1;
    float point_y_2 = y1;

    float point_x_3 = x0 + (float) ((1 - frac) * deltaX - frac * deltaY);
    float point_y_3 = y0 + (float) ((1 - frac) * deltaY + frac * deltaX);

    Path path = new Path();
    path.setFillType(Path.FillType.EVEN_ODD);

    path.moveTo(point_x_1, point_y_1);
    path.lineTo(point_x_2, point_y_2);
    path.lineTo(point_x_3, point_y_3);
    path.lineTo(point_x_1, point_y_1);
    path.lineTo(point_x_1, point_y_1);
    path.close();

    canvas.drawPath(path, paint);
}
Ormand answered 29/7, 2020 at 2:23 Comment(0)
M
0

Here's my arrow drawing code without using trig functions explicitly (although the underlying math obviously uses trig) The math makes an arrow head like half of a square (cut diagonally) where variable L is the length of the diagonal. Also, the arrow ends at point p2 which means that for small difference between p2 and p1, the arrow head will be drawn 'before' p2 for sufficient L. Also if p1 and p2 are the same, the arrow will not be drawn because the math would cause division by zero. I suggest you use Paint.Style.FILL_AND_STROKE to draw this arrow. I'm open for any questions.

void drawArrow(Canvas canvas, Point p1, Point p2, float L) {
        float fsin, fcos;
        double d;

        if(p1.equals(p2))
            return;

        d = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
        fsin = (p2.y - p1.y)/(float)d;
        fcos = (p2.x - p1.x)/(float)d;
        PointF p3 = new PointF(p2.x - L/2*(fsin + fcos), p2.y + L/2*(fcos - fsin));
        PointF p4 = new PointF(p2.x + L/2*(fsin - fcos), p2.y - L/2*(fsin + fcos));

        canvas.drawLine(p1.x, p1.y, p2.x, p2.y, arrowPaint);

        Path path = new Path();
        path.setFillType(Path.FillType.EVEN_ODD);
        path.moveTo(p2.x, p2.y);
        path.lineTo(p3.x, p3.y);
        path.lineTo(p4.x, p4.y);
        path.close();

        canvas.drawPath(path, arrowPaint);
    }
Milkweed answered 11/11, 2021 at 10:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.