本文是这个系列的第三篇,不出意外也是终结篇。由于使用通过重构后的控件已经能够快速实现市面上带 索引导航、悬停分组的列表界面了。
在前两篇里,咱们从0开始,一步一步实现了仿微信通信录、饿了么选餐界面。
(第一篇戳我 第二篇戳我)
这篇文章做为终结篇,和前文相比,主要涉及如下内容:git
代码传送门:喜欢的话,随手点个star。多谢
github.com/mcxtzhang/S…github
老规矩,先上图:
。
api
本文将先举例子如何写,并对其中涉及到的重构部分进行讲解。
若有不明者,建议先观看(第一篇戳我 第二篇戳我),
以及下载Demo,边看代码边阅读,效果更佳。微信
转载请标明出处: gold.xitu.io/post/583c13…
本文出自:【张旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代码传送门:喜欢的话,随手点个star。多谢
github.com/mcxtzhang/S…数据结构
先从简单的用法看起,微信通信录界面和普通的 分组悬停&索引导航 的列表相比:ide
实现:
HeaderView不是本文讨论重点,随意实现之。我用的是我本身以前写的,戳我布局
因为布局一致,则咱们确定偷懒直接用主体Item的Bean,将city设置为相应的数据便可,如 “新的朋友”:post
public class CityBean extends BaseIndexPinyinBean {
private String city;//城市名字复制代码
去掉分组悬停,咱们须要重写isShowSuspension()
方法,返回false。this
它们是一组的,则索引title一致,且须要自定义。
四个头部的Bean调用setBaseIndexTag()
方法,set自定义的title,且一致便可。spa
mDatas.add((CityBean) new CityBean("新的朋友").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
mDatas.add((CityBean) new CityBean("群聊").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
mDatas.add((CityBean) new CityBean("标签").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
mDatas.add((CityBean) new CityBean("公众号").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));复制代码
在CityBean
里引入一个字段 isTop
public class CityBean extends BaseIndexPinyinBean {
private String city;//城市名字
private boolean isTop;//是不是最上面的 不须要被转化成拼音的
...
@Override
public String getTarget() {
return city;
}
@Override
public boolean isNeedToPinyin() {
return !isTop;
}
@Override
public boolean isShowSuspension() {
return !isTop;
}
}复制代码
初始化:
mRv.addItemDecoration(mDecoration = new SuspensionDecoration(this, mDatas));
//indexbar初始化
mIndexBar.setmPressedShowTextView(mTvSideBarHint)//设置HintTextView
.setNeedRealIndex(true)//设置须要真实的索引
.setmLayoutManager(mManager);//设置RecyclerView的LayoutManager复制代码
数据加载:
mDatas = new ArrayList<>();
//微信的头部 也是能够右侧IndexBar导航索引的,
// 可是它不须要被ItemDecoration设一个标题titile
mDatas.add((CityBean) new CityBean("新的朋友").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
mDatas.add((CityBean) new CityBean("群聊").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
mDatas.add((CityBean) new CityBean("标签").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
mDatas.add((CityBean) new CityBean("公众号").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
for (int i = 0; i < data.length; i++) {
CityBean cityBean = new CityBean();
cityBean.setCity(data[i]);//设置城市名称
mDatas.add(cityBean);
}
...
mIndexBar.setmSourceDatas(mDatas)//设置数据
.invalidate();
mDecoration.setmDatas(mDatas);复制代码
上文提到,重构后,SuspensionDecoration
数据源依赖的接口是ISuspensionInterface
,
以下:
public interface ISuspensionInterface {
//是否须要显示悬停title
boolean isShowSuspension();
//悬停的title
String getSuspensionTag();
}复制代码
在BaseIndexBean
里实现,默认显示悬停,分组title和IndexBar的Tag是同样的。
public abstract class BaseIndexBean implements ISuspensionInterface {
private String baseIndexTag;//所属的分类(城市的汉语拼音首字母)
@Override
public String getSuspensionTag() {
return baseIndexTag;
}
@Override
public boolean isShowSuspension() {
return true;
}
}复制代码
而BaseIndexPinyinBean
类,如今以下:
public abstract class BaseIndexPinyinBean extends BaseIndexBean {
private String baseIndexPinyin;//城市的拼音
//是否须要被转化成拼音, 相似微信头部那种就不须要 美团的也不须要
//微信的头部 不须要显示索引
//美团的头部 索引自定义
//默认应该是须要的
public boolean isNeedToPinyin() {
return true;
}
//须要转化成拼音的目标字段
public abstract String getTarget();
}复制代码
因此咱们须要实现微信那种效果,只须要重写isShowSuspension()
和isNeedToPinyin()
这两个方法,并setBaseIndexTag()
直接设置tag便可。
这个页面仍是挺麻烦的,因此步骤也最多。建议结合代码阅读Demo及库地址。
分析美团选择城市列表:
那么逐一实现:
很简单,根据前文最后的封装( 第二篇戳我),若是只有主体部分,咱们须要让主体部分的JavaBean继承自BaseIndexPinyinBean
,而后正常构建数据,最终设置给IndexBar和SuspensionDecoration便可。
public class MeiTuanBean extends BaseIndexPinyinBean {
private String city;//城市名字
...
@Override
public String getTarget() {
return city;
}
}复制代码
这里不论是经过HeaderView添加进来头部布局,仍是经过itemViewType本身去实现,核心都是经过itemViewType去作的。
也就是说头部的HeaderView也是RecyclerView的Item。
既然是Item必定对应着相应的JavaBean。
咱们须要针对这些JavaBean让其分别继承BaseIndexPinyinBean
。
具体怎么实现头部布局不是本文重点,再也不赘述,Demo里有代码可细看Demo及库地址。
定、近、热三个HeaderView有以下特色:
作法:
不过既然是RecyclerView里的Item,又有 悬停分组、索引导航 特性。那么就要继承BaseIndexPinyinBean
。
isNeedToPinyin()
返回false,并调用setBaseIndexTag(indexBarTag)
给右侧索引赋值。getSuspensionTag()
返回title。public class MeituanHeaderBean extends BaseIndexPinyinBean { private ListcityList; //悬停ItemDecoration显示的Tag private String suspensionTag; public MeituanHeaderBean(List 复制代码cityList, String suspensionTag, String indexBarTag) { this.cityList = cityList; this.suspensionTag = suspensionTag; this.setBaseIndexTag(indexBarTag); } @Override public String getTarget() { return null; } @Override public boolean isNeedToPinyin() { return false; } @Override public String getSuspensionTag() { return suspensionTag; } }
用private List
保存定、近、热头部数据源,最终须要将其设置给IndexBar
和SuspensionDecoration
。
mHeaderDatas = new ArrayList<>(); ListlocationCity = new ArrayList<>(); locationCity.add("定位中"); mHeaderDatas.add(new MeituanHeaderBean(locationCity, "定位城市", "定")); List 复制代码recentCitys = new ArrayList<>(); mHeaderDatas.add(new MeituanHeaderBean(recentCitys, "最近访问城市", "近")); List hotCitys = new ArrayList<>(); mHeaderDatas.add(new MeituanHeaderBean(hotCitys, "热门城市", "热"));
最顶部的HeaderView,因为不须要右侧索引,也没有悬停分组。它只是一个普通的HeaderView便可。
对于这种需求的HeaderView,只须要将它们的数量传给IndexBar
和SuspensionDecoration
便可。
在内部我已经作了处理,保证联动坐标和数据源下标的正确。
mDecoration.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size()));
mIndexBar.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size());复制代码
这里用headerView一共的count=4,减去上步中mHeaderDatas
的size =3,得出不须要右侧索引,也没有悬停分组 头部的数量。
咱们前几步中,设计到了三部分数据集,
一部分是主体数据集,
//主体部分数据源(城市数据) private ListmBodyDatas; 复制代码
第二部分是须要特性的头部数据集
//头部数据源 private ListmHeaderDatas; 复制代码
第三部分是不须要特性的数据集,这里忽略。咱们只用到它的count。
咱们须要将第一和第二部分融合,而且设置给IndexBar
和SuspensionDecoration
。
则咱们利用它们共同的基类,BaseIndexPinyinBean
来存储。
核心代码以下:
//设置给InexBar、ItemDecoration的完整数据集 private ListmSourceDatas; mSourceDatas.addAll(mHeaderDatas); mSourceDatas.addAll(mBodyDatas); 复制代码
设置给IndexBar
:
mIndexBar.setmPressedShowTextView(mTvSideBarHint)//设置HintTextView
.setNeedRealIndex(true)//设置须要真实的索引
.setmLayoutManager(mManager)//设置RecyclerView的LayoutManager
.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size());
.setmSourceDatas(mSourceDatas)//设置数据复制代码
设置给SuspensionDecoration
:
mRv.addItemDecoration(new SuspensionDecoration(this, mSourceDatas)
.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size()));复制代码
效果图如文首。
这里再提一点,我已经将排序功能抽离至IndexBar
的IIndexBarDataHelper
类型变量中去作,
在mIndexBar.setmSourceDatas(mSourceDatas)
时会自动排序。
也能够手动调用mIndexBar.getDataHelper().sortSourceDatas(mBodyDatas);
排序。
像本节的案例,能够选择先排序bodyDatas,而后再合并至sourceDatas,最终设置给IndexBar
和SuspensionDecoration
。
如:
//先排序
mIndexBar.getDataHelper().sortSourceDatas(mBodyDatas);
mSourceDatas.addAll(mBodyDatas);
mIndexBar.setmSourceDatas(mSourceDatas)//设置数据
.invalidate();
mDecoration.setmDatas(mSourceDatas);复制代码
除了上节提到的那些数据结构的重构,
我还将之前在IndexBar里完成的:
抽成一个接口表示,与IndexBar分离。
/** * 介绍:IndexBar 的 数据相关帮助类 * 1 将汉语转成拼音 * 2 填充indexTag * 3 排序源数据源 * 4 根据排序后的源数据源->indexBar的数据源 * 做者:zhangxutong * 邮箱:mcxtzhang@163.com * 主页:http://blog.csdn.net/zxt0601 * 时间: 2016/11/28. */ public interface IIndexBarDataHelper { //汉语-》拼音 IIndexBarDataHelper convert(List data); //拼音->tag IIndexBarDataHelper fillInexTag(List data); //对源数据进行排序(RecyclerView) IIndexBarDataHelper sortSourceDatas(List datas); //对IndexBar的数据源进行排序(右侧栏),在 sortSourceDatas 方法后调用 IIndexBarDataHelper getSortedIndexDatas(List sourceDatas, Listdatas); } 复制代码
IndexBar内部持有这个接口的变量,调用其中方法完成需求:
public IndexBar setmSourceDatas(List
mSourceDatas) {
this.mSourceDatas = mSourceDatas;
initSourceDatas();//对数据源进行初始化
return this;
}
/**
* 初始化原始数据源,并取出索引数据源
*
* @return
*/
private void initSourceDatas() {
//add by zhangxutong 2016 09 08 :解决源数据为空 或者size为0的状况,
if (null == mSourceDatas || mSourceDatas.isEmpty()) {
return;
}
if (!isSourceDatasAlreadySorted) {
//排序sourceDatas
mDataHelper.sortSourceDatas(mSourceDatas);
} else {
//汉语->拼音
mDataHelper.convert(mSourceDatas);
//拼音->tag
mDataHelper.fillInexTag(mSourceDatas);
}
if (isNeedRealIndex) {
mDataHelper.getSortedIndexDatas(mSourceDatas, mIndexDatas);
computeGapHeight();
}
}复制代码
我在sortSourceDatas()
实现里,已经调用了convert(datas);
和 fillInexTag(datas);
@Override public IIndexBarDataHelper sortSourceDatas(List datas) { if (null == datas || datas.isEmpty()) { return this; } convert(datas); fillInexTag(datas); //对数据源进行排序 Collections.sort(datas, new Comparator() { @Override public int compare(BaseIndexPinyinBean lhs, BaseIndexPinyinBean rhs) { if (!lhs.isNeedToPinyin()) { return 0; } else if (!rhs.isNeedToPinyin()) { return 0; } else if (lhs.getBaseIndexTag().equals("#")) { return 1; } else if (rhs.getBaseIndexTag().equals("#")) { return -1; } else { return lhs.getBaseIndexPinyin().compareTo(rhs.getBaseIndexPinyin()); } } }); return this; } 复制代码
经过以下变量控制,是否须要排序,是否须要提取索引:
//是否须要根据实际的数据来生成索引数据源(例如 只有 A B C 三种tag,那么索引栏就 A B C 三项)
private boolean isNeedRealIndex;
//源数据 已经有序?
private boolean isSourceDatasAlreadySorted;复制代码
这样作的好处是,当你不喜欢我这种排序方式,亦或你想自定义特殊字符的索引,如今是"#",你均可以经过继承重写IndexBarDataHelperImpl
类的方法来完成。或者干脆实现IIndexBarDataHelper
接口,这就能知足扩展和不一样的定制需求,不用每次修改IndexBar类。
灵活重写ISuspensionInterface
接口中的方法,可控制:
灵活重写BaseIndexPinyinBean
中的方法,可控制:
isNeedToPinyin()
返回false时,不要忘了手动setBaseIndexTag()
设置IndexBar的Tag值.IndexBar
的IIndexBarDataHelper
都提供了setHeaderViewCount(int headerViewCount)
方法,供设置 不须要右侧索引,也没有悬停分组的HeaderView数量。
转载请标明出处: gold.xitu.io/post/583c13…
本文出自:【张旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代码传送门:喜欢的话,随手点个star。多谢
github.com/mcxtzhang/S…