您的位置:首页 > 移动开发 > 微信开发

【Android】快速实现仿美团选择城市界面,微信通讯录界面

2016-11-29 10:33 766 查看

概述

本文是这个系列的第三篇,不出意外也是终结篇。因为使用经过重构后的控件已经可以快速实现市面上带 索引导航、悬停分组的列表界面了。

在前两篇里,我们从0开始,一步一步实现了仿微信通讯录、饿了么选餐界面。

第一篇戳我 第二篇戳我

这篇文章作为终结篇,和前文相比,主要涉及以下内容:

重构悬停分组,将TitleItemDecoration更名为SuspensionDecoration,数据源依赖ISuspensionInterface接口。

重构索引导航,将IndexBar对数据源的操作,如排序,转拼音等分离出去,以接口IIndexBarDataHelper通信。

有N多兄弟给我留言、加QQ问的:如何实现美团选择城市列表页面,

添加一个不带悬停分组的HeaderView(微信通讯录界面)

代码传送门:喜欢的话,随手点个star。多谢

https://github.com/mcxtzhang/SuspensionIndexBar

老规矩,先上图:









(SwipeDelMenuLayout : https://github.com/mcxtzhang/SwipeDelMenuLayout)

本文将先举例子如何写,并对其中涉及到的重构部分进行讲解。

如有不明者,建议先观看(第一篇戳我 第二篇戳我),

以及下载Demo,边看代码边阅读,效果更佳。

转载请标明出处:

http://blog.csdn.net/zxt0601/article/details/53389835

本文出自:【张旭童的博客】(http://blog.csdn.net/zxt0601)

代码传送门:喜欢的话,随手点个star。多谢

https://github.com/mcxtzhang/SuspensionIndexBar

微信通讯录界面写法

先从简单的用法看起,微信通讯录界面和普通的 分组悬停&索引导航 的列表相比:

* 多了四个HeaderView

* 这些HeaderView布局和主体Item一样

* 这些HeaderView 没有分组悬停title

* 这些HeaderView是一组的,索引title自定义

实现:

HeaderView不是本文讨论重点,随意实现之。我用的是我自己之前写的,戳我

布局和主体Item一致

由于布局一致,则我们肯定偷懒直接用主体Item的Bean,将city设置为相应的数据即可,如 “新的朋友”:

public class CityBean extends BaseIndexPinyinBean {
private String city;//城市名字


没有分组悬停

去掉分组悬停,我们需要重写
isShowSuspension()
方法,返回false。

索引title自定义

它们是一组的,则索引title一致,且需要自定义。

四个头部的Bean调用
setBaseIndexTag()
方法,set自定义的title,且一致即可。

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及库地址

分析美团选择城市列表:

* 主体部分仍旧是一个普通的 分组悬停&索引导航 的列表(美团没有悬停功能)。

* 头部是由若干复杂HeaderView组成。

* 从右侧索引栏可以看出,定位、最近、热门这三个Item对应了列表三个HeaderView。

* 最顶部的HeaderView是不需要分组,也不需要索引的。

那么逐一实现:

主体部分

很简单,根据前文最后的封装( 第二篇戳我),如果只有主体部分,我们需要让主体部分的JavaBean继承自
BaseIndexPinyinBean
,然后正常构建数据,最终设置给IndexBar和SuspensionDecoration即可。

public class MeiTuanBean extends BaseIndexPinyinBean {
private String city;//城市名字
...
@Override
public String getTarget() {
return city;
}
}


头部若干HeaderViews

这里不管是通过HeaderView添加进来头部布局,还是通过itemViewType自己去实现,核心都是通过itemViewType去做的。

也就是说头部的HeaderView也是RecyclerView的Item。

既然是Item一定对应着相应的JavaBean。

我们需要针对这些JavaBean让其分别继承
BaseIndexPinyinBean


具体怎么实现头部布局不是本文重点,不再赘述,Demo里有代码可细看Demo及库地址

定、近、热三个HeaderView的处理

定、近、热三个HeaderView有如下特点:

* 右侧导航索引的title 为自定义,不是拼音首字母则也不需要排序。

* 悬停分组的title 和 右侧导航索引的title 不一样,则悬停分组的title也需要自定义

做法:

不过既然是RecyclerView里的Item,又有 悬停分组、索引导航 特性。那么就要继承
BaseIndexPinyinBean


* 不需要转化成拼音且不排序,则重写
isNeedToPinyin()
返回false,并调用
setBaseIndexTag(indexBarTag)
给右侧索引赋值。

* 需要自定义悬停分组的title,则重写
getSuspensionTag()
返回title。

public class MeituanHeaderBean extends BaseIndexPinyinBean {
private List<String> cityList;
//悬停ItemDecoration显示的Tag
private String suspensionTag;

public MeituanHeaderBean(List<String> 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<MeituanHeaderBean> mHeaderDatas;
保存定、近、热头部数据源,最终需要将其设置给
IndexBar
SuspensionDecoration


mHeaderDatas = new ArrayList<>();
List<String> locationCity = new ArrayList<>();
locationCity.add("定位中");
mHeaderDatas.add(new MeituanHeaderBean(locationCity, "定位城市", "定"));
List<String> recentCitys = new ArrayList<>();
mHeaderDatas.add(new MeituanHeaderBean(recentCitys, "最近访问城市", "近"));
List<String> hotCitys = new ArrayList<>();
mHeaderDatas.add(new MeituanHeaderBean(hotCitys, "热门城市", "热"));


最顶部的HeaderView

最顶部的HeaderView,由于不需要右侧索引,也没有悬停分组。它只是一个普通的HeaderView即可。

对于这种需求的HeaderView,只需要将它们的数量传给
IndexBar
SuspensionDecoration
即可。

在内部我已经做了处理,保证联动坐标和数据源下标的正确。

mDecoration.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size()));
mIndexBar.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size());


这里用headerView一共的count=4,减去上步中
mHeaderDatas
的size =3,得出不需要右侧索引,也没有悬停分组 头部的数量。

将主体数据集和头部数据集合并

我们前几步中,设计到了三部分数据集,

一部分是主体数据集,

//主体部分数据源(城市数据)
private List<MeiTuanBean> mBodyDatas;


第二部分是需要特性的头部数据集

//头部数据源
private List<MeituanHeaderBean> mHeaderDatas;


第三部分是不需要特性的数据集,这里忽略。我们只用到它的count。

我们需要将第一和第二部分融合,并且设置给
IndexBar
SuspensionDecoration


则我们利用它们共同的基类,
BaseIndexPinyinBean
来存储。

核心代码如下:

//设置给InexBar、ItemDecoration的完整数据集
private List<BaseIndexPinyinBean> mSourceDatas;

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里完成的:

1 将汉语转成拼音

2 填充indexTag

3 排序源数据源

4 根据排序后的源数据源->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<? extends BaseIndexPinyinBean> data);

//拼音->tag
IIndexBarDataHelper fillInexTag(List<? extends BaseIndexPinyinBean> data);

//对源数据进行排序(RecyclerView)
IIndexBarDataHelper sortSourceDatas(List<? extends BaseIndexPinyinBean> datas);

//对IndexBar的数据源进行排序(右侧栏),在 sortSourceDatas 方法后调用
IIndexBarDataHelper getSortedIndexDatas(List<? extends BaseIndexPinyinBean> sourceDatas, List<String> datas);
}


