博客地址:博客园,版权所有,转载须联系作者。
GitHub地址:JustWeTools
最近做了个绘图的控件,实现了一些有趣的功能。
先上效果图:
1.可直接使用设定按钮来实现已拥有的方法,且拓展性强
2.基础功能:更换颜色、更换橡皮、以及更换橡皮和笔的粗细、清屏、倒入图片
3.特殊功能:保存画笔轨迹帧动画、帧动画导入导出、ReDo和UnDo
GitHub地址:JustWeTools
如何使用该控件可以在GitHub的README中找到,此处不再赘述。
1.绘图控件继承于View,使用canvas做画板,在canvas上设置一个空白的Bitmap作为画布,以保存画下的轨迹。
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,
和往bitmap上画的画笔mBitmapPaint。
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 [email protected] 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 [email protected] 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, "[email protected]"); 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, "[email protected]"); 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
如果觉得对您有帮助请点赞。