這么用GridLayoutManager种吸,你可能還真沒嘗試過

前言

上周我在《抽絲剝繭RecyclerView - LayoutManager》一文中提到利用GridLayoutManager可以實(shí)現(xiàn)一個(gè)如下的首頁:

上期分享

有同學(xué)對(duì)此表示很感興趣,奈何沒有現(xiàn)成的案例毅否,于是自己就簡(jiǎn)單實(shí)現(xiàn)了一個(gè)矮固,最終效果如下:
效果

相信很多同學(xué)都和我有一樣的感覺,認(rèn)為GridLayoutManager只能實(shí)現(xiàn)標(biāo)準(zhǔn)的網(wǎng)格布局氯质,直到我前段時(shí)間決定研究RecyclerView募舟,看了GridLayoutManager的源碼,才發(fā)現(xiàn)闻察,原來它可以做更多的事拱礁,比如說,寫一個(gè)首頁辕漂。

閱讀本文之前呢灶,你需要的一些知識(shí)儲(chǔ)備

  1. 對(duì)View的繪制流程有一些簡(jiǎn)單的了解。
  2. Canvas的簡(jiǎn)單實(shí)用钉嘹。
  3. RecyclerView+GridLayoutManager的使用鸯乃。

目錄

目錄

一、場(chǎng)景

使用RecyclerView+GridLayoutManager+ItemDecoration定制首頁適用的場(chǎng)景:

  • 有多個(gè)功能模塊
  • 子視圖多個(gè)樣式
  • 最后一個(gè)模塊需要刷新(如果有這樣的功能跋涣,肯定也是通過RecyclerView實(shí)現(xiàn)的)缨睡,例如QQ音樂中往下滑推薦用戶可能感興趣的音樂。

個(gè)人覺得該方案的意義在于減少布局的嵌套仆潮,讓界面管理變得更加簡(jiǎn)單宏蛉,但是對(duì)于業(yè)務(wù)特別復(fù)雜的情況下可能會(huì)不適用。

二性置、思路

實(shí)現(xiàn)以上功能需要解決兩個(gè)難點(diǎn)

  1. 如何給不同行展示不同數(shù)量的子視圖
  2. 每個(gè)模塊標(biāo)題的繪制

這兩個(gè)問題的解決方案分別對(duì)應(yīng)著GridLayoutManagerItemDecoration拾并,我們挨個(gè)了解。

1. GridLayoutManager

GridLayoutManager其實(shí)我們已經(jīng)很熟悉了,只是我們平時(shí)沒有了解SpanSize這個(gè)概念嗅义,先看如下一段代碼:

GridLayoutManager gll = new GridLayoutManager(this, 6);
mRecyclerView.setLayoutManager(gll);

上面的代碼中我們創(chuàng)建了一個(gè)縱向屏歹、每行最多容量6個(gè)子View的GridLayoutManager,默認(rèn)情況下之碗,一行總的SpanSize為6蝙眶,每個(gè)子視圖默認(rèn)的SpanSize為1,所以不做處理的情況下GridLayoutManager會(huì)將每一行分成6份褪那,每一份展示一個(gè)子視圖幽纷,如下圖的第一行:

樣式

這時(shí),我如果將子視圖的SpanSize都設(shè)置為2博敬,那么這個(gè)子視圖將占整個(gè)RecyclerView可用寬度的2/6友浸,如上圖第二行,同理偏窝,我將SpanSize上升為3收恢,那么該子視圖的寬度也就上升為可用的寬度的3/6,如上圖第三行祭往,這也是GridLayoutManager能夠在不同行設(shè)置不同數(shù)量的子視圖的原因伦意,當(dāng)然了,你也可以將同一行里面的三個(gè)子視圖SpanSize分別設(shè)置為1硼补、2驮肉、3。

好了括勺,距離代碼實(shí)戰(zhàn)還差一個(gè)如何繪制標(biāo)題缆八。

2. ItemDecoration

分割線ItemDecoration是一個(gè)很有意思的東西,因?yàn)樗梢詫?shí)現(xiàn)一些好玩的東西疾捍,比如以下的通訊錄的字母標(biāo)題時(shí)間軸

