Android 自定义view,画图板练习

作者: 英勇青铜5 | 来源:发表于2017-02-17 21:55 被阅读1287次

    项目中想要实现一个简易画图板的需求,功能并不复杂,就是6个很常用的功能

    画图板

    陈小默同学有一个比较复杂,强大,高效的CrazyPalette,同学间商业互吹下,哈哈。里面基本常用的操作都有,代码写的很好,只是用的Kotlin,不过我需要的只是一个简单的绘图板,我参考了他的一些思路以及另外一篇android项目 之 记事本 ----- 画板功能之撤销、恢复和清空,做了一个简单的PaintView


    1. PaintView

    之前在网上看到别的博客说写的双缓冲是这个思路,这里感觉有错误,不清楚我写的这种方式算不算双缓冲。等过了这段加班,我再查查问问确认下 20170524 21:13

    思路:使用双缓冲思路,有一个mBitmap,来记录最终的绘制。在手指滑动过程中,屏幕上会实时显示出手指滑动时的绘制轨迹。当手指离开屏幕后,显示最终存有内容的mBitmap

    1. 撤销和恢复利用LinkedList来模拟两个储存记录的
    2. 清空,这里偷懒,直接绘制白色,将之前绘制的内容盖住。也可以考虑使用new PorterDuffXfermode(PorterDuff.Mode.CLEAR)。但有些时候,个人感觉这种方式会出现些莫名其妙的情况,能直接绘制成单一纯色,就不使用PorterDuffXfermode
    3. 橡皮擦,这里使用了PorterDuffXfermode,并setBackgroundColor(Color.WHITE)以及把硬件加速关闭了

    关于橡皮擦得额外说明下:
    橡皮擦使用new PorterDuffXfermode(PorterDuff.Mode.CLEAR)是为了复习下PorterDuffXfermode,踩踩坑

    这里有两个坑,硬件加速和背景穿透。当使用PorterDuff.Mode.CLEAR时,利用的是把上次绘制的东西给清除掉,这就导致在保存绘制的图片后,橡皮擦轨迹是透明的,而之前绘制的内容又被擦除了,就会把图片下方的当前系统背景色显示出来

    问题

    当我在电脑打开保存的图片时,橡皮擦轨迹会透出我电脑桌面背景的颜色。在手机打开就会透出手机背景颜色

    解决办法:
    init()方法中,setBackgroundColor(Color.WHITE),绘制了一个白色背景,但这样也就导致了过度绘制

    根据需求,这里更好的思路就是把橡皮擦的颜色也直接设置成白色,更加简单而且没有PorterDuffXfermode的坑。但既然是练习,就踩踩坑


    代码:

    public class PaintView extends View {
        private Paint mPaint;
        private Path mPath;
        private Path eraserPath;
        private Paint eraserPaint;
        private Canvas mCanvas;
        private Bitmap mBitmap;
        private float mLastX, mLastY;//上次的坐标
        private Paint mBitmapPaint;
        //使用LinkedList 模拟栈,来保存 Path
        private LinkedList<PathBean> undoList;
        private LinkedList<PathBean> redoList;
        private boolean isEraserModel;
    
    
        public PaintView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        /***
         * 初始化
         */
        private void init() {
            //关闭硬件加速
            //否则橡皮擦模式下,设置的 PorterDuff.Mode.CLEAR ,实时绘制的轨迹是黑色
             setBackgroundColor(Color.WHITE);//设置白色背景
            setLayerType(View.LAYER_TYPE_SOFTWARE, null);
            //画笔
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mPaint.setStrokeWidth(4f);
            mPaint.setAntiAlias(true);
            mPaint.setColor(Color.BLACK);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeJoin(Paint.Join.ROUND);//使画笔更加圆润
            mPaint.setStrokeCap(Paint.Cap.ROUND);//同上
            mBitmapPaint = new Paint(Paint.DITHER_FLAG);
            //保存签名的画布
            post(new Runnable() {//拿到控件的宽和高
                @Override
                public void run() {
                    //获取PaintView的宽和高
                    //由于橡皮擦使用的是 Color.TRANSPARENT ,不能使用RGB-565
                    mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444);
                    mCanvas = new Canvas(mBitmap);
                    //抗锯齿
                    mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
                    //背景色
                    mCanvas.drawColor(Color.WHITE);
                }
            });
    
            undoList = new LinkedList<>();
            redoList = new LinkedList<>();
        }
    
        /**
         * 绘制
         */
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (mBitmap != null) {
                canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);//将mBitmap绘制在canvas上,最终的显示
                if (!isEraserModel) {
                    if (null != mPath) {//显示实时正在绘制的path轨迹
                        canvas.drawPath(mPath, mPaint);
                    }
                } else {
                    if (null != eraserPath) {
                        canvas.drawPath(eraserPath, eraserPaint);
                    }
                }
            }
        }
    
        /**
         * 撤销操作
         */
        public void undo() {
            if (!undoList.isEmpty()) {
                clearPaint();//清除之前绘制内容
                PathBean lastPb = undoList.removeLast();//将最后一个移除
                redoList.add(lastPb);//加入 恢复操作
                //遍历,将Path重新绘制到 mCanvas
                for (PathBean pb : undoList) {
                    mCanvas.drawPath(pb.path, pb.paint);
                }
                invalidate();
            }
        }
    
    
        /**
         * 恢复操作
         */
        public void redo() {
            if (!redoList.isEmpty()) {
                PathBean pathBean = redoList.removeLast();
                mCanvas.drawPath(pathBean.path, pathBean.paint);
                invalidate();
                undoList.add(pathBean);
            }
        }
    
    
        /**
         * 设置画笔颜色
         */
        public void setPaintColor(@ColorInt int color) {
            mPaint.setColor(color);
        }
    
        /**
         * 清空,包括撤销和恢复操作列表
         */
        public void clearAll() {
            clearPaint();
            mLastY = 0f;
            //清空 撤销 ,恢复 操作列表
            redoList.clear();
            undoList.clear();
        }
    
        /**
         * 设置橡皮擦模式
         */
        public void setEraserModel(boolean isEraserModel) {
            this.isEraserModel = isEraserModel;
            if (eraserPaint == null) {
                eraserPaint = new Paint(mPaint);
                eraserPaint.setStrokeWidth(15f);
                eraserPaint.setColor(Color.TRANSPARENT);
                eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
            }
        }
    
        /**
         * 保存到指定的文件夹中
         */
        public boolean saveImg(String filePath, String imgName) {
            boolean isCanSave = mBitmap != null && mLastY != 0f && !undoList.isEmpty();
            if (isCanSave) {//空白板时,就不保存
                //保存图片
                File file = new File(filePath + File.separator + imgName);
                FileOutputStream fileOutputStream = null;
                try {
                    fileOutputStream = new FileOutputStream(file);
                    if (mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)) {
                        fileOutputStream.flush();
                        return true;
                    }
                } catch (java.io.IOException e) {
                    e.printStackTrace();
                } finally {
                    closeStream(fileOutputStream);
                }
            }
            return false;
        }
    
        /**
         * 是否可以撤销
         */
        public boolean isCanUndo() {
            return undoList.isEmpty();
        }
    
        /**
         * 是否可以恢复
         */
        public boolean isCanRedo() {
            return redoList.isEmpty();
        }
    
        /**
         * 清除绘制内容
         * 直接绘制白色背景
         */
        private void clearPaint() {
            mCanvas.drawColor(Color.WHITE);
            invalidate();
        }
    
    
        /**
         * 触摸事件 触摸绘制
         */
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (!isEraserModel) {
                commonTouchEvent(event);
            } else {
                eraserTouchEvent(event);
            }
            invalidate();
            return true;
        }
    
        /**
         * 橡皮擦事件
         */
        private void eraserTouchEvent(MotionEvent event) {
            int action = event.getAction();
            float x = event.getX();
            float y = event.getY();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    //路径
                    eraserPath = new Path();
                    mLastX = x;
                    mLastY = y;
                    eraserPath.moveTo(mLastX, mLastY);
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dx = Math.abs(x - mLastX);
                    float dy = Math.abs(y - mLastY);
                    if (dx >= 3 || dy >= 3) {//绘制的最小距离 3px
                        eraserPath.quadTo(mLastX, mLastY, (mLastX + x) / 2, (mLastY + y) / 2);
                    }
                    mLastX = x;
                    mLastY = y;
                    break;
                case MotionEvent.ACTION_UP:
                    mCanvas.drawPath(eraserPath, eraserPaint);//将路径绘制在mBitmap上
                    eraserPath.reset();
                    eraserPath = null;
                    break;
            }
        }
    
        /**
         * 普通画笔事件
         */
        private void commonTouchEvent(MotionEvent event) {
            int action = event.getAction();
            float x = event.getX();
            float y = event.getY();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    //路径
                    mPath = new Path();
                    mLastX = x;
                    mLastY = y;
                    mPath.moveTo(mLastX, mLastY);
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dx = Math.abs(x - mLastX);
                    float dy = Math.abs(y - mLastY);
                    if (dx >= 3 || dy >= 3) {//绘制的最小距离 3px
                        //利用二阶贝塞尔曲线,使绘制路径更加圆滑
                        mPath.quadTo(mLastX, mLastY, (mLastX + x) / 2, (mLastY + y) / 2);
                    }
                    mLastX = x;
                    mLastY = y;
                    break;
                case MotionEvent.ACTION_UP:
                    mCanvas.drawPath(mPath, mPaint);//将路径绘制在mBitmap上
                    Path path = new Path(mPath);//复制出一份mPath
                    Paint paint = new Paint(mPaint);
                    PathBean pb = new PathBean(path, paint);
                    undoList.add(pb);//将路径对象存入集合
                    mPath.reset();
                    mPath = null;
                    break;
            }
        }
    
        /**
         * 关闭流
         */
        private void closeStream(Closeable closeable) {
            if (closeable != null) {
                try {
                    closeable.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * 测量
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    
            if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
                setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
            } else if (wSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(200, hSpecSize);
            } else if (hSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(wSpecSize, 200);
            }
        }
    
        /**
         * 路径对象
         */
        class PathBean {
            Path path;
            Paint paint;
    
            PathBean(Path path, Paint paint) {
                this.path = path;
                this.paint = paint;
            }
        }
    
    }
    

    代码中,重要地方都有注释


    2. Activity

    布局代码:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_margin="15dp"
            android:layout_weight="1"
            app:cardElevation="4dp"
            app:cardUseCompatPadding="true">
    
            <com.example.gcc.okhttpl.richeditor.PaintView
                android:id="@+id/activity_paint_pv"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </android.support.v7.widget.CardView>
    
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:gravity="center_vertical"
            android:orientation="horizontal">
    
            <TextView
                android:id="@+id/activity_paint_undo"
                style="@style/paint_menu_text_view_style"
                android:text="撤销" />
    
            <TextView
                android:id="@+id/activity_paint_redo"
                style="@style/paint_menu_text_view_style"
                android:text="恢复" />
    
            <TextView
                android:id="@+id/activity_paint_color"
                style="@style/paint_menu_text_view_style"
                android:text="红色" />
    
            <TextView
                android:id="@+id/activity_paint_clear"
                style="@style/paint_menu_text_view_style"
                android:text="清空" />
    
            <TextView
                android:id="@+id/activity_paint_eraser"
                style="@style/paint_menu_text_view_style"
                android:text="橡皮擦" />
    
            <TextView
                android:id="@+id/activity_paint_save"
                style="@style/paint_menu_text_view_style"
                android:text="保存" />
    
        </LinearLayout>
    </LinearLayout>
    

    Activity代码

    public class PaintViewActivity extends AppCompatActivity {
        private PaintView paintView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_paint_view);
            initView();
            initMenu();
        }
    
        /**
         * 初始化
         */
        private void initView() {
            paintView = (PaintView) findViewById(R.id.activity_paint_pv);
        }
    
        /**
         * 初始化底部菜单
         */
        private void initMenu() {
            //撤销
            menuItemSelected(R.id.activity_paint_undo, new MenuSelectedListener() {
                @Override
                public void onMenuSelected() {
                    ToastUtils.show(PaintViewActivity.this, "撤销");
                    paintView.undo();
                }
            });
            //恢复
            menuItemSelected(R.id.activity_paint_redo, new MenuSelectedListener() {
                @Override
                public void onMenuSelected() {
                    ToastUtils.show(PaintViewActivity.this, "恢复");
                    paintView.redo();
                }
            });
    
            //颜色
            menuItemSelected(R.id.activity_paint_color, new MenuSelectedListener() {
                @Override
                public void onMenuSelected() {
                    ToastUtils.show(PaintViewActivity.this, "红色");
                    paintView.setPaintColor(Color.RED);
                }
            });
            //清空
            menuItemSelected(R.id.activity_paint_clear, new MenuSelectedListener() {
                @Override
                public void onMenuSelected() {
                    ToastUtils.show(PaintViewActivity.this, "清空");
                    paintView.clearAll();
                }
            });
    
            //橡皮擦
            menuItemSelected(R.id.activity_paint_eraser, new MenuSelectedListener() {
                @Override
                public void onMenuSelected() {
                    ToastUtils.show(PaintViewActivity.this, "橡皮擦");
                    paintView.setEraserModel(true);
                }
            });
    
            //保存
            menuItemSelected(R.id.activity_paint_save, new MenuSelectedListener() {
                @Override
                public void onMenuSelected() {
                    String path = Environment.getExternalStorageDirectory().getPath()
                            + File.separator + Strings.FILE_PATH + File.separator + Strings.CACHE_PATH;
                    String imgName = "paint.jpg";
                    if (paintView.saveImg(path,imgName)) {
                        ToastUtils.show(PaintViewActivity.this, "保存成功");
                    }
                }
            });
        }
    
        /**
         * 选中底部 Menu 菜单项
         */
        private void menuItemSelected(int viewId, final MenuSelectedListener listener) {
            findViewById(viewId).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    listener.onMenuSelected();
                }
            });
    
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            ToastUtils.cancel();
        }
    
        interface MenuSelectedListener {
            void onMenuSelected();
        }
    }
    

    这是橡皮擦使用PorterDuffXfermode踩坑思路


    2.1 橡皮擦直接绘制白色背景思路

    简单修改PaintView代码:

    1. 首先把硬件加速打开,也就是把init()方法里,下面行代码注释掉:
      //setBackgroundColor(Color.WHITE);
      //setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    
    2.修改橡皮擦颜色
     eraserPaint.setColor(Color.WHITE);
     //eraserPaint.setColor(Color.TRANSPARENT);
     //eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    

    这种思路即不会导致过度绘制,也不会有硬件加速的坑,但前提是绘图板背景颜色是纯色的


    3. 最后

    即使在使用过渡绘制思路的情况下,暂时感觉效率也可以,在低端机上也没有明显的卡顿感,绘制轨迹蛮跟手的。个人感觉这种绘图板并不需要SurfaceView

    有错误,请指出

    共勉 : )

    相关文章

      网友评论

      • 郑捡书:为什么在init用了post()方法去定义创建那些对象,这里不懂为什么要用post,不用post就报错。还有就是gerWidth和getHeight()是在onLayout后才有值的,这里又是为什么加上post有值
        英勇青铜5: @郑捡书 这个post里面的runnable是就是传给了handler,具体什么时候出发,我不知道了。你看看吧,留言告诉下😀
        郑捡书:@英勇青铜5 那post是什么时候被触发
        英勇青铜5: @郑捡书 因为init在构造方法中,view还没有经过测量阶段呢,所以没有值的。
      • dodo_lihao:学习了
      • 有梦想便可飞翔:赞不错呢
        英勇青铜5: @有梦想便可飞翔 😀😀😀😀

      本文标题:Android 自定义view,画图板练习

      本文链接:https://www.haomeiwen.com/subject/cszlwttx.html