View.requestLayout() 不生效的问题

栏目: Android · 发布时间: 6年前

内容简介: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() 的调用方向刚好相反,如下图:

View.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() 调用失效:

  1. 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 解决,显然它没有。

  2. 即使 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 结束后再延迟调用。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

复杂网络理论及其应用

复杂网络理论及其应用

汪小帆、李翔、陈关荣 / 清华大学出版社 / 2006 / 45.00元

国内首部复杂网络专著 【图书目录】 第1章 引论 1.1 引言 1.2 复杂网络研究简史 1.3 基本概念 1.4 本书内容简介 参考文献 第2章 网络拓扑基本模型及其性质 2.1 引言 2.2 规则网络 2.3 随机图 2.4 小世界网络模型 2.5 无标度网络模型 ......一起来看看 《复杂网络理论及其应用》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试