安卓绘制视图非常缓慢

6
我从一个问题的答案中获取了这段代码,该问题询问如何在Android上绘制。然而,在使用并测试此代码后,我发现在绘制大型物体或多个路径时效率不高。问题出在onDraw方法内的代码上,因为每次调用invalidate()方法时都会调用onDraw方法,该方法包含一个循环,将所有paths重新绘制到canvas上。随着添加更多的路径,这样做会变得非常缓慢。
以下是该类:
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();
    }
}}

但我再次在互联网上搜索,以寻找更好的绘制方式,因此我找到了以下内容:
1. 添加以下内容到构造函数中:
mBitmapPaint = new Paint(Paint.DITHER_FLAG);

使用以下代码重写 onSizeChanged 方法:

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);
}

将以下内容添加到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);
}

这种方法是行得通的,它不会减慢视图,但这种方法的问题在于我不能有撤销和重做功能。我尝试了许多方法来使用第二种方法进行撤销和重做,但我无法做到。所以我在这里提出了以下三个请求之一: 1. 用第二种方法实现撤销和重做的方法 2. 另一种方法使其可以撤销和重做 3. 一个完整的新类,其中所有内容都已完成,如开源库等。
如果您能帮忙就请帮忙,谢谢!
编辑1: 好的,我将它限制到这个程度,然后我什么也做不了了,我已经努力尝试了8个小时。它工作正常,直到撤销(您可以撤销任意数量的路径),然后再次绘制时所有剩余路径都消失了,我不知道是什么原因导致它这样做。
@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;
    }
}

所以,基本上我所做的是首先使用第一种方法(即在调用撤销之前),然后如果点击了撤销,则将undo设置为true,并执行if (undo)下的代码,实际上就是第一种方法(重新计算所有路径),然后我将重新计算所有路径的结果绘制到mBitmap中,因此每当再次调用onDraw时,它都会在其上绘制,但这部分仍需要进一步完善,希望有人可以帮助解决这个问题。

很抱歉,如果您绘制太多路径,那么您的视图将会变得很慢。您应该尝试找到一种方法,涉及绘制一个非常长的路径。 - jcw
如果您总是在旧路径的顶部绘制新路径,则可以将两种方法组合起来,将除最后n个路径之外的所有路径组合成位图,并使用先前的方法绘制最后n个路径。然后,您至少可以在撤消时删除这些最后n个路径。 - Michael Butscher
3个回答

5
处理这种情况的方法是创建一个大小与视图相同的位图。在触摸事件中,绘制到位图的画布上。在onDraw中,只需将位图绘制到画布上的0,0位置。对于撤销/重做,您可以擦除位图并重新绘制所有路径。可能需要花费更长的时间,但每次撤销/重做仅发生一次。如果用户通常只执行一次撤销/重做,则可以通过为向后一步创建另一个位图来进行优化。

请问您能检查一下修改吗?谢谢您的回复。 - Amjad Abu Saa
几件事情:在撤销时,您清除位图,因此下一个onDraw将具有空位图。我不确定您是否在位图中绘制了所有撤消的路径。请在处理撤消单击的位置执行此操作,这样您就不需要担心onDraw中的撤消。另一件要优化的事情,正如其他人建议的那样,是使用单个长路径而不是许多小路径。 - yoah
根据您的使用情况,当视图中的内容未被绘制时,清除背景并进行完整重绘可能更好。这样可以实现相当完整的撤消功能,而无需存储除已有路径之外的任何内容。 - Tatarize

1

好的,这是我最终想出的解决方案,问题在于我在撤销之前将路径绘制到画布上,导致在撤销后 onDraw 中路径丢失:

@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();
        }
    }

重复绘制所有路径仍然存在,但不在onDraw()中,这大大提高了绘图的性能。但是,如果用户已经绘制了很多路径,则在onClickUndo()onClickRedo()中可能会遇到一些延迟,因为路径将从头开始重新绘制,但每次点击只需要一次。

0

我不确定这是否是撤销和重做的最佳方式。然而,以下方法在我的设备(三星Galaxy S3)上运行良好。绘图似乎很快,撤销也正常工作。我认为可以修改以下内容以进一步提高性能。

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>

这仍然存在我问题描述中提到的性能问题。但无论如何还是谢谢。 - Amjad Abu Saa
@AmjadAbuSaa 你用什么设备进行测试? - Raghunandan
S3、Note 10.1和模拟器 正如您所说,S3是最快的,但在制作应用程序时,我们仍然应该考虑较慢的设备,不是吗? - Amjad Abu Saa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接