Android,画布:如何清除(删除)位于surfaceView中的画布(=位图)内容?

111
为了制作一个简单的游戏,我使用了一个模板来绘制带有位图的画布,就像这样:

private void doDraw(Canvas canvas) {
    for (int i=0;i<8;i++)
        for (int j=0;j<9;j++)
            for (int k=0;k<7;k++)   {
    canvas.drawBitmap(mBits[allBits[i][j][k]], i*50 -k*7, j*50 -k*7, null); } }

画布在 "run()" 中被定义 / SurfaceView 在 GameThread 中运行。

我的第一个问题是如何清除(或重新绘制)整个画布以适应新的布局?第二个问题是如何更新屏幕的某一部分?

// This is the routine that calls "doDraw":
public void run() {
    while (mRun) {
        Canvas c = null;
        try {
            c = mSurfaceHolder.lockCanvas(null);
            synchronized (mSurfaceHolder) {
                if (mMode == STATE_RUNNING) 
                    updateGame();
                doDraw(c);          }
        } finally {
            if (c != null) {
                mSurfaceHolder.unlockCanvasAndPost(c);  }   }   }       }
21个回答

297

使用 PorterDuff 清除模式来绘制透明颜色是我想要的效果。

Canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)

2
这应该可以工作,但在4.4上似乎有漏洞(至少在N7.2上)。使用Bitmap#eraseColor(Color.TRANSPARENT),如下面HeMac的答案所示。 - nmr
5
事实上,“Color.TRANSPARENT”是不必要的。对于一个“ARGB_8888”位图,只需要使用“PorterDuff.Mode.CLEAR”,将 alpha 和颜色设置为 [0, 0] 即可。另一种方法是使用带有“PorterDuff.Mode.SRC”的“Color.TRANSPARENT”。 - leoly
6
这对我不起作用,它让表面变成了空白而不是透明的。 - pallav bohara

84
如何清除(或重绘)整个画布以进行新的布局(=尝试游戏)?
只需调用Canvas.drawColor(Color.BLACK),或者使用您想要清除Canvas的任何颜色。
还有:如何更新屏幕的一部分?
由于Android OS在更新屏幕时会重新绘制每个像素,因此没有仅更新“屏幕的一部分”的方法。但是,当您不清除Canvas上的旧图形时,旧图形仍然存在于表面上,这可能是“更新部分”屏幕的一种方式。
因此,如果要“更新屏幕的一部分”,请避免调用Canvas.drawColor()方法。

4
如果您在表面上没有绘制每个像素,您将会得到非常奇怪的结果(因为双缓冲是通过交换指针实现的,所以在您不绘制的部分中,您将看不到之前存在的内容)。您必须在每次迭代中重新绘制表面的每个像素。 - Guillaume Brunerie
2
如果您调用了mSurfaceHolder.unlockCanvasAndPost(c)然后再次调用c = mSurfaceHolder.lockCanvas(null),那么新的c不包含与先前的c相同的内容。你不能只更新SurfaceView的一部分,这就是OP所问的吧。 - Guillaume Brunerie
1
@Guillaume Brunerie:没错,但不是全部。你不能更新屏幕的一部分,就像我写的那样。但你可以保留屏幕上的旧图形,这将产生“仅更新屏幕的一部分”的效果。在示例应用程序中自己尝试一下吧。Canvas会保留旧图形。 - Wroclai
2
我真是太丢人了!!!我只在游戏开始时初始化了数组,而没有在连续的重新启动中进行初始化 - 所有旧的方块都仍然“显示在屏幕上” - 它们只是被重新绘制了!!!非常抱歉我的“愚蠢”!感谢你们两个!!! - samClem
1
那么这可能是因为动态壁纸的工作方式与常规SurfaceView不同。无论如何,@samClem:始终在每个帧重新绘制Canvas的每个像素(如文档中所述),否则您将出现奇怪的闪烁。 - Guillaume Brunerie
显示剩余10条评论

37

在谷歌组中找到了这个,并且对我有用..

Paint clearPaint = new Paint();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawRect(0, 0, width, height, clearPaint); 

这会在保持设置的位图的同时,移除图形矩形等内容。


5
这给了我一个黑色的区域 - 不是我背后的位图 :( - slott
清除操作正常,但位图也被删除了,与您所说的不同。 - Omar Rehman
在https://dev59.com/Xmkw5IYBdhLWcg3wus_K中解释了如何在画布的某个矩形上绘制位图的矩形。因此,更改矩形中的图像是有效的:清除先前的内容,绘制新图像。 - Martin

23
使用 Path 类的 reset 方法。
Path.reset();

如果您正在使用路径,则这确实是最佳答案。其他方法可能会让用户看到黑屏。谢谢。 - j2emanue
超级棒极了。谢谢。我的情况是通过获取用户的触摸事件用路径绘制三角形。因此,我想在绘制新三角形时清除先前绘制的三角形,以模拟其移动。非常感谢。 - Reza
对不起,我不知道如何使用它。如果我在画布上绘制路径,这并不会擦除它。那么如何擦除呢? - Luis A. Florit

19

我尝试了@mobistry的答案:

canvas.drawColor(Color.TRANSPARENT,Mode.CLEAR);

但它对我没用。

对于我来说,解决方案是:

canvas.drawColor(Color.TRANSPARENT,Mode.MULTIPLY);

也许还有其他人有同样的问题。


4
透明度乘法是解决方案,否则在某些设备上可能会呈现为黑色。 - Andreas Rudolph

13
mBitmap.eraseColor(Color.TRANSPARENT);

canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);

