72

http://marakana.com/tutorials/android/2d-graphics-example.html

I am using this example below. But when I move my fingers too fast across the screen the line turns to individual dots.

I am not sure whether I can speed up the drawing. Or I should connect the two last points with a straight line. The second of these two solutions seems like a good option, except when moving your finger very fast you will have long sections of a straight line then sharp curves.

If there are any other solutions it would be great to hear them.

Thanks for any help in advance.

Somk
  • 11,869
  • 32
  • 97
  • 143
  • If straight lines turn out to be "not good enough" for your purpose, you can look into curve-fitting: http://stackoverflow.com/questions/878200/java-curve-fitting-library – HostileFork says dont trust SE Nov 27 '11 at 19:47
  • Thank you that sounds useful. I hadn't thought about using splines. Mainly because I would assume it uses much more resources. Also is that available for android – Somk Nov 27 '11 at 19:52
  • 2
    You've got `Path.quadTo`, `Path.cubicTo`... http://stackoverflow.com/questions/3811529/draw-a-perfect-curve-connecting-three-points – HostileFork says dont trust SE Nov 27 '11 at 19:56
  • http://stackoverflow.com/questions/11328848/drawing-a-circle-where-the-user-has-touched-on-canvas/28263820#28263820 – Yogendra Feb 01 '15 at 14:47

11 Answers11

116

An easy solution, as you mentioned, is to simply connect the points with a straight line. Here's the code to do so:

public void onDraw(Canvas canvas) {
    Path path = new Path();
    boolean first = true;
    for(Point point : points){
        if(first){
            first = false;
            path.moveTo(point.x, point.y);
        }
        else{
            path.lineTo(point.x, point.y);
        }
    }
    canvas.drawPath(path, paint);
}

make sure you change your paint from fill to stroke:

paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.WHITE);

Another option is to connect the points with iterpolation using the quadTo method:

public void onDraw(Canvas canvas) {
    Path path = new Path();
    boolean first = true;
    for(int i = 0; i < points.size(); i += 2){
        Point point = points.get(i);
        if(first){
            first = false;
            path.moveTo(point.x, point.y);
        }

        else if(i < points.size() - 1){
            Point next = points.get(i + 1);
            path.quadTo(point.x, point.y, next.x, next.y);
        }
        else{
            path.lineTo(point.x, point.y);
        }
    }

    canvas.drawPath(path, paint);
}

This still results in some sharp edges.

If you're really ambitious, you can start to calculate the cubic splines as follows:

public void onDraw(Canvas canvas) {
    Path path = new Path();

    if(points.size() > 1){
        for(int i = points.size() - 2; i < points.size(); i++){
            if(i >= 0){
                Point point = points.get(i);

                if(i == 0){
                    Point next = points.get(i + 1);
                    point.dx = ((next.x - point.x) / 3);
                    point.dy = ((next.y - point.y) / 3);
                }
                else if(i == points.size() - 1){
                    Point prev = points.get(i - 1);
                    point.dx = ((point.x - prev.x) / 3);
                    point.dy = ((point.y - prev.y) / 3);
                }
                else{
                    Point next = points.get(i + 1);
                    Point prev = points.get(i - 1);
                    point.dx = ((next.x - prev.x) / 3);
                    point.dy = ((next.y - prev.y) / 3);
                }
            }
        }
    }

    boolean first = true;
    for(int i = 0; i < points.size(); i++){
        Point point = points.get(i);
        if(first){
            first = false;
            path.moveTo(point.x, point.y);
        }
        else{
            Point prev = points.get(i - 1);
            path.cubicTo(prev.x + prev.dx, prev.y + prev.dy, point.x - point.dx, point.y - point.dy, point.x, point.y);
        }
    }
    canvas.drawPath(path, paint);
}

Also, I found that you needed to change the following to avoid duplicate motion events:

