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

前言

上週我在《抽絲剝繭RecyclerView - LayoutManager》一文中提到利用GridLayoutManager能夠實現一個以下左邊的首頁:git

圖片 圖片
上期分享
效果

有同窗對此表示很感興趣,奈何沒有現成的案例,因而本身就簡單實現了一個,最終效果是上表中右側的圖。github

相信不少同窗都和我有同樣的感受,認爲GridLayoutManager只能實現標準的網格佈局,直到我前段時間決定研究RecyclerView,看了GridLayoutManager的源碼,才發現,原來它能夠作更多的事,好比說,寫一個首頁。canvas

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

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

目錄

目錄

1、場景

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

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

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

2、思路

實現以上功能須要解決兩個難點post

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

這兩個問題的解決方案分別對應着GridLayoutManagerItemDecoration,咱們挨個瞭解。ui

1. GridLayoutManager

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

GridLayoutManager gll = new GridLayoutManager(this, 6);
mRecyclerView.setLayoutManager(gll);
複製代碼

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

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

好了,距離代碼實戰還差一個如何繪製標題。

2. ItemDecoration

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

通信錄的字母標題 時間軸
通信錄字母標題
時間軸

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

這裏簡單的介紹一下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的封裝就不貼了,感興趣的同窗能夠查看一下源代碼。如下就是咱們完成的效果:

效果

4、總結

總結
源碼中的一些細節是頗有趣的,正是由於閱讀了 GridLayoutManager的源碼,纔有了本文的出現。讀完本文以後,相信你和我同樣,對 RecyclerView有了更深的瞭解。

Demo地址:github.com/mCyp/Orient…

若是你但願和RecyclerView有着更深刻的交流,歡迎閱讀個人抽絲剝繭RecyclerView系列文章

第一篇:《抽絲剝繭RecyclerView - LayoutManager》
第二篇:《抽絲剝繭RecyclerView - 化整爲零》

相關文章
相關標籤/搜索