You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android/Java绘图应用:如何实现绘制后保存加载Bitmap替代重绘

基于Bitmap历史栈的Android绘图优化方案(支持撤销与FloodFill)

核心思路

放弃每次重绘所有路径的低效方式,改用Bitmap历史栈保存每一步绘制完成后的完整画布状态:

  • 每次完成一笔绘制后,将当前画布的Bitmap副本压入栈
  • 实时绘制时,仅用临时Path显示当前正在画的内容,不写入历史Bitmap
  • 撤销操作直接弹出栈顶Bitmap,恢复到上一步的画布状态
  • 后续FloodFill填充可直接操作当前Bitmap,填充完成后更新历史栈即可

代码修改步骤

1. 初始化核心变量

添加历史栈、当前画布Bitmap及对应的Canvas:

// 保存每一步绘制完成后的Bitmap状态,栈顶是最新状态
private Stack<Bitmap> mBitmapHistory = new Stack<>();
// 当前正在编辑的画布Bitmap
private Bitmap mCurrentBitmap;
// 对应mCurrentBitmap的Canvas,用于绘制完成的笔画
private Canvas mCurrentCanvas;
// 临时Path,用于显示正在绘制中的笔画
private Path mTempPath = new Path();
// 临时Paint,和原绘制参数保持一致
private Paint mTempPaint;

// 初始化方法(建议在surfaceCreated或onCreate中调用)
private void initDrawing() {
    Rect surfaceFrame = mSurfaceHolder.getSurfaceFrame();
    int width = surfaceFrame.width();
    int height = surfaceFrame.height();
    
    // 创建初始Bitmap并加载背景
    mCurrentBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    mCurrentCanvas = new Canvas(mCurrentBitmap);
    mCurrentCanvas.drawBitmap(mBgBitmap, null, surfaceFrame, null);
    
    // 将初始状态压入历史栈(必须复制,避免引用同一对象)
    mBitmapHistory.push(mCurrentBitmap.copy(Bitmap.Config.ARGB_8888, true));
    
    // 初始化临时Paint,和原mPaint参数对齐
    mTempPaint = new Paint(mPaint);
}

2. 修改触摸事件逻辑

仅在touchUp时将临时笔画合并到当前Bitmap并更新历史栈:

protected void touchStart(float x, float y) {
    mTempPath.reset();
    mTempPath.moveTo(x, y);
    mX = x;
    mY = y;
}

protected void touchMove(float x, float y) {
    float dx = Math.abs(x - mX);
    float dy = Math.abs(y - mY);
    if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
        mTempPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
        mX = x;
        mY = y;
    }
    // 触发重绘显示当前正在画的线(SurfaceView用postInvalidate)
    invalidate();
}

protected void touchUp() {
    mTempPath.lineTo(mX, mY);
    
    // 将临时Path绘制到当前Bitmap的Canvas上
    mCurrentCanvas.drawPath(mTempPath, mTempPaint);
    
    // 将当前Bitmap的副本压入历史栈
    mBitmapHistory.push(mCurrentBitmap.copy(Bitmap.Config.ARGB_8888, true));
    
    // 重置临时Path,准备下一次绘制
    mTempPath.reset();
    
    // 更新显示
    invalidate();
}

3. 重写onDraw方法

现在只需绘制当前完整Bitmap和临时笔画,无需遍历所有历史路径:

protected void onDraw() {
    if (mCurrentBitmap == null || mSurfaceHolder == null) {
        return;
    }
    Canvas surfaceCanvas = mSurfaceHolder.lockCanvas();
    if (surfaceCanvas == null) {
        return;
    }
    
    // 绘制当前完整画布状态
    surfaceCanvas.drawBitmap(mCurrentBitmap, 0, 0, null);
    // 绘制正在进行中的临时笔画
    surfaceCanvas.drawPath(mTempPath, mTempPaint);
    
    mSurfaceHolder.unlockCanvasAndPost(surfaceCanvas);
}

4. 实现撤销功能

弹出栈顶的最新状态,恢复到上一步:

public void undoLastDraw() {
    if (mBitmapHistory.size() <= 1) {
        // 只剩初始状态,无法继续撤销
        return;
    }
    // 弹出当前最新状态
    mBitmapHistory.pop();
    // 获取上一步的画布状态
    Bitmap prevBitmap = mBitmapHistory.peek();
    
    // 回收旧Bitmap节省内存
    if (mCurrentBitmap != null) {
        mCurrentBitmap.recycle();
    }
    // 更新当前画布
    mCurrentBitmap = prevBitmap.copy(Bitmap.Config.ARGB_8888, true);
    mCurrentCanvas = new Canvas(mCurrentBitmap);
    
    // 触发重绘
    invalidate();
}

适配FloodFill填充功能

后续实现FloodFill时,直接操作mCurrentBitmap的像素,完成后更新历史栈即可:

public void performFloodFill(int x, int y, int targetColor, int fillColor) {
    // 这里实现你的FloodFill像素操作逻辑
    // ...(省略具体填充代码)
    
    // 填充完成后,将新状态压入历史栈
    mBitmapHistory.push(mCurrentBitmap.copy(Bitmap.Config.ARGB_8888, true));
    // 更新显示
    invalidate();
}

注意事项

  • 内存管理:Bitmap占用内存较高,建议限制历史栈最大长度(比如保留最近50步),避免OOM;替换Bitmap时务必调用recycle()回收旧资源。
  • Bitmap复制:必须使用copy()创建副本,不能直接压入原Bitmap,否则栈中所有元素会引用同一对象,无法保存不同状态。
  • SurfaceView适配:如果使用SurfaceView,重绘时要调用mSurfaceHolder.postInvalidate()而非普通invalidate()。

内容的提问来源于stack exchange,提问作者user18837670

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 08:54:23