與ListView不同,RecyclerView的巢狀解決
1、問題
ListView的item中嵌套了RecyclerView實現水平方向列表,導致RecyclerView高度不能正常顯示。
2、傳統解決方案
巢狀問題最基本的解決方法是重寫onMeasure方法,手動測量ViewGroup的高度或者寬度。這裡,因為我們的RecyclerView是水平的,而且每一個item的高度是相同的,所以只需要測出一個item的所佔用高度(包括父控制元件的上下padding以及item的上下margin)作為RecyclerView的高度即可。程式碼如下:
public class MeasuredRecyclerView extends RecyclerView {
public MeasuredRecyclerView(Context context) {
this(context, null);
}
public MeasuredRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MeasuredRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super (context, attrs, defStyle);
}
@Override
public void onMeasure(int widthSpec, int heightSpec) {
int maxHeight = 0;
if(getChildCount() > 0) {
View child = getChildAt(0);
RecyclerView.LayoutParams params = (LayoutParams) child.getLayoutParams();
child.measure(widthSpec, MeasureSpec.makeMeasureSpec(0 , MeasureSpec.UNSPECIFIED));
maxHeight = child.getMeasuredHeight() + getPaddingTop() + getPaddingBottom()+ params.topMargin + params.bottomMargin;
}
super.onMeasure(widthSpec, MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY));
}
}
上述程式碼,大部分時候執行是正常的,RecyclerView高度正確。在RecyclerView初始化adapter還沒有設定資料時,maxHeight為0;當資料請求完成後呼叫adapter.notifyDataSetChanged(),onMeasure方法再次呼叫,重新測量item高度,顯示正確高度,這是符合期望的。但是測試時注意到,有些時候adapter.notifyDataSetChanged()呼叫後onMeasure操作並沒有重新執行,導致RecyclerView高度不能正確顯示。為了解決這個問題,我們也可以給maxHeight設定預設高度,但是item樣式不一定統一,為了通用性,可能需要尋找其他解決方法。
3、優化方案
google一下發現(最近某度被黑的很慘,已經棄用),大都解決方案是通過重寫LinearLayoutManager實現的。RecyclerView和ListView,GridView等其他控制元件最大不同可能就是LayoutManager了,RecyclerView使用LayoutManager來管理item的佈局。在RecyclerView的資料發生改變時,LayoutManager都需要重新對item進行佈局。可以看下RecyclerView.Adapter關於notifyDataSetChanged() 方法的註釋。
/**
* Notify any registered observers that the data set has changed.
* <p>There are two different classes of data change events, item changes and structural
* changes. Item changes are when a single item has its data updated but no positional
* changes have occurred. Structural changes are when items are inserted, removed or moved
* within the data set.</p>
* <p>This event does not specify what about the data set has changed, forcing
* any observers to assume that all existing items and structure may no longer be valid.
* LayoutManagers will be forced to fully rebind and relayout all visible views.</p>
*/
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
從註釋可以看到,對於data的任何變化,不管是item資料更新還是item插入刪除,layoutManager都需要強制重新整理佈局。因此,可以重寫LayoutManager的onMeasure方法來實現手動測量item高度。程式碼如下:
public class MeasuredLinearLayoutManager extends LinearLayoutManager {
private int maxHeight;
public MeasuredLinearLayoutManager(Context context) {
super(context);
}
@Override
public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
if(maxHeight == 0 && getItemCount() > 0) {
View child = recycler.getViewForPosition(0);
RecyclerView.LayoutParams params = (LayoutParams) child.getLayoutParams();
child.measure(widthSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int height = child.getMeasuredHeight() + getPaddingTop() + getPaddingBottom()
+ params.topMargin + params.bottomMargin;
if (height > maxHeight) {
maxHeight = height;
}
}
heightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
super.onMeasure(recycler, state, widthSpec, heightSpec);
}
}
這種方案可以解決問題,但是值得注意的是,每次item發生改變時,都會重新measure,所以在onMeasure中最好不要做太多複雜的操作。
注:以上程式碼均針對item高度一致的情況,對於高度不同的情況,需要遍歷所有item,找出item的最大高度,並設定成RecyclerView高度即可,思路類似。