聽說你想玩RecyclerView嵌套GridView

效果圖

RecyclerView嵌套GridView

問題及原因

有很多小伙伴們可能會遇到這樣的問題:

為什么不論我傳入多大size的List,我的GridView只能顯示一行?

因為RecyclerView和GridView都屬于可滑動控件 ,兩者嵌套會導致滑動沖突 ,Android不允許這樣的情況出現,所以索性將GridView寬度定死,定為一行Item的高度且不可滑動,所以導致了我們只顯示一行這個問題的出現。

源碼淺析

解決的思路是: 重新計算高度!

想要計算高度,我們就要知道它計算高度的機制。我們來看看 源碼

繼承GridView自定義控件里的onMeasure方法

我們可以看到如果我們自定義控件,且什么都不做時,它會調用父類(GridView)的onMeasure方法,我們來看看GridView里面的onMeasure源碼:

@Override?protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {?
    // Sets up mListPadding?
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);??
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);?
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);?
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);?
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);??
    if (widthMode == MeasureSpec.UNSPECIFIED) {?
        if (mColumnWidth > 0) {?
            widthSize = mColumnWidth + mListPadding.left + mListPadding.right;?
        } else {?
            widthSize = mListPadding.left + mListPadding.right;?
        }?
        widthSize += getVerticalScrollbarWidth();?
    }??
    int childWidth = widthSize - mListPadding.left - mListPadding.right;?
    boolean didNotInitiallyFit = determineColumns(childWidth);??    int childHeight = 0;?
    int childState = 0;??
    mItemCount = mAdapter == null ? 0 : mAdapter.getCount();?
    final int count = mItemCount;?
    if (count > 0) {?
        final View child = obtainView(0, mIsScrap);??
        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();?
        if (p == null) {?
            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();?
            child.setLayoutParams(p);?
        }?
        p.viewType = mAdapter.getItemViewType(0);
?        p.forceAdd = true;
??        int childHeightSpec = getChildMeasureSpec(?
           MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),?
                        MeasureSpec.UNSPECIFIED), 0, p.height);
?        int childWidthSpec = getChildMeasureSpec(?
                MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);?
        child.measure(childWidthSpec, childHeightSpec);??
        childHeight = child.getMeasuredHeight();?
        childState = combineMeasuredStates(childState, child.getMeasuredState());
??        if (mRecycler.shouldRecycleViewType(p.viewType)) {
?            mRecycler.addScrapView(child, -1);?
        }?
    }
??    if (heightMode == MeasureSpec.UNSPECIFIED) {?
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
?                getVerticalFadingEdgeLength() * 2;?
    }??
    if (heightMode == MeasureSpec.AT_MOST) {?
        int ourSize =  mListPadding.top + mListPadding.bottom;
??        final int numColumns = mNumColumns;?
        for (int i = 0; i < count; i += numColumns) {?
            ourSize += childHeight;
?            if (i + numColumns < count) {?
                ourSize += mVerticalSpacing;?
            }
?            if (ourSize >= heightSize) {?
                ourSize = heightSize;
?                break;
?            }?
        }
?        heightSize = ourSize;
?    }??
    if (widthMode == MeasureSpec.AT_MOST && mRequestedNumColumns != AUTO_FIT) {?
        int ourSize = (mRequestedNumColumns*mColumnWidth)?
                + ((mRequestedNumColumns-1)*mHorizontalSpacing)?
                + mListPadding.left + mListPadding.right;
?        if (ourSize > widthSize || didNotInitiallyFit) {
?            widthSize |= MEASURED_STATE_TOO_SMALL;?
        }?
    }??
    setMeasuredDimension(widthSize, heightSize);
?    mWidthMeasureSpec = widthMeasureSpec;
?}

既然我們關注的是高度,我們就來看看高度相關的變量:

GridView的onMeasure

onMeasure()方法傳入了兩個變量: widthMeasureSpecheightMeasureSpec ,它們是什么呢?在這之前我們講一講 MeasureSpec 類的模式,我們用一張圖來說明:

MeasureSpec的模式說明圖

我們可以把MeasureSpec的模式標記理解成一個32位的二進制數字,最高兩位表示 模式 ,其他30位表示 大小 (即像素點或尺寸),MeasureSpec提供了 三種模式 用來計算控件的尺寸:

EXACTLY

在控件寬高設置為 具體數值MATCH_PARENT 時,使用該模式;

AT_MOST

在控件寬高設置為 WRAP_CONTENT 時,使用該模式;

UNSPECIFIED

除上述兩種情況外的其他情況( 即未指定寬高時 ),使用該模式。

知道了MeasureSpec,我們回到GridView的onMeasure方法,方法里獲取了高度的 模式大小

int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

PS: getMode() 及 getSize() 分析見附錄一

我們繼續往下看:

【AT_MOST】

AT_MOST模式

