天天看點

【Android】快速實作仿美團選擇城市界面,微信通訊錄界面概述微信通訊錄界面寫法仿美團選擇城市總結

概述

本文是這個系列的第三篇,不出意外也是終結篇。因為使用經過重構後的控件已經可以快速實作市面上帶 索引導航、懸停分組的清單界面了。

在前兩篇裡,我們從0開始,一步一步實作了仿微信通訊錄、餓了麼選餐界面。

(第一篇戳我 第二篇戳我)

這篇文章作為終結篇,和前文相比,主要涉及以下内容:

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

代碼傳送門:喜歡的話,随手點個star。多謝

https://github.com/mcxtzhang/SuspensionIndexBar

老規矩,先上圖:

【Android】快速實作仿美團選擇城市界面,微信通訊錄界面概述微信通訊錄界面寫法仿美團選擇城市總結
【Android】快速實作仿美團選擇城市界面,微信通訊錄界面概述微信通訊錄界面寫法仿美團選擇城市總結
【Android】快速實作仿美團選擇城市界面,微信通訊錄界面概述微信通訊錄界面寫法仿美團選擇城市總結

(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 = ; 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
 * 郵箱:[email protected]
 * 首頁: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 ;
                } else if (!rhs.isNeedToPinyin()) {
                    return ;
                } else if (lhs.getBaseIndexTag().equals("#")) {
                    return ;
                } else if (rhs.getBaseIndexTag().equals("#")) {
                    return -;
                } 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