博客地址:博客园,版权所有,转载须联系作者。
GitHub地址:JustWeTools
最近做了个绘图的控件,实现了一些有趣的功能。
先上效果图:

PaintView画图工具:
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。
2.橡皮和铅笔的配置:
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));
意思是设定了层叠的方式,当橡皮擦上去的时候,即新加上的一层(橡皮)和原有层有重叠部分时,取原有层去掉重叠部分的剩余部分,这也就达到了橡皮的功能。

3.PaintView重点在于对于按下、移动、抬起的监听:
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 }
这些都不是很主要的东西。
4.设定图片:
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 }
加入图片和将之保存为图片。
5.重点:纪录帧动画
其实说是帧动画,我其实是把每个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 PathList;
23
24
25 public ArrayList 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();
46 }
47
48 public void setPathList(ArrayList 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();
62 }
63
64 }
存入之后,再放到子线程里面逐帧的载入播放:
1 class PreviewThread implements Runnable{
2 private long time;
3 private ArrayList nodes;
4 private View view;
5 public PreviewThread(View view, ArrayList 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
1 public void preview(ArrayList 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 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 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 }
6.Redo 和 Undo:
绘图时撤销和前进的功能也是十分有用的。
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
如果觉得对您有帮助请点赞。