I'm working on a test project which is something similar to FingerPaint example in Android SDK Demos. I was trying to implement undo/redo functionality in my project,but the things that I tried didn't work as I expect. I find some questions similar to this over internet and here,but they didn't help me, that's why I'm asking a new question.
Here is some idea what I'm doing actually :
public class MyView extends View {
//private static final float MINP = 0.25f;
//private static final float MAXP = 0.75f;
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 w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
mCanvas.drawColor(Color.WHITE);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.WHITE);
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
canvas.drawPath(mPath, 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.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;
}
}
Any suggestions/ideas/examples which is the best way to implement this kind of functionality on my project?
I don't know if this is what you had in mind but it's how i am doing it. Instead of storing it in only one path, you store an array with all the paths, like this the user can draw many lines, with a small modification you can add multi touch too.
To make the undo and redo, just remove or add the last path path from the paths variable
and store them in a new array. Something like:
public void onClickUndo () {
if (paths.size()>0) {
undonePaths.add(paths.remove(paths.size()-1))
invalidate();
}
else
//toast the user
}
public void onClickRedo (){
if (undonePaths.size()>0) {
paths.add(undonePaths.remove(undonePaths.size()-1))
invalidate();
}
else
//toast the user
}
Here is my modified panel, I cant try it right now but the methods above should work! Hope it helps! (there are few extra variables just remove them :)
private ArrayList<Path> undonePaths = new ArrayList<Path>();
public class DrawingPanel extends View implements OnTouchListener {
private Canvas mCanvas;
private Path mPath;
private Paint mPaint,circlePaint,outercirclePaint;
private ArrayList<Path> paths = new ArrayList<Path>();
private ArrayList<Path> undonePaths = new ArrayList<Path>();
private float xleft,xright,xtop,xbottom;
public DrawingPanel(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
this.setOnTouchListener(this);
circlePaint = new Paint();
mPaint = new Paint();
outercirclePaint = new Paint();
outercirclePaint.setAntiAlias(true);
circlePaint.setAntiAlias(true);
mPaint.setAntiAlias(true);
mPaint.setColor(0xFFFFFFFF);
outercirclePaint.setColor(0x44FFFFFF);
circlePaint.setColor(0xAADD5522);
outercirclePaint.setStyle(Paint.Style.STROKE);
circlePaint.setStyle(Paint.Style.FILL);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(6);
outercirclePaint.setStrokeWidth(6);
mCanvas = new Canvas();
mPath = new Path();
paths.add(mPath);
cx = 400*DrawActivity.scale;
cy = 30*DrawActivity.scale;
circleRadius = 20*DrawActivity.scale;
xleft = cx-10*DrawActivity.scale;
xright = cx+10*DrawActivity.scale;
xtop = cy-10*DrawActivity.scale;
xbottom = cy+10*DrawActivity.scale;
}
public void colorChanged(int color) {
mPaint.setColor(color);
}
#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 = 0;
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:
if (x <= cx+circleRadius+5 && x>= cx-circleRadius-5) {
if (y<= cy+circleRadius+5 && cy>= cy-circleRadius-5){
paths.clear();
return true;
}
}
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;
}
}
The best solution is that you implement your own Undo / Redo engine.
Save into an array every single action you perform in an array (ie. [0] circle in position x1, y1, [1] line from x2, y2 to x3, y3, etc)
draw
if you need to undo, clear the canvas and repaint all the n - 1 actions from [0] to [n - 1]
if you undo more just have to to paint from [0] to [n - 2] etc
I hope it gives you a hint
Cheers!
One way to implement a do/redo functionality is to encapsulate a method call and all info needed for the call in an object so that you can store it and call it later - the Command Pattern.
In this pattern, each action has its own object: DrawCircleCommand, DrawPathCommand, FillColorCommand, etc. In each object the draw() method is implemented in a unique way but is always called as draw(Canvas canvas) which allows the CommandManager to iterate through the commands. To undo you iterate over the objects calling the undo() method;
Each command object implements an Interface
public interface IDrawCommand {
public void draw(Canvas canvas);
public void undo();
}
An object would look like:
public class DrawPathCommand implements IDrawCommand{
public Path path;
public Paint paint;
public void setPath(path){
this.path = path
}
public void draw(Canvas canvas) {
canvas.drawPath( path, paint );
}
public void undo() {
//some action to remove the path
}
}
Your commands are added to the CommandManager:
mCommandManager.addCommand(IDrawCommand command)
and to undo a command you just call:
mCommandManager.undo();
The CommandManager stores the commands in a data structure e.g. list that allows it to iterative over the command objects.
You can find a complete tutorial here on how to implement the Command Pattern with do/undo for Canvas drawing on Android.
Here is a another tutorial on how implement the Command Pattern in Java;
Fortunately,I solved this today. I find a way to do this.Something like:
private ArrayList<Path> paths = new ArrayList<>();
private ArrayList<Path> undonePaths = new ArrayList<>();
public void undo() {
if (paths.size() > 0) {
LogUtils.d("undo " + paths.size());
clearDraw();
undonePaths.add(paths.remove(paths.size() - 1));
invalidate();
}
}
public void redo() {
if (undonePaths.size() > 0) {
LogUtils.d("redo " + undonePaths.size());
clearDraw();
paths.add(undonePaths.remove(undonePaths.size() - 1));
invalidate();
}
}
public void clearDraw() {
mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
mCanvas.setBitmap(mBitmap);
invalidate();
}
public void clear() {
paths.clear();
undonePaths.clear();
invalidate();
}
in Activity.
private DrawView mDrawView;
if (v == mIvUndo) {
mDrawView.undo();
} else if (v == mIvRedo) {
mDrawView.redo();
in xml
<com.cinread.note.view.DrawView
android:id="#+id/paintView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
If you have a better solution you can contact me.
[here is my blog that I write in
http://blog.csdn.net/sky_pjf/article/details/51086901]
I think that in this case you can use two canvases. You know when user starts drawing and when finishes. So, in touch_start you can create copy of your current canvas. When user clicks undo you replace your current canvas with previously saved.
This should guarantee that you will have previous state of picture, but I am not sure about performance.
Related
Welcome guys,
I'm creating my drawing app using this code:
public class DrawingPanel extends View implements OnTouchListener {
private Canvas mCanvas;
private Path mPath;
private Paint mPaint, circlePaint, outercirclePaint;
// private ArrayList<Path> undonePaths = new ArrayList<Path>();
private float xleft, xright, xtop, xbottom;
public DrawingPanel(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
this.setOnTouchListener(this);
init();
}
public void delete() {
paths.remove(paths.size() - 1);
invalidate();
}
public void init() {
circlePaint = new Paint();
mPaint = new Paint();
outercirclePaint = new Paint();
outercirclePaint.setAntiAlias(true);
circlePaint.setAntiAlias(true);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLACK);
outercirclePaint.setColor(0x44FFFFFF);
circlePaint.setColor(0xAADD5522);
outercirclePaint.setStyle(Paint.Style.STROKE);
circlePaint.setStyle(Paint.Style.FILL);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(6);
outercirclePaint.setStrokeWidth(6);
mCanvas = new Canvas();
mPath = new Path();
paths.add(mPath);
}
public void colorChanged(int color) {
mPaint.setColor(color);
}
#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 = 0;
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);
RectF rectF = new RectF();
mPath.computeBounds(rectF, true);
if(!rectF.isEmpty()){
Log.d("Here", "Empty");
}
regions.add(rectF);
// 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();
invalidate();
if (!isErasing) {
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;
}
} else {
for (int i = 0; i < regions.size(); i++) {
if (regions.get(i).contains((int) x, (int) y)) {
Log.d("Here", "Here");
}
}
}
return true;
}
}
Now I can really undo and redo
I wanted to be able to delete and touch path (Yes the whole path like Samsung write on screen app)
For this reason I tried as shown above the RectF and contains but from what I can tell the RectF selects rectangle which contains the path (it doesn't contain the path's region only but if it's curved the RectF will contain the path and the area around it), I tried regions too.
I couldn't get the only touched path in erasing mode to remove it
So any suggestions will be appreciated :)
I got this code from a answer in one of the questions that was asking how to draw in Android, but then when using it and testing it in my app, I found out that it's not efficient when drawing big things or many paths. The problem comes from the code inside onDraw because each time invalidate() is called onDraw is called which contains a loop that draws all paths again to the canvas, and by adding more paths to it, it gets very very slow.
Here is the Class:
public class DrawingView extends View implements OnTouchListener {
private Canvas m_Canvas;
private Path m_Path;
private Paint m_Paint;
ArrayList<Pair<Path, Paint>> paths = new ArrayList<Pair<Path, Paint>>();
ArrayList<Pair<Path, Paint>> undonePaths = new ArrayList<Pair<Path, Paint>>();
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
public static boolean isEraserActive = false;
private int color = Color.BLACK;
private int stroke = 6;
public DrawingView(Context context, AttributeSet attr) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
setBackgroundColor(Color.WHITE);
this.setOnTouchListener(this);
onCanvasInitialization();
}
public void onCanvasInitialization() {
m_Paint = new Paint();
m_Paint.setAntiAlias(true);
m_Paint.setDither(true);
m_Paint.setColor(Color.parseColor("#000000"));
m_Paint.setStyle(Paint.Style.STROKE);
m_Paint.setStrokeJoin(Paint.Join.ROUND);
m_Paint.setStrokeCap(Paint.Cap.ROUND);
m_Paint.setStrokeWidth(2);
m_Canvas = new Canvas();
m_Path = new Path();
Paint newPaint = new Paint(m_Paint);
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
}
#Override
public void setBackground(Drawable background) {
mBackground = background;
super.setBackground(background);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
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;
}
#Override
protected void onDraw(Canvas canvas) {
for (Pair<Path, Paint> p : paths) {
canvas.drawPath(p.first, p.second);
}
}
private void touch_start(float x, float y) {
if (isEraserActive) {
m_Paint.setColor(Color.WHITE);
m_Paint.setStrokeWidth(50);
Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
} else {
m_Paint.setColor(color);
m_Paint.setStrokeWidth(stroke);
Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
}
m_Path.reset();
m_Path.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) {
m_Path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
}
private void touch_up() {
m_Path.lineTo(mX, mY);
// commit the path to our offscreen
m_Canvas.drawPath(m_Path, m_Paint);
// kill this so we don't double draw
m_Path = new Path();
Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
}
public void onClickUndo() {
if (!paths.isEmpty()) {//paths.size() > 0) {
undonePaths.add(paths.remove(paths.size() - 1));
undo = true;
invalidate();
}
}
public void onClickRedo() {
if (!undonePaths.isEmpty()){//undonePaths.size() > 0) {
paths.add(undonePaths.remove(undonePaths.size() - 1));
undo = true;
invalidate();
}
}}
But I searched on the internet again to find a better way for drawing, so I found the following:
1 Add the following to the constructor:
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
2 Override onSizeChanged with the following code:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
m_Canvas = new Canvas(mBitmap);
}
3 put this in onDraw:
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (!paths.isEmpty())
canvas.drawPath(paths.get(paths.size() - 1).first, paths.get(paths.size() - 1).second);
}
This approach works and it doesn't slow down the view, but the problem with this approach is that I can't have undo and redo functionalities.
I tried many many things to do the undo and redo with the second approach, but I couldn't do it. So what I'm asking here is one of three things:
1. A way to do undo and redo with the second approach
2. Another approach that makes it possible to do undo and redo
3. A whole new class that has everything already done, like an open source library or something.
Please help if you can.
Thanks
EDIT 1
OK, so I limited it down to this and then I couldn't do anything more, I have been trying for over 8 hours now. It works up until undo (you can undo as many paths as you want), then when drawing again all remaining paths disappear, I don't know what makes it do that.
#Override
protected void onDraw(Canvas canvas) {
if (mBitmap != null)
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (!paths.isEmpty() && !undo)
canvas.drawPath(paths.get(paths.size() - 1).first, paths.get(paths.size() - 1).second);
if (undo) {
setBackground(mBackground);
for (Pair<Path, Paint> p : paths)
canvas.drawPath(p.first, p.second);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
m_Canvas = new Canvas(mBitmap);
undo = false;
}
}
so basically what I did is use the first approach at first (before undo is called), then if undo is clicked, undo is set to true and the code under if (undo) is executed which is actually the first approach (calculating all paths again), then I draw the result of calculating all paths again into mBitmap so whenever the onDraw is called again it draws on top of that, but that part is still needs working, I hope someone can help with that part.
The way to handle such a case is to have a Bitmap that has the size of the view. On touch events, draw into the bitmap's canvas. in onDraw, just draw the bitmap into the canvas at 0,0. For undo/redo,. you can erase the bitmap and re-draw all the paths. It make take a bit longer, but it happens only once per undo/redo.
If users typically do one undo/redo. you can optimize by having another bitmap for just one step back.
Ok, here is what I came up with at the end, the problem was that I draw the paths to the canvas before creating the bitmap on undo, which lead to loss of the paths onDraw after undo:
#Override
protected void onDraw(Canvas canvas) {
if (mBitmap != null)
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (!paths.isEmpty()) {
canvas.drawPath(paths.get(paths.size() - 1).first, paths.get(paths.size() - 1).second);
}
}
public void onClickUndo() {
if (paths.size() >= 2) {
undonePaths.add(paths.remove(paths.size() - 2));
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
m_Canvas = new Canvas(mBitmap);
for (Pair<Path, Paint> p : paths)
m_Canvas.drawPath(p.first, p.second);
invalidate();
}
}
public void onClickRedo() {
if (undonePaths.size() >= 2){
paths.add(undonePaths.remove(undonePaths.size() - 2));
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
m_Canvas = new Canvas(mBitmap);
for (Pair<Path, Paint> p : paths)
m_Canvas.drawPath(p.first, p.second);
invalidate();
}
}
Drawing all paths again and again is still there but not in onDraw(), which improves the performance of drawing quite very much.
But the user might experience little bit of delay in onClickUndo() and onClickRedo() if he has drawn a lot of paths because there where the paths are getting drawn again from scratch, but just one time per click.
I am not sure if this is the best way for undo and redo. However the below worked on my device (Samsung galaxy s3). The Draw seems to be fast and the undo works fine. I do think the below can be modified to further enhance performance.
public class MainActivity extends Activity {
MyView mv;
LinearLayout ll;
private ArrayList<Path> undonePaths = new ArrayList<Path>();
private ArrayList<Path> paths = new ArrayList<Path>();
Button b;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mv= new MyView(this);
mv.setDrawingCacheEnabled(true);
ll= (LinearLayout) findViewById(R.id.ll);
ll.addView(mv);
b= (Button) findViewById(R.id.button1);
b.setOnClickListener(new OnClickListener()
{
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (paths.size() > 0) {
undonePaths.add(paths
.remove(paths.size()-2));
mv.invalidate();
}
}
});
}
public class MyView extends View implements OnTouchListener {
private Canvas mCanvas;
private Path mPath;
private Paint mPaint;
// private ArrayList<Path> undonePaths = new ArrayList<Path>();
private float xleft, xright, xtop, xbottom;
public MyView(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
this.setOnTouchListener(this);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
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 = 0;
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;
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<LinearLayout
android:id="#+id/ll"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:orientation="vertical" >
</LinearLayout>
<Button
android:id="#+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Undo" />
</LinearLayout>
Can someone explain me why on earth this piece of code does not draw every object?
public class A extends View {
private Paint paint = new Paint();
private Path path = new Path();
ArrayList<Pair<Path, Paint>> paths = new ArrayList<Pair<Path, Paint>>();
public A(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
for (Pair<Path, Paint> p : paths) {
canvas.drawPath(p.first, p.second);
}
canvas.drawPath(path, paint);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(3f);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int color = Color.rgb(new Random().nextInt(255),
new Random().nextInt(255),
new Random().nextInt(255));
paint.setColor(color);
path.reset(); //new stroke, get old one erased
int historySize = event.getHistorySize();
for (int i = 0; i > historySize; i++) {
path.moveTo(eventX, eventY);
}
path.moveTo(eventX, eventY);
return true;
case MotionEvent.ACTION_MOVE:
path.lineTo(eventX, eventY);
return true;
case MotionEvent.ACTION_UP:
path.lineTo(eventX, eventY);
// End of stroke, add this to the collection
paths.add(new Pair<Path, Paint>(path, paint));
break;
default:
break;
}
// Schedules a repaint.
invalidate();
return true;
}
}
I'm catching every strokes with the onTouchEvent and i create different path/paint ojects stored in a Pair one. Sadly in my OnDraw when I try to draw them all it fails ..
I've read some topic without finding right answer. Each time some people recommend to create and work in a bitmap and draw it to the screen but i'd like to avoid this solution.
Thank's for your help !
The problem is that you always use the same Path and Paint object. You should create new Path and Paint each time MotionEvent.ACTION_DOWN is fired
Remove the path.reset in switch condition. The draw works. But color does change even for previous draw. You resetting the paths which is clearing the previous draw.
I would use a different pair (path and paint) everytime to draw.
In case you want to draw using canvas the below code work's just fine.
I would also add a color picker to allow user to pick color of his choice.
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 w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(0xFFAAAAAA);
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
canvas.drawPath(mPath, 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);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));
// 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;
}
}
You need to use only one Path and Paint object.
A detailed tutorial on this whole thing can in fact be found here:
Making a basic single touch drawing app on Android - Creative Punch
I have worked on the fingerpaint class before and I was very much comfortable but this time I found myself in deep trouble implementing both these functionalities of Undo and deletion
This is my drawing view
public class MyView extends View {
private Bitmap mBitmap;
private Canvas mCanvas;
private Path mPath;
private Paint mBitmapPaint;
public MyView(Context c) {
super(c);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
mPath = new Path();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
#Override
protected void onDraw(Canvas canvas) {
/*canvas.drawColor(Color.WHITE);
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);*/
for (int i=0;i<paths.size();i++){
canvas.drawPath(paths.get(i).path, paths.get(i).paint);
}
canvas.drawPath(mPath, mPaint);
}
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
private void touch_start(float x, float y)
{
if ( erase == true )
{
mPath.reset();
}
undonePaths.clear();
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 (check == 1 || (selectedId == ERASE_MENU_ID) )
{
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE)
{
mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
mCanvas.drawPath(mPath, mPaint);
}
}
if(selectedId == ERASE_MENU_ID)
{
mCanvas.drawPath(mPath, mPaint);
mPath=new Path();
mX = x;
mY = y;
mPath.moveTo(x, y);
}
}
public void onClickUndo () {
if (paths.size()>0)
{
paths.remove(paths.size()-1);
invalidate();
}
else
{
}
//toast the user
}
private void touch_up() {
mCanvas.drawPath(mPath, mPaint);
MyDataHolder md=new MyDataHolder();
md.paint=mPaint;
md.path=mPath;
paths.add(md);
mPath=new Path();
}
#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;
}
}
Now the problem is when I change mPaint's mode to clear
erase=true;
mPaint.setXfermode(null);
mPaint.setStrokeWidth(25.0f);
//mPaint.setColor(Color.TRANSPARENT);
mPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
every drawn path turns black with 25 width somebody suggested that I am using same paint so I made a wrapper class to store paint and path object but it's not working.
I can help you with Deletion (Clearing the screen). It's pretty simple and basic approach.
Create a Boolean variable (say mclears). set this to true when your "Clear" button is pressed.
in ondraw(), use
if(clears){
myBitmap.eraseColor(bcolor);
//Sets your canvas (bitmap) color to your background color. i.e. clears canvas
clears=false;
}
You may have to change your Touch_up method a bit. like
private void touch_up() {
mCanvas.drawPath(mPath, mPaint);
MyDataHolder md=new MyDataHolder();
mPath.reset();
md.paint=mPaint;
md.path=mPath;
paths.add(md);
mPath=new Path();
}
I have implemented this in my app. Hope it works for you.
hi i need perform undo operation in paints.some one suggest use command pattern but i am not getting command pattern.is there any solution is there. help me how to do undo.
i draw paints using below code.
BookType3.class:
public class BookType3 extends Activity implements OnClickListener{
MyView myview;
int counter=0;
private Paint mBitmapPaint;
TextView t1,t2;
private Bitmap mBitmap;
public static boolean action=false;
Button back,erase,undo,save,home;
int image1,image2,image3;
private Canvas mCanvas;
RelativeLayout relative;
RelativeLayout.LayoutParams lp6,lp7;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.booktype1);
List<String> names = Arrays.asList("a","b","c");
relative=(RelativeLayout)findViewById(R.id.relative3);
myview = new MyView(this);
myview.setId(004);
lp6 = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
myview.setLayoutParams(lp6);
myview.setBackgroundResource(R.drawable.writingsapce);
undo=(Button)findViewById(R.id.undobutton);
undo.setOnClickListener(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(4);
String s1="";
t1=(TextView)findViewById(R.id.button1);
t1.setOnClickListener(this);
t2=(TextView)findViewById(R.id.button2);
t2.setOnClickListener(this);
relative.addView(myview,lp6);
}
private Paint mPaint;
public class MyView extends View{
private Path mPath;
public MyView(Context context) {
super(context);
// TODO Auto-generated constructor stub
mPath = new Path();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
canvas.drawPath(mPath, mPaint);
if(action)
{
invalidate();
}
}
private float mX, mY;
private static final float TOUCH_TOLERANCE = 2;
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.reset();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
Log.i("x",""+event.getX());
Log.i("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;
}
}
public void onClick(View v) {
// TODO Auto-generated method stub
if(v==t1)
{
mPaint.setColor(Color.RED);
t30.setText("A");
t31.setText("a");
t32.setText("Aa");
t30.setTextColor(Color.RED);
t31.setTextColor(Color.RED);
t32.setTextColor(Color.RED);
try{
mBitmap.eraseColor(android.graphics.Color.TRANSPARENT);
Canvas Canvas=new Canvas(mBitmap);
action=true;
myview.onDraw(Canvas);
}catch(IllegalStateException ie){
ie.printStackTrace();
}
}
if(v==t2)
{
t30.setTextColor(Color.BLUE);
t31.setTextColor(Color.BLUE);
t32.setTextColor(Color.BLUE);
mPaint.setColor(Color.BLUE);
t30.setText("B");
t31.setText("b");
t32.setText("Bb");
try{
mBitmap.eraseColor(android.graphics.Color.TRANSPARENT);
Canvas Canvas=new Canvas(mBitmap);
action=true;
myview.onDraw(Canvas);
}catch(IllegalStateException ie){
ie.printStackTrace();
}
}
if(v==undo)
{
}
}
}
Instead of the Command Pattern, I think you should use the Memento Pattern, which is designed for things like undo and redo. There are two ways to implement this pattern. Either by saving deltas, or by saving entire states. Saving states would be easiest in your situation, but it has larger memory footprint. This would involve writing a bitmap to the filesystem every time the user modifies the main bitmap. You could decide on a constant number of undo steps (e.g. 10), and then save the 10 most recent bitmaps. Saving deltas would be more complex. You would implement it by recording the touch events, color, brush, etc. of each paint action. You can then playback and/or rollback individual actions using the motion events. Saving deltas would take significantly less memory, and you would be able to store many more actions and you wouldn't have to write to the filesystem.
private Slate mSlate;
private TiledBitmapCanvas mTiledCanvas;
public void clickUndo(View unused) {
mSlate.undo();
}
public void undo() {
if (mTiledCanvas == null) {
Log.v(TAG, "undo before mTiledCanvas inited");
}
mTiledCanvas.step(-1);
invalidate();
}