View 的 requestLayout() 方法顧名思義用來觸發一次 layout 行爲,通常是當咱們改變一些影響 View 佈局的參數後調用,刷新 View 的佈局。常見的使用方式以下:java
view.layoutParams.apply{
width = 100
height = 200
}
view.requestLayout()
複製代碼
要分析調用失效的緣由,首先咱們須要搞清楚 requestLayout() 流程。android
調用 requestLayout() 以後是如何開始一次 layout 的呢?咱們看一下 requestLayout() 的源碼:bash
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() 方法,這個方法代碼以下:app
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
複製代碼
邏輯很簡單,佈局工做應該在 scheduleTraversals()
方法中完成,經過繼續跟進調用關係,最終調到了 performLayout()
方法,在這個方法中調用的是 DecorView 的 layout() 方法,開始了咱們熟悉的佈局流程,自頂向下調用子 View 的 layout() 方法,和 requestLayout() 的調用方向恰好相反,以下圖:佈局
如今咱們來看一下 View.requestLayout() 中剛纔跳過的部分,這裏經過 mAttachInfo.mViewRequestingLayout 變量來肯定發起 requestLayout() 的 View,由於只有發起 View 能觸發 request-during-layout 邏輯,它的祖先 Views 不能夠,至於緣由後面會講到。post
request-during-layout 從字面上看是在進行一次 layout pass 時,當前界面中有 View 調用 requestLayout()。代碼中能夠看到經過 viewRoot.isInLayout()
判斷當前是否在 layout,而後調用 ViewRootImpl.requestLayoutDuringLayout
方法,咱們繼續看看這個方法:ui
...
if (!mLayoutRequesters.contains(view)) {
mLayoutRequesters.add(view);
}
...
複製代碼
核心邏輯就上面這一句,將發起 View 添加在 ViewRootImpl 的 mLayoutRequesters 列表中。繼續看看何時使用這個列表,經過查看代碼發現使用的地方在 performLayout() 中,前一句代碼是 mInLayout = false
說明是在上一次 layout pass 結束後處理這個列表。處理的邏輯也比較簡單,先對這個列表進行過濾拿到有效的 View,而後再依次調用 requestLayout() 的方法。this
因此 request-during-layout 的處理能夠簡單理解爲將在 layout 過程當中的 requestLayout() 調用延遲到當前 layout pass 結束後再調用,這樣也就理解了爲何只有發起 View 須要觸發 request-during-layout 邏輯。spa
根據 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 結束後再延遲調用。