简单自定义View 的实现

开始自定义View 的实现

我们自定义view 需要重写其中的两个方法:
onMeasure() 是测量当前View 的尺寸
onDraw() 负责把这个View 绘制出来

还得重写至少两个函数:

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

onMeasure

自定义View 的时候首先需要测量一下这个View 的尺寸。为什么要测量呢?首先我们来看google在xml 中给我们了两个
属性layout_widhtlayout_height并且还有还有两个值:match_parentwrap_content 一个是包裹内容,一个自适应。但是这两个属性值并没有给我们具体的数值,但是在屏幕上显示东西必须得有数值,所以只能我们来计算一下。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

乍一看参数中的这两个东西是什么鬼?看着无非是宽高呀?其实这个里面不只是有宽高,还有测量模式。那么一定会有人问什么测量模式是什么东西?这个我们稍后讲解。我们知道,我们在设置宽高时有3个选择:wrap_contentmatch_parent以及指定固定尺寸,而测量模式也有3种:UNSPECIFIEDEXACTLYAT_MOST。可以看出来我们设置的无非就是这三种。
如果使用二进制数据保存,2个bit 就可以搞定了,因为两个bit 的取值范围是[0,3] 里面四个数据完全够使用的了。我们知道int型数据占用32个bit,而google实现的是,将int数据的前面2个bit用于区分不同的布局模式,后面30个bit存放的是尺寸的数据。
那么我们怎么从里面取出来测量模式与尺寸呢?

       int mode = MeasureSpec.getMode(measureSpec);
       int size = MeasureSpec.getSize(measureSpec);

这里的尺寸并不是最终的大小,否则测量模式又有什么用呢。请看下面

MeasureSpec 的mode 解释

MeasureSpec 的测量模式mode 一共有三种分别是:
MeasureSpec.UNSPECIFIED 这个表示 父容器没有对当前View有任何限制,当前View可以任意取尺寸也就是我们自定义的 大小
MeasureSpec.AT_MOST 可以用wrap_content 表示,是当前View能取的最大尺寸,使用系统测量出来的大小
MeasureSpec.EXACTLY 可以用 match_parent 表示,当前View应该取的尺寸,使用系统测量出来的大小

由此可知测量模式的具体用法。

动手写写 onMeasure 函数

假设我们要实现一个正方形的View 那么该怎么写呢? 首先宽高需要一样,并且默认的宽高是 100dp 。


  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      int width = getMySize(defaultSize, widthMeasureSpec);
      int height = getMySize(defaultSize, heightMeasureSpec);

      if (width < height) {
          height = width;
      } else {
          width = height;
      }


      setMeasuredDimension(width, height);


  }
  /**
   * 获取View尺寸大小
   * @param defaultSize  默认大小
   * @param measureSpec 
   * @return
   */
  private int getMySize(int defaultSize, int measureSpec) {

      int mySize = defaultSize;

      int mode = MeasureSpec.getMode(measureSpec);
      //系统测量的大小
      int size = MeasureSpec.getSize(measureSpec);

      switch (mode) {
          case MeasureSpec.UNSPECIFIED:  //没有设置大小就用默认的大小
              mySize = defaultSize;
              break;
          case MeasureSpec.AT_MOST:  // wrap_content 自适应大小,取测量出来的数据
              mySize = size;
              break;
          case MeasureSpec.EXACTLY:  //match_parent或者设置了默认的大小
              mySize = size;
              break;
      }

      return mySize;

  }

看我们的布局

<com.stevefat.myapplication.MyView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@color/colorAccent"/>

最终效果图
简单自定义View 的实现
不重写onMeasure,效果则是如下:
简单自定义View 的实现

重写onDraw()

我们已经学会了如何测量View 的尺寸,那么我们来结合实际画一个圆来实操一下:

	@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //设置圆的的半径,
        int r = getMeasuredWidth()/2; 

        //圆心的横坐标为左起加上半径
        int centenX = r+getLeft();
        //圆心的纵坐标为头部加上半径
        int centexY= r+getTop();
        
        //定义画笔
        Paint paint = new Paint();
        //画笔设置颜色
        paint.setColor(Color.GREEN);

        canvas.drawCircle(centenX,centexY,r,paint);

    }

效果如下简单自定义View 的实现

自定义属性

有时候我们需要灵活运用,不在代码中写死,这时候我们就需要自定义属性了
res/values/styles.xml 中 声明我们的自定义属性