IndexBar内部持有这个接口的变量,调用其中方法完成需求:

public IndexBar setmSourceDatas(List<? extends BaseIndexPinyinBean> 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<? extends BaseIndexPinyinBean> datas) {
if (null == datas || datas.isEmpty()) {
return this;
}
convert(datas);
fillInexTag(datas);
//对数据源进行排序
Collections.sort(datas, new Comparator<BaseIndexPinyinBean>() {
@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
接口中的方法,可控制:

* 是否需要显示悬停title

* 悬停显示的titles

灵活重写
BaseIndexPinyinBean
中的方法,可控制:

* 是否需要被转化成拼音, 类似微信头部那种就不需要 美团的也不需要

* 微信的头部 不需要显示索引

* 美团的头部 索引自定义

* 默认应该是需要的

* 在
isNeedToPinyin()
返回false时,不要忘了手动
setBaseIndexTag()
设置IndexBar的Tag值.

IndexBar
IIndexBarDataHelper
都提供了
setHeaderViewCount(int headerViewCount)
方法,供设置 不需要右侧索引,也没有悬停分组的HeaderView数量。

转载请标明出处:

http://blog.csdn.net/zxt0601/article/details/53389835

本文出自:【张旭童的博客】(http://blog.csdn.net/zxt0601)

代码传送门:喜欢的话,随手点个star。多谢

https://github.com/mcxtzhang/SuspensionIndexBar
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息