onMeasure、onLayout 能够说是自定 View 的核心,可是不少开发者都没能理解其含义与做用,也不理解 onMeasure 、 xml 指定大小这两者的关系与差别,也不能区分 getMeasureWidth 与 getWidth 的本质区别又是什么。本文将经过理论加实践的方法带领你们深刻理解 onMeasure 、onLayout 的定义、流程、具体使用方法与须要注意的细节。php
对于每个 View:java
运行前,开发者会根据本身的需求在 xml 文件中写下对于 View 大小的指望值android
在运行的时候,父 View 会在 onMeaure()
中,根据开发者在 xml 中写的对子 View 的要求, 和自身的实际可用空间,得出对于子 View 的具体尺寸要求git
子 View 在本身的 onMeasure
中,根据 xml 中指定的指望值和自身特色(指 View 的定义者在onMeasrue
中的声明)算出本身的**指望*github
若是是 ViewGroup 还会在
onMeasure
中,调用每一个子 View 的 measure () 进行测量.面试
父 View 在子 View 计算出指望尺寸后,得出⼦ View 的实际尺⼨和位置canvas
⼦ View 在本身的 layout() ⽅法中将父 View 传进来的实际尺寸和位置保存app
若是是 ViewGroup,还会在 onLayout() ⾥调用每一个字 View 的 layout() 把它们的尺寸 置传给它们框架
measure 的测量过程可能不止一次,好比有三个子 View 在一个 ViewGroup 里面,ViewGroup 的宽度是 warp_content,A 的宽度是 match_parent, B 和 C 是 warp_content, 此时 ViewGroup 的宽度是不固定的,怎么肯定 A 的 match_parent 到底有多大呢?此时是如何测量的呢?ide
以 LinearLayout 为例:第一次测量 LinearLayout 的大小也是没有肯定的,因此没法肯定 A 的 match_parent 到底有多大,这时候的 LinearLayout 会对 A 直接测量为 0 ,而后测量 B、C 的宽度,由于 B、C 的大小是包裹内容的,在测量后就能够肯定 LinearLayout 的宽度了:即为最长的 B 的宽度。
这时候再对 A 进行第二次测量,直接设置为与 LinearLayout 相同的宽度,至此达到了 match_parent 的效果。
若是将 measure 和 layout 的过程糅合在一块儿,会致使两次测量的时候进行无用的 layout,消耗了更多的资源,因此为了性能,将其两者分开。
也是两者的职责相互独立,分为两个过程,可使流程、代码更加清晰。
上面例子中的状况仅仅存在于 LinearLayout中,每种布局的测量机制是不一样的。那么若是 A B C 三个 View 都是 match_parent LinearLayout 是如何作的呢?
第二轮测量:都没有大小,LinearLayout 会让全部子 View 自由测量(父 View 不限制宽度)。每一个测量以后都会变为和最宽的同样的宽度。
注意:
onMeasure 与 measure() 、onDraw 与 draw 的区别
onXX 方法是调度过程,而 measure、draw 才是真正作事情的。能够从源码中看到 measure 中调用了 onMeasure 方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ……………
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
// ………………
}
}
复制代码
为何不把对于尺寸的要求直接交个子 View 而是要交给父 View 呢?
由于有些场景子 View 的大小须要父 View 进行规划,例如上面的例子中 LinearLayout 的子 View 设置了 weight。
##onMeasure 方法
要明确的一个问题是: 何时须要咱们本身实现 onMeasure 方法呢?
答:具体开发的时候有如下三种场景:
onMeasure()
和 onLayout()
方法。例以下文中的「综合演练 —— 自定义 Layout」onLayout 方法是 ViewGroup 中用于控制子 View 位置的方法。放置子 View 位置的过程很简单,只需重写 onLayout 方法,而后获取子 View 的实例,调用子 View 的 layout 方法实现布局。在实际开发中,通常要配合 onMeasure 测量方法一块儿使用。在下文「综合演练 —— 自定义 Layout」中会详细演示。
/** * 自定义正方形 ImageView * * Created by im_dsd on 2019-08-24 */
public class SquareImageView extends android.support.v7.widget.AppCompatImageView {
public SquareImageView(Context context) {
super(context);
}
public SquareImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquareImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void layout(int l, int t, int r, int b) {
// 使用宽高的最大值设置边长
int width = r - l;
int height = b - t;
int size = Math.max(width, height);
super.layout(l, t, l + size, t + size);
}
}
复制代码
代码很简单,获取宽与高的最大值用于设置正方形 View 的边长。再看一下布局文件的设置
<?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" android:orientation="horizontal" tools:context=".MainActivity">
<com.example.dsd.demo.ui.custom.measure.SquareImageView android:background="@color/colorAccent" android:layout_width="200dp" android:layout_height="300dp"/>
<View android:background="@android:color/holo_blue_bright" android:layout_width="200dp" android:layout_height="200dp"/>
</LinearLayout>
复制代码
经过布局文件的描述若是是普通的 View 显示的状态应该是这样的
而咱们期待的状态应该是这样的:SquareImageView 的宽高均为 300dp。
可是最终的结果倒是下图,虽然咱们使用了 LinearLayout 可是咱们经过layout()
方法改变了 SquareImageView 的大小,对于这个变化LinearLayout 并不知道,因此会发生布局重叠的问题。可见通常状况下不要使用 layout()
方法。
onMeasure
方法更改尺寸。@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure 中已经完成了 View 的测量
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取测量的结果比较后得出最大值
int height = getMeasuredHeight();
int width = getMeasuredWidth();
int size = Math.max(width, height);
// 将结果设置回去
setMeasuredDimension(size, size);
}
复制代码
总结
简单来讲,更改已有 View 的尺寸主要分为如下步骤
onMeasure()
getMeasureWidth
和 getMeasureHeight()
获取测量尺寸setMeasuredDimension(width, height)
把结果保存此处用绘制圆形的 CircleView 作一个例子。对于这个 View 的指望是:View 的大小有内部的圆决定。
首先画一个圆形看看
/** * 自定义 View 简单测量 * Created by im_dsd on 2019-08-15 */
public class CircleView extends View {
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/** * 为了方便简单,固定尺寸 */
private static final float PADDING = DisplayUtils.dp2px(20);
private static final float RADIUS = DisplayUtils.dp2px(80);
public CircleView(Context context) {
super(context);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(Color.RED);
canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, mPaint);
}
}
复制代码
<com.example.dsd.demo.ui.custom.layout.CircleView
android:background="@android:color/background_dark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
复制代码
此时将大小设置为 wrap_content 包裹布局,结果会是怎么样的呢?
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 没有必要再让 view 本身测量一遍了,浪费资源
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 计算指望的 size
int size = (int) ((PADDING + RADIUS) * 2);
// 获取父 View 传递来的可用大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 开始计算
int result = 0;
switch (widthMode) {
// 不超过
case MeasureSpec.AT_MOST:
// 在 AT_MOST 模式下,取两者的最小值
if (widthSize < size) {
result = widthSize;
} else {
result = size;
}
break;
// 精准的
case MeasureSpec.EXACTLY:
// 父 View 给多少用多少
result = widthSize;
break;
// 无限大,没有指定大小
case MeasureSpec.UNSPECIFIED:
// 使用计算出的大小
result = size;
break;
default:
result = 0;
break;
}
// 设置大小
setMeasuredDimension(result, result);
}
复制代码
上面的代码就是 onMeasure(int,int)
的模板代码了,要注意一点的是须要注释 super.onMeasure
方法,此处面试的时候广泛会问。
// 没有必要再让 view 本身测量一遍了,浪费资源
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
复制代码
这段模版代码其实 Android SDK 里面早就有了很好的封装 : resolveSize(int size, int measureSpec)
和 resolveSizeAndState(int size, int measureSpec, int childMeasuredState)
,两行代码直接搞定。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 没有必要再让 view 本身测量一遍了,浪费资源
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 计算指望的 size
int size = (int) ((PADDING + RADIUS) * 2);
// 指按期望的 size
int width = resolveSize(size, widthMeasureSpec);
int height = resolveSize(size, heightMeasureSpec);
// 设置大小
setMeasuredDimension(width, height);
}
复制代码
使用的时候彻底能够这样作,可是很是建议你们都本身手写几遍理解其中的含义,由于面试会问到其中的细节。
还有一点很遗憾,就是 resolveSizeAndState(int, int, int)
很差用。很差用的缘由不是方法有问题,而是不少自定义 View 包括原生的 View 都没有使用 resolveSizeAndState(int, int, int)
方法,或者没用指定 sate (state 传递父 View 对于子 View 的指望,相比resolveSize(int, in)
方法对于子 View 的控制更好)因此就算设置了,也不会起做用。
总结
彻底自定义 View 的尺寸主要分为如下步骤:
onMeasure()
resolveSize()
或者 resolveSizeAndState()
修正结果setMeasuredDimension(width, height)
保存结果以 TagLayout 为例一步一步实现一个自定义 Layout。具体指望的效果以下图:
onLayout()
在继承 ViewGroup 的时候 onLayout()
是必需要实现的,这意味着子 View 的位置摆放的规则,所有交由开发者定义。
/** * 自定义 Layout Demo * * Created by im_dsd on 2019-08-11 */
public class TagLayout extends ViewGroup {
public TagLayout(Context context) {
super(context);
}
public TagLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 此时全部的子 View 都和 TagLayout 同样大
child.layout(l, t, r, b);
}
}
}
复制代码
实验一下是否和指望的效果同样呢
<?xml version="1.0" encoding="utf-8"?>
<com.example.dsd.demo.ui.custom.layout.TagLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dp" android:padding="5dp" android:background="#ffee00" android:textSize="16sp" android:textStyle="bold" android:text="音乐" />
</com.example.dsd.demo.ui.custom.layout.TagLayout>
复制代码
的确和指望一致。若是想要 TextView 显示为 TagLayout 的四分之一呢?
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); // 子 View 显示为 TagLayout 的 1/4 child.layout(l, t, r / 4, b / 4); } } 复制代码
效果达成!!!很明显onLayout
能够很是灵活的控制 View 的位置
再尝试让两个 View 呈对角线布局呢?
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (i == 0 ){ child.layout(0, 0, (r - l) / 2, (b - t) / 2); } else { child.layout((r - l) / 2, (b - t) / 2, (r - l), (b - t)); } } } 复制代码
onLayout
的方法仍是很简单的,可是在真正布局中怎么获取 View 的位置才是难点!如何获取呢,这时候就须要 onMeasure
的帮助了!
在写具体的代码以前,先来搭建大致的框架。主要的思路就是在 onMeasure()
方法中计算好子 View 的尺寸和位置信息包括 TagLayout 的具体尺寸,而后在onLayout()
方法中摆放子 View。
在计算过程当中涉及到三个难点,具体请看注释
private List<Rect> mChildRectList = new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 没有必要让 View 本身算了,浪费资源。
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 难点1: 计算出对于每一个子 View 的尺寸
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// 难点2:计算出每个子 View 的位置并保存。
Rect rect = new Rect(?, ?, ?, ?);
mChildRectList.add(rect);
}
// 难点3:根据全部子 View 的尺寸计算出 TagLayout 的尺寸
int measureWidth = ?;
int measureHeight = ?;
setMeasuredDimension(measureWidth, measureHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mChildRectList.size() == 0) {
return;
}
for (int i = 0; i < getChildCount(); i++) {
if (mChildRectList.size() <= i) {
return;
}
View child = getChildAt(i);
// 经过保存好的位置,设置子 View
Rect rect = mChildRectList.get(i);
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
复制代码
主要涉及两点:开发者对于子 View 的尺寸设置和父 View 的具体可用空间。获取开发者对于子 View 尺寸的设置就比较简单了:
// 获取开发者对于子 View 尺寸的设置
LayoutParams layoutParams = child.getLayoutParams();
int width = layoutParams.width;
int height = layoutParams.height;
复制代码
获取父 View (TagLayout) 的可用空间要结合两点:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// TagLayout 已经使用过的空间,此处的计算是个难点,此处不是本例子重点,一下子讨论
int widthUseSize = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 获取开发者对于子 View 尺寸的设置
LayoutParams layoutParams = child.getLayoutParams();
int childWidthMode;
int childWidthSize;
// 获取父 View 具体的可用空间
switch (layoutParams.width) {
// 若是说子 View 被开发者设置为 match_parent
case LayoutParams.MATCH_PARENT:
switch (widthMode) {
case MeasureSpec.EXACTLY:
// TagLayout 为 EXACTLY 模式下,子 View 能够填充的部位就是 TagLayout 的可用空间
case MeasureSpec.AT_MOST:
// TagLayout 为 AT_MOST 模式下有一个最大可用空间,子 View 要是想 match_parent 实际上是和
// EXACTLY 模式同样的
childWidthMode = MeasureSpec.EXACTLY;
childWidthSize = widthSize - widthUseSize;
break;
case MeasureSpec.UNSPECIFIED:
// 当 TagLayout 为 UNSPECIFIED 不限制尺寸的时候,意味着可用空间无限大!空间无限大还想
// match_parent 两者彻底是悖论,因此咱们也要将子 View 的 mode 指定为 UNSPECIFIED
childWidthMode = MeasureSpec.UNSPECIFIED;
// 此时 size 已经没有做用了,写 0 就能够了
childWidthSize = 0;
break;
}
case LayoutParams.WRAP_CONTENT:
break;
default:
// 具体设置的尺寸
break;
}
// 获取 measureSpec
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, childWidthMode);
复制代码
补充一下何时会是 UNSPECIFIED 模式呢?好比说横向或纵向滑动的 ScrollView,他的宽度或者高度的模式就是 UNSPECIFIED
伪代码仅仅模拟了开发者将子 View 的 size 设置为 match_parent 的状况,其余的状况读者要是感兴趣能够本身分析一下。笔者就不作过多的分析了!由于 Android SDK 早就为咱们提供好了可用的 API: measureChildWithMargins(int, int, int, int)
一句话就完成了对于子 View 的测量。
有了 measureChildWithMargins
方法,对于子 View 的测量就很简单啦。 一口气解决难点 2 3。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int lineHeightUsed = 0;
int lineWidthUsed = 0;
int widthUsed = 0;
int heightUsed = 0;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 测量子 View 尺寸。TagLayout 的子 view 是能够换行的,因此设置 widthUsed 参数为 0
// 让子 View 的尺寸不会受到挤压。
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
if (widthMode != MeasureSpec.UNSPECIFIED && lineWidthUsed + child.getMeasuredWidth() > widthSize) {
// 须要换行了
lineWidthUsed = 0;
heightUsed += lineHeightUsed;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
Rect childBound;
if (mChildRectList.size() >= i) {
// 不存在则建立
childBound = new Rect();
mChildRectList.add(childBound);
} else {
childBound = mChildRectList.get(i);
}
// 存储 child 位置信息
childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(),
heightUsed + child.getMeasuredHeight());
// 更新位置信息
lineWidthUsed += child.getMeasuredWidth();
// 获取一行中最大的尺寸
lineHeightUsed = Math.max(lineHeightUsed, child.getMeasuredHeight());
widthUsed = Math.max(lineWidthUsed, widthUsed);
}
// 使用的宽度和高度就是 TagLayout 的宽高啦
heightUsed += lineHeightUsed;
setMeasuredDimension(widthUsed, heightUsed);
}
复制代码
终于写完代码啦,运行起来瞧瞧看。
居然奔溃了!经过日志能够定位到是
// 对于子 View 的测量
measureChildWithMargins(child, widthMeasureSpec, widthUsed,
heightMeasureSpec, heightUsed);
复制代码
这一句出了问题,经过源码得知measureChildWithMargins
方法会有一个类型转换致使了崩溃
protected void measureChildWithMargins(int, int ,int, int) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
………………
}
复制代码
解决办法就是在 TagLayout 中重写方法 generateLayoutParams(AttributeSet)
返回一个 MarginLayoutParams 就能够解决问题了。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
复制代码
再次运行达到最终目标!
总结
自定义 Layout 的主要步骤分为如下几点:
onMeasure()
measureChildWidthMargins()
测量 View
setMeasuredDimension(width, height)
保存onLayout()
getMeasureXX 表明的是 onMeasure 方法结束后(准确的说应该是测量结束后)测量的值,而 getXX 表明的是 layout 阶段 right - left、bottom - top 的真实显示值,因此第一个不一样点就是赋值的阶段不一样,可见 getXXX 在 layout() 以前一直为 0, 而 getMeasureXX 可能不是最终值( onMeasure 可能会被调用屡次),可是最终的时候两者的数值都会是相同的。使用那个还须要看具体的场景。
总结: getMeasureXX 获取的是临时的值,而 getXX 获取的时候最终定稿的值,通常在绘制阶段、触摸反馈阶段使用 getXXX,在 onMeasure 阶段被迫使用 getMeasureXX 。
个人 Android 知识体系,欢迎 Star https://github.com/daishengda2018/AndroidKnowledgeSystem