Android學習PDF+架構視頻+面試文檔+源碼筆記git
閱讀本文以前,你須要的一些知識儲備:github
使用RecyclerView+GridLayoutManager+ItemDecoration定製首頁適用的場景:面試
我的以爲該方案的意義在於減小布局的嵌套,讓界面管理變得更加簡單,可是對於業務特別複雜的狀況下可能會不適用。canvas
實現以上功能須要解決兩個難點:架構
這兩個問題的解決方案分別對應着GridLayoutManager和ItemDecoration,咱們挨個瞭解。ide
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
分割線ItemDecoration是一個頗有意思的東西,由於它能夠實現一些好玩的東西,好比如下的通信錄的字母標題和時間軸:this
通信錄的字母標題 | 時間軸 |
---|---|
|
|
還能夠利用它作一些特殊的效果,例如字母標題的吸頂,這裏我分別推薦兩個庫:
這裏簡單的介紹一下ItemDecoration的原理,這裏我就默認同窗們已經瞭解View的測繪流程,主要分爲兩部分:
有了上面的知識儲備,下面就簡單了。
自定義ItemDecoration須要實現的三個方法,跟咱們上面說起的原理相關:
方法名 | 解釋 |
---|---|
onDraw |
繪製子視圖下層的分隔線 |
getItemOffsets |
一般爲了顯示下層分隔線而預留的空間 |
onDrawOver |
繪製上層的分隔線 |
咱們的任務僅僅是繪製一個標題,因此使用上面的兩個方法就夠了。
/** * 數據約束 */ public interface IGridItem { /** * 是否啓用分割線 * @return true */ boolean isShow(); /** * 分類標籤 */ String getTag(); /** * 權重 */ int getSpanSize(); }
核心代碼就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); } } } }
總的邏輯就是:
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的封裝就不貼了,感興趣的同窗能夠查看一下源代碼。如下就是咱們完成的效果:
源碼中的一些細節是頗有趣的,正是由於閱讀了GridLayoutManager的源碼,纔有了本文的出現。讀完本文以後,相信你和我同樣,對RecyclerView有了更深的瞭解。
Demo地址:https://github.com/mCyp/Orien...
Android學習PDF+架構視頻+面試文檔+源碼筆記