Android 自定义view之画图板实现方法

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

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

自定义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对于参数提示的新特性

通过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就会有一个刮刮卡的效果

/**
   * 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 ,这个时候我们就要利用这个数组来得到这个比例。

在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,一个展示抹去比例。

(0)

相关推荐

  • Android 自定义view之画图板实现方法

    看效果: 中间一个画图板 上方小控件用来显示实时画出的图形 下方小控件用来做一些画图的控制 2个小控件都能移动 顺带还有一个刮刮卡效果,只需要改一个参数: 自定义view首先要自定义属性: 在values下面创建attrs.xml: <!--画图板--> <declare-styleable name="DrawImg"> <attr name="PaintColor" /> //画笔颜色 <attr name="

  • Android自定义View绘制贝塞尔曲线的方法

    本文实例为大家分享了Android自定义View绘制贝塞尔曲线的具体代码,供大家参考,具体内容如下 在平面内任选 3 个不共线的点,依次用线段连接. 在第一条线段上任选一个点 D.计算该点到线段起点的距离 AD,与该线段总长 AB 的比例. 根据上一步得到的比例,从第二条线段上找出对应的点 E,使得 AD:AB = BE:BC. 连接这两点 DE. 从新的线段 DE 上再次找出相同比例的点 F,使得 DF:DE = AD:AB = BE:BC. 到这里,我们就确定了贝塞尔曲线上的一个点 F.接下

  • Android自定义View实现绘制虚线的方法详解

    前言 说实话当第一次看到这个需求的时候,第一反应就是Canvas只有drawLine方法,并没有drawDashLine方法啊!这咋整啊,难道要我自己做个遍历不断的drawLine?不到1秒,我就放弃这个想法了,因为太恶心了.方法肯定是有的,只不过我不知道而已. 绘制方法 最简单的方法是利用ShapeDrawable,比如说你想用虚线要隔开两个控件,就可以在这两个控件中加个View,然后给它个虚线背景. 嗯,理论上就是这样子的,实现上也很简单. <!-- drawable 文件 --> <

  • Android自定义View绘制贝塞尔曲线中小红点的方法

    目录 前言 需求 效果图 代码 主要问题 简单画法 使用贝塞尔曲线 前言 上一篇文章用扇形图练习了一下安卓的多点触控,实现了单指旋转.二指放大.三指移动,四指以上同时按下进行复位的功能.今天这篇文章用很多应用常见的小红点,来练习一下贝塞尔曲线的使用. 需求 这里想法来自QQ的拖动小红点取消显示聊天条数功能,不过好像是记忆里的了,现在看了下好像效果变了.总而言之,就是一个小圆点,拖动的时候变成水滴状,超过一定范围后触发消失回调,核心思想如下: 1.一个正方形view,中间是小红点,小红点距离边框有

  • Android自定义View绘制的方法及过程(二)

    上一篇<Android 自定义View(一) Paint.Rect.Canvas介绍>讲了最基础的如何自定义一个View,以及View用到的一些工具类.下面讲下View绘制的方法及过程 public class MyView extends View { private String TAG = "--------MyView"; private int width, height; public MyView(Context context, AttributeSet a

  • Android自定义View画圆功能

    本文实例为大家分享了Android自定义View画圆的具体代码,供大家参考,具体内容如下 引入布局 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools&q

  • Android 自定义View实现单击和双击事件的方法

    自定义View, 1. 自定义一个Runnable线程TouchEventCountThread ,  用来统计500ms内的点击次数 2. 在MyView中的 onTouchEvent 中调用 上面的线程 3. 自定义一个Handler, 在TouchEventHandler 中 处理 统计到的点击事件, 单击, 双击, 三击, 都可以处理 核心代码如下: public class MyView extends View { ...... // 统计500ms内的点击次数 TouchEvent

  • Android自定义View的实现方法实例详解

    一.自绘控件 下面我们准备来自定义一个计数器View,这个View可以响应用户的点击事件,并自动记录一共点击了多少次.新建一个CounterView继承自View,代码如下所示: 可以看到,首先我们在CounterView的构造函数中初始化了一些数据,并给这个View的本身注册了点击事件,这样当CounterView被点击的时候,onClick()方法就会得到调用.而onClick()方法中的逻辑就更加简单了,只是对mCount这个计数器加1,然后调用invalidate()方法.通过 Andr

  • Android自定义view之利用drawArc方法实现动态效果(思路详解)

    目录 前言 一.准备 1.测量 2.初始化画笔 3.自定义属性 二.关键方法介绍 drawArc 三.实现 1.思路 2.效果图 前言 前几天看了一位字节Android工程师的一篇博客,他实现的是歌词上下滚动的效果,实现的关键就是定义一个偏移量,然后根据情况去修改这个值,最后触发View的重绘来达到效果.于是今天根据这个思路来写一篇简单的文章.欢迎留言 一.准备 在这之前呢,还是得简单描述一下自定义view中的一些准备工作 1.测量 @Override protected void onSize

  • Android自定义view绘制表格的方法

    本文实例为大家分享了Android自定义view绘制表格的具体代码,供大家参考,具体内容如下 先上效果图 平时很少有这样的表格需求,不过第一想法就是自定义view绘制表格,事实上我确实是用的canvas来绘制的,整个过程看似复杂,实为简单,计算好各个点的坐标后事情就完成一半了.不废话show code import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; imp

随机推荐