Android-高级-UI-进阶之路-(三)-理解-View-工作原理并带你入自定义-View-门

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, “measure”);
try {
//调用 View 的 measure 方法
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

//说明 2.
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {

final View host = mView;//代表 DecorView

//内部在调用 View onLayout
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

}

//说明 3.
private void performDraw() {

//内部通过调用 GPU/CPU 来绘制
draw(fullRedrawNeeded);

}
复制代码

到这里上面对应的函数会对应调用 View 的 onMeasure -> onLayout -> ondraw 方法,下面我们就具体来说明下绘制过程。

measure 过程

measure 过程要分情况来看,如果一个原始的 View ,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量过程外,还会遍历它所有的子 View 的 measure 方法,各个子元素在递归去执行这个流程(有子 View 的情况),下面针对这两种情况分别讨论。

View 的 measure 过程

//V
iew.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);

onMeasure(widthMeasureSpec, heightMeasureSpec);


}
复制代码

View 的 measure 过程由其 measure 方法来完成,通过 View#measure 源码可以知道 它是被 final 修饰的,那么就代表了子类不能重写,通过上面源码我们知道在 View#measure 内部又会去调用 onMeasure 方法,我们接着看它的源码实现,代码如下:

//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码

上面的代码就做了一件事儿,就是设置测量之后的宽高值,我们先来看看 getDefaultSize 方法,代码如下:

//View.java
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//根据 measureSpec 拿到当前 View 的 specMode
int specMode = MeasureSpec.getMode(measureSpec);
//根据 measureSpec 拿到当前 View 的 specSize
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED: //一般用于系统内部的测量过程
result = size;//直接返回传递进来的 size
break;
case MeasureSpec.AT_MOST:// wrap_content 模式,大小由子类决定但是不能超过父类
case MeasureSpec.EXACTLY://精准模式
result = specSize;//返回测量之后的大小
break;
}
return result;
}
复制代码

通过上面代码可以看出,getDefaultSize 内部逻辑不多,也比较简单,对于我们来说只需要关心 AT_MOST, EXACTLY 这两种情况就行,其最终就是返回测量之后的大小。这里要注意的是这里测量之后的大小并不是最终 View 的大小,最终大小是在 layout 阶段确定的,所以这里一定要注意。

我们来看一下 getSuggestedMinimumXXXX() 源码实现:

//View.java
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
复制代码

可以看到 getSuggestedMinimumXXX 内部的代码意思就是,如果 View 没有设置背景,那么返回 android:minWidth 这个属性所指定的值,这个值可以为 0,如果 View 设置了背景,则返回 android:minWidth 和背景的 最小宽度/最小高度 这两者的者中的最大值,它们返回的就是 UNSPECIFIED 情况下的宽高。

从 getDefaultSize 方法实现来看, View 的宽高由 specSize 决定,所以我们可以得到如下结论:既然 measure 被 final 修饰不能重写,可是我们在它内部也发现了新大陆 onMeasure 方法,我们可以直接继承 View 然后重写 onMeasure 方法并设置自身大小。

这里在重写 onMeasure 方法的时候设置自身宽高需要注意一下,如果在 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽高等于 specSize,也就是父类控件空剩余可以使用的空间大小,这种效果和在布局中使用 match_parent 完全一致,那么如何解决这个问题勒,可以参考下面代码:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)

val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)

/**

  • 说明在布局中使用了 wrap_content 模式
    */
    if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(mWidth,mHeight)
    }else if (widthMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(mWidth,heightSize)
    }else if (heightMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(widthSize,mHeight)
    }else {
    setMeasuredDimension(widthSize,heightSize)
    }
    }
    复制代码

在上面代码中,我们只需要给 View 指定一个默认的内部宽高(mWidth、mHeight),并在 wrap_content 的时候设置此宽高即可。对于非 wrap_content 情形,我们就沿用系统的测量值即可。

ViewGroup 的 measure 过程

对于 ViewGroup 来说,初了完成自己的 measure 过程以外,还会去遍历调用所有的子 View 的 measure 方法,各个元素递归去执行这个过程,和 View 不同的是 ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它定义了一个 measureChild 方法,代码如下:

//ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
//拿到所有子 View
final View[] children = mChildren;
//遍历子 View
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//依次对子 View 进行测量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
复制代码

上面代码也很简单,ViewGroup 在 measure 时,会对每一个子元素进行 measure ,如下代码所示:

//ViewGroup.java
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//拿到该子 View 在布局XML中或者代码中定义的属性
final LayoutParams lp = child.getLayoutParams();
//通过 getChildMeasureSpec 方法,根据父元素的宽高测量规则拿到子元素的测量规则
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//拿到子元素的测量规则之后传递到 View 中,开始 measure 流程
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码

measureChild 代码逻辑也很容易理解,首先取出设置的 LayoutParams 参数,然后通过 getChildMeasureSpec 方法,根据父元素的宽高测量规格拿到子元素的测量规格,最后将拿到的测量规格直接传递给 View#measure 来进行测量。

measure 小总结:

  1. 获取 View 最终宽高,需要在 onLayout 中获取,因为 measure 在某些极端的情况下需要测量多次。
  2. 在 Activity 中获取 View 的宽高需要使用 Activity/View#onWindowFocusChangedview.post(runnable)ViewTreeObserver 的 onGlobalLayoutListener 回调手动调用 view.measure(int width,int height) ,最终使用哪个以实际情况来定。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值