通訊錄字母標(biāo)題
時(shí)間軸

還可以利用它做一些特殊的效果奈辰,例如字母標(biāo)題的吸頂,這里我分別推薦兩個(gè)庫:

這里簡(jiǎn)單的介紹一下ItemDecoration的原理奖恰,這里我就默認(rèn)同學(xué)們已經(jīng)了解View的測(cè)繪流程,主要分為兩部分:

  1. 將分隔線繪制在RecyclerView子視圖的下層宛裕,因?yàn)榉指艟€ItemDecoration第一個(gè)繪制方法ItemDecoration#onDraw發(fā)生在繪制RecyclerView子視圖之前瑟啃,如果你想讓其顯示出來,需要給ItemDecoration設(shè)置偏移量揩尸,讓子視圖偏移蛹屿,從而不會(huì)遮擋ItemDecoration
  2. 將分隔線繪制在RecyclerView子視圖的上層岩榆,因?yàn)槠淅L制方法ItemDecoration#onDrawOver發(fā)生在RecyclerView子視圖繪制繪制完成以后错负,這也是ItemDecoration能夠?qū)崿F(xiàn)吸頂?shù)男Ч?/li>

三坟瓢、代碼實(shí)戰(zhàn)

有了上面的知識(shí)儲(chǔ)備,下面就簡(jiǎn)單了犹撒。

1. 自定義ItemDecoration

自定義ItemDecoration需要實(shí)現(xiàn)的三個(gè)方法折联,跟我們上面提及的原理相關(guān):

方法名 解釋
onDraw 繪制子視圖下層的分隔線
getItemOffsets 通常為了顯示下層分隔線而預(yù)留的空間
onDrawOver 繪制上層的分隔線

我們的任務(wù)僅僅是繪制一個(gè)標(biāo)題,所以使用上面的兩個(gè)方法就夠了识颊。

1.1 定義數(shù)據(jù)接口
/**
 * 數(shù)據(jù)約束
 */
public interface IGridItem {
    /**
     * 是否啟用分割線
     * @return true
     */
    boolean isShow();

    /**
     * 分類標(biāo)簽
     */
    String getTag();

    /**
     * 權(quán)重
     */
    int getSpanSize();
}
1.2 自定義ItemDecoration類

核心代碼就100多行:

/**
 * 適用于GridLayoutManager的分割線
 */
public class GridItemDecoration extends RecyclerView.ItemDecoration {
    // 記錄上次偏移位置 防止一行多個(gè)數(shù)據(jù)的時(shí)候視圖偏移
    private List<Integer> offsetPositions = new ArrayList<>();
    // 顯示數(shù)據(jù)
    private List<? extends IGridItem> gridItems;
    // 畫筆
    private Paint mTitlePaint;
    // 存放文字
    private Rect mRect;
    // 顏色
    private int mTitleBgColor;
    private int mTitleColor;
    private int mTitleHeight;
    private int mTitleFontSize;
    private Boolean isDrawTitleBg = false;
    private Context mContext;
    // 總的SpanSize
    private int totalSpanSize;
    private int mCurrentSpanSize;