public boolean onTouch(View view, MotionEvent event) {
    if(event.getAction() != MotionEvent.ACTION_UP){
        Point point = new Point();
        point.x = event.getX();
        point.y = event.getY();
        points.add(point);
        invalidate();
        Log.d(TAG, "point: " + point);
        return true;
    }
    return super.onTouchEvent(event);
}

and add the dx & dy values to the Point class:

class Point {
    float x, y;
    float dx, dy;

    @Override
    public String toString() {
        return x + ", " + y;
    }
}

This produces smooth lines, but sometimes has to connect the dots using a loop. Also, for long drawing sessions, this becomes computationally intensive to calculate.

Edit

I threw together a quick project demonstrating these different techniques, including Square's suggessted signature implementation. Enjoy: https://github.com/johncarl81/androiddraw

starball
  • 20,030
  • 7
  • 43
  • 238
John Ericksen
  • 10,995
  • 4
  • 45
  • 75
  • 11
    This is an interesting article on the subject. Seems like android batches the touch events: http://corner.squareup.com/2010/07/smooth-signatures.html – John Ericksen Jun 18 '12 at 20:31
  • 4
    The first loop cubicTo() example seems to be wrong. It should loop over all points, not only the last one. – Eric Obermühlner Nov 05 '12 at 07:23
  • Eric Obermühlner: I am looping over all points in the for loop around the cubicTo() call. – John Ericksen Nov 05 '12 at 20:08
  • 4
    @johncarl I agree with Eric. I think the curves code is wrong - the first loop only smooths out the last few points. I propose the first loop run from 0, not from "points.size() - 2". (this is based on the fact that I actually copied this code into my graph app and tested it) – Richard Le Mesurier Mar 30 '13 at 16:15
  • hmmm, I still don't see the issue. I have the code on github if you would like to submit a pull request: https://github.com/johncarl81/androiddraw – John Ericksen Mar 30 '13 at 17:30
  • @johncarl i am trying to draw smooth line as here http://stackoverflow.com/questions/16455896/android-signature-using-bezier-but-not-draw-any-line but its not working..Any idea? – kyogs May 09 '13 at 07:18
  • I have used this code in my class and its working fine,But thing is that when one line drawn then on drawing second class it starts from end point of 1st line..means drawing is connected..so please help me for it a free drwaing practice..! – jigar Aug 13 '13 at 04:19
  • @johncarl can u please look at my question, and see if there any solution for that – AndroidDev Jan 07 '14 at 09:54
  • This is a great answer, but it should be for(int i = 0 ; i < points.size(); i++){ when calculating cubic splines. – QuinnBaetz Mar 04 '14 at 00:43
  • 6
    Android has already a built-in functionality the same as this one with much more feautre, w/c you can refer [here](http://android-coding.blogspot.com/2014/05/composepatheffect-example.html). Just use the first example of johncarl and you can achieve the smooth edge lines. – mr5 Jun 09 '14 at 06:05
  • can you tell me how to remove and clear out all the canvas? I mean what ever I have drawn , how can I clear out all the drawn things from it ? – Coas Mckey Oct 14 '15 at 08:11
35

This might be not important anymore for you but I struggled a lot to solve it and I want to share, might be useful to someone else.

The tutorial with the solution @johncarl offered are great to drawing but they offered a limitation for my purposes. If you take your finger out of the screen and put it back, this solution will draw a line between the last click and your new click, making the whole drawing connected always. So I was trying to find a solution for that, and finally I got it!( sorry if sounds obvious, I am a beginner with graphics)

public class MainActivity extends Activity {
    DrawView drawView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Set full screen view
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                     WindowManager.LayoutParams.FLAG_FULLSCREEN);
        requestWindowFeature(Window.FEATURE_NO_TITLE);

        drawView = new DrawView(this);
        setContentView(drawView);
        drawView.requestFocus();
    }
}


public class DrawingPanel extends View implements OnTouchListener {
    private static final String TAG = "DrawView";

    private static final float MINP = 0.25f;
    private static final float MAXP = 0.75f;

