1. 程式人生 > >與ListView不同,RecyclerView的巢狀解決

與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高度即可,思路類似。