Android RecyclerView瀑布流中Item寬度異常的問題(原始碼分析)
問題描述
通過RecyclerView配合StaggeredGridLayoutManager可以很方便的實現瀑布流效果,一般情況下會把作為Item的子View寬度設定為MATCH_PARENT,那麼子View將根據列數(假定是垂直排列)平均分配RecyclerView的寬度。但是如果我們為子View的width設定一個確切的值(記為x),並且為RecyclerView新增ItemDecoration(為了設定Item的間距),最終Item的寬度將會被預期的要窄(小於x),本文將從原始碼的角度分析這種結果的產生的原因。
原因分析
經過分析,發現StaggeredGridLayoutManager會通過measureChildWithDecorationsAndMargin
private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp,
boolean alreadyMeasured) {
if (lp.mFullSpan) { // 如果Item需要佔據整行時執行這裡的邏輯
.......
} else { // 正常情況下的邏輯
if (mOrientation == VERTICAL) {
measureChildWithDecorationsAndMargin(child,
getChildMeasureSpec(mSizePerSpan, getWidthMode(), 0 , lp.width, false),
getChildMeasureSpec(getHeight(), getHeightMode(), 0, lp.height, true),
alreadyMeasured);// 呼叫另一個版本的過載方法
} else {
.......
}
}
}
這裡的getChildMeasureSpec方法是用於確認子View原始寬度(未減去左右間距的寬度)的,其關鍵程式碼如下:
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
......
} else {// 針對不可滑動的情況,比如現在水平方向就是不可滑動的
if (childDimension >= 0) { // View的width是一個確切的值時
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = parentMode;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
resultMode = MeasureSpec.AT_MOST;
} else {
resultMode = MeasureSpec.UNSPECIFIED;
}
}
}
//noinspection WrongConstant
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
這裡的childDimension就是子View在LayoutParams中的width,其實也就是XML檔案中設定的layout_width。此時childDimension>0,根據程式碼邏輯現在子View的寬度就等於XML檔案中設定的layout_width了。隨後,將返回的MeasureSpec作為引數,呼叫了另一個過載版本的measureChildWithDecorationsAndMargin方法,這個方法的關鍵程式碼如下:
private void measureChildWithDecorationsAndMargin(View child, int widthSpec,
int heightSpec, boolean alreadyMeasured) {
calculateItemDecorationsForChild(child, mTmpRect); // 通過ItemDecoration獲取Item的左右間距
LayoutParams lp = (LayoutParams) child.getLayoutParams();
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mTmpRect.left,
lp.rightMargin + mTmpRect.right); // 重新計算Item的寬度(減去左右間距)
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mTmpRect.top,
lp.bottomMargin + mTmpRect.bottom);
final boolean measure = alreadyMeasured
? shouldReMeasureChild(child, widthSpec, heightSpec, lp)
: shouldMeasureChild(child, widthSpec, heightSpec, lp);
if (measure) {
child.measure(widthSpec, heightSpec); // 更新子View的寬度
}
}
注意這裡的calculateItemDecorationsForChild方法,主要是通過ItemDecoration獲取Item的左右間距,並儲存在mTmpRect
這個物件中。此後,通過updateSpecWithExtra更新Item的寬度(減去左右間距)。最後將最終的寬高設定給子View。
到這裡情況已經很清晰了,由於我們在XML檔案中為子View設定的寬度在測量中減去了子View左右間距的距離(根據ItemDecoration獲得),導致Item的實際寬度小於我們設定的寬度。
解決方案
如果我們確實希望為Item指定一個確切的寬度,並且希望這個寬度不被ItemDecoration影響,只需要在子View的外面套一層ViewGroup就行了。比如在子View外面巢狀一層FrameLayout,並將FrameLayout寬度設定為MATCH_PARENT
或者WRAP_CONTENT
(最好為MATCH_PARENT),就可以保證Item的寬度被正確測量了。
原理也很簡單,由於現在瀑布流的Item實際上是FrameLayout,那麼在測量的時候就是去測量FrameLayout的寬度。此時只會對FrameLayout的原始寬度(一列的寬度)減去左右間距,並不影響FrameLayout中子View的寬度,因此Item的最終寬度就不會出現問題了。