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+架构视频+面试文档+源码笔记