概述
本文是這個系列的第三篇,不出意外也是終結篇。因為使用經過重構後的控制元件已經可以快速實現市面上帶 索引導航、懸停分組的列表介面了。
在前兩篇裡,我們從0開始,一步一步實現了仿微信通訊錄、餓了麼選餐介面。
(第一篇戳我 第二篇戳我)
這篇文章作為終結篇,和前文相比,主要涉及以下內容:
- 重構懸停分組,將TitleItemDecoration更名為SuspensionDecoration,資料來源依賴ISuspensionInterface介面。
- 重構索引導航,將IndexBar對資料來源的操作,如排序,轉拼音等分離出去,以介面IIndexBarDataHelper通訊。
- 有N多兄弟給我留言、加QQ問的:如何實現美團選擇城市列表頁面,
- 新增一個不帶懸停分組的HeaderView(微信通訊錄介面)
程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/S…
老規矩,先上圖:
。
(SwipeDelMenuLayout : github.com/mcxtzhang/S…)
本文將先舉例子如何寫,並對其中涉及到的重構部分進行講解。
如有不明者,建議先觀看(第一篇戳我 第二篇戳我),
以及下載Demo,邊看程式碼邊閱讀,效果更佳。
轉載請標明出處: gold.xitu.io/post/583c13…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/S…
微信通訊錄介面寫法
先從簡單的用法看起,微信通訊錄介面和普通的 分組懸停&索引導航 的列表相比:
- 多了四個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 cityList;
//懸停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<>();
List locationCity = 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即可。
對於這種需求的HeaderView,只需要將它們的數量傳給IndexBar
和SuspensionDecoration
即可。
在內部我已經做了處理,保證聯動座標和資料來源下標的正確。
mDecoration.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size()));
mIndexBar.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size());複製程式碼
這裡用headerView一共的count=4,減去上步中mHeaderDatas
的size =3,得出不需要右側索引,也沒有懸停分組 頭部的數量。
將主體資料集和頭部資料集合並
我們前幾步中,設計到了三部分資料集,
一部分是主體資料集,
//主體部分資料來源(城市資料)
private List mBodyDatas; 複製程式碼
第二部分是需要特性的頭部資料集
//頭部資料來源
private List mHeaderDatas; 複製程式碼
第三部分是不需要特性的資料集,這裡忽略。我們只用到它的count。
我們需要將第一和第二部分融合,並且設定給IndexBar
和SuspensionDecoration
。
則我們利用它們共同的基類,BaseIndexPinyinBean
來儲存。
核心程式碼如下:
//設定給InexBar、ItemDecoration的完整資料集
private List 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 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() {
@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數量。
轉載請標明出處: gold.xitu.io/post/583c13…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/S…