    private Canvas  mCanvas;
    private Path    mPath;
    private Paint       mPaint;   
    private LinkedList<Path> paths = new LinkedList<Path>();

    public DrawingPanel(Context context) {
        super(context);
        setFocusable(true);
        setFocusableInTouchMode(true);

        this.setOnTouchListener(this);

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(6);
        mCanvas = new Canvas();
        mPath = new Path();
        paths.add(mPath);
    }               

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

    @Override
    protected void onDraw(Canvas canvas) {            
        for (Path p : paths){
            canvas.drawPath(p, mPaint);
        }
    }

    private float mX, mY;
    private static final float TOUCH_TOLERANCE = 4;

    private void touch_start(float x, float y) {
        mPath.reset();
        mPath.moveTo(x, y);
        mX = x;
        mY = y;
    }

    private void touch_move(float x, float y) {
        float dx = Math.abs(x - mX);
        float dy = Math.abs(y - mY);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
            mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
            mX = x;
            mY = y;
        }
    }

    private void touch_up() {
        mPath.lineTo(mX, mY);
        // commit the path to our offscreen
        mCanvas.drawPath(mPath, mPaint);
        // kill this so we don't double draw            
        mPath = new Path();
        paths.add(mPath);
    }

    @Override
    public boolean onTouch(View arg0, MotionEvent event) {
        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touch_start(x, y);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                touch_move(x, y);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                touch_up();
                invalidate();
                break;
        }
        return true;
    } 
}  

I took the android sample for drawing with your finger and modified it a little to store every path instead of just the last one! Hope it helps someone!

Cheers.

caiocpricci2
  • 7,714
  • 10
  • 56
  • 88
  • 1
    Hey... Do try to clear the List somewhere, else it will slow down gradually as you keep drawing more and more at once.. – Arun Abraham Jul 17 '12 at 10:22
  • That was not a problem for application that do not run this for long (such as the one I did), for my implementation this never runs for longer than 1 minute or so. But thanks for pointing that problem! – caiocpricci2 Jul 17 '12 at 18:17
  • 1
    Hey. With storing the paths for drawing, it is easy to implement the "undo" or "redo" functions. But how can we implements the erase function if we do not store the drawing into bitmap? – Yeung Jan 09 '13 at 02:39
  • I don't think you can. If you don't store it how can you find out which one to delete? – caiocpricci2 Jan 09 '13 at 10:04
  • Just curious, but what's `mCanvas` used for? You mentioned "commit the path to our offscreen" in `touch_up()` but it doesn't look like `mCanvas` is ever used anywhere else. – Jake Stoeffler Jun 01 '13 at 06:11
  • Could you helpme with my [question](http://stackoverflow.com/questions/16989718/load-bitmap-into-canvas-and-draw-over-it)?? – Leonardo Sapuy Jun 11 '13 at 15:55
  • too good , but can you tell me how to remove and clear out all the canvas? I mean what ever I have drawn , how can I clear out all the drawn things from it ? – Coas Mckey Oct 14 '15 at 08:11
  • great answer. It helped a lot :D – Abir Hasan Shawon Jan 24 '17 at 11:38
23

I have experimented with several ways to render the accumulated points of the motion events. In the end I had the best results by calculating the mid-points between two points and treating the points in the list as anchor points of quadratic Bezier curves (except the first and last point which are connected by simple lines to the next mid-point).

This gives a smooth curve without any corners. The drawn path will not touch the actual points in the list but go through every mid-point.

Path path = new Path();
if (points.size() > 1) {
    Point prevPoint = null;
    for (int i = 0; i < points.size(); i++) {
        Point point = points.get(i);

        if (i == 0) {
            path.moveTo(point.x, point.y);
        } else {
            float midX = (prevPoint.x + point.x) / 2;
            float midY = (prevPoint.y + point.y) / 2;

            if (i == 1) {
                path.lineTo(midX, midY);
            } else {
                path.quadTo(prevPoint.x, prevPoint.y, midX, midY);
            }
        }
        prevPoint = point;
    }
    path.lineTo(prevPoint.x, prevPoint.y);
}
Eric Obermühlner
  • 1,076
  • 9
  • 9
10

If you want it simple:

public class DrawByFingerCanvas extends View {

    private Paint brush = new Paint(Paint.ANTI_ALIAS_FLAG);
    private Path path = new Path();

    public DrawByFingerCanvas(Context context) {
        super(context);
        brush.setStyle(Paint.Style.STROKE);
        brush.setStrokeWidth(5);
    }

    @Override
    protected void onDraw(Canvas c) {
        c.drawPath(path, brush);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                path.moveTo(x,y);
                break;
            case MotionEvent.ACTION_MOVE:
                path.lineTo(x, y);
                break;
            default:
                return false;
        }
        invalidate();
        return true;
    }
}

