I am new to java drawing and I would like to implement a navigation system so a user can navigate on a map. The java function 'onDraw' is implement so it shows the location of the user on the map and also show his movements. Below are my codes:
class CanvasView extends View {
private Paint paint = new Paint();
private Path path = new Path();
private float lineX = Position.x_axis;
private float lineY = Position.y_axis;
private float lastX = Position.x_axis;
private float lastY = Position.y_axis;
public CanvasView(Context context) {
super(context);
paint.setAntiAlias(true);
paint.setStrokeWidth(10f);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
}
#Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
drawRotateImage(canvas);
canvas.drawPoint(Position.x_axis, Position.y_axis, paint);
Path path = new Path();
path.moveTo(lastX, lastY);
path.lineTo(lineX,lineY);
canvas.drawPath(path, paint);
}
private void stepDetector (float step) {
stepCount += (int) step;
distance = stepCount * 30;
lineX = (float) (lineX + 30);
lineY = (float) (lineY + 30);
invalidate();
lastX = lineX;
lastY = lineY;
}
}
The stepdetector is called each time a step has been detected and it should repaint the canvas to show to movements. Noting that the previous movement line needs to be visible, that is it should be a continuous line.
I am having issues to get the navigation line path. It would be grateful if I can get some help on it.
Thank you in advance.
try this, make list and for every step add points, and in ondraw draw all points from list
class CanvasView extends View {
List<Point> points;
private Paint paint = new Paint();
private Path path = new Path();
private float lineX = Position.x_axis;
private float lineY = Position.y_axis;
private float lastX = Position.x_axis;
private float lastY = Position.y_axis;
public CanvasView(Context context) {
super(context);
paint.setAntiAlias(true);
paint.setStrokeWidth(10f);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
points=new ArrayList<>();
}
#Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
drawRotateImage(canvas);
canvas.drawPoint(Position.x_axis, Position.y_axis, paint);
// now here draw all points from list
Path path = new Path();
path.moveTo(lastX, lastY);
path.lineTo(lineX,lineY);
canvas.drawPath(path, paint);
}
private void stepDetector (float step) {
stepCount += (int) step;
distance = stepCount * 30;
lineX = (float) (lineX + 30);
lineY = (float) (lineY + 30);
invalidate();
lastX = lineX;
lastY = lineY;
// add every point to the list after step
}
class Point{
float x,y;
}
}
The onDraw draws a fresh copy of the contents everytime and does not remain old information. For what you are trying to draw, you can keep a list of type Position and add the new position to the list when stepDetector is called.
Then, inside onDraw, iterate over the list and draw lines connecting all the points from the list .
Related
I am trying to make a painting app. Zoom kinda works, when i try to draw on canvas after the zoom, it does not draw on the location of the finger. instead it draws scale to the device screen.
I did some research but could not find the answer to the solution.
-> float x = (event.getX() - scalePointX)/mScaleFactor; // does not work,
I tried to implement matrix but was unsuccessful.
Can someone please help me to draw on the finger when the canvas is zoomed in or out?
Thank you!
public class paintView extends View {
Paint paint;
Path path;
Bitmap bitmap;
Canvas mcanvas;
private final float TOUCH_TOLERANCE = 4;
private float mX, mY;
public static final int DEFAULT_BG_COLOR = Color.WHITE;
private int backgroundColor = DEFAULT_BG_COLOR;
private ArrayList<FingerPath> paths = new ArrayList<>();
private Paint mBitmapPaint = new Paint(Paint.DITHER_FLAG);
private final ScaleGestureDetector mScaleGesture;
private float mScaleFactor = 1.f;
private float mPosX;
private float mPosY;
private float scalePointX, scalePointY;
private Rect clipBounds_canvas;
public paintView(Context context) {
this(context, null);
}
public paintView(Context context, AttributeSet attrs){
super(context, attrs);
mScaleGesture = new ScaleGestureDetector(context, new ScaleListener());
paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(8f);
}
public void init(DisplayMetrics metrics){
int height = metrics.heightPixels;
int width = metrics.widthPixels;
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mcanvas = new Canvas(bitmap);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
mcanvas.drawColor(backgroundColor);
clipBounds_canvas = canvas.getClipBounds();
for(FingerPath fp: paths){
paint.setMaskFilter(null);
mcanvas.drawPath(fp.path, paint);
}
canvas.translate(mPosX, mPosY);
canvas.scale(mScaleFactor, mScaleFactor,scalePointX,scalePointY);
canvas.drawBitmap(bitmap, 0, 0, mBitmapPaint);
canvas.restore();
}
private void touchStart(float x, float y){
path = new Path();
FingerPath fp = new FingerPath(path);
paths.add(fp);
path.reset();
path.moveTo(x,y);
mX = x;
mY = y;
}
private void touchMove(float x, float y){
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if(dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE ){
path.quadTo(mX, mY, (x+mX)/2, (y+mY)/2);
mX = x;
mY = y;
}
}
private void touchUp(){
path.lineTo(mX,mY);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
final float x = (event.getX() - scalePointX)/mScaleFactor;
final float y = (event.getY() - scalePointY)/mScaleFactor;
mScaleGesture.onTouchEvent(event);
final int action = event.getAction();
switch(action & MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_DOWN:
touchStart(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touchMove(x,y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touchUp();
invalidate();
break;
}
return true;
}
public class FingerPath {
public Path path;
public FingerPath(Path path) {
this.path = path;
}
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
scalePointX = detector.getFocusX();
scalePointY = detector.getFocusY();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(0.5f, Math.min(mScaleFactor, 3.0f));
//zoom out 'up to' the size of canvas(screen size)
//mScaleFactor = (mScaleFactor < 1 ? 1 : mScaleFactor);
invalidate();
return true;
}
#Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
}
}
I had that problem and was able to solve it with This stack overflow question. What I did was store the current scale offset and when I draw the new paths, I'd offset the drawing matrix by this stored value. Your solution is really close, instead of scaling the canvas you can scale the drawing matrix. Also a friendly tip, you might need to scale the line thickness after zooming also, so use the same value that was stored to scale.
Im trying to draw an arc in android, between two textView using drawArc. My requirement is to achieve an arc as shown in the image which is correct arc shape.
But when i tried ,i'm not getting an perfect arc shape and i got the arc which is of semi ovalshaped.
Here is my code snippet:
public class ArcView extends View {
final RectF oval = new RectF();
Path myPath = new Path();
Paint paint;
float left, top, right, bottom;
public FlightStatusArcView(Context context) {
super(context);
}
public FlightStatusArcView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public FlightStatusArcView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet set) {
if (set == null)
return;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
public void setArcProperties(float l, float t, float r, float b) {
left = l;
top = t;
right = r;
bottom = b;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
oval.set(left, top, right, bottom);
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5f);
canvas.drawArc(oval, 180f, 180f, false, paint);
drawArrow(canvas);
}
private void drawArrow(Canvas canvas) {
canvas.save();
Paint p = new Paint();
p.setColor(Color.WHITE);
p.setStyle(Paint.Style.FILL);
p.setStrokeWidth(4f);
float startX = oval.left + 20;
float startY = oval.top + (oval.centerY() - oval.top)/ 2;
Path path = new Path();
path.moveTo(startX, startY-20);
path.lineTo(startX + 20, startY + 20);
path.lineTo(startX + 30, startY -17);
path.close();
canvas.drawPath(path, p);
canvas.restore();
}
}
Please help how to achieve the perfect arc shape using the canvas apis.
You could use a Path together with quadTo or cubicTo to draw a curve like this. This will allow you to draw quadratic or cubic bezier curves. For instance, a cubic bezier curve with two identical control points (x2, y2) is constructed like this:
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5f);
Path p = new Path();
float x1 = 100;
float y1 = 256;
float x2 = 400;
float y2 = 32;
float x3 = 700;
float y3 = 256;
p.moveTo(x1, y1);
p.cubicTo(x2, y2, x2, y2, x3, y3);
canvas.drawPath(p, paint);
drawArrow(canvas);
}
This is the result:
You can find more information on cubicTo and quadTo in the official documentation
I'm trying to implement undo/redo in JavaFX - I draw all my shapes using graphicsContext(). I have looked around and found that there's a save method on Graphics Context but it just saves attributes and not the actual shape/state of the canvas. What would be the best way of going about this?
This is one of my code snippets when I create a circle, for instance:
public CircleDraw(Canvas canvas, Scene scene, BorderPane borderPane) {
this.borderPane = borderPane;
this.scene = scene;
this.graphicsContext = canvas.getGraphicsContext2D();
ellipse = new Ellipse();
ellipse.setStrokeWidth(1.0);
ellipse.setFill(Color.TRANSPARENT);
ellipse.setStroke(Color.BLACK);
pressedDownMouse = event -> {
startingPosX = event.getX();
startingPosY = event.getY();
ellipse.setCenterX(startingPosX);
ellipse.setCenterY(startingPosY);
ellipse.setRadiusX(0);
ellipse.setRadiusY(0);
borderPane.getChildren().add(ellipse);
};
releasedMouse = event -> {
borderPane.getChildren().remove(ellipse);
double width = Math.abs(event.getX() - startingPosX);
double height = Math.abs(event.getY() - startingPosY);
graphicsContext.setStroke(Color.BLACK);
graphicsContext.strokeOval(Math.min(startingPosX, event.getX()), Math.min(startingPosY, event.getY()), width, height);
removeListeners();
};
draggedMouse = event -> {
ellipse.setCenterX((event.getX() + startingPosX) / 2);
ellipse.setCenterY((event.getY() + startingPosY) / 2);
ellipse.setRadiusX(Math.abs((event.getX() - startingPosX) / 2));
ellipse.setRadiusY(Math.abs((event.getY() - startingPosY) / 2));
};
}
The problem here is that there is that information like this is not saved in a Canvas. Furthermore there is no inverse operation that allows you to get back to the previous state for every draw information. Surely you could stroke the same oval, but with backgrund color, however the information from previous drawing information could have been overwritten, e.g. if you're drawing multiple intersecting ovals.
You could store the drawing operations using the command pattern however. This allows you to redraw everything.
public interface DrawOperation {
void draw(GraphicsContext gc);
}
public class DrawBoard {
private final List<DrawOperation> operations = new ArrayList<>();
private final GraphicsContext gc;
private int historyIndex = -1;
public DrawBoard(GraphicsContext gc) {
this.gc = gc;
}
public void redraw() {
Canvas c = gc.getCanvas();
gc.clearRect(0, 0, c.getWidth(), c.getHeight());
for (int i = 0; i <= historyIndex; i++) {
operations.get(i).draw(gc);
}
}
public void addDrawOperation(DrawOperation op) {
// clear history after current postion
operations.subList(historyIndex+1, operations.size()).clear();
// add new operation
operations.add(op);
historyIndex++;
op.draw(gc);
}
public void undo() {
if (historyIndex >= 0) {
historyIndex--;
redraw();
}
}
public void redo() {
if (historyIndex < operations.size()-1) {
historyIndex++;
operations.get(historyIndex).draw(gc);
}
}
}
class EllipseDrawOperation implements DrawOperation {
private final double minX;
private final double minY;
private final double width;
private final double height;
private final Paint stroke;
public EllipseDrawOperation(double minX, double minY, double width, double height, Paint stroke) {
this.minX = minX;
this.minY = minY;
this.width = width;
this.height = height;
this.stroke = stroke;
}
#Override
public void draw(GraphicsContext gc) {
gc.setStroke(stroke);
gc.strokeOval(minX, minY, width, height);
}
}
Pass a DrawBoard instance to your class instead of the Canvas and replace
graphicsContext.setStroke(Color.BLACK);
graphicsContext.strokeOval(Math.min(startingPosX, event.getX()), Math.min(startingPosY, event.getY()), width, height);
with
drawBoard.addDrawOperation(new EllipseDrawOperation(
Math.min(startingPosX, event.getX()),
Math.min(startingPosY, event.getY()),
width,
height,
Color.BLACK));
The undo and redo to move through the history.
This is actually our Thesis, we are required to use the Ramer-Douglas-Peucker Algorithm in simplifying lines, can anyboy help me how to implement this in an Android App.
I just want to know how to get the string of points from the line I've drawn and simplify the line by reducing the total no. points based on the given code below?
This is the main class.
public class SketchTimeNewActivity extends GraphicsView implements ColorOption.OnColorChangedListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new MyView(this));
myPaint = new Paint();
myPaint.setAntiAlias(true);
myPaint.setDither(true);
myPaint.setColor(Color.CYAN);
myPaint.setStyle(Paint.Style.STROKE);
myPaint.setStrokeJoin(Paint.Join.ROUND);
myPaint.setStrokeCap(Paint.Cap.ROUND);
myPaint.setStrokeWidth(12);
}
private Paint myPaint;
public void colorChanged(int color) {
myPaint.setColor(color);
}
public class MyView extends View {
private static final float MINP = 0.25f;
private static final float MAXP = 0.75f;
private Bitmap mBitmap;
private Canvas mCanvas;
private Path mPath;
private Paint mBitmapPaint;
public MyView(Context c) {
super(c);
mPath = new Path();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
}
#Override
protected void onSizeChanged(int width, int height, int oldwidth, int oldheight) {
super.onSizeChanged(width, height, oldwidth, oldheight);
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(color.black);
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
canvas.drawPath(mPath, myPaint);
}
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, myPaint);
// kill this so we don't double draw
mPath.reset();
}
#Override
public boolean onTouchEvent(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;
}
}
private static final int COLOR_MENU_ID = Menu.FIRST;
private static final int EXISTING_MENU_ID = Menu.FIRST + 2;
private static final int ENHANCED_MENU_ID = Menu.FIRST + 3;
private static final int ERASE_MENU_ID = Menu.FIRST + 1;
#Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(0, COLOR_MENU_ID, 0, "Color").setShortcut('1', 'c');
menu.add(0, EXISTING_MENU_ID, 0, "Enhanced").setShortcut('2', 's');
menu.add(0, ENHANCED_MENU_ID, 0, "Existing").setShortcut('3', 'z');
menu.add(0, ERASE_MENU_ID, 0, "Erase").setShortcut('4', 'z');
return true;
}
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
myPaint.setXfermode(null);
myPaint.setAlpha(0xFFAAAAAA);
When the EXISTING MENU is clicked, it will simplify the line being drawn and display a line that has lesser points or a line that is already simplified. I'm planning to create a new class for it but I don't know how to get the string of points from the line being drawn in the canvas.
switch (item.getItemId()) {
case COLOR_MENU_ID:
new ColorOption(this, this, myPaint.getColor()).show();
return true;
/** case EXISTING_MENU_ID:
return true;
case ENHANCED_MENU_ID:
return true;*/
case ERASE_MENU_ID:{
myPaint.setColor(color.black);
myPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
return true;
}
}
return super.onOptionsItemSelected(item);
}
}
Copy paste from my original comment for some context:
You are generating the points on the line from your onTouchEvent, so
in stead of trying to query the Canvas afterwards, why not simply keep
a list of these created points? You can add a point as you draw each
new line segment.
In terms of code, your most basic example will look something like this:
List<Point> mPoints = new ArrayList<Point>();
private void touch_up() {
// save this point on the line
mPoints.add(new Point(mX, mY);
// add point to path
mPath.lineTo(mX, mY);
// commit the path to our offscreen
mCanvas.drawPath(mPath, myPaint);
// kill this so we don't double draw
mPath.reset();
}
I'm assuming here that touch_up() is where you add each line segment's start or endpoint to the path.
//Edit: Upon reading your problem once more, I feel like you might be asking for all points on the line you've drawn - since your code snippet includes curves? My guess is that the only way to realise this is by evaluating every (x,y) using the different underlying mathematical equations and store the result for each point. In other words: by writing your own lineTo and quadTo functions.
For straight lines this is relatively trivial, but curves will increase the level of difficulty. You might want to take a look at the Path's source code, which internally delegates most of its work to a java.awt.geom.GeneralPath object.
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.
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
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.
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);
}
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:
To erase all drawings just rotate the screen.
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
}
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
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:
It allows you to draw different lines
It works with larger brush strokes and without using complicated cubic splines
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.
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.
Here is a simplified solution that draws a line that follows your finger and is always straight:
https://stackoverflow.com/a/68076519/15463816
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)...
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
}
}
}
}