Android|Android 自定义view画图板练习

项目中想要实现一个简易画图板的需求,功能并不复杂,就是6个很常用的功能
Android|Android 自定义view画图板练习
文章图片

画图板
陈小默同学有一个比较复杂,强大,高效的CrazyPalette,同学间商业互吹下,哈哈。里面基本常用的操作都有,代码写的很好,只是用的Kotlin,不过我需要的只是一个简单的绘图板,我参考了他的一些思路以及另外一篇android项目 之 记事本 ----- 画板功能之撤销、恢复和清空,做了一个简单的PaintView
1. PaintView

之前在网上看到别的博客说写的双缓冲是这个思路,这里感觉有错误,不清楚我写的这种方式算不算双缓冲。等过了这段加班,我再查查问问确认下 20170524 21:13
思路:使用双缓冲思路,有一个mBitmap,来记录最终的绘制。在手指滑动过程中,屏幕上会实时显示出手指滑动时的绘制轨迹。当手指离开屏幕后,显示最终存有内容的mBitmap
  1. 撤销和恢复利用LinkedList来模拟两个储存记录的
  2. 清空,这里偷懒,直接绘制白色,将之前绘制的内容盖住。也可以考虑使用new PorterDuffXfermode(PorterDuff.Mode.CLEAR)。但有些时候,个人感觉这种方式会出现些莫名其妙的情况,能直接绘制成单一纯色,就不使用PorterDuffXfermode
  3. 橡皮擦,这里使用了PorterDuffXfermode,并setBackgroundColor(Color.WHITE)以及把硬件加速关闭了
关于橡皮擦得额外说明下:
橡皮擦使用new PorterDuffXfermode(PorterDuff.Mode.CLEAR)是为了复习下PorterDuffXfermode,踩踩坑
这里有两个坑,硬件加速和背景穿透。当使用PorterDuff.Mode.CLEAR时,利用的是把上次绘制的东西给清除掉,这就导致在保存绘制的图片后,橡皮擦轨迹是透明的,而之前绘制的内容又被擦除了,就会把图片下方的当前系统背景色显示出来
Android|Android 自定义view画图板练习
文章图片

