Android 自定义 View 三步骤

自定义 View 三步骤

自定义View三步骤,即:onMeasure()(测量),onLayout()(布局),onDraw()(绘制)。

onMeasure()

首先我们需要弄清楚,自定义 View 为什么需要重新测量。正常情况下,我们直接在 XML 布局文件中定义好 View 的宽高,然后让自定义 View 在此宽高的区域内显示即可。但是为了更好地兼容不同尺寸的屏幕,Android 系统提供了 wrap_content 和 match_parent 属性来规范控件的显示规则。它们分别代表自适应大小和填充父视图的大小,但是这两个属性并没有指定具体的大小,因此我们需要在 onMeasure 方法中过滤出这两种情况,真正的测量出自定义 View 应该显示的宽高大小。

    /**
     * 测量
     * @param widthMeasureSpec 包含测量模式和宽度信息
     * @param heightMeasureSpec 包含测量模式和高度信息
     * int型数据,采用二进制,占32个bit。其中前2个bit为测量模式。后30个bit为测量数据(尺寸大小)。
     * 这里测量出的尺寸大小,并不是View的最终大小,而是父View提供的参考大小。
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.e("TAG","onMeasure()");

    }

MeasureSpec:

  • 测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。只是测量宽高,不一定等于实际宽高。
  • MeasureSpec代表一个32位int值(避免过多的对象内存分配),高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)。并提供了打包和解包方法。
SpecMode 说明
UNSPECIFIED 父容器没有对当前 View 有任何限制,当前 View 可以取任意尺寸,比如 ListView 中的 item。这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY 父容器已检测出View所需要的精确大小,就是SpecSize所指定的值。它对应于LayoutParams中的Match_parent和具体数值这两种模式。
AT_MOST 父容器指定SpecSize,View不能大于这个值。它对应于LayoutParams中的wrap_content。

MeasureSpec和LayoutParams的对应关系:

  • 在测量时,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,再根据MeasureSpec来确定View测量后宽高。(需要注意的是,决定MeasureSpec的有两点。即LayoutParams和父容器约束)
  • 对于顶级View(DecorView)和普通View,MeasureSpec的转换过程略有不同。除了自身的LayoutParams这点,前者由窗口的尺寸,后者由父容器的MeasureSpec来约束决定。MeasureSpec一定确定,onMeasure中就可以确定View的测量宽高。

当继承 View 或 ViewGroup 时,如果没有复写 onMeasure 方法时,默认使用父类也就是 View 中的实现,View 中的 onMeasure 默认实现如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   
  // setMeasuredDimension 是一个非常重要的方法,这个方法传入的值直接决定 View 的宽高,也就是说如果调用 setMeasuredDimension(100,200),最终 View 就显示宽 100 * 高 200 的矩形范围。
  // getDefaultSize 返回的是默认大小,默认为父视图的剩余可用空间。
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

查看 setMeasuredDimension 方法。其它现有控件的 onMeasure 方法的 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 经过一系列计算,最后也是调用到 setMeasuredDimension 方法。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
   
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
   
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

一种情况是:在 XML 中指定的是 wrap_content,但是实际使用的宽高值却是父视图的剩余可用空间,从 getDefaultSize 方法中可以看出是整个屏幕的宽高。解决方法只要复写 onMeasure,过滤出 wrap_content 的情况,并主动调用 setMeasuredDimension 方法设置正确的宽高即可:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        // 判断是 wrap_content 的测量模式
        if (MeasureSpec.AT_MOST == widthMode || MeasureSpec.AT_MOST == heightMode){
   
            int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
            int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
//            int size = measuredWidth > measuredHeight ? measuredHeight : measuredWidth;
            // 将宽高设置为传入宽高的最小值
            int size = Math.min(measuredWidth, measuredHeight);
            // 设置 View 实际大小
            setMeasuredDimension(size,size);
        }
    }

ViewGroup 中的 onMeasure

如果自定义的控件是一个容器,onMeasure 方法会更加复杂一些。因为 ViewGroup 在测量自己的宽高之前,需要先确定其内部子 View 的所占大小,然后才能确定自己的大小。比如 LinearLayout 的宽高为 wrap_content 表示由子控件的大小决定,那 LinearLayout 的最终宽度由其内部最大的子 View 宽度决定。

onLayout()

    /**
     * 布局
     * @param changed 当前View的大小和位置改变了
     * @param left 左部位置(相对于父视图)
     * @param top 顶部位置(相对于父视图)
     * @param right 右部位置(相对于父视图)
     * @param bottom 底部位置(相对于父视图)
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
   
        super.onLayout(changed, left, top, right, bottom);
        Log.e("TAG","onLayout()");
        // 一般在自定义ViewGroup时使用,来定义子View的位置。

    }

这里扩展一些View位置相关知识点:

  • View的位置参数:
    由View的四个属性决定;left(左上角横坐标),right(右下角横坐标),top(左上角纵坐标),bottom(右下角纵坐标)。是一种相对坐标,相对父容器。
    四个参数对应View源码中的mLeft等四个成员变量,通过getLeft()等方法来获取。
  • View的宽高和坐标的关系:
    width=right-left;
    height=bottom-top;
  • 从Android3.0开始,新增额外的四个参数:
    x,y,translationX,translationY。前两者是View左上角坐标,后两者是View左上角相对于父容器的偏移量,并且默认值0。和四个基本位置参数一样,也提供了get/set方法。
  • 换算关系如下;
    x=left+translationX;
    y=top+translationY;
    注意;在View平移过程中,top和left表示的是原始左上角的位置信息,值并不会改变。发生改变的是;x,y,translationX,translationY这四个参数。

它是一个抽象方法,也就是说每一个自定义 ViewGroup 都必须主动实现如何排布子 View,具体就是遍历每一个子 View,调用 child.(l, t, r, b) 方法来为每个子 View 设置具体的布局位置。四个参数分别代表左上右下的坐标位置,一个简易的 FlowLayout 实现如下:

在大多数 App 的搜索界面经常会使用 FlowLayout 来展示历史搜索记录或者热门搜索项。
FlowLayout 的每一行上的 item 个数不一定,当每行的 item 累计宽度超过可用总宽度,则需要重启一行摆放 item 项。因此我们需要在 onMeasure 方法中主动的分行计算出 FlowLayout 的最终高度,如下所示:

public class FlowLayout extends ViewGroup {
   

    //存放容器中所有的View
    private List<List<View>> mAllViews = new ArrayList<List<View>>();
    //存放每一行最高View的高度
    private List<Integer> mPerLineMaxHeight = new ArrayList<>();

    public FlowLayout(Context context) {
   
        super(context);
    }

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

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

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
   
        super.generateLayoutParams(p);
        return new MarginLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
   
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
   
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    /**
    * 测量控件的宽和高
    *
    * onMeasure 方法的主要目的有 2 个:
    * 1.调用 measureChild 方法递归测量子 View;
    * 2.通过叠加每一行的高度,计算出最终 FlowLayout 的最终高度 totalHeight。
    */ 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获得宽高的测量模式和测量值
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //获得容器中子View的个数
        int childCount = getChildCount();
        //记录每一行View的总宽度
        int totalLineWidth = 0;
        //记录每一行最高View的高度
        int perLineMaxHeight = 0;
        //记录当前ViewGroup的总高度
        int totalHeight = 0;
        for (int i = 0; i < childCount; i++) {
   
            View childView = getChildAt(i);
            //对子View进行测量
            measureChild(childView, widthMeasureSpec
  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值