1. 程式人生 > >Android簡單塗鴉以及撤銷、重做的實現方法

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.    String fileUrl = Environment.getExternalStorageDirectory()
  113.      .toString() + "/android/data/test.png";
  114.    try {
  115.     FileOutputStream fos = new FileOutputStream(new File(fileUrl));
  116.     mBitmap.compress(CompressFormat.PNG, 100, fos);
  117.     fos.flush();
  118.     fos.close();
  119.    } catch (FileNotFoundException e) {
  120.     e.printStackTrace();
  121.    } catch (IOException e) {
  122.     e.printStackTrace();
  123.    }
  124.   }
  125. }
  126. /**
  127.   * 重做的核心思想就是將撤銷的路徑儲存到另外一個集合裡面(棧),
  128.   * 然後從redo的集合裡面取出最頂端物件,
  129.   * 畫在畫布上面即可。
  130.   */
  131. public void redo(){
  132.   //如果撤銷你懂了的話,那就試試重做吧。
  133. }
  134. @Override
  135. public boolean onTouchEvent(MotionEvent event) {
  136.   float x = event.getX();
  137.   float y = event.getY();
  138.   switch (event.getAction()) {
  139.   case MotionEvent.ACTION_DOWN:
  140.    // 每次down下去重新new一個Path
  141.    mPath = new Path();
  142.    //每一次記錄的路徑物件是不一樣的
  143.    dp = new DrawPath();
  144.    dp.path = mPath;
  145.    dp.paint = mPaint;
  146.    touch_start(x, y);
  147.    invalidate();
  148.    break;
  149.   case MotionEvent.ACTION_MOVE:
  150.    touch_move(x, y);
  151.    invalidate();
  152.    break;
  153.   case MotionEvent.ACTION_UP:
  154.    touch_up();
  155.    invalidate();
  156.    break;
  157.   }
  158.   return true;
  159. }
  160. }
複製程式碼