<resources>
   <!--name为声明的"属性集合"名,最好是设置为跟我们的View一样的名称-->
    <declare-styleable name="MyView">
         <!--声明我们的属性,名称为default_size,取值类型为尺寸类型(dp,px等)-->
        <attr name="default_size" format="dimension"/>
    </declare-styleable>
</resources>

接下来在布局中运用起来

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.stevefat.myapplication.MyView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:default_size="100dp"
        android:background="@color/colorAccent"/>



</LinearLayout>

注意:需要在根标签(LinearLayout)里面设定命名空间,命名空间名称可以随便取,比如hc,命名空间后面取得值是固定的:"http://schemas.android.com/apk/res-auto"
最后在我们的类里面取出来自定义的属性

 	private int defaultSize;
 	
    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        
        //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
        //即属性集合的标签,在R文件中名称为R.styleable+name
        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.MyView);
        
         //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
        //第二个参数为,如果没有设置这个属性,则设置的默认的值
        defaultSize = a.getDimensionPixelSize(R.styleable.MyView_default_size,100);
        
         //最后记得将TypedArray对象回收
        a.recycle();      

    }

最后附上完整的自定义View

package com.stevefat.myapplication;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

public class MyView extends View {

    private int defaultSize;


    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }


    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
        //即属性集合的标签,在R文件中名称为R.styleable+name
        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.MyView);

        //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
        //第二个参数为,如果没有设置这个属性,则设置的默认的值
        defaultSize = a.getDimensionPixelSize(R.styleable.MyView_default_size,100);

        //最后记得将TypedArray对象回收
        a.recycle();

    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMySize(defaultSize, widthMeasureSpec);
        int height = getMySize(defaultSize, heightMeasureSpec);

        if (width < height) {
            height = width;
        } else {
            width = height;
        }


        setMeasuredDimension(width, height);


    }
    /**
     * 获取View尺寸大小
     * @param defaultSize  默认大小
     * @param measureSpec
     * @return
     */
    private int getMySize(int defaultSize, int measureSpec) {

        int mySize = defaultSize;

        int mode = MeasureSpec.getMode(measureSpec);
        //系统测量的大小
        int size = MeasureSpec.getSize(measureSpec);

        switch (mode) {
            case MeasureSpec.UNSPECIFIED:  //没有设置大小就用默认的大小
                mySize = defaultSize;
                break;
            case MeasureSpec.AT_MOST:  // wrap_content 自适应大小,取测量出来的数据
                mySize = size;
                break;
            case MeasureSpec.EXACTLY:  //match_parent或者设置了默认的大小
                mySize = size;
                break;
        }

        return mySize;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //设置圆的的半径,
        int r = getMeasuredWidth()/2;

        //圆心的横坐标为左起加上半径
        int centenX = r+getLeft();
        //圆心的纵坐标为头部加上半径
        int centexY= r+getTop();

        //定义画笔
        Paint paint = new Paint();
        //画笔设置颜色
        paint.setColor(Color.GREEN);

        canvas.drawCircle(centenX,centexY,r,paint);

    }
}

自定义ViewGroup 布局

自定义View 很简单,但是自定义ViewGroup 可就不一样了,不仅要兼顾自身,还要考虑子View的设置。ViewGroup 是个容器,里面存放了很多子View。怎么设计一个ViewGroup 的容器呢?

1.首先需要知道每个View 的大小,只有知道子View 的大小才知道ViewGroup 需要多大才能包容他们
2.根据子View 的大小,以及ViewGroup 要实现的功能,来巨鼎ViewGroup 的大小
3.ViewGroup的大小计算出来了,那么就需要按照自己想要的格式去摆放了。
4.知道怎么摆放还不行,摆放的位置决定了要把已有的空间划分成一个一个的的空间,然后根据摆放规则对号入座。

