android实现画板功能

作者: wensefu | 来源:发表于2017-03-24 03:16 被阅读6536次

    这两天闲来无事做了一个简易的画板程序,和大家分享一下。
    效果图:

    这是一个灰常简单的画板,不过麻雀虽小,五脏俱全:

    1. 支持撤销(undo);
    2. 支持反撤销(redo);
    3. 支持橡皮擦(eraser);
    4. 支持清除功能(clear);
    5. 支持保存为图像(save)。

    github地址点这里,欢迎fork,star


    关键代码

    非常简短,只有200来行

    /**
     * Created by wensefu on 17-3-21.
     */
    public class PaletteView extends View {
    
        private Paint mPaint;
        private Path mPath;
        private float mLastX;
        private float mLastY;
        private Bitmap mBufferBitmap;
        private Canvas mBufferCanvas;
    
        private static final int MAX_CACHE_STEP = 20;
    
        private List<DrawingInfo> mDrawingList;
        private List<DrawingInfo> mRemovedList;
    
        private Xfermode mClearMode;
        private float mDrawSize;
        private float mEraserSize;
    
        private boolean mCanEraser;
    
        private Callback mCallback;
    
        public enum Mode {
            DRAW,
            ERASER
        }
    
        private Mode mMode = Mode.DRAW;
    
        public PaletteView(Context context) {
            this(context,null);
        }
    
        public PaletteView(Context context, AttributeSet attrs) {
            super(context, attrs);
            setDrawingCacheEnabled(true);
            init();
        }
    
        public interface Callback {
            void onUndoRedoStatusChanged();
        }
    
        public void setCallback(Callback callback){
            mCallback = callback;
        }
    
        private void init() {
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setFilterBitmap(true);
            mPaint.setStrokeJoin(Paint.Join.ROUND);
            mPaint.setStrokeCap(Paint.Cap.ROUND);
            mDrawSize = 20;
            mEraserSize = 40;
            mPaint.setStrokeWidth(mDrawSize);
            mPaint.setColor(0XFF000000);
    
            mClearMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
        }
    
        private void initBuffer(){
            mBufferBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            mBufferCanvas = new Canvas(mBufferBitmap);
        }
    
        private abstract static class DrawingInfo {
            Paint paint;
            abstract void draw(Canvas canvas);
        }
    
        private static class PathDrawingInfo extends DrawingInfo{
    
            Path path;
    
            @Override
            void draw(Canvas canvas) {
                canvas.drawPath(path, paint);
            }
        }
    
        public Mode getMode() {
            return mMode;
        }
    
        public void setMode(Mode mode) {
            if (mode != mMode) {
                mMode = mode;
                if (mMode == Mode.DRAW) {
                    mPaint.setXfermode(null);
                    mPaint.setStrokeWidth(mDrawSize);
                } else {
                    mPaint.setXfermode(mClearMode);
                    mPaint.setStrokeWidth(mEraserSize);
                }
            }
        }
    
        public void setEraserSize(float size) {
            mEraserSize = size;
        }
    
        public void setPenRawSize(float size) {
            mEraserSize = size;
        }
    
        public void setPenColor(int color) {
            mPaint.setColor(color);
        }
    
        public void setPenAlpha(int alpha) {
            mPaint.setAlpha(alpha);
        }
    
        private void reDraw(){
            if (mDrawingList != null) {
                mBufferBitmap.eraseColor(Color.TRANSPARENT);
                for (DrawingInfo drawingInfo : mDrawingList) {
                    drawingInfo.draw(mBufferCanvas);
                }
                invalidate();
            }
        }
    
        public boolean canRedo() {
            return mRemovedList != null && mRemovedList.size() > 0;
        }
    
        public boolean canUndo(){
            return mDrawingList != null && mDrawingList.size() > 0;
        }
    
        public void redo() {
            int size = mRemovedList == null ? 0 : mRemovedList.size();
            if (size > 0) {
                DrawingInfo info = mRemovedList.remove(size - 1);
                mDrawingList.add(info);
                mCanEraser = true;
                reDraw();
                if (mCallback != null) {
                    mCallback.onUndoRedoStatusChanged();
                }
            }
        }
    
        public void undo() {
            int size = mDrawingList == null ? 0 : mDrawingList.size();
            if (size > 0) {
                DrawingInfo info = mDrawingList.remove(size - 1);
                if (mRemovedList == null) {
                    mRemovedList = new ArrayList<>(MAX_CACHE_STEP);
                }
                if (size == 1) {
                    mCanEraser = false;
                }
                mRemovedList.add(info);
                reDraw();
                if (mCallback != null) {
                    mCallback.onUndoRedoStatusChanged();
                }
            }
        }
    
        public void clear() {
            if (mBufferBitmap != null) {
                if (mDrawingList != null) {
                    mDrawingList.clear();
                }
                if (mRemovedList != null) {
                    mRemovedList.clear();
                }
                mCanEraser = false;
                mBufferBitmap.eraseColor(Color.TRANSPARENT);
                invalidate();
                if (mCallback != null) {
                    mCallback.onUndoRedoStatusChanged();
                }
            }
        }
    
        public Bitmap buildBitmap() {
            Bitmap bm = getDrawingCache();
            Bitmap result = Bitmap.createBitmap(bm);
            destroyDrawingCache();
            return result;
        }
    
        private void saveDrawingPath(){
            if (mDrawingList == null) {
                mDrawingList = new ArrayList<>(MAX_CACHE_STEP);
            } else if (mDrawingList.size() == MAX_CACHE_STEP) {
                mDrawingList.remove(0);
            }
            Path cachePath = new Path(mPath);
            Paint cachePaint = new Paint(mPaint);
            PathDrawingInfo info = new PathDrawingInfo();
            info.path = cachePath;
            info.paint = cachePaint;
            mDrawingList.add(info);
            mCanEraser = true;
            if (mCallback != null) {
                mCallback.onUndoRedoStatusChanged();
            }
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            if (mBufferBitmap != null) {
                canvas.drawBitmap(mBufferBitmap, 0, 0, null);
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int action = event.getAction() & MotionEvent.ACTION_MASK;
            final float x = event.getX();
            final float y = event.getY();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mLastX = x;
                    mLastY = y;
                    if (mPath == null) {
                        mPath = new Path();
                    }
                    mPath.moveTo(x,y);
                    break;
                case MotionEvent.ACTION_MOVE:
                    //这里终点设为两点的中心点的目的在于使绘制的曲线更平滑,如果终点直接设置为x,y,效果和lineto是一样的,实际是折线效果
                    mPath.quadTo(mLastX, mLastY, (x + mLastX) / 2, (y + mLastY) / 2);
                    if (mBufferBitmap == null) {
                        initBuffer();
                    }
                    if (mMode == Mode.ERASER && !mCanEraser) {
                        break;
                    }
                    mBufferCanvas.drawPath(mPath,mPaint);
                    invalidate();
                    mLastX = x;
                    mLastY = y;
                    break;
                case MotionEvent.ACTION_UP:
                    if (mMode == Mode.DRAW || mCanEraser) {
                        saveDrawingPath();
                    }
                    mPath.reset();
                    break;
            }
            return true;
        }
    }
    

    原理分析

    总的来讲,思路其实很简单:

    • 接收到move事件后,在屏幕上画出相应的轨迹;
    • 撤销功能:在画轨迹时,记录每一步的轨迹和画笔属性,每次撤销时把最后一步删除,然后重绘;
    • 反撤销功能:撤销时把撤销的轨迹和画笔属性保存在另一个列表里,反撤销时从这个列表里取出来放到记录绘制信息的列表里,然后重绘;
    • 橡皮擦功能:这里主要应用到android的图象混合(Xfermode)知识,后面会对其进行讲解;
    • 清除功能:这个非常简单,清除屏上的像素记录即可;
    • 绘制:考虑到性能问题,这里使用了双缓冲绘图技术。

    android的图象混合(Xfermode)

    图象混合本质上用一句话解释就是:

    按照某种算法将画布上你想要绘制的区域的每个像素的ARGB和你将要在这个区域绘制的ARGB进行组合变换。

    举个例子,
    我现在有个自定义View, 背景画成绿色的,
    我再在上面绘制一个蓝色的圆,在不设置Xfermode(不进行图像混合)的情况下效果是这样的:

    代码:

    @Override
        protected void onDraw(Canvas canvas) {
    
            canvas.drawColor(Color.GREEN);
    
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, 300, mPaint);
        }
    

    现在我改一下代码,给Paint设置一个Xfermode,

     public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mPaint.setColor(Color.BLUE);
            mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
    
            canvas.drawColor(Color.GREEN);
    
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, 300, mPaint);
        }
    

    效果是这样的:


    为什么设置Xfermode后圆会变成黑色呢?代码里并没有设置Paint的颜色为黑色呀!

    原因是,设置Xfermode为new PorterDuffXfermode(PorterDuff.Mode.CLEAR)后,按照这个混合模式的算法,canvas上圆绘制的区域的所有像素点ARGB全部被置0了,
    因此实际上圆绘制区域是透明的,显示为黑色是因为activity的window背景色是黑色的。

    Xfermode有三个子类,AvoidXfermode,PixelXorXfermode,PorterDuffXfermode,前两个在API 16上已经过时了,现在最常用的是PorterDuffXfermode,目前支持18种图像混合算法,分别产生不同的混合效果,做橡皮擦功能用到了CLEAR算法,其他算法的效果大家有兴趣可以参考google官方的介绍,api demo里也有相关的例子。
    需要注意的是,Xfermode的某些算法不支持硬件加速,例如PorterDuffXfermode的DARKEN,LIGHTEN以及OVERLAY是不支持硬件加速的。具体参见android developer文档(需要梯子fk):hardware-accel.html#unsupported


    android双缓冲绘图技术##

    在理解android的双缓冲绘图概念之前,我们先想一想,何谓缓冲?
    所谓缓冲,简单地说就是将多个将要执行的独立的任务集结起来,一起提交。
    打个比方,现实生活中,你现在要将很多砖从A处搬到B处,原始的方法是徒手一次搬几块,这就是没有使用“缓存”的方法。你也可以用一辆拖车,先把砖搬到拖车上,再把拖车拉到B处,这就是使用了“缓存”的方法。

    每个canvas都有对应的一个bitmap,绘图的过程实际上就是往这个bitmap上写入ARGB信息,然后把这些信息交给GPU进行显示。这里面其实已经包含了一次缓冲的过程。

    所以,讲到这里,双缓冲的概念我想你已经明白了。没错,绘图时的双缓冲其实就是再增加一个canvas,把想要绘制的内容先绘制到这个增加的canvas对应的bitmap上,写完后再把这个bitmap的ARGB信息一次提交给上下文的canvas去绘制。双缓冲技术在绘制数据量较大时在性能上有明显的提升,画板程序之所以用到了双缓存,也是基于提高绘制效率的考虑。
    关于双缓冲技术更详细的分析,可以参考我的另一篇博文:

    android双缓冲绘图技术分析

    转载请说明出处:http://www.jianshu.com/p/548d2799fd6e

    相关文章

      网友评论

      • 蚂蚁绊倒象:想要画图形,手指没松开就一直画。。怎么解决啊?
      • Obadiah:设置 Xfermode 后怎么能不变黑?
      • 镇元大仙:不知道博主你有没有发现,你的demo画出来的线条锯齿比较明显,好像抗锯齿的flag没有起作用一样。
      • h_h_m2632:不错哦 ,刚好需要,但是目前把这个涂鸦层放置于SurfaceView视图上,无法运行背景图没有出现。该如何解决呢
        因帅被判刑:您好 请问你实现了么,我也要做这个 也想用SurfaceView,希望得到指点谢谢

      本文标题:android实现画板功能

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