Amjad Abu Saa
Amjad Abu Saa

Reputation: 1664

Android Drawing View is very slow

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.

Upvotes: 6

Views: 6610

Answers (3)

Amjad Abu Saa
Amjad Abu Saa

Reputation: 1664

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.

Upvotes: 1

yoah
yoah

Reputation: 7230

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.

Upvotes: 5

Raghunandan
Raghunandan

Reputation: 133580

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>

Upvotes: 0

Related Questions