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

Android Studio中Canvas直线预览优化:消除拖动残留线条问题

自定义View直线预览清除旧线条问题解决

问题描述

开发自定义View时,希望实现手指拖动时显示从初始触摸点到当前位置的直线预览,但拖动过程中每次手指移动都会留下旧预览线条,最终形成“太阳状”图形,无法清除旧线条。

问题根源

  1. 原代码将临时预览线条直接绘制到mExtraBitmap(持久化画布)中,每次移动都会叠加新线条,旧线条无法被清除;
  2. 对Path的复用和重置逻辑有误,导致路径累积了旧的绘制指令;
  3. 混淆了临时预览和最终确认绘制的载体,临时线条不该写入持久化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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 06:07:10