Android自定义控件学习笔记三

概要

本文主要介绍自定义控件中常用的onMeasure方法,简要分析了一下measure和onMeasure方法的递归调用过程,对View中MeasureSpec内部类进行了简单的概述,最后简单说明了一下getHeight与getMeasuredHeight方法的区别。

Android自定义控件学习笔记一Android自定义控件学习笔记二写的时间比较早一些,今天又补上一篇,也许因为是一个学习时的记录笔记吧,所以写文章更显得是随性而为。我们知道如果要实现一个自定义控件,必须要深刻理解三个方法:onMeasure(),onLayout(),onDraw()。今天主要讲述的就是measure和onMeasure方法。

我们知道在Layout.xml文件中layout_width和layout_height属性设置的宽高并不是视图本身的大小,而是父视图给该View设置的“窗口”大小,这就是为什么这个属性是以“layout_”为前缀,而不是直接使用width和height的原因。该属性是一个相对值,如WRAP_CONTENT、MATCH_CONTENT,也可以是一个具体的值,如100dp。因此我们平常所说的视图大小或者某一个控件的大小,实际上是父视图为子视图分配的“窗口”的大小,更确切的讲应该是视图的布局(layout)的大小,View类内部用两个变量measuredWidth和measuredHeight保存其值。如果有人看过源码就应该知道了,View内部不是有mLeft、mRight、mTop、mBottom四个变量吗,事实上这四个变量指的是该View在父视图所占的区域,mRight-mLeft的大小一般就是measuredWidth的大小,mBottom-mTop一般就是measuredHeight的大小,细心地人应该发现了这里所说的是一般,而不是就是。

measure过程的本质就是把视图布局时使用的“相对值”转换为具体值的过程,即把WRAP_CONTENT及MATCH_PARENT转换为具体的值。

measure()方法

measure

一般我们的一个布局的根布局都是一个ViewGroup,如果ViewGroup没有重载onMeasure(),则会执行View中默认的onMeasure()。一般情况下程序员需要在重载onMeasure()函数中逐一对所包含的子视图进行measure操作,但是ViewGroup类内部提供了measureChildWidthMargins()对下一层的子视图进行measure操作;如果子视图是一个具体的View实例,则在重载onMeasure()函数内部不需要再次调用measureChildWidthMargins(),从而一次measure()过程结束。

上述过程是View系统定义的一个框架模型,在这个模型中,ViewGroup类是一个抽象类,应用程序必须实现一个具体的ViewGroup实例。在该实例中,程序员调用measureChildWidthMargins()对下一层子视图进行measure操作,但是这不是必须的,因为measure的结果仅仅是把layout_width和layout_height所设置 的相对值转换为具体值,这些值将在layout中辅助父视图进行操作布局。如果某个ViewGroup实例对子视图的布局不依赖于子视图的大小,那么就不需要对所包含的子视图进行measure操作。

View中measure()函数原型为:

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

在该函数定义中,final关键字说明,该函数是不可以被重载的,即View所定义的这个measure框架是不可更改的,本模块核心内容参考自《Android内核剖析》。

MeasureSpec

SDK说明如下:

A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.

MeasureSpc类封装了父View传递给子View的布局(layout)要求。每个MeasureSpc实例代表宽度或者高度(只能是其一)要求。 它有三种模式:

  • UNSPECIFIED(未指定)父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;
  • EXACTLY(完全)父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
  • AT_MOST(至多)子元素至多达到指定大小的值。

它常用的三个函数:

  • static int getMode(int measureSpec):根据提供的测量值(格式)提取模式(上述三个模式之一)
  • static int getSize(int measureSpec):根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)
  • static int makeMeasureSpec(int size,int mode):根据提供的大小值和模式创建一个测量值MeasureSpec (格式)

平常我们在xml布局文件中所设置的高宽的填充模式:MATCH_PARENT或者WRAP_CONTENT,或者是在LayoutParams设置的值,实际上就是对measure和onMeasure两个参数模式的设置:

  • 具体的值(如width=200dp)和MATCH_PARENT/FILL_PARENT,对应模式中的MeasureSpec.EXACTLY
  • 包裹内容(width=WRAP_CONTENT)则对应模式中的MeasureSpec.AT_MOST

系统调用measure方法,从父控件到子控件的MeasureSpec的传递是有一套对应的判断规则的,列表如下:

父View的MeasureSpec 子View的LayoutParams 子View的MeasureSpec
EXACTLY+size dip EXACTLY+dip
WRAP_CONTENT AT_MOST+size
MATCH_PARENT EXACTLY+dip
AT_MOST+size dip EXACTLY+dip
WRAP_CONTENT AT_MOST+size
MATCH_PARENT AT_MOST+size
UPSPECIFIED+size dip EXACTLY+dip
WRAP_CONTENT UPSPECIFIED+0
MATCH_PARENT UPSPECIFIED+0

一个view的宽高尺寸,只有在测量之后才能得到,也就是measure方法被调用之后。大家都应该使用过View.getWidth()和View.getHeight()方法,这两个方法可以返回view的宽和高,但是它们也不是在一开始就可以得到的,比如oncreate方法中,因为这时候measure方法还没有被执行,测量还没有完成,我们可以来作一个简单的实验:自定义一个MyView,继承View类,然后在OnCreate方法中,将其new出来,通过addview方法,添加到现在的布局中。然后调用MyView对象的getWidth()和getHeight()方法,会发现得到的都是0。

onMeasure方法

这里主要讲一下View中onMeasure方法:

在View中onMeasure方法非常简单里面就调用了一个函数,但是该函数setMeasuredDimension是必须的,我们可以看一下API中的描述:

This method must be called by onMeasure(int, int) to store the measured width and measured height. Failing to do so will trigger an exception at measurement time.

如果我们没有调用该方法就会抛出下面异常:

java.lang.IllegalStateException: onMeasure() did not set the measured dimension by calling setMeasuredDimension()

因此如果必须得重写onMeasure方法,如果不准备在代码中重置高宽都会在第一行加上super.onMeasure(widthMeasureSpec, heightMeasureSpec),上面我们已经分析过了,控件的高宽必须得measure过之后才可以获取,因此调用该在onMeasure方法之后我们都可以获取控件的高宽。如果我们想要重置宽高,只需要调用setMeasuredDimension(int measuredWidth, int measuredHeight),传入相对应得高measuredHeight宽measuredWidth就可以了。

其它

今天既然写的是有关View的measure的一些知识点,在平常开发中我们常常会用到动态获取一个控件View的宽高,而Android官方文档给我们提供了两个API,一个是getHeight()一个是getMeasuredHeight(),文章开始部分也说了View内部有mLeft、mRight、mTop、mBottom四个变量,事实上这四个变量指的是该View在父视图所占的区域,mRight-mLeft的大小一般就是measuredWidth的大小,而mBottom-mTop一般是控件的高,接下来我们就来看看为什么说是一般是控件的高宽,下面是View中getHeight()源码:

上面说到的getHeight确实是控件的高,但是它不一定是控件本身实际高宽,而是你能看到的可是区域的高宽,如果出现滚动条,这时候如果想要获取控件的高宽,该函数就不行了。

该方法获得的是原始的测量宽度。所以说 getMeasuredHeight()是对View上的内容进行测量后得到的View内容占据的高度。 前提是你必须在父布局中或者此View的onMeasure()后面的方法里调用measure(0,0);(measure中的参数的值你自己可以定义),否则你得到的结果和getHeight()得到的结果是一样的。

本文地址www.sunnyang.com/328.html

参考资料

Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一)

android中onMeasure初看,深入理解布局之一!

Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(下)

One comment

发表评论