原始碼分析ScrollView巢狀ListView展示不全的問題
解決方式網上一大把,有兩種方法,一種是遍歷 item,將 itme 的高度累加,另一個重寫 ListView 的 onMeasure。我們這裡主要說下,產生問題的原因,以及第二種解決方式的原理。
原始碼分析
ListView 展示不全,首先想到 ListView 的高度,高度測量是在 onMeasure 中進行的,看下原始碼。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Sets up mListPadding super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec);// ① int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec);// ② ... if (heightMode == MeasureSpec.UNSPECIFIED) { heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;// ③ } if (heightMode == MeasureSpec.AT_MOST) { // TODO: after first layout we should maybe start at the first visible position, not 0 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);// ④ } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; }
在 onMeasure 的關鍵程式碼是heightSize
的值,只有在②、③、④三處程式碼,對heightSize
賦值進行賦值,可以大膽的猜測一下,可能在執行③的時候,ListView 出現了問題,展示一個 item 的高度,因為只加了一個childHeight
。
這裡介紹一下①、②程式碼,MeasureSpec 是測量規格,int 值,32位,前面兩位當做 mode,後面 30位當做 value。①是從 MeasureSpec 取 mode,②是從 MeasureSpec 取 value。
我們先來看看④的程式碼。
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, int maxHeight, int disallowPartialChildPosition) { ... for (i = startPosition; i <= endPosition; ++i) { child = obtainView(i, isScrap); measureScrapChild(child, i, widthMeasureSpec, maxHeight); if (i > 0) { // Count the divider for all but one child returnedHeight += dividerHeight; } // Recycle the view before we possibly return from the method if (recyle && recycleBin.shouldRecycleViewType( ((LayoutParams) child.getLayoutParams()).viewType)) { recycleBin.addScrapView(child, -1); } returnedHeight += child.getMeasuredHeight(); if (returnedHeight >= maxHeight) { // We went over, figure out which height to return.If returnedHeight > maxHeight, // then the i'th position did not fit completely. return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) && (i > disallowPartialChildPosition) // We've past the min pos && (prevHeightWithoutPartialChild > 0) // We have a prev height && (returnedHeight != maxHeight) // i'th child did not fit completely ? prevHeightWithoutPartialChild : maxHeight; } if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { prevHeightWithoutPartialChild = returnedHeight; } } // At this point, we went through the range of children, and they each // completely fit, so return the returnedHeight return returnedHeight; }
這裡通過遍歷計算每個 child 的高度,這個才是常規的方式,更加認證了,bug 出現的原因是,ScrollView 給了一個不確定的測量規格MeasureSpec.UNSPECIFIED
,導致 ListView 在 onMeasure 的時候執行了③的程式碼。
繼續在 ScrollView 中尋找答案。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec);// ① if (!mFillViewport) { return; } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode == MeasureSpec.UNSPECIFIED) { return; } if (getChildCount() > 0) { final View child = getChildAt(0);// ② 解釋了為什麼只能巢狀一個子控制元件 ... } }
先說句題外話,上面②的位置解釋了,ScrollView 為什麼只能有一個子控制元件,就算有多個,也只測量一個。
關鍵程式碼在①處,父類 FrameLayout 的 onMeasure 中。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); ... for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { // 關鍵程式碼 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); ... } } }
這裡需要注意一下,點選進入的是 ViewGroup 的 measureChildWithMargins,是看不到關鍵程式碼的,需要進入 ScrollView,ScrollView 重寫了 measureChildWithMargins 方法。
@Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed; final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal), MeasureSpec.UNSPECIFIED);// 關鍵程式碼 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
這裡就可以看到 childHeightMeasureSpec 設定了測量規格為MeasureSpec.UNSPECIFIED
,傳遞到 child,也就是 ListView 的 measure 方法,在 measure 又呼叫了 ListView 的 onMeasure,在回過頭來,看文章第一段程式碼①處,取到的 widthMode 值就是MeasureSpec.UNSPECIFIED
,所以自然會出現 ListView 展示不全的問題。
解決方式
原因分析清楚了,大家應該還是明白,為什麼重寫 ListView 的 onMeasure,就可以修復該問題。
public void onMeasure() { // 第一個引數是value,第二個引數是mode,通過makeMeasureSpec工具類,合併為一個int int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); }
其實makeMeasureSpec
是 MeasureSpec 拼接 value 和 mode 的方法,mode 設定為MeasureSpec.AT_MOST
很好理解,value 設定為 Integer.MAX_VALUE 是為了保證高度足夠大,也很好理解。關鍵是位運算,這裡就涉及到MeasureSpec的構成,MeasureSpec的前兩位是mode,後面30位才是value,所以這裡,value的最大值就是Integer.MAX_VALUE >> 2
了。
我是JamFF,希望今天的文章對你有幫助。
END.