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
相关产品推荐
相关产品推荐

