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)Path
、Paint
、Bitmap
和 Canvas的工作原理
这四个类(Path
、Paint
、Bitmap
和 Canvas
)在 Android 绘图系统中各司其职,共同协作完成绘图过程。下面详细解释它们如何协同工作:
1. Path(路径)
作用:记录用户绘制的轨迹(线条形状)
工作原理:
当用户手指触摸屏幕移动时,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(画布)
作用:执行实际绘制操作的平台,类似画布
双重角色:
关联到
Bitmap
:mCanvas = 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)二次贝塞尔曲线
使用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的历史点进一步提高曲线精度:这个对笔锋效果的提升也很大,后面说到笔锋章节时再详细说明