内容简介:View 的 requestLayout() 方法顾名思义用来触发一次 layout 行为,一般是当我们改变一些影响 View 布局的参数后调用,刷新 View 的布局。常见的使用方式如下:要分析调用失效的原因,首先我们需要搞清楚 requestLayout() 流程。调用 requestLayout() 之后是如何开始一次 layout 的呢?我们看一下 requestLayout() 的源码:
View 的 requestLayout() 方法顾名思义用来触发一次 layout 行为,一般是当我们改变一些影响 View 布局的参数后调用,刷新 View 的布局。常见的使用方式如下:
view.layoutParams.apply{ width = 100 height = 200 } view.requestLayout() 复制代码
要分析调用失效的原因,首先我们需要搞清楚 requestLayout() 流程。
requestLayout 调用流程
调用 requestLayout() 之后是如何开始一次 layout 的呢?我们看一下 requestLayout() 的源码:
public void requestLayout() { if (mMeasureCache != null) mMeasureCache.clear(); if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) { // Only trigger request-during-layout logic if this is the view requesting it, // not the views in its parent hierarchy ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null && viewRoot.isInLayout()) { if (!viewRoot.requestLayoutDuringLayout(this)) { return; } } mAttachInfo.mViewRequestingLayout = this; } mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); } if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) { mAttachInfo.mViewRequestingLayout = null; } } 复制代码
这个方法逻辑比较简单,首先是将 MeasureCache 清掉,为即将开始的 layout 做准备。接下来一段代码根据注释应该是 request-during-layout 的逻辑,这部分我们先略过后面再讲。再下来的代码是这个方法的核心:设置绘制状态位和调用 parent 的 requestLayout。PFLAG_FORCE_LAYOUT 表示当前 View 需要 layout,可以理解为 View 当前的布局数据已经过期,需要下一次 layout pass 重新布局,PFLAG_INVALIDATED 和 PFLAG_FORCE_LAYOUT 类似,只不过表示的是绘制数据。为什么要调用 parent 的 requestLayout() 这里稍微解释下,因为父 View 是包含着子 View 的,子 View 的布局一般决定着父 View 的布局,所以当子 View 布局发生改变时也要通知父 View 刷新自己的布局。通过一级一级向上调用最终调用到 ViewRootImpl 的 requestLayout() 方法,这个方法代码如下:
if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } 复制代码
逻辑很简单,布局工作应该在 scheduleTraversals()
方法中完成,通过继续跟进调用关系,最终调到了 performLayout()
方法,在这个方法中调用的是 DecorView 的 layout() 方法,开始了我们熟悉的布局流程,自顶向下调用子 View 的 layout() 方法,和 requestLayout() 的调用方向刚好相反,如下图:
request-during-layout 处理
现在我们来看一下 View.requestLayout() 中刚才跳过的部分,这里通过 mAttachInfo.mViewRequestingLayout 变量来确定发起 requestLayout() 的 View,因为只有发起 View 能触发 request-during-layout 逻辑,它的祖先 Views 不可以,至于原因后面会讲到。
request-during-layout 从字面上看是在进行一次 layout pass 时,当前界面中有 View 调用 requestLayout()。代码中可以看到通过 viewRoot.isInLayout()
判断当前是否在 layout,然后调用 ViewRootImpl.requestLayoutDuringLayout
方法,我们继续看看这个方法:
... if (!mLayoutRequesters.contains(view)) { mLayoutRequesters.add(view); } ... 复制代码
核心逻辑就上面这一句,将发起 View 添加在 ViewRootImpl 的 mLayoutRequesters 列表中。继续看看什么时候使用这个列表,通过查看代码发现使用的地方在 performLayout() 中,前一句代码是 mInLayout = false
说明是在上一次 layout pass 结束后处理这个列表。处理的逻辑也比较简单,先对这个列表进行过滤拿到有效的 View,然后再依次调用 requestLayout() 的方法。
所以 request-during-layout 的处理可以简单理解为将在 layout 过程中的 requestLayout() 调用延迟到当前 layout pass 结束后再调用,这样也就理解了为什么只有发起 View 需要触发 request-during-layout 逻辑。
requestLayout() 调用失效原因
根据 requestLayout() 的调用流程可以发现,如果由下到上的调用中断无法调到 ViewRootImpl.requestLayout() 的话就会导致无法刷新布局。通过查看源码我们发现调用父 View 的 requestLayout() 之前需要满足两个条件 parent != null 和 !parent.isLayoutRequested(),如果 parent 为空说明当前 View 不在界面上,那也不需要刷新布局,这个条件是合理的。
另外一个条件表示 parent 已经调用过 requestLayout(),这个判断为了防止正在进行的布局没有结束时开始下一次布局。但如果我们确实需要刷新当前界面的布局该怎么办呢?没事,View 的设计者想到了这种情况,对应的解决方案就是上面的 request-during-layout 处理。
不过 request-during-layout 处理并不是万无一失的,它有两个漏洞还是会造成 requestLayout() 调用失效:
-
request-during-layout 的处理必须是在 View.isInLayout == true 时才能奏效,如果当前不在 layout pass 中而且 requestLayout() 调用链无法作用到 ViewRootImpl.requestLayout() 时调用还是会失效。我们前面有提到祖先 View.isLayoutRequested() == true 的情况就是当前界面在进行 layout,但这里却说 isInLayout == false,是不是和前面说的自相矛盾了?当然不是。首先我们先看看 View.isInLayout() 的代码:
public boolean isInLayout() { ViewRootImpl viewRoot = getViewRootImpl(); return (viewRoot != null && viewRoot.isInLayout()); } 复制代码
可以看到 isInLayout() 依赖于 ViewRootImpl.isInLayout(),继续看看这个方法:
boolean isInLayout() { return mInLayout; } 复制代码
而
mInLayout = true
仅在 ViewRootImpl.performLayout() 中存在,换句话说只有这个方法触发的布局刷新才会令 View.isInLayout() == true,如果通过别的途径触发布局刷新就会导致这种 requestLayout() 调用失效。具体会有什么布局刷新调用不是通过 ViewRootImpl.performLayout() 发起的呢?目前遇到的一种情况是在 RecyclerView 中滑动页面引起的 itemView 布局刷新,具体来说是将界面外的 itemView 滑动到界面内时。一个调用栈例子如下:... at android.view.View.layout(View.java:22254) at android.view.ViewGroup.layout(ViewGroup.java:6310) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1818) at android.widget.LinearLayout.onLayout(LinearLayout.java:1584) at android.view.View.layout(View.java:22254) at android.view.ViewGroup.layout(ViewGroup.java:6310) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673) at android.widget.LinearLayout.onLayout(LinearLayout.java:1582) at android.view.View.layout(View.java:22254) at android.view.ViewGroup.layout(ViewGroup.java:6310) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673) at android.widget.LinearLayout.onLayout(LinearLayout.java:1582) at android.view.View.layout(View.java:22254) at android.view.ViewGroup.layout(ViewGroup.java:6310) at androidx.recyclerview.widget.RecyclerView$LayoutManager.layoutDecoratedWithMargins(RecyclerView.java:9322) at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1615) at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1517) at androidx.recyclerview.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1331) at androidx.recyclerview.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1075) at androidx.recyclerview.widget.RecyclerView.scrollStep(RecyclerView.java:1832) at androidx.recyclerview.widget.RecyclerView.scrollByInternal(RecyclerView.java:1927) at androidx.recyclerview.widget.RecyclerView.onTouchEvent(RecyclerView.java:3187) ... at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:448) at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1840) at android.app.Activity.dispatchTouchEvent(Activity.java:3873) at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69) at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69) at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:406) at android.view.View.dispatchPointerEvent(View.java:14056) ... at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7621) ... at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:188) ... 复制代码
从上面的调用栈可以清楚的看到 InputEvent -> TouchEvent -> RecyclerView.scroll*() -> LinearLayoutManager.scroll*() -> LinearLayoutManager.layoutChunk() -> itemView.layout() 这样一个调用流程,这里的 itemView 确实处于 layout 过程中,但不是 ViewRootImpl.performLayout 发起的,所以 View.isInLayout() == false,就会触发我们这条调用失效。这个漏洞是 View 的设计者的责任吗?我认为不是的,由滚动触发 layout 的行为是 RecyclerView 的特殊处理,而对这种特殊处理导致的 requestLayout() 调用失效就应该由触发者 RecyclerView 解决,显然它没有。
-
即使 request-during-layout 能够被触发,在延迟调用 requestLayout() 前还会对发起 View 进行一次过滤,该 View 和它的祖先 View 的 visibility 必须不是 GONE,并且被设置了 View.PFLAG_FORCE_LAYOUT 状态,对应代码在
ViewRootImpl.getValidLayoutRequesters()
可见。第一个过滤条件可以理解,不可见的 View 不需要布局。第二个可能会造成调用失效,该状态表示是否需要被重新布局,调用 requestLayout() 时该状态被启用,layout 完成后被清掉。比如在一次 layout 中刚通过调用 requestLayout() 设置了 View.PFLAG_FORCE_LAYOUT,然后还没等到 request-during-layout 处理,这个标志位就被清掉了。有这种可能么?有的,代码如下:view.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> v.layoutParams.width = 100 v.requestLayout() } 复制代码
上面代码中的 requestLayout() 不会起作用,为什么呢?我们看看 onLayoutChangeListener 在哪里被调用:
public void layout(int l, int t, int r, int b) { ... if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } final boolean wasLayoutValid = isLayoutValid(); mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; ... } 复制代码
从上面的代码可以看到 onLayoutChangeListener 被调用后 PFLAG_FORCE_LAYOUT 就被清掉了。
解决方案
既然知道了 requestLayout() 失效的原因,那如何才能解决这个问题呢?具体代码如下:
fun View.isSafeToRequestDirectly():Boolean { return if (isInLayout) { // when isInLayout == true and isLayoutRequested == true, // means that this layout pass will layout current view which will // make currentView.isLayoutRequested == false, and this will let currentView // ignored in process handling requests called during last layout pass. isLayoutRequested.not() } else { var ancestorLayoutRequested = false var p: ViewParent? = parent while (p != null) { if (p.isLayoutRequested) { ancestorLayoutRequested = true break } p = p.parent } ancestorLayoutRequested.not() } } fun View.safeRequestLayout() { if (isSafeToRequestDirectly()) { requestLayout() } else { post { requestLayout() } } } 复制代码
通过 isSafeToRequestDirectly() 来判断调用 requestLayout() 是否奏效,这个方法里面分别从 isInLayout == true/false 两种情况判断,对应上面分析的两种失效情况。如果是的话就直接调用否则通过 post() 方法等当前 layout 结束后再延迟调用。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- @Transactional事务生效问题
- properties文件改变不生效的问题
- C# Redis 过期机制不生效问题
- 解决vue页面DOM操作不生效的问题
- 案例解析:springboot自动配置未生效问题定位(条件断点)
- 案例解析:springboot自动配置未生效问题定位(条件断点)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。