In the activity just:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(new DrawByFingerCanvas(this));
}

Result:

enter image description here

To erase all drawings just rotate the screen.

Andrew
  • 36,676
  • 11
  • 141
  • 113
6

I had very similar problem. When you're calling onTouch method, you should also use method (inside onTouch(MotionEvent event))

event.getHistorySize();

and something like that

int histPointsAmount = event.getHistorySize(); 
for(int i = 0; i < histPointsAmount; i++){
    // get points from event.getHistoricalX(i);
    // event.getHistoricalY(i); and use them for your purpouse
}
y434y
  • 633
  • 1
  • 8
  • 27
4

Motion events with ACTION_MOVE may batch together multiple movement samples within a single object. The most current pointer coordinates are available using getX(int) and getY(int). Earlier coordinates within the batch are accessed using getHistoricalX(int, int) and getHistoricalY(int, int). Using them for building path makes it much smoother :

    int historySize = event.getHistorySize();
    for (int i = 0; i < historySize; i++) {
      float historicalX = event.getHistoricalX(i);
      float historicalY = event.getHistoricalY(i);
      path.lineTo(historicalX, historicalY);
    }

    // After replaying history, connect the line to the touch point.
    path.lineTo(eventX, eventY);

Here is a good tutorial on this from Square : http://corner.squareup.com/2010/07/smooth-signatures.html

amukhachov
  • 5,822
  • 1
  • 41
  • 60
3

I had to make some modifications to this recently, and have now developed what I believe to be the best solution here because it does three things:

  1. It allows you to draw different lines
  2. It works with larger brush strokes and without using complicated cubic splines
  3. It is faster than a lot of the solutions here because the canvas.drawPath() method is outside the for loop, so it is not called multiple times.

public class DrawView extends View implements OnTouchListener {
private static final String TAG = "DrawView";

List<Point> points = new ArrayList<Point>();
Paint paint = new Paint();
List<Integer> newLine = new ArrayList<Integer>();

public DrawView(Context context, AttributeSet attrs){
        super(context, attrs);
        setFocusable(true);
        setFocusableInTouchMode(true);
        setClickable(true);

        this.setOnTouchListener(this);

        paint.setColor(Color.WHITE);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(20);

    }

    public void setColor(int color){
        paint.setColor(color);
    }
    public void setBrushSize(int size){
        paint.setStrokeWidth((float)size);
    }
    public DrawView(Context context) {
        super(context);
        setFocusable(true);
        setFocusableInTouchMode(true);

        this.setOnTouchListener(this);


        paint.setColor(Color.BLUE);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(20);
    }

    @Override
    public void onDraw(Canvas canvas) {
        Path path = new Path();
        path.setFillType(Path.FillType.EVEN_ODD);
        for (int i = 0; i<points.size(); i++) {
            Point newPoint = new Point();
            if (newLine.contains(i)||i==0){
                newPoint = points.get(i)
                path.moveTo(newPoint.x, newPoint.y);
            } else {
                newPoint = points.get(i);

                path.lineTo(newPoint.x, newPoint.y);
            }

        }
        canvas.drawPath(path, paint);
    }