    //... 省略一些方法

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        // 繪制標(biāo)題的邏輯:
        // 如果該行的數(shù)據(jù)的需要顯示的標(biāo)題不同于上行的標(biāo)題诚镰,就繪制標(biāo)題
        final int paddingLeft = parent.getPaddingLeft();
        final int paddingRight = parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int pos = params.getViewLayoutPosition();
            IGridItem item = gridItems.get(pos);
            if (item == null || !item.isShow())
                            continue;
            if (i == 0) {
                drawTitle(c, paddingLeft, paddingRight, child
                                        , (RecyclerView.LayoutParams) child.getLayoutParams(), pos);
            } else {
                IGridItem lastItem = gridItems.get(pos - 1);
                if (lastItem != null && !item.getTag().equals(lastItem.getTag())) {
                    drawTitle(c, paddingLeft, paddingRight, child,
                                                (RecyclerView.LayoutParams) child.getLayoutParams(), pos);
                }
            }
        }
    }
    /**
     * 繪制標(biāo)題
     *
     * @param canvas 畫布
     * @param pl     左邊距
     * @param pr     右邊距
     * @param child  子View
     * @param params RecyclerView.LayoutParams
     * @param pos    位置
     */
    private void drawTitle(Canvas canvas, int pl, int pr, View child, RecyclerView.LayoutParams params, int pos) {
        if (isDrawTitleBg) {
            mTitlePaint.setColor(mTitleBgColor);
            canvas.drawRect(pl, child.getTop() - params.topMargin - mTitleHeight, pl
                                , child.getTop() - params.topMargin, mTitlePaint);
        }
        IGridItem item = gridItems.get(pos);
        String content = item.getTag();
        if (TextUtils.isEmpty(content))
                    return;
        mTitlePaint.setColor(mTitleColor);
        mTitlePaint.setTextSize(mTitleFontSize);
        mTitlePaint.setTypeface(Typeface.DEFAULT_BOLD);
        mTitlePaint.getTextBounds(content, 0, content.length(), mRect);
        float x = UIUtils.dip2px(20f);
        float y = child.getTop() - params.topMargin - (mTitleHeight - mRect.height()) / 2;
        canvas.drawText(content, x, y, mTitlePaint);
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        // 預(yù)留邏輯:
        // 只要是標(biāo)題下面的一行,無論這行幾個(gè)祥款,都要預(yù)留空間給標(biāo)題顯示
        int position = parent.getChildAdapterPosition(view);
        IGridItem item = gridItems.get(position);
        if (item == null || !item.isShow())
                    return;
        if (position == 0) {
            outRect.set(0, mTitleHeight, 0, 0);
            mCurrentSpanSize = item.getSpanSize();
        } else {
            if (!offsetPositions.isEmpty() && offsetPositions.contains(position)) {
                outRect.set(0, mTitleHeight, 0, 0);
                return;
            }
            if (!TextUtils.isEmpty(item.getTag()) && !item.getTag().equals(gridItems.get(position - 1).getTag())) {
                mCurrentSpanSize = item.getSpanSize();
            } else
                            mCurrentSpanSize += item.getSpanSize();
            if (mCurrentSpanSize <= totalSpanSize) {
                outRect.set(0, mTitleHeight, 0, 0);
                offsetPositions.add(position);
            }
        }
    }
}

總的邏輯就是:

  1. 如果所處的RecyclerView子視圖的位置處在標(biāo)題的下方清笨,那么就需要預(yù)留空間,設(shè)置在outRect中刃跛,需要注意的是函筋,同一行的多個(gè)子視圖都需要預(yù)留空間。
  2. 對(duì)不同于上一個(gè)數(shù)據(jù)標(biāo)題的當(dāng)前數(shù)據(jù)進(jìn)行標(biāo)題的繪制奠伪。
  3. 重復(fù)執(zhí)行1、2首懈。

2. 界面部分

public class SpecialGridActivity extends AppCompatActivity {

    // GridItem實(shí)現(xiàn)了IGridItem接口
    private List<GridItem> values;
    private RecyclerView mRecyclerView;
    private GridItemDecoration itemDecoration;
    // 自己封裝的RecyclerAdapter
    private RecyclerAdapter<GridItem> mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_special_grid);

        initWidget();
    }

    private void initWidget() {
        mRecyclerView = findViewById(R.id.rv_content);

        // 創(chuàng)建GridLayoutManager绊率,并設(shè)置SpanSizeLookup
        GridLayoutManager gll = new GridLayoutManager(this, 3);
        gll.setSpanSizeLookup(new SpecialSpanSizeLookup());
        mRecyclerView.setLayoutManager(gll);
        values = initData();
        
        // 自己封裝的RecyclerAdapter
        mRecyclerView.setAdapter(mAdapter = new RecyclerAdapter<GridItem>(values,null) {
            @Override
            public ViewHolder<GridItem> onCreateViewHolder(View root, int viewType) {
                switch (viewType) {
                    case R.layout.small_grid_recycle_item:
                        return new SmallHolder(root);
                    case R.layout.normal_grid_recycle_item:
                        return new NormalHolder(root);
                    case R.layout.special_grid_recycle_item:
                        return new SpecialHolder(root);
                    default:
                        return null;
                }

            }

            @Override
            public int getItemLayout(GridItem gridItem, int position) {
                switch (gridItem.getType()) {
                    case GridItem.TYPE_SMALL:
                        return R.layout.small_grid_recycle_item;
                    case GridItem.TYPE_NORMAL:
                        return R.layout.normal_grid_recycle_item;
                    case GridItem.TYPE_SPECIAL:
                        return R.layout.special_grid_recycle_item;
                }
                return 0;
            }
        });
        
        //...

        // 分隔線生成
        // 之前的GridItemDecoration代碼中我將構(gòu)建者模式部分省略了
        itemDecoration = new GridItemDecoration.Builder(this,values, 3)
                .setTitleTextColor(Color.parseColor("#4e5864"))
                .setTitleFontSize(22)
                .setTitleHeight(52)
                .build();
        mRecyclerView.addItemDecoration(itemDecoration);
    }

    // 數(shù)據(jù)初始化
    private List<GridItem> initData() {
        List<GridItem> values = new ArrayList<>();
        values.add(new GridItem("我很忙", "", R.drawable.head_1,"最近常聽",1,GridItem.TYPE_SMALL));
        values.add(new GridItem("治愈:有些歌比閨蜜更懂你", "", R.drawable.head_2,"最近常聽",1,GridItem.TYPE_SMALL));
        values.add(new GridItem("「華語」90后的青春紀(jì)念手冊(cè)", "", R.drawable.head_3,"最近常聽",1,GridItem.TYPE_SMALL));
      
        values.add(new GridItem("流行創(chuàng)作女神你霉,泰勒斯威夫特的創(chuàng)作歷程", "", R.drawable.special_2
                ,"更多為你推薦",3,GridItem.TYPE_SPECIAL));
        values.add(new GridItem("行走的CD寫給別人的歌", "給「跟我走吧」幾分究履,試試這些", R.drawable.normal_1
                ,"更多為你推薦",3,GridItem.TYPE_NORMAL));
        values.add(new GridItem("愛情里的酸甜苦辣滤否,讓人捉摸不透", "聽完「靠近一點(diǎn)點(diǎn)」,他們等你翻牌", R.drawable.normal_2
                ,"更多為你推薦",3,GridItem.TYPE_NORMAL));
        values.add(new GridItem("關(guān)于喜歡你這件事最仑,我都寫在了歌里", "「好想你」聽罷藐俺,聽它們吧", R.drawable.normal_3
                ,"更多為你推薦",3,GridItem.TYPE_NORMAL));
        values.add(new GridItem("周杰倫暖心混剪,短短幾分鐘是多少人的青春", "", R.drawable.special_1
                ,"更多為你推薦",3,GridItem.TYPE_SPECIAL));
        values.add(new GridItem("我好想和你一起聽雨滴", "給「發(fā)如雪」幾分泥彤,那這些呢", R.drawable.normal_4
                ,"更多為你推薦",3,GridItem.TYPE_NORMAL));
        values.add(new GridItem("油管周杰倫熱門單曲Top20", "「周杰倫」的這些哥欲芹,你聽了嗎", R.drawable.normal_5
                ,"更多為你推薦",3,GridItem.TYPE_NORMAL));

        return values;
    }

    class SpecialSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {

        @Override
        public int getSpanSize(int i) {
            // 返回在數(shù)據(jù)中定義的SpanSize
            GridItem gridItem = values.get(i);
            return gridItem.getSpanSize();
        }
    }

    class SmallHolder extends RecyclerAdapter.ViewHolder<GridItem> {    
        //... 代碼省略,就是設(shè)置圖片和文字的操作
        // 小的Holder
    }

    class NormalHolder extends RecyclerAdapter.ViewHolder<GridItem> {
        //... 中等的Holder
    }

    class SpecialHolder extends RecyclerAdapter.ViewHolder<GridItem> {
        //... 橫向大的Holder
    }
}

與我們平時(shí)使用GridLayoutManager不一樣的是吟吝,GridLayoutManager需要設(shè)置SpanSizeLookUp菱父,就是需要我們給每個(gè)子視圖的設(shè)置SpanSize,因?yàn)槲覀兠總€(gè)數(shù)據(jù)都實(shí)現(xiàn)了IGridItem接口剑逃,該接口會(huì)向外提供SpanSize浙宜,所以這里返回我們?cè)跀?shù)據(jù)中設(shè)置的SpanSize即可。

限于篇幅蛹磺,布局文件以及ReyclerAdapter的封裝就不貼了粟瞬,感興趣的同學(xué)可以查看一下源代碼。以下就是我們完成的效果:


效果

四萤捆、總結(jié)

總結(jié)

源碼中的一些細(xì)節(jié)是很有趣的裙品,正是因?yàn)殚喿x了GridLayoutManager的源碼俗批,才有了本文的出現(xiàn)。讀完本文之后清酥,相信你和我一樣扶镀,對(duì)RecyclerView有了更深的了解。

Demo地址:https://github.com/mCyp/Orient-Ui

如果你希望和RecyclerView有著更深入的交流焰轻,歡迎閱讀我的抽絲剝繭RecyclerView系列文章

第一篇:《抽絲剝繭RecyclerView - LayoutManager》
第二篇:《抽絲剝繭RecyclerView - 化整為零》
第三篇:《抽絲剝繭RecyclerView - ItemAnimator & Adapter》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末臭觉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辱志,更是在濱河造成了極大的恐慌蝠筑,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揩懒,死亡現(xiàn)場(chǎng)離奇詭異什乙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)已球,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門臣镣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人智亮,你說我怎么就攤上這事忆某。” “怎么了阔蛉?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵弃舒,是天一觀的道長。 經(jīng)常有香客問我状原,道長聋呢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任颠区,我火速辦了婚禮削锰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘毕莱。我一直安慰自己喂窟,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布央串。 她就那樣靜靜地躺著磨澡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪质和。 梳的紋絲不亂的頭發(fā)上稳摄,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音饲宿,去河邊找鬼厦酬。 笑死胆描,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的仗阅。 我是一名探鬼主播昌讲,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼减噪!你這毒婦竟也來了短绸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤筹裕,失蹤者是張志新(化名)和其女友劉穎醋闭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體朝卒,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡证逻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抗斤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片囚企。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瑞眼,靈堂內(nèi)的尸體忽然破棺而出洞拨,到底是詐尸還是另有隱情,我是刑警寧澤负拟,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站歹河,受9級(jí)特大地震影響掩浙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秸歧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一厨姚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧键菱,春花似錦谬墙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至侵蒙,卻和暖如春造虎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纷闺。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工算凿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留份蝴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓氓轰,卻偏偏與公主長得像婚夫,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子署鸡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 抽絲剝繭RecyclerView系列文章的目的在于幫助Android開發(fā)者提高對(duì)RecyclerView的認(rèn)...
    九心_閱讀 7,816評(píng)論 5 113
  • RecyclerView 概要 RecyclerView是Android 5.0開始提供一個(gè)可回收容器案糙,它比 Li...
    rexyren閱讀 5,623評(píng)論 10 27
  • 這篇文章分三個(gè)部分,簡(jiǎn)單跟大家講一下 RecyclerView 的常用方法與奇葩用法储玫;工作原理與ListView比...
    LucasAdam閱讀 4,391評(píng)論 0 27
  • 孤單一人走侍筛,天寒汗透身。 獨(dú)休山下石撒穷,思念卻深深匣椰。
    徐一村閱讀 332評(píng)論 0 1
  • 仿佛夢(mèng)跌到在了這茫茫的黑夜里窗戶敞開著被關(guān)在房子里的燈,放一些光出去尋它賣花的小女孩推銷給我的那支玫瑰還在床前花瓶...
    馬驥北閱讀 2,285評(píng)論 51 53