GridLayoutManager這麼用,你可能還真沒嘗試過

前言

Android學習PDF+架構視頻+面試文檔+源碼筆記git

閱讀本文以前,你須要的一些知識儲備:github

  1. 對View的繪製流程有一些簡單的瞭解。
  2. Canvas的簡單實用。
  3. RecyclerView+GridLayoutManager的使用。

目錄

1、場景

使用RecyclerView+GridLayoutManager+ItemDecoration定製首頁適用的場景:面試

  • 有多個功能模塊
  • 子視圖多個樣式
  • 最後一個模塊須要刷新(若是有這樣的功能,確定也是經過RecyclerView實現的),例如QQ音樂中往下滑推薦用戶可能感興趣的音樂。

我的以爲該方案的意義在於減小布局的嵌套,讓界面管理變得更加簡單,可是對於業務特別複雜的狀況下可能會不適用。canvas

2、思路

實現以上功能須要解決兩個難點:架構

  1. 如何給不一樣行展現不一樣數量的子視圖
  2. 每一個模塊標題的繪製

這兩個問題的解決方案分別對應着GridLayoutManager和ItemDecoration,咱們挨個瞭解。ide

1. GridLayoutManager

GridLayoutManager其實咱們已經很熟悉了,只是咱們平時沒有了解SpanSize這個概念,先看以下一段代碼:佈局

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

上面的代碼中咱們建立了一個縱向、每行最多容量6個子View的GridLayoutManager,默認狀況下,一行總的SpanSize爲6,每一個子視圖默認的SpanSize爲1,因此不作處理的狀況下GridLayoutManager會將每一行分紅6份,每一份展現一個子視圖,以下圖的第一行:學習

這時,我若是將子視圖的SpanSize都設置爲2,那麼這個子視圖將佔整個RecyclerView可用寬度的2/6,如上圖第二行,同理,我將SpanSize上升爲3,那麼該子視圖的寬度也就上升爲可用的寬度的3/6,如上圖第三行,這也是GridLayoutManager可以在不一樣行設置不一樣數量的子視圖的緣由,固然了,你也能夠將同一行裏面的三個子視圖SpanSize分別設置爲一、二、3。好了,距離代碼實戰還差一個如何繪製標題。ui

2. ItemDecoration

分割線ItemDecoration是一個頗有意思的東西,由於它能夠實現一些好玩的東西,好比如下的通信錄的字母標題和時間軸:this

通信錄的字母標題 時間軸

|

|

還能夠利用它作一些特殊的效果,例如字母標題的吸頂,這裏我分別推薦兩個庫:

  • vivian的時間軸TimeLine
  • mcxtzhang的通信錄標題,可實現吸頂SuspensionIndexBar

這裏簡單的介紹一下ItemDecoration的原理,這裏我就默認同窗們已經瞭解View的測繪流程,主要分爲兩部分:

  1. 將分隔線繪製在RecyclerView子視圖的下層,由於分隔線ItemDecoration第一個繪製方法ItemDecoration#onDraw發生在繪製RecyclerView子視圖以前,若是你想讓其顯示出來,須要給ItemDecoration設置偏移量,讓子視圖偏移,從而不會遮擋ItemDecoration。
  2. 將分隔線繪製在RecyclerView子視圖的上層,由於其繪製方法ItemDecoration#onDrawOver發生在RecyclerView子視圖繪製繪製完成之後,這也是ItemDecoration可以實現吸頂的效果。

3、代碼實戰

有了上面的知識儲備,下面就簡單了。

1. 自定義ItemDecoration

自定義ItemDecoration須要實現的三個方法,跟咱們上面說起的原理相關:

方法名 解釋
onDraw 繪製子視圖下層的分隔線
getItemOffsets 一般爲了顯示下層分隔線而預留的空間
onDrawOver 繪製上層的分隔線

咱們的任務僅僅是繪製一個標題,因此使用上面的兩個方法就夠了。

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

    /**
     * 分類標籤
     */
    String getTag();

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

核心代碼就100多行:

/**
 * 適用於GridLayoutManager的分割線
 */
public class GridItemDecoration extends RecyclerView.ItemDecoration {
    // 記錄上次偏移位置 防止一行多個數據的時候視圖偏移
    private List<Integer> offsetPositions = new ArrayList<>();
    // 顯示數據
    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);
        // 繪製標題的邏輯:
        // 若是該行的數據的須要顯示的標題不一樣於上行的標題,就繪製標題
        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);
                }
            }
        }
    }
    /**
     * 繪製標題
     *
     * @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);
        // 預留邏輯:
        // 只要是標題下面的一行,不管這行幾個,都要預留空間給標題顯示
        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子視圖的位置處在標題的下方,那麼就須要預留空間,設置在outRect中,須要注意的是,同一行的多個子視圖都須要預留空間。
  2. 對不一樣於上一個數據標題的當前數據進行標題的繪製。
  3. 重複執行一、2。
2. 界面部分
public class SpecialGridActivity extends AppCompatActivity {

      // GridItem實現了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);

          // 建立GridLayoutManager,並設置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代碼中我將構建者模式部分省略了
        itemDecoration = new GridItemDecoration.Builder(this,values, 3)
                .setTitleTextColor(Color.parseColor("#4e5864"))
                .setTitleFontSize(22)
                .setTitleHeight(52)
                .build();
        mRecyclerView.addItemDecoration(itemDecoration);
    }

    // 數據初始化
    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後的青春記念手冊", "", R.drawable.head_3,"最近常聽",1,GridItem.TYPE_SMALL));

        values.add(new GridItem("流行創做女神你黴,泰勒斯威夫特的創做歷程", "", 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("愛情裏的酸甜苦辣,讓人捉摸不透", "聽完「靠近一點點」,他們等你翻牌", R.drawable.normal_2
                ,"更多爲你推薦",3,GridItem.TYPE_NORMAL));
        values.add(new GridItem("關於喜歡你這件事,我都寫在了歌裏", "「好想你」聽罷,聽它們吧", R.drawable.normal_3
                ,"更多爲你推薦",3,GridItem.TYPE_NORMAL));
        values.add(new GridItem("周杰倫暖心混剪,短短几分鐘是多少人的青春", "", R.drawable.special_1
                ,"更多爲你推薦",3,GridItem.TYPE_SPECIAL));
        values.add(new GridItem("我好想和你一塊兒聽雨滴", "給「發如雪」幾分,那這些呢", 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) {
              // 返回在數據中定義的SpanSize
            GridItem gridItem = values.get(i);
            return gridItem.getSpanSize();
        }
    }

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

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

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

與咱們平時使用GridLayoutManager不同的是,GridLayoutManager須要設置SpanSizeLookUp,就是須要咱們給每一個子視圖的設置SpanSize,由於咱們每一個數據都實現了IGridItem接口,該接口會向外提供SpanSize,因此這裏返回咱們在數據中設置的SpanSize便可。限於篇幅,佈局文件以及ReyclerAdapter的封裝就不貼了,感興趣的同窗能夠查看一下源代碼。如下就是咱們完成的效果:image

4、總結

源碼中的一些細節是頗有趣的,正是由於閱讀了GridLayoutManager的源碼,纔有了本文的出現。讀完本文以後,相信你和我同樣,對RecyclerView有了更深的瞭解。
Demo地址:https://github.com/mCyp/Orien...
Android學習PDF+架構視頻+面試文檔+源碼筆記

相關文章
相關標籤/搜索