PaintView 绘图控件解析

  博客地址:博客园,版权所有,转载须联系作者。

  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<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     }

 

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 

  如果觉得对您有帮助请点赞。

 

posted @ 2015-08-29 12:06 刘丰恺 阅读(...) 评论(...) 编辑 收藏