Android 项目:画图白板APP开发(一)——曲线优化、颜色、粗细、透明度

在移动应用开发中,画图白板类APP是一个既能展示技术实力又能带来良好用户体验的项目。今天我将分享开发这样一个APP的第一部分,重点介绍曲线绘制优化以及颜色粗细透明度的实现。

一、基础画板搭建

首先我们需要创建一个自定义View作为画板:

java 复制代码
/**
 * 自定义View类,实现画板绘图功能
 * 支持手指触摸绘制路径,可设置画笔颜色、粗细等属性
 */
public class DrawingView extends View {
    // 绘制路径对象,记录用户手指移动轨迹
    private Path mPath;
    // 画笔对象,设置绘制样式和属性
    private Paint mPaint;
    // 位图对象,作为绘图的缓冲区
    private Bitmap mBitmap;
    // 画布对象,用于在位图上绘制
    private Canvas mCanvas;
    
    /**
     * 构造函数
     * @param context 上下文环境
     * @param attrs 属性集合
     */
    public DrawingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化绘图设置
        setupDrawing();
    }
    
    /**
     * 初始化绘图相关对象和参数
     */
    private void setupDrawing() {
        // 创建新的路径对象
        mPath = new Path();
        // 创建并配置画笔
        mPaint = new Paint();
        // 设置默认画笔颜色为黑色
        mPaint.setColor(Color.BLACK);
        // 开启抗锯齿,使绘制更平滑
        mPaint.setAntiAlias(true);
        // 设置画笔宽度为5像素
        mPaint.setStrokeWidth(5f);
        // 设置画笔样式为描边(画线)
        mPaint.setStyle(Paint.Style.STROKE);
        // 设置路径连接处为圆角
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        // 设置线帽为圆头
        mPaint.setStrokeCap(Paint.Cap.ROUND);
    }
    
    /**
     * View尺寸变化时回调
     * @param w 新宽度
     * @param h 新高度
     * @param oldw 旧宽度
     * @param oldh 旧高度
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 创建与View相同尺寸的位图
        mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        // 创建画布并关联到位图
        mCanvas = new Canvas(mBitmap);
    }
    
    /**
     * 绘制View内容
     * @param canvas 系统提供的画布对象
     */
    @Override
    protected void onDraw(Canvas canvas) {
        // 1. 先绘制已经保存到bitmap的内容(历史路径)
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        // 2. 再绘制当前正在绘制的路径(实时显示)
        canvas.drawPath(mPath, mPaint);
    }
    
    /**
     * 处理触摸事件
     * @param event 触摸事件对象
     * @return 是否处理了该事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 获取触摸点坐标
        float x = event.getX();
        float y = event.getY();
        
        // 根据不同的触摸动作进行处理
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:  // 手指按下
                // 将路径起点移动到触摸点
                mPath.moveTo(x, y);
                break;
            case MotionEvent.ACTION_MOVE:   // 手指移动
                // 从上一个点画线到当前点
                mPath.lineTo(x, y);
                break;
            case MotionEvent.ACTION_UP:    // 手指抬起
                // 1. 将当前路径绘制到bitmap上(永久保存)
                mCanvas.drawPath(mPath, mPaint);
                // 2. 重置路径,准备下一次绘制
                mPath.reset();
                break;
            default:
                return false;  // 不处理其他事件
        }
        
        // 请求重绘View
        invalidate();
        // 返回true表示已处理该事件
        return true;
    }
}

(1)PathPaintBitmapCanvas的工作原理

这四个类(PathPaintBitmapCanvas)在 Android 绘图系统中各司其职,共同协作完成绘图过程。下面详细解释它们如何协同工作:

1. Path(路径)
  1. 作用:记录用户绘制的轨迹(线条形状)

  2. 工作原理

    • 当用户手指触摸屏幕移动时,Path会记录这些点的坐标

    • 使用 moveTo() 设置起点,lineTo() 添加线段,quadTo() 创建曲线

    • 就像一个"铅笔轨迹记录器",只记录形状不负责显示

2. Paint(画笔)
  • 作用:定义如何绘制(样式和外观)

  • 关键属性

    java 复制代码
    mPaint.setColor(Color.BLACK);        // 颜色
    mPaint.setStrokeWidth(5f);           // 线宽
    mPaint.setStyle(Paint.Style.STROKE); // 样式(描边/填充)
    mPaint.setAntiAlias(true);           // 抗锯齿
    mPaint.setAlpha(255);                // 透明度
  • 就像真实的画笔,决定线条的颜色、粗细、样式、透明度等视觉特性

3. Bitmap(位图)
  • 作用 :作为绘图的"画纸"

  • 特点

    • 实际存储像素数据(ARGB_8888 表示每个像素占4字节)

    • 相当于一个"永久画布",保存所有已确认的绘制内容

4. Canvas(画布)
  • 作用 :执行实际绘制操作的平台,类似画布

  • 双重角色

    • 关联到 BitmapmCanvas = new Canvas(mBitmap)

    • 系统传入的 canvas 参数:onDraw(Canvas canvas)

可视化比喻:

手指移动

→ 记录到Path(像铅笔轨迹)

→ 用Paint样式渲染

→ 通过Canvas画到Bitmap(像把草图描到正式画纸)

→ 最终通过系统Canvas显示到屏幕

(2)onTouchEvent

onTouchEvent是 Android 中 Activity 级别的全局触摸事件处理方法,用于处理整个 Activity 的触摸事件。其核心作用是拦截并处理未被任何子视图消费的触摸事件。绘图所用的事件(MotionEvent)都从onTouchEvent中获取。

MotionEvent 是 Android 中处理触摸事件的核心类,包含触摸动作、坐标、历史轨迹等信息。

动作常量 触发时机 典型用途
ACTION_DOWN 手指首次触摸屏幕 开始新路径/绘制
ACTION_MOVE 手指在屏幕上移动 更新路径/绘制
ACTION_UP 手指离开屏幕 结束绘制
ACTION_CANCEL 手势被系统取消 清理临时状态

目前在demo中使用的是 event.getAction() ,在后续的开发过程中就就得使用 event.getActionMasked()

getAction()和getActionMasked()的区别:

对 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 之外的事件,getAction()返回值和getActionMasked()是相同的

对 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP ,getAction()返回值和getActionMasked()返回值稍有不同

getAction()返回值包含了操作类型和产生此事件的pointer对应的pointer index两个信息,其中低8位代表操作类型,高8位代表pointer index 。

简单理解:getAction保存了 动作类型(低8位) + 指针索引(高8位) 使用getActionMasked() 会更方便些,不是说getAction无法完成判断。

方法 适合场景 是否支持多指 代码复杂度
getAction() 单点触控 ❌ 需要手动处理 高(需位运算)
getActionMasked() 单点+多点触控 ✅ 直接支持 低(逻辑清晰)

(3)双缓冲机制

先通过setBitmap方法将要绘制的所有的图形绘制到一个Bitmap上也就是先在内存空间完成,然后再来调用drawBitmap方法绘制出这个Bitmap,显示在屏幕上。

java 复制代码
private Bitmap mBitmap;  // Back Buffer(后台位图)
private Canvas mCanvas;  // 关联到 mBitmap 的 Canvas
private Path mPath;      // 临时绘制路径

@Override
protected void onDraw(Canvas canvas) {
    // 1. 将 Back Buffer(mBitmap)绘制到屏幕
    canvas.drawBitmap(mBitmap, 0, 0, null);
    // 2. 叠加当前正在绘制的路径(临时内容)
    canvas.drawPath(mPath, mPaint);
}
  • mBitmap :存储所有已确认的绘制内容(持久化)

  • mPath :存储当前正在绘制的临时路径(实时更新)

二、颜色、粗细、透明度

这三者都是通过 Paint(画笔) 配置的,在上面已经交代过了。不过透明度需要注意一下:在初始化画笔的时候,透明度一定要在颜色之后设置,因为颜色中也存在透明通道,会覆盖已设置的透明度。

透明度有两种使用效果:

1.透明度不叠加:

java 复制代码
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));

2.透明度叠加:无需配置PorterDuffXfermode

具体该怎么使用,完全根据实际情况考虑。PorterDuffXfermode类主要用于图形合成时的图像过渡模式计算,共有18种过渡模式,可以灵活使用。

三、曲线绘制优化

在基础实现中,我们通常使用Path.lineTo()连接触摸点:

这种方法存在明显问题:

  1. 锯齿感明显:快速移动时会出现明显的折线段

  2. 采样率不足:系统触摸事件采样率有限,导致关键点丢失

  3. 不自然过渡:转角处生硬,缺乏真实手绘的流畅感

优化方案

(1)二次贝塞尔曲线

使用Path.quadTo()替代lineTo()实现平滑连接:

java 复制代码
// 用于存储上一个触摸点的坐标
private float mPreviousX, mPreviousY;

@Override
public boolean onTouchEvent(MotionEvent event) {
    // 获取当前触摸点的坐标
    float x = event.getX();
    float y = event.getY();
    
    // 根据不同的触摸动作进行处理
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:  // 手指按下事件
            // 将路径起点移动到当前触摸点
            mPath.moveTo(x, y);
            // 记录当前点作为下一个动作的前一个点
            mPreviousX = x;
            mPreviousY = y;
            break;
            
        case MotionEvent.ACTION_MOVE:  // 手指移动事件
            // 计算控制点坐标(取前一点和当前点的中点)
            float controlX = (x + mPreviousX) / 2;
            float controlY = (y + mPreviousY) / 2;
            
            // 使用二次贝塞尔曲线连接
            // 参数说明:
            // 前两个参数(mPreviousX,mPreviousY) - 控制点坐标
            // 后两个参数(controlX,controlY) - 结束点坐标
            mPath.quadTo(mPreviousX, mPreviousY, controlX, controlY);
            
            // 更新前一个点的坐标为当前点
            mPreviousX = x;
            mPreviousY = y;
            break;
    }
    
    // 请求重绘视图
    invalidate();
    
    // 返回true表示已消费该触摸事件
    return true;
}

(2)历史点插值

利用MotionEvent的历史点进一步提高曲线精度:这个对笔锋效果的提升也很大,后面说到笔锋章节时再详细说明

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui