一步步教你实现完整的复杂列表布局
来源:互联网 发布:centos 7iso镜像安装 编辑:程序博客网 时间:2024/06/05 05:54
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
引子:我们在工作中遇到最多的视图场景恐怕就是各种样式的列表了,这也是由手机屏幕有限的尺寸决定的,随着需求的日益丰满,我们会发现列表的样式也随之做着各种各样的变更:样式越来越多了,布局越来越复杂了,如果我们前面的布局是单纯将各种ViewGroup拼接到一块的,那改动起来就费事了,暂且不说数据量大引起的卡顿问题,面临的工作量绝不是修改布局文件就能搞定的,数据的绑定、事件触发的设置、滑动的处理、手势冲突的解决…甚至还可能要加上些高级UI特效。打开淘宝或京东,我们能看到这样的布局样式:
图1:
图2:
图3:
图4:
图5:
图6:
相信做过电商类app的朋友在产品拿到我们面前这样的页面时都纠结过如何去实现它,大概有这样几种思路:
1、 老老实实写布局,UI有多少内容统统手写出来。呵呵。
2、 利用滚动控件做嵌套,例如图1可以利用ScrollView嵌套多个GridView实现,图2、4、5可以利用ListView嵌套GridView实现;
3、 利用RecyclerView的多级嵌套实现,例如实现这样的布局:
当数据量较大、分屏页数较多的时候,2和3会出现明显的卡顿,这是因为cpu需要同时处理各个滑动布局的内部item位置关系以及数据的赋值,这样一来就容易出现cpu和gpu的计算与展示出现不同步的现象,导致屏幕显示丢帧,造成视觉卡顿的现象,尤其是当item的布局又很复杂时更容易出现此情况,甚至可能出现oom。
4、 使用RecyclerView实现全布局,用一个RecyclerView实现复杂的布局列表,这一种是完全符合谷歌的设计标准的,同时充分利用了RecyclerView双缓存的原理,下面我通过一个很典型的例子,带着大家一步步实现它,并通过该例子,解析以上几种常见的布局样式,相信看完后你可能跟我有同样的感觉:绝大部分的复杂列表都是有规可循的。
该篇将会依次介绍到:
1. RecyclerView的双缓存技术简介
2. 复杂布局的典型样式;
3. 实现典型复杂布局的列表适配器;
4. 实现RecyclerView的上拉加载功能;
5. 分割线的原理介绍,实现复杂布局的分割线;
6. 添加空布局的实现
7. 逐个分析以上电商app出现的页面布局,提供具体的实现思路。
说明:本文旨在为实现复杂列表布局提供一种完整的思路,并没有做过多的封装,希望能起到抛砖引玉的作用,文中所涉及代码已上传至GitHub,文末给出链接。另外,我打算一篇文章讲完所有内容,不再做切割,如果有对RecyclerView不了解的同学,建议先去查阅相关资料。
一、 RecyclerView的双缓存技术简单介绍:
RecyclerView内部维护了一个二级缓存(算上用户设置的,实际上拥有三级缓存),这些缓存是由RecyclerView的一个final类型的内部类所管理的,实际上由其以下缓存变量决定:
缓存与复用的原理,看下 Google IO 视频中的一张截图:
我们看到,当ViewHolder滑出页面时,会暂时存放到Cache中,而从Cache中移除的holder,会存放到RecyclerViewPool的循环缓存池之中,默认情况下,Cache缓存2个holder,RecyclerViewPool缓存5个holder,另外不同的viewType的缓存互相没有影响。
二、 复杂布局的典型样式:
实际上,以上列举的布局样式,可以大致归纳为下图所示:
看着好复杂的样子,举个例子:
我们约定:
列表的最顶部的布局叫做Header,例如常见的轮播图;
列表中间区域我们称之为分组;
分组的标题部分我们称之为SectionHeader;
分组的内容项我们称之为SectionBody;
分组的结尾称之为SectionFooter;
列表的结束布局称之为Footer;
三、 实现典型复杂布局的列表适配器:
先看下我们要实现的效果:
首先定义一个抽象类SectionedRecyclerViewAdapter,继承RecyclerView.Adapter,
接着对数据分组,定义四个数组,分别记录每项分组的头部数据的section的位置,分组内的每一项的position的位置:
//用来保存分组section位置 private int[] sectionForPosition = null; //用来保存分组内的每项的position位置 private int[] positionWithinSection = null; //用来记录每个位置是否是一个组内Header private boolean[] isHeader = null; //用来记录每个位置是否是一个组内Footer private boolean[] isFooter = null; //item的总数,注意,是总数,包含所有项 private int count = 0;
例如有这样一种数据结构:
,
其中的年级个班级可分别表示为section和position;
接着准备游标,标记各组的view:
//用来标记每个分组的Header protected static final int TYPE_SECTION_HEADER = -1; //用来标记每个分组的Footer protected static final int TYPE_SECTION_FOOTER = -2; //用来标记每个分组的内容 protected static final int TYPE_ITEM = -3; //用来标记整个列表的Header protected static final int TYPE_HEADER = 0; //顶部HeaderView //用来标记整个列表的Footer protected static final int TYPE_FOOTER = 1; //底部FooterView //上拉加载更多 public static final int PULLUP_LOAD_MORE = 0; //正在加载中 public static final int LOADING_MORE = 1; //加载完成 public static final int LOADING_FINISH = 2; //空布局 public static final int TYPE_EMPTY = -4; //上拉加载默认状态--默认为-1 public int load_more_status = -1;
源码里我尽可能的都加上了注释,这里我只截取关键部分,
实际上最关键的部分是做各个item所在位置关系的计算:
第1步:计算出item的总数量,这里定义了一个抽象方法,用来标识当前的分组是否含有SectionFooter,有的话,遍历时要多加1;
第2步:得到item的总数量后,初始化几个数组:初始化与position相对应的section数组,初始化与section相对应的position的数组,初始化当前位置是否是一个Header的数组,初始化当前位置是否是一个Footer的数组;
第3步:通过计算每个item的位置信息,将上一步初始化后的数组填充数据,最终这几个数组保存了每个位置的item的状态信息,即:是否是header,是否是footer,所在的position是多少,所在的section是多少:
private void setupPosition() { count = countItems();//计算出item的总数量 setupArrays(count);//得到item的总数量后,初始化几个数组:初始化与position相对应的section数组,初始化与section相对应的position // 的数组,初始化当前位置是否是一个Header的数组,初始化当前位置是否是一个Footer的数组 calculatePositions();//通过计算每个item的位置信息,将上一步初始化后的数组填充数据,最终这几个数组保存了每个位置的item // 的状态信息,即:是否是header,是否是footer,所在的position是多少,所在的section是多少 } /** * 计算item的总数量 * * @return */ private int countItems() { int count = 0; int sections = getSectionCount(); for (int i = 0; i < sections; i++) { count += 1 + getItemCountForSection(i) + (hasFooterInSection(i) ? 1 : 0); } return count; } /** * 通过item的总数量,初始化几个数组:初始化与position相对应的section数组, * 初始化与section相对应的position的数组,初始化当前位置是否是一个Header的数组, * 初始化当前位置是否是一个Footer的数组 * * @param count */ private void setupArrays(int count) { sectionForPosition = new int[count]; positionWithinSection = new int[count]; isHeader = new boolean[count]; isFooter = new boolean[count]; } /** * 通过计算每个item的位置信息,将上一步初始化后的数组填充数据, * 最终这几个数组保存了每个位置的item的状态信息,即:是否是header,是否是footer, * 所在的position是多少,所在的section是多少 */ private void calculatePositions() { int sections = getSectionCount(); int index = 0; for (int i = 0; i < sections; i++) { setupItems(index, true, false, i, 0); index++; for (int j = 0; j < getItemCountForSection(i); j++) { setupItems(index, false, false, i, j); index++; } if (hasFooterInSection(i)) { setupItems(index, false, true, i, 0); index++; } } } /** * 保存每个位置对应的数据信息 * * @param index 从0开始的每个最小单位所在的位置,从0开始,到count结束 * @param isHeader 所在index位置的item是否是header * @param isFooter 所在index位置的item是否是footer * @param section 所在index位置的item对应的section * @param position 所在index位置的item对应的position */ private void setupItems(int index, boolean isHeader, boolean isFooter, int section, int position) { this.isHeader[index] = isHeader; this.isFooter[index] = isFooter; sectionForPosition[index] = section; positionWithinSection[index] = position; }
RecyclerView有一个内部类AdapterDataObserver,看起名称就知道是用来监控adapter数据集变化的,我们自定义一个内部类,继承它,复写onChanged()方法,当数据集合放生变化时,重新计算各ItemView之间的位置关系:
//定义一个内部类,每当数据集合发生改变时,设置控件的位置信息 class SectionDataObserver extends RecyclerView.AdapterDataObserver { @Override public void onChanged() { setupPosition(); checkEmpty();//检查数据是否为空,设置空布局 } @Override public void onItemRangeInserted(int positionStart, int itemCount) { checkEmpty();//检查数据是否为空,设置空布局 } @Override public void onItemRangeRemoved(int positionStart, int itemCount) { checkEmpty();//检查数据是否为空,设置空布局 } }
接着复写onCreateViewHolder,绑定布局类型,这里定义了6种类型的布局:
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { RecyclerView.ViewHolder viewHolder; if (viewType == TYPE_EMPTY) { viewHolder = new EmptyViewHolder(emptyView); } else { if (isSectionHeaderViewType(viewType)) { viewHolder = onCreateSectionHeaderViewHolder(parent, viewType); } else if (isSectionFooterViewType(viewType)) { viewHolder = onCreateSectionFooterViewHolder(parent, viewType); } else if (isFooterViewType(viewType)) { viewHolder = onCreateFooterViewHolder(parent, viewType); } else if (isHeaderViewType(viewType)) { viewHolder = onCreateHeaderViewHolder(parent, viewType); } else { viewHolder = onCreateItemViewHolder(parent, viewType); } } return viewHolder; }
接着实现onBindViewHolder,这里做了不同情况的区分:当整个列表拥有头布局的时候是一种情况,没有头布局的时候是一种情况:
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (emptyViewVisible) {//此时数据集为空,需要设置空布局 } else { setViewHolder(holder, position); } } private void setViewHolder(RecyclerView.ViewHolder holder, final int position) { if (hasHeader()) {//如果整个列表有header if (position == 0) { onBindHeaderViewHolder((RH) holder); } else if (position + 1 < getItemCount()) { final int section = sectionForPosition[position - 1]; int index = positionWithinSection[position - 1]; if (isSectionHeaderPosition(position - 1)) {//当前位置是分组header onBindSectionHeaderViewHolder((H) holder, section); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onSectionHeaderClickListener.onSectionHeaderClick(section); } }); } else if (isSectionFooterPosition(position - 1)) {//当前位置是分组的footer onBindSectionFooterViewHolder((F) holder, section); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (onSectionFooterClickListener != null) { onSectionFooterClickListener.onSectionFooterClick(section); } } }); } else {//当前位置是组内item onBindItemViewHolder((VH) holder, section, index); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onItemClickListener.onItemClick(section, position - 1); } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { if (onItemLongClickListener != null) { onItemLongClickListener.onItemLongClick(section, position - 1); } return true; } }); } } else {//当前位置是整个列表的footer onBindFooterViewHolder((FO) holder); } } else {//整个列表没有Header if (position + 1 < getItemCount()) { final int section = sectionForPosition[position]; int index = positionWithinSection[position]; if (isSectionHeaderPosition(position)) {//当前位置是分组Header onBindSectionHeaderViewHolder((H) holder, section); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (onSectionHeaderClickListener != null) { onSectionHeaderClickListener.onSectionHeaderClick(section); } } }); } else if (isSectionFooterPosition(position)) {//当前位置是分组footer onBindSectionFooterViewHolder((F) holder, section); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (onSectionFooterClickListener != null) { onSectionFooterClickListener.onSectionFooterClick(section); } } }); } else {//当前位置是分组的item onBindItemViewHolder((VH) holder, section, index); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (onItemClickListener != null) { onItemClickListener.onItemClick(section, position); } } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { if (onItemLongClickListener != null) { onItemLongClickListener.onItemLongClick(section, position); } return true; } }); } } else {//当前位置是整个列表的footer onBindFooterViewHolder((FO) holder); } } }
接着复写getItemViewType,告知RecyclerView在各个位置的item是属于哪一个布局类型的:
@Override public int getItemViewType(int position) { if (sectionForPosition == null) { setupPosition(); } if (emptyViewVisible) { return TYPE_EMPTY; } else { if (hasHeader()) { if (position == 0) { return getHeaderViewType(); } else if (position + 1 < getItemCount()) { int section = sectionForPosition[position - 1]; int index = positionWithinSection[position - 1]; if (isSectionHeaderPosition(position - 1)) { return getSectionHeaderViewType(section); } else if (isSectionFooterPosition(position - 1)) { return getSectionFooterViewType(section); } else { return getSectionItemViewType(section, index); } } return getFooterViewType(); } else { if (position + 1 < getItemCount()) { int section = sectionForPosition[position]; int index = positionWithinSection[position]; if (isSectionHeaderPosition(position)) { return getSectionHeaderViewType(section); } else if (isSectionFooterPosition(position)) { return getSectionFooterViewType(section); } else { return getSectionItemViewType(section, index); } } return getFooterViewType(); } } }
这样我们的基本的多布局的adapter基类就算完成了,里面我定义了分项item的点击事件和取数据的方法;
使用很简单,我们定义好各项的布局:Header的布局、Footer的布局、SectionHeader的布局、SectionFooter的布局、上拉加载的布局,接着实现各自的ViewHolder:
public class FooterHolder extends RecyclerView.ViewHolder { public TextView tvFooter; public FooterHolder(View itemView) { super(itemView); initView(); } private void initView() { tvFooter = (TextView) itemView.findViewById(R.id.tv_footer); }}
,然后就可以定义我们具体的适配器,让它继承自写好的SectionedRecyclerViewAdapter,在里面完成数据的绑定与展示,在这里,有个地方需要注意的是,需要动态设置SectionBody的每个item长和宽,并设置其左右边距,需要做一个计算,看图:
代码设置:
@Override protected void onBindItemViewHolder(EvaluateSectionBodyHolder holder, int section, int position) { int screenWidth = DisplayUtil.getScreenWidthPixels((Activity)mContext); int imgWidth = (screenWidth - DisplayUtil.dp2px(mContext, 40)) / 3; ViewGroup.MarginLayoutParams params = null; if (holder.llRoot.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { params = (ViewGroup.MarginLayoutParams) holder.llRoot.getLayoutParams(); } else { params = new ViewGroup.MarginLayoutParams(holder.llRoot.getLayoutParams()); } params.width = imgWidth; params.height = imgWidth; if (position % 3 == 0) { params.leftMargin = DisplayUtil.dp2px(mContext, 10); } else if (position % 3 == 1) { params.leftMargin = DisplayUtil.dp2px(mContext, 20/3); } else { params.leftMargin = DisplayUtil.dp2px(mContext, 10/3); } holder.llRoot.setLayoutParams(params); }
接着,每行的列数,实际上是由GridLayoutManager.SpanSizeLookup这个类去控制的,我们继承它,实现控制我们任何地方要展示的列数:
public class SectionedSpanSizeLookup extends GridLayoutManager.SpanSizeLookup { protected SectionedRecyclerViewAdapter<?, ?, ?, ?, ?> adapter = null; protected GridLayoutManager layoutManager = null; public SectionedSpanSizeLookup(SectionedRecyclerViewAdapter<?, ?, ?, ?, ?> adapter, GridLayoutManager layoutManager) { this.adapter = adapter; this.layoutManager = layoutManager; } @Override public int getSpanSize(int position) { if (adapter.hasHeader()) { if (position == 0) { return layoutManager.getSpanCount(); } else if (position + 1 < adapter.getItemCount()) { if (adapter.isSectionHeaderPosition(position -1) || adapter.isSectionFooterPosition(position -1)) { return layoutManager.getSpanCount(); } else { return 1; } } else { return layoutManager.getSpanCount(); } } else { if (position + 1 < adapter.getItemCount()) { if (adapter.isSectionHeaderPosition(position) || adapter.isSectionFooterPosition(position)) { return layoutManager.getSpanCount(); } else { return 1; } } else { return layoutManager.getSpanCount(); } } }}
四、 实现RecyclerView的上拉加载功能:
我们需要监听RecyclerView的OnScrollListener,定义一个类,继承自它,这里我们需要做的是:
1、 当滚动状态为SCROLL_STATE_IDLE时,判断当前item的总数是否填充满了一屏,如果没满,也就没有上拉加载了;
2、 当可见item的最后一个可见的item的位置与item的总数一致时,进行下一步;
3、 加一个标识符isLoading,为true表示正在请求,请求结束后置为false,防止多次请求;
这里有一个细节,就是滑动边界的容差值,当childView边界完全显示在界面中时才会检测成功.这就导致了一个可能的情况是只差一点点滑动到边界时,也不会检测成功而出发上拉加载的回调,所以要求很高的灵敏度,故加上上下两个容差值,我们认为,当滑动到接近边界时,就认为需要进行上拉加载了,关键代码如下:
/** * 检查是否满一屏 * * @param recyclerView * @return */ public boolean isFullAScreen(RecyclerView recyclerView) { //获取item总个数,一般用mAdapter.getItemCount(),用mRecyclerView.getLayoutManager().getItemCount()也可以 //获取当前可见的item view的个数,这个数字是不固定的,随着recycleview的滑动会改变, // 比如有的页面显示出了6个view,那这个数字就是6。此时滑一下,第一个view出去了一半,后边又加进来半个view,此时getChildCount() // 就是7。所以这里可见item view的个数,露出一半也算一个。 int visiableItemCount = recyclerView.getChildCount(); if (visiableItemCount > 0) { View lastChildView = recyclerView.getChildAt(visiableItemCount - 1); //获取第一个childView View firstChildView = recyclerView.getChildAt(0); int top = firstChildView.getTop(); int bottom = lastChildView.getBottom(); //recycleView显示itemView的有效区域的bottom坐标Y int bottomEdge = recyclerView.getHeight() - recyclerView.getPaddingBottom() + bottomOffset; //recycleView显示itemView的有效区域的top坐标Y int topEdge = recyclerView.getPaddingTop() + topOffset; //第一个view的顶部小于top边界值,说明第一个view已经部分或者完全移出了界面 //最后一个view的底部小于bottom边界值,说明最后一个view已经完全显示在界面 //若满足这两个条件,说明所有子view已经填充满了recycleView,recycleView可以"真正地"滑动 if (bottom <= bottomEdge && top < topEdge) { //满屏的recyceView return true; } return false; } else { return false; } }
@Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (isFullAScreen(recyclerView)) { //查找最后一个可见的item的position lastItemPosition = gridLayoutManager.findLastVisibleItemPosition(); if (newState == RecyclerView.SCROLL_STATE_IDLE && lastItemPosition + 1 == gridLayoutManager.getItemCount()) { if (!isLoading) { onLoadMore(); } } } }
五、 分割线的原理介绍,实现复杂布局的分割线:
要实现分割线,需要自定义类继承自RecyclerView.ItemDecoration,ItemDecoration有三个方法提供给开发者拓展,依次为:
getItemoffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state ):outRect为包裹在itemView外层View的坐标参数,如下图,例如:设置outRect.set(0,0,0,0);表示itemView的四周没有任何的分割空间存在,set的四个参数分别表示外围View距离itemView左边、上方、右边、下方四个方向的间隔距离,尤其需要注意的是,在此处设置了outRect的四个方向的参数之后,会默认将这个间距设置到itemView的四个方向的padding上,由此我们可以知道,getItemoffsets这个方法是我们必须要复写且实现的。
onDraw(Canvas c, RecyclerView parent, RecyclerView.State state):外围View的绘制将被itemView遮盖。
onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state):外围View将绘制在itemView之上,即遮盖itemView,当我们绘制一些特殊需求时,此方法很是受用,例如:为每个item绘制一个角标,表示其状态,例如很多商品都有热卖或者优惠券的角标,利用画笔将bitmap绘制出即可:
我先把我们最终要实现的效果图贴出来,看下我们要绘制的分割线的模样和位置:
我们看到,只有每个分组的顶部存在分割线,并且列表的第一个的顶部是不需要分割线的,实现的思路也很简单,我们只需要计算出每个分组Header所在的位置,然在在Header的顶部设置好外围空间即可:
@Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int totalCount = parent.getAdapter().getItemCount(); int itemPosition = parent.getChildAdapterPosition(view); if (isDraw(parent, view, totalCount)) { if (itemPosition == 0) { outRect.set(0, 0, 0, 0); } else { outRect.set(0, mDividerHeight, 0, 0); } } } /** * 是否可以绘制分割线 * @param parent 当前的RecyclerView * @param itemView 当前的内容项 * @param totalCount 适配器的item总数,可能大于RecyclerView的item总数 * @return */ private boolean isDraw(RecyclerView parent, View itemView, int totalCount) { int itemPosition = parent.getChildAdapterPosition(itemView); if (totalCount > 1 && itemPosition < totalCount - 1) {//要除去footer占有的一个位置 if (parent.getAdapter() instanceof SectionedRecyclerViewAdapter) { if (((SectionedRecyclerViewAdapter) parent.getAdapter()).isSectionHeaderPosition (itemPosition)) { return true; } } } return false; } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); drawHorizontal(c, parent); } /** * 绘制水平的分割线 * @param c * @param parent */ private void drawHorizontal(Canvas c, RecyclerView parent) { int totalCount = parent.getAdapter().getItemCount(); //获取当前可见的item的数量,半个也算 int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { //获取当前可见的view View child = parent.getChildAt(i); if (isDraw(parent, child, totalCount)) { RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); int left = child.getLeft() - params.leftMargin;//组件在容器X轴上的起点,需要注意,如果用户设置了left方向的Margin值,需要在取得itemViewleft属性后,将该margin抵消掉,因为,用户设置margin的意图明显不是想让分割线覆盖掉的 int right = child.getRight() + params.rightMargin ; int top = child.getTop() - mDividerHeight - params.bottomMargin;//组件在容器Y轴上的起点 int bottom = top + mDividerHeight; if (mDividerDrawable != null) { mDividerDrawable.setBounds(left, top, right, bottom); mDividerDrawable.draw(c); } if (mDividerPaint != null) { c.drawRect(left, top, right, bottom, mDividerPaint); } } } }
需要注意,如果用户设置了各方向的Margin值,需要在取得itemView的各方向的margin属性后,将该margin抵消掉,因为,用户设置margin的意图明显不是想让分割线覆盖掉的。
最后补充的是,各子View的点击事件,实际上,在SectionedRecyclerViewAdapter之中已经预定义了点击回调的接口,我们依然可以通过EventBus这种广播框架便捷的实现我们的效果,不赘述。
以上便是实现一个典型的复杂布局的全部思路过程,源码在最后放出,基于此,下面挨个突破各种不规则的复杂布局。
六、 添加空布局
对于空布局,我们希望在使用的时候只需要一行代码setEmtyView(view),就行了,adapter无数据时自动调用空布局,看下实现思路:
首先在SectionedRecyclerViewAdapter中添加两个私有变量:
private View emptyView; private boolean emptyViewVisible; public void setEmptyView(View emptyView) { this.emptyView = emptyView; }
一个是空布局View,一个是记录空布局是否显示,然后定义一个方法checkEmpty(),当数据集合发生改变时,检查是否是空布局:
private void checkEmpty() { if (emptyView != null) { if (hasHeader()) { emptyViewVisible = getItemCount() == 2; } else { emptyViewVisible = getItemCount() == 1; } emptyView.setVisibility(emptyViewVisible ? View.VISIBLE : View.GONE); } }
最后再修改onCreateViewHolder()和onBindViewHolder()方法,添加空布局的情况即可,使用的时候,只需要adapter.setEmptyView(view)一行代码。
七、 逐个分析以上电商app出现的页面布局,提供具体的实现思路
先看淘宝的首页,也就是上面图1,整个滚动视图,顶部是一个广告轮播,接着是一个分类的网格视图,再接着是类似公告的广告显示区域,下面是一条分割线,再往下又是一个网格视图,广告轮播我们可以当做整个列表的顶部Header,广告公告的视图当做每一个分组的footer,不需要显示的做隐藏。当然分类区域每行的列数和分割线下面的推荐商品区域的每行列数有差异的,这个在SpanSizeLookup这个继承类中去控制即可。我们又看到,不仅列数不一致,连样式都变了!实际上,有两种方案可以控制样式的变动,一种是将所有的样式都写好到一个布局里面,控制其显示与隐藏(可以使用ViewStub做隐藏与显示),考虑到性能问题,不大推荐这种方法,另外一种是为不同的分组加一个类型判断,例如section=0的分组是分类的分组类型,我们定义一个SECTION_TYPE_0的常量来标识它,section=1的分组是商品推荐分组类型,我们再定义一个常量SECTION_TYPE_1的常量来标识它,以此类推,然后修改我们写好的SectionedRecyclerViewAdapter这个类,添加泛型类型,然后分别在复写方法onCreateViewHolder、onBindViewHolder、和getItemViewType里作区分,最后在我们继承SpanSizeLookup的类中,按照定义好的常量去做区分即可,思路有了,实现起来并不复杂。实际上,甚至连分割线以上的部分,即:轮播、分类、广告公告都可以成为列表的Header,只不过这样的话,分类需要我们单独去完成布局的构建,单省去了分组布局多类型的步骤。
图2、3、4、5和我们的demo一样,按思路实现即可。
接着看京东的分类,这是一个非典型的分类布局,不过依然可以找到规则,思路是,将分组的标题、输入价格的两个控件、以及下方的分割线合并到一起,作为每个分组的Header便可轻松解决,在需要的位置控制相关控件的隐藏与显示即可。画出来是这样的:
八、 小结:加点题外话吧,实际上,技术的积累没有什么捷径可言,点点滴滴都是自己一步一个脚印走过来的,在学习新的东西的时候,我们除了要抱着务实本分的基本原则外,我觉得还要有敢于钻研、不怕争辩的的态度。最后,路漫漫其修远兮,希望各位都能在后半年有长足的进步,完善自己的一套知识体系。
源码地址:https://github.com/gycold/SectionRecyclerViewAdapter
- 一步步教你实现完整的复杂列表布局
- 一步步教你实现完整的复杂列表布局
- 一步步教你实现完整的复杂列表布局
- 不一样的RecyclerView优雅实现复杂列表布局(一)
- 不一样的RecyclerView优雅实现复杂列表布局(二)
- 学习RecyclerView优雅实现复杂列表布局
- 复杂的列表布局 开发思路
- Android复杂列表的实现
- RecycleView实现复杂的布局
- Recyclview复杂布局的实现
- 教你一步步搭建和运行完整的开源搜索引擎
- 浅谈带有复杂布局列表项的列表视图
- RecyclerView下拉刷新、上拉加载更多以及复杂列表布局的实现
- 一步步教你实现跨游览器的JS日历
- 学习的步伐(二)Kotlin 实现Recyclerview列表(补充:tab选项卡+CoordinatorLayout收缩布局+复杂Recyclerview列表)
- 一步步教你实现弹出窗口
- 教你一步步实现一个虚拟摇杆
- 教你一步步实现bibibi弹幕功能。
- Unity中贴图融合之弹痕融合
- 八、rabbitMQ RPC
- Arrays-----118. Pascal's Triangle
- python历史以及基础知识
- 字符,字符编码,字符集,mysql字符
- 一步步教你实现完整的复杂列表布局
- 改变鼠标形状
- app中进行wifi的打开与关闭
- bzoj 1050: [HAOI2006]旅行comf(尺取+最短路)
- 时间规划
- Weights Update
- Ubuntu中用到的指令
- 《SDN核心技术剖析和实战指南》
- 简单描述Struts2.x的运行过程和Struts2.x的标签使用