Android 自定义view:画图板实现

作者: 达峰a | 来源:发表于2017-10-28 13:38 被阅读0次

    本文重在对自定义view,以及其常用类,常用方法的初步了解,提供一个思路,效果是其次,画板只是例子。

    看效果:
    中间一个画图板
    上方小控件用来显示实时画出的图形
    下方小控件用来做一些画图的控制
    2个小控件都能移动

    画图板.jpg

    顺带还有一个刮刮卡效果,只需要改一个参数:

    刮刮卡.jpg

    自定义view首先要自定义属性:

    在values下面创建attrs.xml:

        <!--画图板-->
        <declare-styleable name="DrawImg">
            <attr name="PaintColor" />            //画笔颜色
            <attr name="PaintWidth" />           // 画笔宽度
            <attr name="CanvasImg" />           //画板图片
        </declare-styleable>
    
        <!--指定单位-->
        <attr name="PaintColor" format="color" />        
        <attr name="PaintWidth" format="dimension" />          
        <attr name="CanvasImg" format="reference" />          
    

    对于下面3行指定单位的代码可以放出来,可以让多个自定义view 都能使用。

    接下来新建自定义view类继承view,重写前3个构造方法

    红线标注是android studio 3.0.0对于参数提示的新特性

    构造方法.jpg
    • 通过this 让前2个构造方法都实现3个参数的构造方法。
      简单说一下构造方法。一个参数的构造方法是在代码中 new 时用到,2个参数的构造方法在布局xml中用到,3个参数的基本就是自定义view类中使用,大概就是这样。

    接下来从attrs.xml中通过TypedArray取出自定义属性:

            //从attrs文件中取出各个属性
            TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DrawImg, defStyleAttr, 0);
            for (int i = 0; i < a.getIndexCount(); i++) {
                int attr = a.getIndex(i);
                switch (attr) {
                    case R.styleable.DrawImg_PaintWidth:        //画笔宽度
                        paintWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                                TypedValue.COMPLEX_UNIT_DIP, -1, getResources().getDisplayMetrics()));
                        break;
                    case R.styleable.DrawImg_PaintColor:        //画笔颜色
                        paintColor = a.getColor(attr, Color.GREEN);
                        break;
                    case R.styleable.DrawImg_CanvasImg:         //画板图片
                        hasCanvasImg = a.getResourceId(attr, -1);
                        break;
                }
            }
            //设置默认画笔宽度
            if (paintWidth == -1) {
                paintWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());
            }
            //取出bitmap
            if (hasCanvasImg != -1) {
                bitmap = BitmapFactory.decodeResource(getResources(), hasCanvasImg);
            }
            //onMeasure可能走多次,onDraw创建对象更不好 所以把画笔路径new在这里
            path = new Path();
    
    • 需要默认值的设置默认值,以免布局中没有用到自定义属性导致报错。

    重写自定义view关键方法onMeasure(),onDraw()。onMeasure()用来指定这个自定义view 的大小,onDraw()用来进行实时绘图

    • 最重要的3个东西:画布Canvas,画笔Paint,路径Path
    • 看代码中的注释,超级详细,把需要注意的提出来

    在newPaint()方法中,paint有一个setXfermode()方法,这个表示图形混合方式,有18种 (比下图多了ADD和OVERLAY)。给张图看一下。这里我们用到2种 SRC_IN和 DST_OUT。

    • SRC_IN:取两层交集部分,显示上层
    • DST_OUT:取两层非交集部分,显示下层
      说实话这么说也很难懂,还是要自己动手试一试,不过这里只要知道:
      使用SRC_IN就会有一个画图板的效果
      使用DST_OUT就会有一个刮刮卡的效果
    绘制方式.jpg
        /**
         * onMeasure常见方法
         * 1)  getChildCount():获取子View的数量;
         * 2)  getChildAt(i):获取第i个子控件;
         * 3)  subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
         * 4)  measureChild(child, widthMeasureSpec, heightMeasureSpec):测量子View的宽高;
         * 5)  child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;
         * 6)  getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
         * 7)  setMeasuredDimension(width, height):重新设置控件的宽高。如果写了这句代码,就需要删除
         * “super. onMeasure(widthMeasureSpec, heightMeasureSpec);”这行代码。
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            /**
             *  getMode获取测量模式(下面3种) 和 getSize获取测量值
             *
             *  EXACTLY:当宽高值设置为具体值时使用,如100dp、match_parent等,此时取出的size是精确的尺寸;
             *  AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的空间;
             *  UNSPECIFIED:当没有指定宽高值时使用(很少见)。
             *
             * */
            //测量模式_宽
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            //测量模式_高
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            //宽度
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            //高度
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            //设置view宽度
            //如果布局中给出了准确的宽度,直接使用宽度,否则设置图片宽度为view宽度
            if (widthMode == MeasureSpec.EXACTLY) {
                width = widthSize;
            } else {
                if (hasCanvasImg != -1) {
                    //如果设置了图片,使用图片宽
                    width = bitmap.getWidth();
                } else {
                    //没有设置图片并且也没给准确的view宽高  设置一个宽默认值
                    width = 500;
                }
            }
            //设置view高度同上
            if (heightMode == MeasureSpec.EXACTLY) {
                height = heightSize;
            } else {
                if (hasCanvasImg != -1) {
                    height = bitmap.getHeight();
                } else {
                    height = 500;
                }
            }
            //重新设置view的宽高
            setMeasuredDimension(width, height);
    
            //设置画布以及画笔
            newPaint();
        }
        private void newPaint() {
            //根据参数创建一个新的bitmap  最后一个参数为为储存形式
            newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            //保存bitmap中所有像素点的数组  
            bmPixels = new int[newBitmap.getWidth() * newBitmap.getHeight()];
            //new带参的Canvas,其中的bitmap参数 必须通过createBitmap得到;
            //否则会报错:IllegalStateException : Immutable bitmap passed to Canvas constructor
            canvas = new Canvas(newBitmap);
            if (hasCanvasImg == -1) {
                //如果没有设置图片,则默认用灰色覆盖
                canvas.drawColor(Color.GRAY);
            } else {
                //把设置的图片缩放到view大小
                bitmap = zoomBitmap(this.bitmap, width, height);
                canvas.drawBitmap(bitmap, 0, 0, null);
            }
            // 准备绘制刮卡线条的画笔
            paint = new Paint();
            paint.setColor(paintColor);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(paintWidth);
            //设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢
            paint.setAntiAlias(true);
            //设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰
            paint.setDither(true);
            //当设置画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式
            paint.setStrokeCap(Paint.Cap.ROUND);
            //设置绘制时各图形的结合方式
            paint.setStrokeJoin(Paint.Join.ROUND);
            //设置图形重叠时的处理方式
            /**
             * SRC_IN:取两层绘制交集。显示上层
             */
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        }
    
        //这个onDraw方法只有一句代码,意思是在手指移动的同时把画板图片绘制出来
        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawBitmap(newBitmap, 0, 0, null);
            super.onDraw(canvas);
        }
    
        //将指定图片缩放到指定宽高,返回新的图片Bitmap对象
        public static Bitmap zoomBitmap(Bitmap bm, int newWidth, int newHeight) {
            // 获得图片的宽高
            int width = bm.getWidth();
            int height = bm.getHeight();
            // 计算缩放比例
            float scaleWidth = ((float) newWidth) / width;
            float scaleHeight = ((float) newHeight) / height;
            // 取得想要缩放的matrix参数
            Matrix matrix = new Matrix();
            matrix.postScale(scaleWidth, scaleHeight);
            // 得到新的图片
            return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
        }
    
    • 这是一堆对于这个view来说比较复杂的代码,但是功能很简单,我们做了2件事:
      1.通过MeasureSpec.getMode(测量模式),计算出整个控件的宽高
      2.通过canvas.drawBitmap在画布上画出bitmap,同时 new 出画笔 Paint 给它设置颜色,粗细等属性
    • 注意:
      1.onDraw()方法在每次调用invalidate(),或者视图变化时都会重走,所以不能在里面 new 东西.
      2.有一个int[]类型的数组 bmPixels,这里大概说一下是个什么意思,具体的解释在Bitmap类getPixels和createBitmap方法详解中有说道。
      bmPixels: 我们通过bitmap的宽度乘以高度,可以的到一个int[]类型的数组,这个数组就是组成bitmap的所有像素点,某一个像素点为0的时候就说明他是没有颜色,!0就说明是有颜色的。

    既然是画图,那肯定要监听手指移动,onTouchEvent()方法:

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int currX = (int) event.getX();
            int currY = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //按下时,设置线条的起始点准备绘制
                    path.moveTo(currX, currY);
                    break;
                case MotionEvent.ACTION_MOVE:
                    //滑动时,绘制路径
                    path.lineTo(currX, currY);
                    break;
                case MotionEvent.ACTION_UP:
            }
            // 绘制线条,请求重绘整个控件
            canvas.drawPath(path, paint);
            //请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。
            invalidate();
            return true;
        }
    

    这个就很简单,手指按下时记录位置,path.moveTo给path设置起始点位置,移动时通过path.lineTo()方法记录路径,同时使用 canvas.drawPath(path, paint)直接绘制出来,invalidate()通知视图更新。

    写到这里,在xml布局中使用这个view,已经能画一画了

    我们的画笔Paint类,可以指定颜色,粗细,模式,等等,这样我们就可以写一些公开的方法,给它动态的设置这些属性,从而让画笔更加多样性。

        //设置画笔颜色
        public void setPaintColor(int color) {
            //path = new Path();
               path.reset();
            paint.setColor(color);
        }
    
        //设置画笔类型
        public void setPaintMode(int style) {
            //path = new Path();
               path.reset();
            /**
             * SRC_IN:取两层交集部分,显示上层
             * DST_OUT:取两层非交集部分,显示下层
             */
            if (style == 1) {
                paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
            } else {
                paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
            }
            resetCanvaas();
        }
    
        //设置画布重置
        public void resetCanvaas() {
            //path = new Path();
               path.reset();
            canvas.drawBitmap(bitmap, 0, 0, null);
            invalidate();
            listener.bitmapChangeListener(bitmap);
        }
    

    上面代码 设置画笔颜色 ,设置画笔类型以及画布重置为什么都要new Path呢,因为如果不新开一个路径给画笔,当你设置了新的颜色,用的还是以前的Path,画笔就会把以前的Path也重新设置新颜色,而不是保持原来的颜色。

    • 这样就会出现一个问题,每次都在new Path,new一次创建一次,占用一次内存,想到一些避免方法,但是本文画图不是重点,就不在论述。(已改用path.reset())

    效果中的右上角,显示了一个float类型的数,它是在刮刮卡模式下,已经抹掉部分所占bitmap的比例,onMeasure()方法中有一个int[]类型的数组 bmPixels ,这个时候我们就要利用这个数组来得到这个比例。

    右上角抹去比例.jpg

    在onTouchEvent()方法的case MotionEvent.ACTION_UP加上一些代码:

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int currX = (int) event.getX();
            int currY = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //按下时,设置线条的起始点准备绘制
                    path.moveTo(currX, currY);
                    break;
                case MotionEvent.ACTION_MOVE:
                    //滑动时,绘制路径
                    path.lineTo(currX, currY);
                    //通过回调,实时把bitmap显示出去
                    listener.bitmapChangeListener(newBitmap);
                    break;
                case MotionEvent.ACTION_UP:
                    //抬起手指时,计算图片抹去了多少
                    int nullPixel = 0;
                    newBitmap.getPixels(bmPixels, 0, width, 0, 0, width, height);
                    for (int i = 0; i < bmPixels.length; i++) {
                        //抹去部分的像素点在数组中就会表示为0,找出为0的个数
                        if (bmPixels[i] == 0) {
                            nullPixel++;
                        }
                    }
                    //计算抹去部分所占的百分比
                    listener.showBitmapClear((float) nullPixel / (float) bmPixels.length);
                    break;
            }
            // 绘制线条,请求重绘整个控件
            canvas.drawPath(path, paint);
            //请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。
            invalidate();
            return true;
        }
    

    有一句 newBitmap.getPixels(bmPixels, 0, width, 0, 0, width, height);在getPixels方法详解中有解释,它的作用就是把newBitmap 中所有的像素点全部取出来,放到方法中的第一个参数bmPixels中。这个时候,我们再通过for循环遍历bmPixels数组,等于0的说明是没有颜色被抹掉的,统计他们的数量,计算他们所占的比例,就能算出抹掉的比例。同理我们也可以改变等于0这个判断条件,让他等于其他颜色,这样也就可以计算其他颜色所占比例。

    写个回调接口,在代码中取出来就OK了。

        //回调接口
        public interface bitmapListener {
            //实时的把绘制的bitmap显示在imageview 上
            void bitmapChangeListener(Bitmap bitmap);
            //显示抹掉比例
            void showBitmapClear(float clear);
        }
    
        public void addBitmapListener(bitmapListener bitmapListener) {
            this.listener = bitmapListener;
        }
    

    有2个接口,一个实时的展示bitmap,一个展示抹去比例。

    下周应该是把Bitmap类getPixels和createBitmap方法详解写完。

    对于生活理想,应该像宗教徒对待宗教一样充满虔诚与热情!

    相关文章

      网友评论

        本文标题:Android 自定义view:画图板实现

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