【Android】快速實現仿美團選擇城市界面,微信通信錄界面

概述

本文是這個系列的第三篇,不出意外也是終結篇。由於使用通過重構後的控件已經能夠快速實現市面上帶 索引導航、懸停分組的列表界面了。
在前兩篇裏,咱們從0開始,一步一步實現了仿微信通信錄、餓了麼選餐界面。
第一篇戳我 第二篇戳我
這篇文章做爲終結篇,和前文相比,主要涉及如下內容:git

  • 重構懸停分組,將TitleItemDecoration改名爲SuspensionDecoration,數據源依賴ISuspensionInterface接口。
  • 重構索引導航,將IndexBar對數據源的操做,如排序,轉拼音等分離出去,以接口IIndexBarDataHelper通訊。
  • 有N多兄弟給我留言、加QQ問的:如何實現美團選擇城市列表頁面,
  • 添加一個不帶懸停分組的HeaderView(微信通信錄界面)

代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/S…github

老規矩,先上圖:

api

美團選擇城市界面,先刷新Body主體數據,再定向刷新頭部數據

微信通信錄界面

配合我另外一個庫組裝的效果(SuspensionIndexBar + SwipeMenuLayout)

(SwipeDelMenuLayout : github.com/mcxtzhang/S…)

本文將先舉例子如何寫,並對其中涉及到的重構部分進行講解。
若有不明者,建議先觀看(第一篇戳我 第二篇戳我),
以及下載Demo,邊看代碼邊閱讀,效果更佳。微信


轉載請標明出處: gold.xitu.io/post/583c13…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/S…數據結構


微信通信錄界面寫法

先從簡單的用法看起,微信通信錄界面和普通的 分組懸停&索引導航 的列表相比:ide

  • 多了四個HeaderView
  • 這些HeaderView佈局和主體Item同樣
  • 這些HeaderView 沒有分組懸停title
  • 這些HeaderView是一組的,索引title自定義

實現:
HeaderView不是本文討論重點,隨意實現之。我用的是我本身以前寫的,戳我佈局

佈局和主體Item一致

因爲佈局一致,則咱們確定偷懶直接用主體Item的Bean,將city設置爲相應的數據便可,如 「新的朋友」:post

public class CityBean extends BaseIndexPinyinBean {
    private String city;//城市名字複製代碼

沒有分組懸停

去掉分組懸停,咱們須要重寫isShowSuspension()方法,返回false。this

索引title自定義

它們是一組的,則索引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及庫地址
分析美團選擇城市列表:

  • 主體部分仍舊是一個普通的 分組懸停&索引導航 的列表(美團沒有懸停功能)。
  • 頭部是由若干複雜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 mHeaderDatas; 保存定、近、熱頭部數據源,最終須要將其設置給IndexBarSuspensionDecoration

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,只須要將它們的數量傳給IndexBarSuspensionDecoration 便可。
在內部我已經作了處理,保證聯動座標和數據源下標的正確。

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。
咱們須要將第一和第二部分融合,而且設置給IndexBarSuspensionDecoration
則咱們利用它們共同的基類,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()));複製代碼

效果圖如文首。

核心代碼

這裏再提一點,我已經將排序功能抽離至IndexBarIIndexBarDataHelper類型變量中去作,
mIndexBar.setmSourceDatas(mSourceDatas)時會自動排序。
也能夠手動調用mIndexBar.getDataHelper().sortSourceDatas(mBodyDatas);排序。
像本節的案例,能夠選擇先排序bodyDatas,而後再合併至sourceDatas,最終設置給IndexBarSuspensionDecoration
如:

//先排序
                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
  
  
  

  data);

    //拼音->tag
    IIndexBarDataHelper fillInexTag(List
  
  
  

  data);

    //對源數據進行排序(RecyclerView)
    IIndexBarDataHelper sortSourceDatas(List
  
  
  

  datas);

    //對IndexBar的數據源進行排序(右側欄),在 sortSourceDatas 方法後調用
    IIndexBarDataHelper getSortedIndexDatas(List
  
  
  

  sourceDatas, List
  
  
  

 
  
  datas); } 

 複製代碼

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接口中的方法,可控制:

  • 是否須要顯示懸停title
  • 懸停顯示的titles

靈活重寫BaseIndexPinyinBean中的方法,可控制:

  • 是否須要被轉化成拼音, 相似微信頭部那種就不須要 美團的也不須要
  • 微信的頭部 不須要顯示索引
  • 美團的頭部 索引自定義
  • 默認應該是須要的
  • isNeedToPinyin()返回false時,不要忘了手動setBaseIndexTag()設置IndexBar的Tag值.

IndexBarIIndexBarDataHelper都提供了setHeaderViewCount(int headerViewCount)方法,供設置 不須要右側索引,也沒有懸停分組的HeaderView數量。


轉載請標明出處: gold.xitu.io/post/583c13…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/S…

相關文章
相關標籤/搜索