问题
当我在电脑打开保存的图片时,橡皮擦轨迹会透出我电脑桌面背景的颜色。在手机打开就会透出手机背景颜色
解决办法:
init()方法中,setBackgroundColor(Color.WHITE),绘制了一个白色背景,但这样也就导致了过度绘制
根据需求,这里更好的思路就是把橡皮擦的颜色也直接设置成白色,更加简单而且没有PorterDuffXfermode的坑。但既然是练习,就踩踩坑
代码:
public class PaintView extends View { private Paint mPaint; private Path mPath; private Path eraserPath; private Paint eraserPaint; private Canvas mCanvas; private Bitmap mBitmap; private float mLastX, mLastY; //上次的坐标 private Paint mBitmapPaint; //使用LinkedList 模拟栈,来保存 Path private LinkedList undoList; private LinkedList redoList; private boolean isEraserModel; public PaintView(Context context, AttributeSet attrs) { super(context, attrs); init(); }/*** * 初始化 */ private void init() { //关闭硬件加速 //否则橡皮擦模式下,设置的 PorterDuff.Mode.CLEAR ,实时绘制的轨迹是黑色 setBackgroundColor(Color.WHITE); //设置白色背景 setLayerType(View.LAYER_TYPE_SOFTWARE, null); //画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); mPaint.setStrokeWidth(4f); mPaint.setAntiAlias(true); mPaint.setColor(Color.BLACK); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeJoin(Paint.Join.ROUND); //使画笔更加圆润 mPaint.setStrokeCap(Paint.Cap.ROUND); //同上 mBitmapPaint = new Paint(Paint.DITHER_FLAG); //保存签名的画布 post(new Runnable() {//拿到控件的宽和高 @Override public void run() { //获取PaintView的宽和高 //由于橡皮擦使用的是 Color.TRANSPARENT ,不能使用RGB-565 mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444); mCanvas = new Canvas(mBitmap); //抗锯齿 mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG)); //背景色 mCanvas.drawColor(Color.WHITE); } }); undoList = new LinkedList<>(); redoList = new LinkedList<>(); }/** * 绘制 */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mBitmap != null) { canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint); //将mBitmap绘制在canvas上,最终的显示 if (!isEraserModel) { if (null != mPath) {//显示实时正在绘制的path轨迹 canvas.drawPath(mPath, mPaint); } } else { if (null != eraserPath) { canvas.drawPath(eraserPath, eraserPaint); } } } }/** * 撤销操作 */ public void undo() { if (!undoList.isEmpty()) { clearPaint(); //清除之前绘制内容 PathBean lastPb = undoList.removeLast(); //将最后一个移除 redoList.add(lastPb); //加入 恢复操作 //遍历,将Path重新绘制到 mCanvas for (PathBean pb : undoList) { mCanvas.drawPath(pb.path, pb.paint); } invalidate(); } }/** * 恢复操作 */ public void redo() { if (!redoList.isEmpty()) { PathBean pathBean = redoList.removeLast(); mCanvas.drawPath(pathBean.path, pathBean.paint); invalidate(); undoList.add(pathBean); } }/** * 设置画笔颜色 */ public void setPaintColor(@ColorInt int color) { mPaint.setColor(color); }/** * 清空,包括撤销和恢复操作列表 */ public void clearAll() { clearPaint(); mLastY = 0f; //清空 撤销 ,恢复 操作列表 redoList.clear(); undoList.clear(); }/** * 设置橡皮擦模式 */ public void setEraserModel(boolean isEraserModel) { this.isEraserModel = isEraserModel; if (eraserPaint == null) { eraserPaint = new Paint(mPaint); eraserPaint.setStrokeWidth(15f); eraserPaint.setColor(Color.TRANSPARENT); eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); } }/** * 保存到指定的文件夹中 */ public boolean saveImg(String filePath, String imgName) { boolean isCanSave = mBitmap != null && mLastY != 0f && !undoList.isEmpty(); if (isCanSave) {//空白板时,就不保存 //保存图片 File file = new File(filePath + File.separator + imgName); FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(file); if (mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)) { fileOutputStream.flush(); return true; } } catch (java.io.IOException e) { e.printStackTrace(); } finally { closeStream(fileOutputStream); } } return false; }/** * 是否可以撤销 */ public boolean isCanUndo() { return undoList.isEmpty(); }/** * 是否可以恢复 */ public boolean isCanRedo() { return redoList.isEmpty(); }/** * 清除绘制内容 * 直接绘制白色背景 */ private void clearPaint() { mCanvas.drawColor(Color.WHITE); invalidate(); }/** * 触摸事件 触摸绘制 */ @Override public boolean onTouchEvent(MotionEvent event) { if (!isEraserModel) { commonTouchEvent(event); } else { eraserTouchEvent(event); } invalidate(); return true; }/** * 橡皮擦事件 */ private void eraserTouchEvent(MotionEvent event) { int action = event.getAction(); float x = event.getX(); float y = event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: //路径 eraserPath = new Path(); mLastX = x; mLastY = y; eraserPath.moveTo(mLastX, mLastY); break; case MotionEvent.ACTION_MOVE: float dx = Math.abs(x - mLastX); float dy = Math.abs(y - mLastY); if (dx >= 3 || dy >= 3) {//绘制的最小距离 3px eraserPath.quadTo(mLastX, mLastY, (mLastX + x) / 2, (mLastY + y) / 2); } mLastX = x; mLastY = y; break; case MotionEvent.ACTION_UP: mCanvas.drawPath(eraserPath, eraserPaint); //将路径绘制在mBitmap上 eraserPath.reset(); eraserPath = null; break; } }/** * 普通画笔事件 */ private void commonTouchEvent(MotionEvent event) { int action = event.getAction(); float x = event.getX(); float y = event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: //路径 mPath = new Path(); mLastX = x; mLastY = y; mPath.moveTo(mLastX, mLastY); break; case MotionEvent.ACTION_MOVE: float dx = Math.abs(x - mLastX); float dy = Math.abs(y - mLastY); if (dx >= 3 || dy >= 3) {//绘制的最小距离 3px //利用二阶贝塞尔曲线,使绘制路径更加圆滑 mPath.quadTo(mLastX, mLastY, (mLastX + x) / 2, (mLastY + y) / 2); } mLastX = x; mLastY = y; break; case MotionEvent.ACTION_UP: mCanvas.drawPath(mPath, mPaint); //将路径绘制在mBitmap上 Path path = new Path(mPath); //复制出一份mPath Paint paint = new Paint(mPaint); PathBean pb = new PathBean(path, paint); undoList.add(pb); //将路径对象存入集合 mPath.reset(); mPath = null; break; } }/** * 关闭流 */ private void closeStream(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException e) { e.printStackTrace(); } } }/** * 测量 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int wSpecMode = MeasureSpec.getMode(widthMeasureSpec); int wSpecSize = MeasureSpec.getSize(widthMeasureSpec); int hSpecMode = MeasureSpec.getMode(heightMeasureSpec); int hSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) { setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); } else if (wSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, hSpecSize); } else if (hSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(wSpecSize, 200); } }/** * 路径对象 */ class PathBean { Path path; Paint paint; PathBean(Path path, Paint paint) { this.path = path; this.paint = paint; } }}

代码中,重要地方都有注释
2. Activity 布局代码:

Activity代码
public class PaintViewActivity extends AppCompatActivity { private PaintView paintView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_paint_view); initView(); initMenu(); }/** * 初始化 */ private void initView() { paintView = (PaintView) findViewById(R.id.activity_paint_pv); }/** * 初始化底部菜单 */ private void initMenu() { //撤销 menuItemSelected(R.id.activity_paint_undo, new MenuSelectedListener() { @Override public void onMenuSelected() { ToastUtils.show(PaintViewActivity.this, "撤销"); paintView.undo(); } }); //恢复 menuItemSelected(R.id.activity_paint_redo, new MenuSelectedListener() { @Override public void onMenuSelected() { ToastUtils.show(PaintViewActivity.this, "恢复"); paintView.redo(); } }); //颜色 menuItemSelected(R.id.activity_paint_color, new MenuSelectedListener() { @Override public void onMenuSelected() { ToastUtils.show(PaintViewActivity.this, "红色"); paintView.setPaintColor(Color.RED); } }); //清空 menuItemSelected(R.id.activity_paint_clear, new MenuSelectedListener() { @Override public void onMenuSelected() { ToastUtils.show(PaintViewActivity.this, "清空"); paintView.clearAll(); } }); //橡皮擦 menuItemSelected(R.id.activity_paint_eraser, new MenuSelectedListener() { @Override public void onMenuSelected() { ToastUtils.show(PaintViewActivity.this, "橡皮擦"); paintView.setEraserModel(true); } }); //保存 menuItemSelected(R.id.activity_paint_save, new MenuSelectedListener() { @Override public void onMenuSelected() { String path = Environment.getExternalStorageDirectory().getPath() + File.separator + Strings.FILE_PATH + File.separator + Strings.CACHE_PATH; String imgName = "paint.jpg"; if (paintView.saveImg(path,imgName)) { ToastUtils.show(PaintViewActivity.this, "保存成功"); } } }); }/** * 选中底部 Menu 菜单项 */ private void menuItemSelected(int viewId, final MenuSelectedListener listener) { findViewById(viewId).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { listener.onMenuSelected(); } }); }@Override protected void onDestroy() { super.onDestroy(); ToastUtils.cancel(); }interface MenuSelectedListener { void onMenuSelected(); } }

这是橡皮擦使用PorterDuffXfermode踩坑思路
2.1 橡皮擦直接绘制白色背景思路 简单修改PaintView代码:
1. 首先把硬件加速打开,也就是把init()方法里,下面行代码注释掉: //setBackgroundColor(Color.WHITE); //setLayerType(View.LAYER_TYPE_SOFTWARE, null); 2.修改橡皮擦颜色 eraserPaint.setColor(Color.WHITE); //eraserPaint.setColor(Color.TRANSPARENT); //eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

这种思路即不会导致过度绘制,也不会有硬件加速的坑,但前提是绘图板背景颜色是纯色的
3. 最后 即使在使用过渡绘制思路的情况下,暂时感觉效率也可以,在低端机上也没有明显的卡顿感,绘制轨迹蛮跟手的。个人感觉这种绘图板并不需要SurfaceView
有错误,请指出
共勉 : )
【Android|Android 自定义view画图板练习】

    推荐阅读