如果高度模式為 AT_MOST ,則它首先會計算GridView的內容高度,內容高度的計算式為:

paddingTop: GridView上內邊距
paddingBottom: GridView下內邊距
verticalSpacing: GridView每行垂直方向間距
spaceNum: GridView行數 - 1
lineNum: GridView行數
itemHeight: GridView每項高度

內容高度 = paddingTop + paddingBottom + verticalSpacing * spacingNum + itemHeight * lineNum

其中, childHeight 通過 child.getMeasuredHeight() 獲得。

計算好內容高度以后,它會和最大允許高度比較:

如果內容高度 未超過 最大高度,則 內容高度 作為GridView的高度;

如果內容高度 超過 最大高度,則 最大高度 作為GridView的高度;

【UNSPECIFIED】

UNSPECIFIED模式

如果高度模式為 UNSPECIFIED ,則它會計算包含一個Item的GridView的內容高度,其計算式為:

內容高度 = 上內邊距 + 下內邊距 + 一個子項高度 + 邊寬 * 2

個人猜測, 當RecyclerView嵌套GridView的時候,其GridView的MeasureSpec的模式為 UNSPECIFIED

【EXACTLY】

EXACTLY模式

因EXACTLY模式下,GridView的高度已經設定好了,所以不用獲取子項的高度及邊距等,源碼中通過 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 在heightSize聲明的時候,將其獲取到了,所以沒有將 EXACTLY 單獨IF語句進行判斷。

解決方案

之前我們說過,解決思路是重新計算GridView高度,這里我們介紹兩種計算GridView的高度的方法:

【自定義控件】

自定義控件

通過自定義控件繼承GridView,重寫onMeasure方法,將計算高度的模式設置為 AT_MOST 即可。

代碼如下:

import android.content.Context;
import android.util.AttributeSet;
import android.widget.GridView;
/**
 * Created by inerdstack on 2016/9/14.
 */
public class MyGridView extends GridView {

    public MyGridView(Context context) {
        super(context);
    }

    public MyGridView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyGridView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);

    }

    @Override
    public int getNumColumns() {
        return 2;
    }
}

【手動計算GridView高度】

計算GridView高度

這里我們計算的是相同類型View下的GridView的高度,切記要在 setAdapter以后調用這個方法 ,否則會無效。本人嘗試使用這種方法解決問題,但是沒有成功,但是同事使用了這種方法寫了一個Demo成功了。

個人猜想可能跟我的布局有關,我的 GridView 所在的環境是 ActivityFragmentPtrFrameLayout (下拉刷新框架的一個控件)的 RecyclerViewItem 里面,不過不排除我的代碼問題,如果小伙伴們找到了我的問題所在,歡迎在文章下方評論留言。

文章至此基本告終,接下來說說getSize()、getMode的源碼淺析,感興趣的小伙伴可以看看。

附錄一 getSize()、getMode()源碼分析

之前我們在GridView類的onMeasure方法里看到這樣的方法:

onMeasure方法

我們講到過MeasureSpec的模式組成是 模式 + 大小 組成的32位二進制整型數字:

MeasureSpec的模式說明圖

那么它是怎么獲取模式和大小的呢?

getMode源碼

getSize源碼

我們可以看到兩個方法里都用了 MODE_MASK ,我們來看看 MODE_MASK 是何方神圣:

MeasureSpec.MODE_MASK

在MeasureSpec類里面,聲明了如下幾個常量:

MODE_SHIFT

30位表示大小的位數的標記

MODE_MASK

經移位轉換為二進制后的數值為: 11000000000000000000000000000000

UNSPECIFIED

經移位轉換為二進制后的數值為: 00000000000000000000000000000000

EXACTLY

經移位轉換為二進制后的數值為: 01000000000000000000000000000000

AT_MOST

經移位轉換為二進制后的數值為: 10000000000000000000000000000000

科普:
<< 為二進制數的左移標記,其計算方式為:
a << b
則先將a轉化為二進制數,然后左移b位
如:10 << 3
10 = 101
10 << 3 = 101000

在getMode方法中, measureSpec & MODE_MASK 的邏輯計算結果是 XX000000000000000000000000000000 ,也就是說,不論我們measureSpec的低30位上數字是什么最終都會轉化為 000000000000000000000000000000 ,去掉了大小位上的非0數字,得到了與模式標記數字相同的結果,實現了提取模式的作用。

而在getSize方法中, ~MODE_MASK 是 MODE_MASK 取反,即 001111111111111111111111111111111 , measureSpec & ~MODE_MASK 的邏輯計算結果則為 00XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ,與getMode同理,去掉了模式位上的非0數字,只留下了大小值。

以上就是getSize()、getMode()的源碼淺析,有不足的地方,歡迎小伙伴們批評指正!

 

 

來自:http://www.jianshu.com/p/0883583f9074

 

 本文由用戶 xmqk1792 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!