Android简单涂鸦以及撤销、重做的实现方法

前段时间研究了下涂鸦功能的实现,其实单独的涂鸦实现起来还是挺简单的,关键的技术难点是撤销与重做功能的实现。但是这里暂时只说明下涂鸦功能的实现,高手勿喷哈,而且该功能在Android SDK提供的APIDemo当中就有的,但是如果能够将该地方的知识点搞懂的话,我认为View画图基本上是难不倒你了,特别是里面为什么要用一个中间的Bitmap。老规矩,还是先看看效果图吧:
1.jpg


代码如下:

  1. package cn.ych.tuya;
  2. import java.io.File;
  3. import java.io.FileNotFoundException;
  4. import java.io.FileOutputStream;
  5. import java.io.IOException;
  6. import java.util.ArrayList;
  7. import java.util.Iterator;
  8. import java.util.List;
  9. import android.content.Context;
  10. import android.graphics.Bitmap;
  11. import android.graphics.Canvas;
  12. import android.graphics.Paint;
  13. import android.graphics.Path;
  14. import android.graphics.Bitmap.CompressFormat;
  15. import android.os.Environment;
  16. import android.view.MotionEvent;
  17. import android.view.View;
  18. /**
  19. *
  20. * @category: View实现涂鸦、撤销以及重做功能
  21. * @author: 锋翼
  22. * @link: www.apkstory.com
  23. * @date: 2012.1.4
  24. *
  25. */
  26. public class TuyaView extends View {
  27. private Bitmap mBitmap;
  28. private Canvas mCanvas;
  29. private Path mPath;
  30. private Paint mBitmapPaint;// 画布的画笔
  31. private Paint mPaint;// 真实的画笔
  32. private float mX, mY;//临时点坐标
  33. private static final float TOUCH_TOLERANCE = 4;
  34. private int screenWidth, screenHeight;// 屏幕長寬
  35. public TuyaView(Context context, int w, int h) {
  36.   super(context);
  37.   screenWidth = w;
  38.   screenHeight = h;
  39.   mBitmap = Bitmap.createBitmap(screenWidth, screenHeight,
  40.     Bitmap.Config.ARGB_8888);
  41.   // 保存一次一次绘制出来的图形
  42.   mCanvas = new Canvas(mBitmap);
  43.   mBitmapPaint = new Paint(Paint.DITHER_FLAG);
  44.   mPaint = new Paint();
  45.   mPaint.setAntiAlias(true);
  46.   mPaint.setStyle(Paint.Style.STROKE);
  47.   mPaint.setStrokeJoin(Paint.Join.ROUND);// 设置外边缘
  48.   mPaint.setStrokeCap(Paint.Cap.SQUARE);// 形状
  49.   mPaint.setStrokeWidth(5);// 画笔宽度
  50. }
  51. @Override
  52. public void onDraw(Canvas canvas) {
  53.   canvas.drawColor(0xFFAAAAAA);
  54.   // 将前面已经画过得显示出来
  55.   canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
  56.   if (mPath != null) {
  57.    // 实时的显示
  58.    canvas.drawPath(mPath, mPaint);
  59.   }
  60. }
  61. private void touch_start(float x, float y) {
  62.   mPath.moveTo(x, y);
  63.   mX = x;
  64.   mY = y;
  65. }
  66. private void touch_move(float x, float y) {
  67.   float dx = Math.abs(x - mX);
  68.   float dy = Math.abs(mY - y);
  69.   触摸间隔大于阈值才绘制路径
  70.   if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
  71.    // 从x1,y1到x2,y2画一条贝塞尔曲线,更平滑(直接用mPath.lineTo也是可以的)
  72.    mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
  73.    mX = x;
  74.    mY = y;
  75.   }
  76. }
  77. private void touch_up() {
  78.   mPath.lineTo(mX, mY);
  79.   mCanvas.drawPath(mPath, mPaint);
  80.   }
  81. @Override
  82. public boolean onTouchEvent(MotionEvent event) {
  83.   float x = event.getX();
  84.   float y = event.getY();
  85.   switch (event.getAction()) {
  86.   case MotionEvent.ACTION_DOWN:
  87.    // 每次down下去重新new一个Path
  88.    mPath = new Path();
  89.    touch_start(x, y);
  90.    invalidate();
  91.    break;
  92.   case MotionEvent.ACTION_MOVE:
  93.    touch_move(x, y);
  94.    invalidate();
  95.    break;
  96.   case MotionEvent.ACTION_UP:
  97.    touch_up();
  98.    invalidate();
  99.    break;
  100.   }
  101.   return true;
  102. }
  103. }