2
这怎么是最正确的呢?既然可以使用drawColor,为什么还要使用位图呢? - RichieHH
虽然这可能对原帖作者不起作用(他们不一定能访问位图),但在可用时清除位图内容绝对是有用的。在这些情况下,只需要第一行即可。有用的技巧,+1。 - head in the codes

6
canvas.drawColor(Color.TRANSPARENT, Mode.MULTIPLY);

2
请添加您的代码如何解决问题的说明。这篇文章被标记为低质量帖子 - 来自评论 - Suraj Rao

4
请将以下代码粘贴到SurfaceView扩展类的构造函数中.............
构造函数代码
    SurfaceHolder holder = getHolder();
    holder.addCallback(this);

    SurfaceView sur = (SurfaceView)findViewById(R.id.surfaceview);
    sur.setZOrderOnTop(true);    // necessary
    holder = sur.getHolder();
    holder.setFormat(PixelFormat.TRANSPARENT);

XML 编码
    <com.welcome.panelview.PanelViewWelcomeScreen
        android:id="@+id/one"
        android:layout_width="600px"
        android:layout_height="312px"
        android:layout_gravity="center"
        android:layout_marginTop="10px"
        android:background="@drawable/welcome" />

尝试以上代码...

holder.setFormat(PixelFormat.TRANSPARENT); 这对我有效。 - Aaron Lee

3
这里是一个最简示例的代码,展示了在每一帧都必须重新绘制Canvas的每一个像素。
这个活动每秒在SurfaceView上绘制一个新的Bitmap,不清除屏幕。如果你测试它,你会发现Bitmap并不总是被写入同一个缓冲区,屏幕会在两个缓冲区之间交替显示。
我在我的手机(Nexus S,Android 2.3.3)和模拟器(Android 2.2)上进行了测试。
public class TestCanvas extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new TestView(this));
    }
}

class TestView extends SurfaceView implements SurfaceHolder.Callback {

    private TestThread mThread;
    private int mWidth;
    private int mHeight;
    private Bitmap mBitmap;
    private SurfaceHolder mSurfaceHolder;

    public TestView(Context context) {
        super(context);
        mThread = new TestThread();
        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon);
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        mWidth = width;
        mHeight = height;
        mThread.start();
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {/* Do nothing */}

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (mThread != null && mThread.isAlive())
            mThread.interrupt();
    }

    class TestThread extends Thread {
        @Override
        public void run() {
            while (!isInterrupted()) {
                Canvas c = null;
                try {
                    c = mSurfaceHolder.lockCanvas(null);
                    synchronized (mSurfaceHolder) {
                        c.drawBitmap(mBitmap, (int) (Math.random() * mWidth), (int) (Math.random() * mHeight), null);
                    }
                } finally {
                    if (c != null)
                        mSurfaceHolder.unlockCanvasAndPost(c);
                }

                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    interrupt();
                }
            }
        }   
    }
}

好的,看起来你错了,看看我的屏幕截图:http://img12.imageshack.us/i/devicey.png/# 当你添加了一秒钟的延迟后,双缓冲更加明显,但是屏幕上仍然有之前的绘画。此外,你的代码是错误的:应该是SurfaceHolder.Callback,而不仅仅是Callback - Wroclai
1
我觉得你不理解我的意思。人们可能期望的是,第 n 帧和第 n+1 帧之间的区别在于多出了一个位图。但这是完全错误的,实际上在第 n 帧和第 n+2 帧之间有一个额外的位图,即使我只是添加了一个位图,第 n 帧和第 n+1 帧也是完全无关的。 - Guillaume Brunerie
不,我完全理解你。但是,正如你所看到的,你的代码无法工作。一段时间后,屏幕上充满了图标。因此,如果我们想要完全重新绘制整个屏幕,我们需要清除“画布”。这使得我的关于“先前绘图”的说法成立。 “画布”保留先前的绘图。 - Wroclai
4
如果你愿意的话,一个“Canvas”可以保留之前的绘画内容。但是这完全没有用,问题在于当你使用“lockCanvas()”时,你并不知道那些“之前的绘画内容”是什么,也无法对它们做出任何假设。也许如果有两个尺寸相同的SurfaceView活动,它们将共享同一个Canvas。也许你会得到一块未初始化的RAM内存区域,其中包含随机字节。也许画布上总是写着Google标志。你无法知道。任何未在“lockCanvas(null)”后绘制每个像素的应用程序都是有问题的。 - Guillaume Brunerie
好的,我并没有说你知道之前绘画的任何内容,但它们仍然存在。Canvas 不知道对象的存在。Canvas 保留的是以前的绘画。这是真实的,按照我的看法,按照您自己的应用程序,并且这是许多其他人制造的事实。我不知道你在争论什么;你已经证明了你的案例是错误的。 - Wroclai
1
@GuillaumeBrunerie 事隔2.5年,我终于看到了你的帖子。官方的Android文档支持你有关“绘制每个像素”的建议。只有一种情况下,画布的一部分才能保证在后续调用lockCanvas()时仍然存在。请参阅Android文档中SurfaceHolder及其两个lockCanvas()方法的相关内容。http://developer.android.com/reference/android/view/SurfaceHolder.html#lockCanvas(android.graphics.Rect) 和 http://developer.android.com/reference/android/view/SurfaceHolder.html#lockCanvas() - UpLate

3

对于我来说,调用Canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)或类似的方法只有在我触摸屏幕后才能起作用。因此,我会调用上述代码行,但屏幕只有在我触摸屏幕后才会清除。所以,对我而言有效的方法是先调用invalidate(),然后再调用init(),该方法在创建时被调用以初始化视图。

private void init() {
    setFocusable(true);
    setFocusableInTouchMode(true);
    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();
    mPaths = new LinkedList<>();

    addNewPath();
}

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