Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 301
Amjad Abu Saa Avatar asked Jun 09 '13 15:06

Amjad Abu Saa


People also ask

Why does Android studio take so long to open?

There may be many plugins in Android Studio that you are not using it. Disabling it will free up some space and reduce complex processes. To do so, Open Preferences >> Plugins and Disable the plugins you are not using.

How much time does a UI thread have to render a screen?

Because the screen is refreshed every 16 ms (1 s/60 fps = 16 ms per frame), it is crucial to ensure that all of your rendering can occur in less than 16 ms.


2 Answers

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.

like image 183
yoah Avatar answered Sep 30 '22 17:09

yoah


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.

like image 22
Amjad Abu Saa Avatar answered Sep 30 '22 17:09

Amjad Abu Saa