博客地址:博客園,版權全部,轉載須聯繫做者。java
GitHub地址:JustWeTools node
最近作了個繪圖的控件,實現了一些有趣的功能。android
先上效果圖:git
1.可直接使用設定按鈕來實現已擁有的方法,且拓展性強
2.基礎功能:更換顏色、更換橡皮、以及更換橡皮和筆的粗細、清屏、倒入圖片
3.特殊功能:保存畫筆軌跡幀動畫、幀動畫導入導出、ReDo和UnDogithub
GitHub地址:JustWeTools 算法
如何使用該控件能夠在GitHub的README中找到,此處再也不贅述。json
1.繪圖控件繼承於View,使用canvas作畫板,在canvas上設置一個空白的Bitmap做爲畫布,以保存畫下的軌跡。canvas
mPaint = new Paint(); mEraserPaint = new Paint(); Init_Paint(UserInfo.PaintColor,UserInfo.PaintWidth); Init_Eraser(UserInfo.EraserWidth); WindowManager manager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); width = manager.getDefaultDisplay().getWidth(); height = manager.getDefaultDisplay().getHeight(); mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBitmap); mPath = new Path(); mBitmapPaint = new Paint(Paint.DITHER_FLAG);
mPaint做爲畫筆, mEraserPaint 做爲橡皮,使用兩個在onDraw的刷新的時候就會容易一點,接着獲取了屏幕的寬和高,使之爲Bitmap的寬和高。 新建canvas,路徑Path,api
和往bitmap上畫的畫筆mBitmapPaint。app
1 // init paint 2 private void Init_Paint(int color ,int width){ 3 mPaint.setAntiAlias(true); 4 mPaint.setDither(true); 5 mPaint.setColor(color); 6 mPaint.setStyle(Paint.Style.STROKE); 7 mPaint.setStrokeJoin(Paint.Join.ROUND); 8 mPaint.setStrokeCap(Paint.Cap.ROUND); 9 mPaint.setStrokeWidth(width); 10 } 11 12 13 // init eraser 14 private void Init_Eraser(int width){ 15 mEraserPaint.setAntiAlias(true); 16 mEraserPaint.setDither(true); 17 mEraserPaint.setColor(0xFF000000); 18 mEraserPaint.setStrokeWidth(width); 19 mEraserPaint.setStyle(Paint.Style.STROKE); 20 mEraserPaint.setStrokeJoin(Paint.Join.ROUND); 21 mEraserPaint.setStrokeCap(Paint.Cap.SQUARE); 22 // The most important 23 mEraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 24 }
鉛筆的屬性不用說,查看一下源碼就知道了,橡皮的顏色隨便設置應該均可以, 重點在最後一句。
mEraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
意思是設定了層疊的方式,當橡皮擦上去的時候,即新加上的一層(橡皮)和原有層有重疊部分時,取原有層去掉重疊部分的剩餘部分,這也就達到了橡皮的功能。
1 private void Touch_Down(float x, float y) { 2 mPath.reset(); 3 mPath.moveTo(x, y); 4 mX = x; 5 mY = y; 6 if(IsRecordPath) { 7 listener.AddNodeToPath(x, y, MotionEvent.ACTION_DOWN, IsPaint); 8 } 9 } 10 11 12 private void Touch_Move(float x, float y) { 13 float dx = Math.abs(x - mX); 14 float dy = Math.abs(y - mY); 15 if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { 16 mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); 17 mX = x; 18 mY = y; 19 if(IsRecordPath) { 20 listener.AddNodeToPath(x, y, MotionEvent.ACTION_MOVE, IsPaint); 21 } 22 } 23 } 24 private void Touch_Up(Paint paint){ 25 mPath.lineTo(mX, mY); 26 mCanvas.drawPath(mPath, paint); 27 mPath.reset(); 28 if(IsRecordPath) { 29 listener.AddNodeToPath(mX, mY, MotionEvent.ACTION_UP, IsPaint); 30 } 31 }
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Touch_Down(x, y); invalidate(); break; case MotionEvent.ACTION_MOVE: Touch_Move(x, y); invalidate(); break; case MotionEvent.ACTION_UP: if(IsPaint){ Touch_Up(mPaint); }else { Touch_Up(mEraserPaint); } invalidate(); break; } return true; }
Down的時候移動點過去,Move的時候利用塞貝爾曲線將至連成一條線,Up的時候降至畫在mCanvas上,並將path重置,而且每一次操做完都調用invalidate();以實現刷新。
另外clean方法:
1 public void clean() { 2 mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 3 mCanvas.setBitmap(mBitmap); 4 try { 5 Message msg = new Message(); 6 msg.obj = PaintView.this; 7 msg.what = INDIVIDE; 8 handler.sendMessage(msg); 9 Thread.sleep(0); 10 } catch (InterruptedException e) { 11 // TODO Auto-generated catch block 12 e.printStackTrace(); 13 } 14 } 15 private Handler handler=new Handler(){ 16 17 @Override 18 public void handleMessage(Message msg) { 19 switch (msg.what){ 20 case INDIVIDE: 21 ((View) msg.obj).invalidate(); 22 break; 23 case CHOOSEPATH: 24 JsonToPathNode(msg.obj.toString()); 25 break; 26 } 27 super.handleMessage(msg); 28 } 29 30 };
clean方法就是重設Bitmap而且刷新界面,達到清空的效果。
還有一些set的方法:
1 public void setColor(int color) { 2 showCustomToast("已選擇顏色" + colorToHexString(color)); 3 mPaint.setColor(color); 4 } 5 6 7 public void setPenWidth(int width) { 8 showCustomToast("設定筆粗爲:" + width); 9 mPaint.setStrokeWidth(width); 10 } 11 12 public void setIsPaint(boolean isPaint) { 13 IsPaint = isPaint; 14 } 15 16 public void setOnPathListener(OnPathListener listener) { 17 this.listener = listener; 18 } 19 20 public void setmEraserPaint(int width){ 21 showCustomToast("設定橡皮粗爲:"+width); 22 mEraserPaint.setStrokeWidth(width); 23 } 24 25 public void setIsRecordPath(boolean isRecordPath,PathNode pathNode) { 26 this.pathNode = pathNode; 27 IsRecordPath = isRecordPath; 28 } 29 30 public void setIsRecordPath(boolean isRecordPath) { 31 IsRecordPath = isRecordPath; 32 } 33 public boolean isShowing() { 34 return IsShowing; 35 } 36 37 38 private static String colorToHexString(int color) { 39 return String.format("#%06X", 0xFFFFFFFF & color); 40 } 41 42 // switch eraser/paint 43 public void Eraser(){ 44 showCustomToast("切換爲橡皮"); 45 IsPaint = false; 46 Init_Eraser(UserInfo.EraserWidth); 47 } 48 49 public void Paint(){ 50 showCustomToast("切換爲鉛筆"); 51 IsPaint = true; 52 Init_Paint(UserInfo.PaintColor, UserInfo.PaintWidth); 53 } 54 55 public Paint getmEraserPaint() { 56 return mEraserPaint; 57 } 58 59 public Paint getmPaint() { 60 return mPaint; 61 }
這些都不是很主要的東西。
1 /** 2 * @author lfk_dsk@hotmail.com 3 * @param uri get the uri of a picture 4 * */ 5 public void setmBitmap(Uri uri){ 6 Log.e("圖片路徑", String.valueOf(uri)); 7 ContentResolver cr = context.getContentResolver(); 8 try { 9 mBitmapBackGround = BitmapFactory.decodeStream(cr.openInputStream(uri)); 10 // RectF rectF = new RectF(0,0,width,height); 11 mCanvas.drawBitmap(mBitmapBackGround, 0, 0, mBitmapPaint); 12 } catch (FileNotFoundException e) { 13 e.printStackTrace(); 14 } 15 invalidate(); 16 } 17 18 /** 19 * @author lfk_dsk@hotmail.com 20 * @param file Pictures' file 21 * */ 22 public void BitmapToPicture(File file){ 23 FileOutputStream fileOutputStream = null; 24 try { 25 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); 26 Date now = new Date(); 27 File tempfile = new File(file+"/"+formatter.format(now)+".jpg"); 28 fileOutputStream = new FileOutputStream(tempfile); 29 mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream); 30 showCustomToast(tempfile.getName() + "已保存"); 31 } catch (FileNotFoundException e) { 32 e.printStackTrace(); 33 } 34 }
加入圖片和將之保存爲圖片。
其實說是幀動畫,我實際上是把每一個onTouchEvent的動做的座標、筆的顏色、等等記錄了下來,再清空了在子線程重繪以實現第一幅效果圖裏點擊一鍵重繪的效果,
但從原理上說仍可歸於逐幀動畫。
首先設置一個Linstener監聽存儲。
package com.lfk.drawapictiure; /** * Created by liufengkai on 15/8/26. */ public interface OnPathListener { void AddNodeToPath(float x, float y ,int event,boolean Ispaint); }
再在監聽裏進行存儲:
1 paintView.setOnPathListener(new OnPathListener() { 2 @Override 3 public void AddNodeToPath(float x, float y, int event, boolean IsPaint) { 4 PathNode.Node tempnode = pathNode.new Node(); 5 tempnode.x = x; 6 tempnode.y = y; 7 if (IsPaint) { 8 tempnode.PenColor = UserInfo.PaintColor; 9 tempnode.PenWidth = UserInfo.PaintWidth; 10 } else { 11 tempnode.EraserWidth = UserInfo.EraserWidth; 12 } 13 tempnode.IsPaint = IsPaint; 14 Log.e(tempnode.PenColor + ":" + tempnode.PenWidth + ":" + tempnode.EraserWidth, tempnode.IsPaint + ""); 15 tempnode.TouchEvent = event; 16 tempnode.time = System.currentTimeMillis(); 17 pathNode.AddNode(tempnode); 18 } 19 });
其中PathNode是一個application類,用於存儲存下來的arraylist:
1 package com.lfk.drawapictiure; 2 import android.app.Application; 3 4 import java.util.ArrayList; 5 6 /** 7 * Created by liufengkai on 15/8/25. 8 */ 9 public class PathNode extends Application{ 10 public class Node{ 11 public Node() {} 12 public float x; 13 public float y; 14 public int PenColor; 15 public int TouchEvent; 16 public int PenWidth; 17 public boolean IsPaint; 18 public long time; 19 public int EraserWidth; 20 21 } 22 private ArrayList<Node> PathList; 23 24 25 public ArrayList<Node> getPathList() { 26 return PathList; 27 } 28 29 public void AddNode(Node node){ 30 PathList.add(node); 31 } 32 33 public Node NewAnode(){ 34 return new Node(); 35 } 36 37 38 public void ClearList(){ 39 PathList.clear(); 40 } 41 42 @Override 43 public void onCreate() { 44 super.onCreate(); 45 PathList = new ArrayList<Node>(); 46 } 47 48 public void setPathList(ArrayList<Node> pathList) { 49 PathList = pathList; 50 } 51 52 public Node getTheLastNote(){ 53 return PathList.get(PathList.size()-1); 54 } 55 56 public void deleteTheLastNote(){ 57 PathList.remove(PathList.size()-1); 58 } 59 60 public PathNode() { 61 PathList = new ArrayList<Node>(); 62 } 63 64 }
存入以後,再放到子線程裏面逐幀的載入播放:
1 class PreviewThread implements Runnable{ 2 private long time; 3 private ArrayList<PathNode.Node> nodes; 4 private View view; 5 public PreviewThread(View view, ArrayList<PathNode.Node> arrayList) { 6 this.view = view; 7 this.nodes = arrayList; 8 } 9 public void run() { 10 time = 0; 11 IsShowing = true; 12 clean(); 13 for(int i = 0 ;i < nodes.size();i++) { 14 PathNode.Node node=nodes.get(i); 15 Log.e(node.PenColor+":"+node.PenWidth+":"+node.EraserWidth,node.IsPaint+""); 16 float x = node.x; 17 float y = node.y; 18 if(i<nodes.size()-1) { 19 time=nodes.get(i+1).time-node.time; 20 } 21 IsPaint = node.IsPaint; 22 if(node.IsPaint){ 23 UserInfo.PaintColor = node.PenColor; 24 UserInfo.PaintWidth = node.PenWidth; 25 Init_Paint(node.PenColor,node.PenWidth); 26 }else { 27 UserInfo.EraserWidth = node.EraserWidth; 28 Init_Eraser(node.EraserWidth); 29 } 30 switch (node.TouchEvent) { 31 case MotionEvent.ACTION_DOWN: 32 Touch_Down(x,y); 33 break; 34 case MotionEvent.ACTION_MOVE: 35 Touch_Move(x,y); 36 break; 37 case MotionEvent.ACTION_UP: 38 if(node.IsPaint){ 39 Touch_Up(mPaint); 40 }else { 41 Touch_Up(mEraserPaint); 42 } 43 break; 44 } 45 Message msg=new Message(); 46 msg.obj = view; 47 msg.what = INDIVIDE; 48 handler.sendMessage(msg); 49 if(!ReDoOrUnDoFlag) { 50 try { 51 Thread.sleep(time); 52 } catch (InterruptedException e) { 53 e.printStackTrace(); 54 } 55 } 56 } 57 ReDoOrUnDoFlag = false; 58 IsShowing = false; 59 IsRecordPath = true; 60 } 61 }
1 public void preview(ArrayList<PathNode.Node> arrayList) { 2 IsRecordPath = false; 3 PreviewThread previewThread = new PreviewThread(this, arrayList); 4 Thread thread = new Thread(previewThread); 5 thread.start(); 6 }
這是播放的幀動畫,接下來講保存幀動畫,我將之輸出成json並輸出到文件中去。
1 public void PathNodeToJson(PathNode pathNode,File file){ 2 ArrayList<PathNode.Node> arrayList = pathNode.getPathList(); 3 String json = "["; 4 for(int i = 0;i < arrayList.size();i++){ 5 PathNode.Node node = arrayList.get(i); 6 json += "{"+"\""+"x"+"\""+":"+px2dip(node.x)+"," + 7 "\""+"y"+"\""+":"+px2dip(node.y)+","+ 8 "\""+"PenColor"+"\""+":"+node.PenColor+","+ 9 "\""+"PenWidth"+"\""+":"+node.PenWidth+","+ 10 "\""+"EraserWidth"+"\""+":"+node.EraserWidth+","+ 11 "\""+"TouchEvent"+"\""+":"+node.TouchEvent+","+ 12 "\""+"IsPaint"+"\""+":"+"\""+node.IsPaint+"\""+","+ 13 "\""+"time"+"\""+":"+node.time+ 14 "},"; 15 } 16 json = json.substring(0,json.length()-1); 17 json += "]"; 18 try { 19 json = enCrypto(json, "lfk_dsk@hotmail.com"); 20 } catch (InvalidKeySpecException e) { 21 e.printStackTrace(); 22 } catch (InvalidKeyException e) { 23 e.printStackTrace(); 24 } catch (NoSuchPaddingException e) { 25 e.printStackTrace(); 26 } catch (IllegalBlockSizeException e) { 27 e.printStackTrace(); 28 } catch (BadPaddingException e) { 29 e.printStackTrace(); 30 } 31 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); 32 Date now = new Date(); 33 File tempfile = new File(file+"/"+formatter.format(now)+".lfk"); 34 try { 35 FileOutputStream fileOutputStream = new FileOutputStream(tempfile); 36 byte[] bytes = json.getBytes(); 37 fileOutputStream.write(bytes); 38 fileOutputStream.close(); 39 showCustomToast(tempfile.getName() + "已保存"); 40 } catch (FileNotFoundException e) { 41 e.printStackTrace(); 42 } catch (IOException e) { 43 e.printStackTrace(); 44 } 45
另外還可將文件從json中提取出來:
1 private void JsonToPathNode(String file){ 2 String res = ""; 3 ArrayList<PathNode.Node> arrayList = new ArrayList<>(); 4 try { 5 Log.e("絕對路徑1",file); 6 FileInputStream in = new FileInputStream(file); 7 ByteArrayOutputStream bufferOut = new ByteArrayOutputStream(); 8 byte[] buffer = new byte[1024]; 9 for(int i = in.read(buffer, 0, buffer.length); i > 0 ; i = in.read(buffer, 0, buffer.length)) { 10 bufferOut.write(buffer, 0, i); 11 } 12 res = new String(bufferOut.toByteArray(), Charset.forName("utf-8")); 13 Log.e("字符串文件",res); 14 } catch (FileNotFoundException e) { 15 e.printStackTrace(); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 try { 20 res = deCrypto(res, "lfk_dsk@hotmail.com"); 21 } catch (InvalidKeyException e) { 22 e.printStackTrace(); 23 } catch (InvalidKeySpecException e) { 24 e.printStackTrace(); 25 } catch (NoSuchPaddingException e) { 26 e.printStackTrace(); 27 } catch (IllegalBlockSizeException e) { 28 e.printStackTrace(); 29 } catch (BadPaddingException e) { 30 e.printStackTrace(); 31 } 32 try { 33 JSONArray jsonArray = new JSONArray(res); 34 for(int i = 0;i < jsonArray.length();i++){ 35 JSONObject jsonObject = new JSONObject(jsonArray.getString(i)); 36 PathNode.Node node = new PathNode().NewAnode(); 37 node.x = dip2px(jsonObject.getInt("x")); 38 node.y = dip2px(jsonObject.getInt("y")); 39 node.TouchEvent = jsonObject.getInt("TouchEvent"); 40 node.PenWidth = jsonObject.getInt("PenWidth"); 41 node.PenColor = jsonObject.getInt("PenColor"); 42 node.EraserWidth = jsonObject.getInt("EraserWidth"); 43 node.IsPaint = jsonObject.getBoolean("IsPaint"); 44 node.time = jsonObject.getLong("time"); 45 arrayList.add(node); 46 } 47 } catch (JSONException e) { 48 e.printStackTrace(); 49 } 50 pathNode.setPathList(arrayList); 51 }
另外若是不想讓別人看出輸出的是json的話能夠使用des加密算法:
1 /** 2 * 加密(使用DES算法) 3 * 4 * @param txt 5 * 須要加密的文本 6 * @param key 7 * 密鑰 8 * @return 成功加密的文本 9 * @throws InvalidKeySpecException 10 * @throws InvalidKeyException 11 * @throws NoSuchPaddingException 12 * @throws IllegalBlockSizeException 13 * @throws BadPaddingException 14 */ 15 private static String enCrypto(String txt, String key) 16 throws InvalidKeySpecException, InvalidKeyException, 17 NoSuchPaddingException, IllegalBlockSizeException, 18 BadPaddingException { 19 StringBuffer sb = new StringBuffer(); 20 DESKeySpec desKeySpec = new DESKeySpec(key.getBytes()); 21 SecretKeyFactory skeyFactory = null; 22 Cipher cipher = null; 23 try { 24 skeyFactory = SecretKeyFactory.getInstance("DES"); 25 cipher = Cipher.getInstance("DES"); 26 } catch (NoSuchAlgorithmException e) { 27 e.printStackTrace(); 28 } 29 SecretKey deskey = skeyFactory != null ? skeyFactory.generateSecret(desKeySpec) : null; 30 if (cipher != null) { 31 cipher.init(Cipher.ENCRYPT_MODE, deskey); 32 } 33 byte[] cipherText = cipher != null ? cipher.doFinal(txt.getBytes()) : new byte[0]; 34 for (int n = 0; n < cipherText.length; n++) { 35 String stmp = (java.lang.Integer.toHexString(cipherText[n] & 0XFF)); 36 37 if (stmp.length() == 1) { 38 sb.append("0" + stmp); 39 } else { 40 sb.append(stmp); 41 } 42 } 43 return sb.toString().toUpperCase(); 44 } 45 46 /** 47 * 解密(使用DES算法) 48 * 49 * @param txt 50 * 須要解密的文本 51 * @param key 52 * 密鑰 53 * @return 成功解密的文本 54 * @throws InvalidKeyException 55 * @throws InvalidKeySpecException 56 * @throws NoSuchPaddingException 57 * @throws IllegalBlockSizeException 58 * @throws BadPaddingException 59 */ 60 private static String deCrypto(String txt, String key) 61 throws InvalidKeyException, InvalidKeySpecException, 62 NoSuchPaddingException, IllegalBlockSizeException, 63 BadPaddingException { 64 DESKeySpec desKeySpec = new DESKeySpec(key.getBytes()); 65 SecretKeyFactory skeyFactory = null; 66 Cipher cipher = null; 67 try { 68 skeyFactory = SecretKeyFactory.getInstance("DES"); 69 cipher = Cipher.getInstance("DES"); 70 } catch (NoSuchAlgorithmException e) { 71 e.printStackTrace(); 72 } 73 SecretKey deskey = skeyFactory != null ? skeyFactory.generateSecret(desKeySpec) : null; 74 if (cipher != null) { 75 cipher.init(Cipher.DECRYPT_MODE, deskey); 76 } 77 byte[] btxts = new byte[txt.length() / 2]; 78 for (int i = 0, count = txt.length(); i < count; i += 2) { 79 btxts[i / 2] = (byte) Integer.parseInt(txt.substring(i, i + 2), 16); 80 } 81 return (new String(cipher.doFinal(btxts))); 82 }
繪圖時撤銷和前進的功能也是十分有用的。
public void ReDoORUndo(boolean flag){ if(!IsShowing) { ReDoOrUnDoFlag = true; try { if (flag) { ReDoNodes.add(pathNode.getTheLastNote()); pathNode.deleteTheLastNote(); preview(pathNode.getPathList()); } else { pathNode.AddNode(ReDoNodes.get(ReDoNodes.size() - 1)); ReDoNodes.remove(ReDoNodes.size() - 1); preview(pathNode.getPathList()); } } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); showCustomToast("沒法操做=-="); } } }
其實就是把PathNode的尾節點轉移到一個新的鏈表中,根據須要再處理,而後調用重繪,區別是中間不加sleep的線程休眠,這樣看上去不會有重繪的過程,只會一閃就少了一節。
把它綁定在音量鍵上就能輕鬆使用兩個音量鍵來調節Redo OR Undo。
博客地址:博客園,版權全部,轉載須聯繫做者。
GitHub地址:JustWeTools
若是以爲對您有幫助請點贊。