简单写一个案例,参照Linearlayout 从上到下摆放
首先重新onMeasureSpec,实现测量子View 以及设置ViewGroup的大小

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //将所有的子View进行测量,这会触发每个子View的onMeasure函数
        //注意要与measureChild区分,measureChild是对单个view进行测量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //获取子View 的总数
        int childCount = getChildCount();

        //如果没有子View,当前ViewGroup 没有存在的意义,不占用空间
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else {
            //如果宽高都是包裹内容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我们将高度设置为所有子View的高度总,宽度设置为子View 最大的宽度
                int heightTotal = getHeightTotal();
                int widthMax = getwidthMax();
                setMeasuredDimension(widthMax, heightTotal);
            } else if (heightMode == MeasureSpec.AT_MOST) { //如果只有高度是包裹内容
                //宽度设置为测量的宽度,高度为所有子view 的综合
                setMeasuredDimension(widthSize, getHeightTotal());
            } else if (widthMode == MeasureSpec.AT_MOST) {  //如果只有宽度是包裹内容
                //宽度设置为最宽的那个,高度设置为测量的高度
                setMeasuredDimension(getwidthMax(), heightSize);
            }

        }

    }

    /**
     * 获取子View的高度总和
     *
     * @return
     */
    private int getHeightTotal() {
        int childCount = getChildCount();
        int heightTotal = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            heightTotal += childView.getMeasuredHeight();
        }
        return heightTotal;
    }

    /**
     * 获取最宽的那个view的宽度
     *
     * @return
     */
    private int getwidthMax() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth) {
                maxWidth = childView.getMeasuredWidth();
            }
        }
        return maxWidth;

    }

以上代码功能可以看注释,不在解释了,子view 都已经测量好了,下面就是开始摆放子View 的位置了

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        //获取子view 的总数
        int childCount = getChildCount();
        //记录当前的高度位置
        int currentHeight= top;
        //将子View 逐一摆放
        for (int i = 0; i < childCount; i++) {
            View  childView = getChildAt(i);
            int height= childView.getMeasuredHeight();
            int width = childView.getMeasuredWidth();
            //设置子View 的位置,左 上 右 下
            childView.layout(left,currentHeight,left+width,currentHeight+height);

            currentHeight +=height;
        }


    }

我们来测试一下,向容器里面摆放三个Button 试试看效果。

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.stevefat.myapplication.MyViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent">

        <Button
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="btn1" />

        <Button
            android:layout_width="180dp"
            android:layout_height="wrap_content"
            android:text="btn2" />

        <Button
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:text="btn3" />


    </com.stevefat.myapplication.MyViewGroup>


</LinearLayout>

最后效果图
简单自定义View 的实现

至此我们已经实现了LinnerLayout 的效果。
附上完整代码:

package com.stevefat.myapplication;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * 实现一个LinerLayout 的布局
 */
public class MyViewGroup extends ViewGroup {

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        //获取子view 的总数
        int childCount = getChildCount();
        //记录当前的高度位置
        int currentHeight= top;
        //将子View 逐一摆放
        for (int i = 0; i < childCount; i++) {
            View  childView = getChildAt(i);
            int height= childView.getMeasuredHeight();
            int width = childView.getMeasuredWidth();
            //设置子View 的位置,左 上 右 下
            childView.layout(left,currentHeight,left+width,currentHeight+height);

            currentHeight +=height;
        }


    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //将所有的子View进行测量,这会触发每个子View的onMeasure函数
        //注意要与measureChild区分,measureChild是对单个view进行测量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //获取子View 的总数
        int childCount = getChildCount();

        //如果没有子View,当前ViewGroup 没有存在的意义,不占用空间
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else {
            //如果宽高都是包裹内容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我们将高度设置为所有子View的高度总,宽度设置为子View 最大的宽度
                int heightTotal = getHeightTotal();
                int widthMax = getwidthMax();
                setMeasuredDimension(widthMax, heightTotal);
            } else if (heightMode == MeasureSpec.AT_MOST) { //如果只有高度是包裹内容
                //宽度设置为测量的宽度,高度为所有子view 的综合
                setMeasuredDimension(widthSize, getHeightTotal());
            } else if (widthMode == MeasureSpec.AT_MOST) {  //如果只有宽度是包裹内容
                //宽度设置为最宽的那个,高度设置为测量的高度
                setMeasuredDimension(getwidthMax(), heightSize);
            }

        }

    }

    /**
     * 获取子View的高度总和
     *
     * @return
     */
    private int getHeightTotal() {
        int childCount = getChildCount();
        int heightTotal = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            heightTotal += childView.getMeasuredHeight();
        }
        return heightTotal;
    }

    /**
     * 获取最宽的那个view的宽度
     *
     * @return
     */
    private int getwidthMax() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth) {
                maxWidth = childView.getMeasuredWidth();
            }
        }
        return maxWidth;

    }
}