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的历史点进一步提高曲线精度:这个对笔锋效果的提升也很大,后面说到笔锋章节时再详细说明

相关推荐
bytebeats1 小时前
# Android Studio Narwhal Agent 模式简介
android·android studio
吴Wu涛涛涛涛涛Tao2 小时前
Flutter 实现类似抖音/TikTok 的竖向滑动短视频播放器
android·flutter·ios
bytebeats2 小时前
Jetpack Compose 1.8 新增了 12 个新特性
android·android jetpack
limingade2 小时前
手机实时提取SIM卡打电话的信令声音-整体解决方案规划
android·智能手机·usb蓝牙·手机拦截电话通话声音
Harry技术2 小时前
Trae搭建Android开发:项目中Ktor的引入与使用实践
android·kotlin·trae
猪哥帅过吴彦祖2 小时前
Flutter 插件工作原理深度解析:从 Dart 到 Native 的完整调用链路
android·flutter·ios
AI工具测评与分析3 小时前
EhViewer安卓ios全版本类下载安装工具的完整路径解析
android·ios
非凡ghost3 小时前
Control Center 安卓版:个性化手机控制中心
android·智能手机·生活·软件需求
月夜风雨磊13 小时前
Android NDK从r10c版本到r29版本的下载链接
android·gitee·android ndk