    public boolean onTouch(View view, MotionEvent event) {
        Point point = new Point();
        point.x = event.getX();
        point.y = event.getY();
        points.add(point);
        invalidate();
        Log.d(TAG, "point: " + point);
        if(event.getAction() == MotionEvent.ACTION_UP){
            // return super.onTouchEvent(event);
            newLine.add(points.size());
        }
        return true;
    }
    }

    class Point {
        float x, y;

    @Override
    public String toString() {
        return x + ", " + y;
    }
    }

This also works, just not quite as well

  import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.util.*;

public class DrawView extends View implements OnTouchListener {
    private static final String TAG = "DrawView";
List<Point> points = new ArrayList<Point>();
Paint paint = new Paint();
List<Integer> newLine = new ArrayList<Integer>();

public DrawView(Context context, AttributeSet attrs){
    super(context, attrs);
    setFocusable(true);
    setFocusableInTouchMode(true);

    this.setOnTouchListener(this);

    paint.setColor(Color.WHITE);
    paint.setAntiAlias(true);
}
public DrawView(Context context) {
    super(context);
    setFocusable(true);
    setFocusableInTouchMode(true);

    this.setOnTouchListener(this);

    paint.setColor(Color.WHITE);
    paint.setAntiAlias(true);
    }

@Override
public void onDraw(Canvas canvas) {
    for (int i = 0; i<points.size(); i++) {
        Point newPoint = new Point();
        Point oldPoint = new Point();
        if (newLine.contains(i)||i==0){
            newPoint = points.get(i);
            oldPoint = newPoint;
        } else {
            newPoint = points.get(i);
            oldPoint = points.get(i-1);
        }
            canvas.drawLine(oldPoint.x, oldPoint.y, newPoint.x, newPoint.y, paint);
    }
}

public boolean onTouch(View view, MotionEvent event) {
    Point point = new Point();
    point.x = event.getX();
    point.y = event.getY();
    points.add(point);
    invalidate();
    Log.d(TAG, "point: " + point);
    if(event.getAction() == MotionEvent.ACTION_UP){
        // return super.onTouchEvent(event);
        newLine.add(points.size());
    }
    return true;
    }
}

class Point {
    float x, y;

    @Override
    public String toString() {
        return x + ", " + y;
    }
}

It lets you draw lines reasonably well, the only problem is if you make the line thicker, which makes the lines drawn look a little odd, and really, I would recommend using the first one anyways.

jcw
  • 5,132
  • 7
  • 45
  • 54
1

Here is a simplified solution that draws a line that follows your finger and is always straight:

https://stackoverflow.com/a/68076519/15463816

Cloud Town
  • 83
  • 5
1

You may have a lot more information available in your MotionEvent than you realize that can provide some data inbetween.

The example in your link ignores the historical touch points included within the event. See the 'Batching' section near the top of MotionEvent's documentation: http://developer.android.com/reference/android/view/MotionEvent.html Beyond that connecting the points with lines may not be a bad idea.

adamp
  • 28,862
  • 9
  • 81
  • 69
0

I had this issue, i was drawing a point instead of a line. You should create a path first to hold your line. call path.moveto on your first touch event only. Then on your canvas draw the path and then reset or rewind the path after your done (path.reset)...

j2emanue
  • 60,549
  • 65
  • 286
  • 456
0

Here is a simple method for smoothing points drawn with Path.lineTo

fun applySmoothing(smoothingIterations: Int) {
    for (z in 1..smoothingIterations) {
        for (i in graphPoints.indices) {
            if (i > 0 && i < graphPoints.size-1) {
                val previousPoint = graphPoints[i-1]
                val currentPoint = graphPoints[i]
                val nextPoint = graphPoints[i+1]
                val midX = (previousPoint.x + currentPoint.x + nextPoint.x) / 3
                val midY = (previousPoint.y + currentPoint.y + nextPoint.y) / 3
                graphPoints[i].x = midX
                graphPoints[i].y = midY
            }
        }
    }
}
Adam Johns
  • 35,397
  • 25
  • 123
  • 176