Android Studio中Canvas直线预览优化:消除拖动残留线条问题
自定义View直线预览清除旧线条问题解决
问题描述
开发自定义View时,希望实现手指拖动时显示从初始触摸点到当前位置的直线预览,但拖动过程中每次手指移动都会留下旧预览线条,最终形成“太阳状”图形,无法清除旧线条。
问题根源
- 原代码将临时预览线条直接绘制到
mExtraBitmap(持久化画布)中,每次移动都会叠加新线条,旧线条无法被清除; - 对
Path的复用和重置逻辑有误,导致路径累积了旧的绘制指令; - 混淆了临时预览和最终确认绘制的载体,临时线条不该写入持久化Bitmap。
解决方案思路
- 拆分绘制层次:
mExtraBitmap只保存用户最终确认的绘制内容(比如手指抬起时的直线); - 临时预览线条在
onDraw方法中直接绘制到View的画布上,每次invalidate()都会重绘,自然覆盖旧的预览线条; - 记录直线的起始点,在
ACTION_MOVE时更新终点,ACTION_UP时将最终直线写入持久化画布。
修改后的完整代码
public class MyCanvasView extends View { public final int BRUSH_FREE = 0; public final int BRUSH_LINE = 1; private int brush_mode; private Paint mPaint; private Paint mBackgroundPaint; private Path mPath; private int mDrawColor; private int mBackgroundColor; private Canvas mExtraCanvas; private Bitmap mExtraBitmap; private Rect mFrame; private ArrayList<Path> paths = new ArrayList<>(); // 直线预览的起始点和当前终点 private float lineStartX, lineStartY; private float lineCurrentX, lineCurrentY; // 标记是否正在进行直线预览 private boolean isPreviewingLine = false; public MyCanvasView(Context context) { this(context, null); } public MyCanvasView(Context context, AttributeSet attributeSet) { super(context, attributeSet); mBackgroundColor = ResourcesCompat.getColor(getResources(), R.color.opaque_orange, null); mDrawColor = ResourcesCompat.getColor(getResources(), R.color.opaque_yellow, null); brush_mode = BRUSH_FREE; mPath = new Path(); mPaint = new Paint(); mPaint.setColor(mDrawColor); mPaint.setAntiAlias(true); mPaint.setDither(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeJoin(Paint.Join.ROUND); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeWidth(12); mBackgroundPaint = new Paint(mPaint); mBackgroundPaint.setColor(mBackgroundColor); } @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { super.onSizeChanged(width, height, oldWidth, oldHeight); mExtraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mExtraCanvas = new Canvas(mExtraBitmap); mExtraCanvas.drawColor(mBackgroundColor); int inset = 40; mFrame = new Rect(inset, inset, width - inset, height - inset); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制持久化的已完成内容 canvas.drawBitmap(mExtraBitmap, 0, 0, null); // 如果是直线预览模式且正在预览,绘制临时直线 if (brush_mode == BRUSH_LINE && isPreviewingLine) { canvas.drawLine(lineStartX, lineStartY, lineCurrentX, lineCurrentY, mPaint); } canvas.drawRect(mFrame, mPaint); } private float mX, mY; private static final float TOUCH_TOLERANCE = 4; private void touchStart(float x, float y) { mPath.moveTo(x, y); mX = x; mY = y; // 直线模式下记录起始点,开启预览标记 if (brush_mode == BRUSH_LINE) { lineStartX = x; lineStartY = y; lineCurrentX = x; lineCurrentY = y; isPreviewingLine = true; } } private 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) { switch (brush_mode) { case BRUSH_FREE: mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); mX = x; mY = y; paths.add(mPath); mExtraCanvas.drawPath(mPath, mPaint); break; case BRUSH_LINE: // 更新当前终点,触发重绘显示新的预览直线 lineCurrentX = x; lineCurrentY = y; break; } } } private void touchUp() { if (brush_mode == BRUSH_LINE && isPreviewingLine) { // 手指抬起,将最终直线写入持久化画布 mExtraCanvas.drawLine(lineStartX, lineStartY, lineCurrentX, lineCurrentY, mPaint); isPreviewingLine = false; } mPath.reset(); } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: touchStart(x, y); break; case MotionEvent.ACTION_MOVE: touchMove(x, y); invalidate(); // 每次移动都触发重绘,更新预览线条 break; case MotionEvent.ACTION_UP: touchUp(); invalidate(); // 抬起后重绘,清除临时预览 break; default: } return true; } public static int getScreenWidth() { return Resources.getSystem().getDisplayMetrics().widthPixels; } public static int getScreenHeight() { return Resources.getSystem().getDisplayMetrics().heightPixels; } public void clearMyCanvas() { mExtraCanvas.drawColor(mBackgroundColor); mExtraCanvas.drawRect(mFrame, mPaint); invalidate(); } public void setBrush_mode(int brushtype) { brush_mode = brushtype; isPreviewingLine = false; // 切换模式时取消预览状态 } }
关键修改点
- 新增
lineStartX/Y、lineCurrentX/Y记录直线的起始和当前终点,isPreviewingLine标记是否处于预览状态; - 直线预览不再写入
mExtraBitmap,而是在onDraw中直接绘制到View画布,每次invalidate()都会刷新,旧预览线条会被新的内容覆盖; ACTION_UP时才将最终的直线写入mExtraBitmap,完成持久化绘制;- 简化了原代码中混乱的
moveLastX/Y逻辑,改用更清晰的状态标记。
内容的提问来源于stack exchange,提问作者Yanivonix
相关产品推荐
相关产品推荐

