当前位置: 首页 > news >正文

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

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

一、基础画板搭建

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

/*** 自定义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 旧高度*/@Overrideprotected 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 系统提供的画布对象*/@Overrideprotected void onDraw(Canvas canvas) {// 1. 先绘制已经保存到bitmap的内容(历史路径)canvas.drawBitmap(mBitmap, 0, 0, mPaint);// 2. 再绘制当前正在绘制的路径(实时显示)canvas.drawPath(mPath, mPaint);}/*** 处理触摸事件* @param event 触摸事件对象* @return 是否处理了该事件*/@Overridepublic 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;  // 不处理其他事件}// 请求重绘Viewinvalidate();// 返回true表示已处理该事件return true;}
}

(1)PathPaintBitmap 和 Canvas的工作原理

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

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

  2. 工作原理

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

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

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

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

  • 关键属性

    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,显示在屏幕上。

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.透明度不叠加:

mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));

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

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

三、曲线绘制优化

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

这种方法存在明显问题:

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

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

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

优化方案

(1)二次贝塞尔曲线

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

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

http://www.xdnf.cn/news/1289161.html

相关文章:

  • OpenHarmony编译与烧录
  • 1小时 MySQL 数据库基础速通
  • 服务端配置 CORS解决跨域问题的原理
  • 安卓主题定制实践:17.45MB轻量级主题引擎技术解析
  • LDAP 登录配置参数填写指南
  • WireShark:非常好用的网络抓包工具
  • 间隙锁(Gap Lock)
  • 力扣top100(day01-05)--矩阵
  • 【自动化备份全网服务器数据项目】
  • TF-IDF——红楼梦案例
  • 2025年渗透测试面试题总结-15(题目+回答)
  • 前端css学习笔记3:伪类选择器与伪元素选择器
  • VUE+SPRINGBOOT从0-1打造前后端-前后台系统-会议记录
  • Cookies和Sessions
  • 晓知识: 如何理解反射
  • Seed-VC:零样本语音转换与扩散transformer
  • 启保停-----------单相照明灯的接法
  • 【数据库】 MySQL 表的操作详解
  • 编程模型设计空间的决策思路
  • 贪心----4.划分字母区间
  • 【科研绘图系列】R语言绘制特定区域颜色标记散点图
  • Seata深度剖析:微服务分布式事务解决方案
  • 自然语言处理( NLP)基础
  • docker-compose搭建 redis 集群
  • Gartner 《IAM for LLM-Based AI Agents》学习心得
  • archlinux中VLC无法播放视频的解决办法
  • 【AI生成+补充】高频 hql的面试问题 以及 具体sql
  • ARM芯片架构之CoreSight SoC-400 组件介绍
  • dag实现案例 02、实现简易版dag调度系统(基于01之上升级)
  • C语言—数组和指针练习题合集(二)