复制代码

上一讲当中,已经讲解了普通View实现涂鸦的功能,现在再来给涂鸦添加上撤销与重做的功能吧。撤销与重做在很多地方都是很重要的功能,比如PS里面、Word里面等等,而且大部分童鞋都能够想到要实现该功能应该需要用到堆栈,对于一些大牛的话可能就直接想到设计模式上面去了,比如命令模式就可以解决撤销与重做的问题。我们这里要讲解的是利用集合来完成该功能,其实也就是模拟栈,我相信你懂得。

老规矩,先上效果图:
代码如下:

  1. package cn.ych.tuya;
  2. import java.io.File;
  3. import java.io.FileNotFoundException;
  4. import java.io.FileOutputStream;
  5. import java.io.IOException;
  6. import java.util.ArrayList;
  7. import java.util.Iterator;
  8. import java.util.List;
  9. import android.content.Context;
  10. import android.graphics.Bitmap;
  11. import android.graphics.Canvas;
  12. import android.graphics.Paint;
  13. import android.graphics.Path;
  14. import android.graphics.Bitmap.CompressFormat;
  15. import android.os.Environment;
  16. import android.view.MotionEvent;
  17. import android.view.View;
  18. /**
  19. *
  20. * @category: View实现涂鸦、撤销以及重做功能
  21. * @author: 锋翼
  22. * @link: www.apkstory.com
  23. * @date: 2012.1.4
  24. *
  25. */
  26. public class TuyaView extends View {
  27. private Bitmap mBitmap;
  28. private Canvas mCanvas;
  29. private Path mPath;
  30. private Paint mBitmapPaint;// 画布的画笔
  31. private Paint mPaint;// 真实的画笔
  32. private float mX, mY;// 临时点坐标
  33. private static final float TOUCH_TOLERANCE = 4;
  34. // 保存Path路径的集合,用List集合来模拟栈
  35. private static List<DrawPath> savePath;
  36. // 记录Path路径的对象
  37. private DrawPath dp;
  38. private int screenWidth, screenHeight;// 屏幕長寬
  39. private class DrawPath {
  40.   public Path path;// 路径
  41.   public Paint paint;// 画笔
  42. }
  43. public TuyaView(Context context, int w, int h) {
  44.   super(context);
  45.   screenWidth = w;
  46.   screenHeight = h;
  47.   mBitmap = Bitmap.createBitmap(screenWidth, screenHeight,
  48.     Bitmap.Config.ARGB_8888);
  49.   // 保存一次一次绘制出来的图形
  50.   mCanvas = new Canvas(mBitmap);
  51.   mBitmapPaint = new Paint(Paint.DITHER_FLAG);
  52.   mPaint = new Paint();
  53.   mPaint.setAntiAlias(true);
  54.   mPaint.setStyle(Paint.Style.STROKE);
  55.   mPaint.setStrokeJoin(Paint.Join.ROUND);// 设置外边缘
  56.   mPaint.setStrokeCap(Paint.Cap.SQUARE);// 形状
  57.   mPaint.setStrokeWidth(5);// 画笔宽度
  58.   savePath = new ArrayList<DrawPath>();
  59. }
  60. @Override
  61. public void onDraw(Canvas canvas) {
  62.   canvas.drawColor(0xFFAAAAAA);
  63.   // 将前面已经画过得显示出来
  64.   canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
  65.   if (mPath != null) {
  66.    // 实时的显示
  67.    canvas.drawPath(mPath, mPaint);
  68.   }
  69. }
  70. private void touch_start(float x, float y) {
  71.   mPath.moveTo(x, y);
  72.   mX = x;
  73.   mY = y;
  74. }
  75. private void touch_move(float x, float y) {
  76.   float dx = Math.abs(x - mX);
  77.   float dy = Math.abs(mY - y);
  78.   if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
  79.    // 从x1,y1到x2,y2画一条贝塞尔曲线,更平滑(直接用mPath.lineTo也是可以的)
  80.    mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
  81.    mX = x;
  82.    mY = y;
  83.   }
  84. }
  85. private void touch_up() {
  86.   mPath.lineTo(mX, mY);
  87.   mCanvas.drawPath(mPath, mPaint);
  88.   //将一条完整的路径保存下来(相当于入栈操作)
  89.   savePath.add(dp);
  90.   mPath = null;// 重新置空
  91. }
  92. /**
  93.   * 撤销的核心思想就是将画布清空,
  94.   * 将保存下来的Path路径最后一个移除掉,
  95.   * 重新将路径画在画布上面。
  96.   */
  97. public void undo() {
  98.   mBitmap = Bitmap.createBitmap(screenWidth, screenHeight,
  99.     Bitmap.Config.ARGB_8888);
  100.   mCanvas.setBitmap(mBitmap);// 重新设置画布,相当于清空画布
  101.   // 清空画布,但是如果图片有背景的话,则使用上面的重新初始化的方法,用该方法会将背景清空掉...
  102.   if (savePath != null && savePath.size() > 0) {
  103.    // 移除最后一个path,相当于出栈操作
  104.    savePath.remove(savePath.size() - 1);
  105.    Iterator<DrawPath> iter = savePath.iterator();
  106.    while (iter.hasNext()) {
  107.     DrawPath drawPath = iter.next();
  108.     mCanvas.drawPath(drawPath.path, drawPath.paint);
  109.    }
  110.    invalidate();// 刷新
  111.    
  112.    /*在这里保存图片纯粹是为了方便,保存图片进行验证*/
  113.    String fileUrl = Environment.getExternalStorageDirectory()
  114.      .toString() + "/android/data/test.png";
  115.    try {
  116.     FileOutputStream fos = new FileOutputStream(new File(fileUrl));
  117.     mBitmap.compress(CompressFormat.PNG, 100, fos);
  118.     fos.flush();
  119.     fos.close();
  120.    } catch (FileNotFoundException e) {
  121.     e.printStackTrace();
  122.    } catch (IOException e) {
  123.     e.printStackTrace();
  124.    }
  125.   }
  126. }
  127. /**
  128.   * 重做的核心思想就是将撤销的路径保存到另外一个集合里面(栈),
  129.   * 然后从redo的集合里面取出最顶端对象,
  130.   * 画在画布上面即可。
  131.   */
  132. public void redo(){
  133.   //如果撤销你懂了的话,那就试试重做吧。
  134. }
  135. @Override
  136. public boolean onTouchEvent(MotionEvent event) {
  137.   float x = event.getX();
  138.   float y = event.getY();
  139.   switch (event.getAction()) {
  140.   case MotionEvent.ACTION_DOWN:
  141.    // 每次down下去重新new一个Path
  142.    mPath = new Path();
  143.    //每一次记录的路径对象是不一样的
  144.    dp = new DrawPath();
  145.    dp.path = mPath;
  146.    dp.paint = mPaint;
  147.    touch_start(x, y);
  148.    invalidate();
  149.    break;
  150.   case MotionEvent.ACTION_MOVE:
  151.    touch_move(x, y);
  152.    invalidate();
  153.    break;
  154.   case MotionEvent.ACTION_UP:
  155.    touch_up();
  156.    invalidate();
  157.    break;
  158.   }
  159.   return true;
  160. }
  161. }
复制代码
原文地址:https://www.cnblogs.com/zhwl/p/2400491.html