【Android 仿微信通訊錄 導(dǎo)航分組列表-下】自定義View為RecyclerView打造右側(cè)索引導(dǎo)航欄IndexBar

**本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布 **

轉(zhuǎn)載請(qǐng)標(biāo)明出處: http://www.reibang.com/p/3eff217839fc
本文出自:【張旭童的博客】 (http://www.reibang.com/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點(diǎn)個(gè)star。多謝
https://github.com/mcxtzhang/ItemDecorationIndexBar


一 概述

在上篇文章(http://www.reibang.com/p/0d49d9f51d2c) 里,我們用ItemDecoration為RecyclerView打造了帶懸停頭部的分組列表簸州。
其實(shí)Android版微信的通訊錄界面垄提,它的分組title也不是懸停的挺智,我們已經(jīng)領(lǐng)先了微信一小步(認(rèn)真臉)~
再看看市面上常見(jiàn)的分組列表(例如餓了么點(diǎn)餐商品列表),不僅有懸停頭部芳誓,懸停頭部在切換時(shí)醉锅,還會(huì)伴有切換動(dòng)畫兔簇。
關(guān)于ItemDecoration還有一個(gè)問(wèn)題,簡(jiǎn)單布局還好荣挨,我們可以draw出來(lái)男韧,如果是復(fù)雜的頭部呢朴摊?能否寫個(gè)xml默垄,inflate進(jìn)來(lái),這樣使用起來(lái)才簡(jiǎn)單甚纲,即另一種簡(jiǎn)單使用onDraw和onDrawOver的姿勢(shì)口锭。
so,本文開(kāi)頭我們就先用兩節(jié)完善一下我們的ItemDecoration介杆。
然后進(jìn)入正題:自定義View實(shí)現(xiàn)右側(cè)索引導(dǎo)航欄IndexBar鹃操,對(duì)數(shù)據(jù)源的排序字段按照拼音排序,最后將RecyclerView和IndexBar聯(lián)動(dòng)起來(lái)春哨,觸摸IndexBar上相應(yīng)字母荆隘,RecyclerView滾動(dòng)到相應(yīng)位置。(在屏幕中間顯示的其實(shí)就是一個(gè)TextView赴背,我們set個(gè)體IndexBar即可)

由于大部分使用右側(cè)索引導(dǎo)航欄的場(chǎng)景椰拒,都需要這幾個(gè)固定步驟,對(duì)數(shù)據(jù)源排序凰荚,set給IndexBar燃观,和RecyclerView聯(lián)動(dòng)等,所以最后再將其封裝一把便瑟,成一個(gè)高度封裝缆毁,因此擴(kuò)展性不太高的控件,更方便使用到涂,如果需要擴(kuò)展的話脊框,反正看完本文再其基礎(chǔ)上修改應(yīng)該很簡(jiǎn)單~颁督。

最終版預(yù)覽:


這里寫圖片描述

本文摘要:

  1. 用ItemDecoration實(shí)現(xiàn)懸停頭部切換動(dòng)畫
  2. 另一種簡(jiǎn)單使用onDraw()和onDrawOver()的姿勢(shì)
  3. 自定義View實(shí)現(xiàn)右側(cè)索引導(dǎo)航欄IndexBar
  4. 使用TinyPinyin對(duì)數(shù)據(jù)源排序
  5. 聯(lián)動(dòng)IndexBar和RecyclerView。
  6. 封裝重復(fù)步驟浇雹,方便二次使用适篙,并可定制導(dǎo)航數(shù)據(jù)源

二 懸停頭部的“切換動(dòng)畫”

實(shí)現(xiàn)了兩種箫爷,
第一種就是仿餓了么點(diǎn)餐時(shí),商品列表的懸停頭部切換“動(dòng)畫效果”嚷节,如下:


這里寫圖片描述

第二種是一種頭部折疊起來(lái)的視效,個(gè)人覺(jué)得也還不錯(cuò)~如下:(估計(jì)沒(méi)人喜歡)


這里寫圖片描述

果然比上部殘篇里的效果好看多了虎锚,那么代碼多不多呢硫痰,看我的git show 記錄:
這里寫圖片描述

就綠色部分的不到十行代碼就搞定~先上這個(gè)圖是為了讓大家安心,代碼不多窜护,分分鐘看完效斑。

下面放上文字版代碼,江湖人稱 注釋張 的我柱徙,已經(jīng)寫滿了注釋缓屠,
再簡(jiǎn)單說(shuō)下吧,
滑動(dòng)時(shí)护侮,在判斷頭部即將切換(當(dāng)前pos的tag和pos+1的tag不等)的時(shí)候敌完,
1.計(jì)算出當(dāng)前懸停頭部應(yīng)該上移的位移,
利用Canvas的畫布移動(dòng)方法Canvas.translate()羊初,即可實(shí)現(xiàn)“餓了么”懸停頭部切換效果滨溉。
2.計(jì)算出當(dāng)前懸停頭部應(yīng)該在屏幕上還剩余的空間高度,作為頭部繪制的高度
利用Canvas的Canvas.clipRect()方法长赞,剪切畫布晦攒,即可實(shí)現(xiàn)“折疊”的視效。

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {//最后調(diào)用 繪制在最上層
        int pos = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();

        String tag = mDatas.get(pos).getTag();
        //View child = parent.getChildAt(pos);
        View child = parent.findViewHolderForLayoutPosition(pos).itemView;//出現(xiàn)一個(gè)奇怪的bug得哆,有時(shí)候child為空脯颜,所以將 child = parent.getChildAt(i)。-》 parent.findViewHolderForLayoutPosition(pos).itemView

        boolean flag = false;//定義一個(gè)flag贩据,Canvas是否位移過(guò)的標(biāo)志
        if ((pos + 1) < mDatas.size()) {//防止數(shù)組越界(一般情況不會(huì)出現(xiàn))
            if (null != tag && !tag.equals(mDatas.get(pos + 1).getTag())) {//當(dāng)前第一個(gè)可見(jiàn)的Item的tag栋操,不等于其后一個(gè)item的tag,說(shuō)明懸浮的View要切換了
                Log.d("zxt", "onDrawOver() called with: c = [" + child.getTop());//當(dāng)getTop開(kāi)始變負(fù)乐设,它的絕對(duì)值讼庇,是第一個(gè)可見(jiàn)的Item移出屏幕的距離,
                if (child.getHeight() + child.getTop() < mTitleHeight) {//當(dāng)?shù)谝粋€(gè)可見(jiàn)的item在屏幕中還剩的高度小于title區(qū)域的高度時(shí)近尚,我們也該開(kāi)始做懸浮Title的“交換動(dòng)畫”
                    c.save();//每次繪制前 保存當(dāng)前Canvas狀態(tài)蠕啄,
                    flag = true;

                    //一種頭部折疊起來(lái)的視效,個(gè)人覺(jué)得也還不錯(cuò)~
                    //可與123行 c.drawRect 比較,只有bottom參數(shù)不一樣歼跟,由于 child.getHeight() + child.getTop() < mTitleHeight和媳,所以繪制區(qū)域是在不斷的減小,有種折疊起來(lái)的感覺(jué)
                    //c.clipRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + child.getHeight() + child.getTop());

                    //類似餓了么點(diǎn)餐時(shí),商品列表的懸停頭部切換“動(dòng)畫效果”
                    //上滑時(shí)哈街,將canvas上移 (y為負(fù)數(shù)) ,所以后面canvas 畫出來(lái)的Rect和Text都上移了留瞳,有種切換的“動(dòng)畫”感覺(jué)
                    c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);
                }
            }
        }
        mPaint.setColor(COLOR_TITLE_BG);
        c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mPaint);
        mPaint.setColor(COLOR_TITLE_FONT);
        mPaint.getTextBounds(tag, 0, tag.length(), mBounds);
        c.drawText(tag, child.getPaddingLeft(),
                parent.getPaddingTop() + mTitleHeight - (mTitleHeight / 2 - mBounds.height() / 2),
                mPaint);
        if (flag)
            c.restore();//恢復(fù)畫布到之前保存的狀態(tài)


    }

這份代碼核心處c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);實(shí)現(xiàn)的是餓了么效果,被注釋掉的

//c.clipRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + child.getHeight() + child.getTop());

實(shí)現(xiàn)的是效果二骚秦。


三 另一種使用onDraw()和onDrawOver()的姿勢(shì)

之前我們使用onDraw(),onDrawOver()她倘,都是用canvas的方法活生生的繪制一個(gè)出View,這對(duì)于很多人(包括我)來(lái)說(shuō)都不容易作箍,xy坐標(biāo)的確認(rèn)硬梁,尺寸都較難把握,基本上調(diào)UI效果時(shí)間都很長(zhǎng)胞得。尤其是canvas.drawText()方法的y坐標(biāo)荧止,其實(shí)是baseLine的位置,不了解的童鞋肯定要踩很多坑阶剑。
當(dāng)我們想要繪制的分類title跃巡、懸停頭部復(fù)雜一點(diǎn)時(shí),我都不敢想象要調(diào)試多久了牧愁,這個(gè)時(shí)候我們還敢用ItemDecoration嗎素邪。
有沒(méi)有一種方法,就像我們平時(shí)使用的那樣递宅,在Layout布局xml里畫好View娘香,然后inflate出來(lái)就可以了呢。
這個(gè)問(wèn)題開(kāi)始確實(shí)也把我難住了办龄,難道又要從入門到放棄了嗎?
于是我又搜尋資料淋昭,功夫不負(fù)有心人俐填。
解決問(wèn)題的辦法就是,View類的:public void draw(Canvas canvas) {方法
下面我們就看一個(gè)用法Demo吧:
布局layout:header_complex.xml(注意有個(gè)ProgressBar哦)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorPrimaryDark"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorAccent"
        android:text="復(fù)雜頭部" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="復(fù)雜頭部"
        android:textColor="@color/colorAccent" />

    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

onDrawOver代碼如下:簡(jiǎn)單講解下翔忽,先inflate這個(gè)復(fù)雜的Layout英融,然后拿到它的LayoutParams,利用這個(gè)lp拿到寬和高的MeasureSpec歇式,然后依次調(diào)用 measure,layout,draw方法驶悟,將復(fù)雜頭部顯示在屏幕上。
(小安利一下材失,MeasureSpec不太了解的可以看看我的這篇http://blog.csdn.net/zxt0601/article/details/52331007

        View toDrawView = mInflater.inflate(R.layout.header_complex, parent, false);
        int toDrawWidthSpec;//用于測(cè)量的widthMeasureSpec
        int toDrawHeightSpec;//用于測(cè)量的heightMeasureSpec
        //拿到復(fù)雜布局的LayoutParams痕鳍,如果為空,就new一個(gè)。
        // 后面需要根據(jù)這個(gè)lp 構(gòu)建toDrawWidthSpec笼呆,toDrawHeightSpec
        ViewGroup.LayoutParams lp = toDrawView.getLayoutParams();
        if (lp == null) {
            lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);//這里是根據(jù)復(fù)雜布局layout的width height熊响,new一個(gè)Lp
            toDrawView.setLayoutParams(lp);
        }
        if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
            //如果是MATCH_PARENT,則用父控件能分配的最大寬度和EXACTLY構(gòu)建MeasureSpec诗赌。
            toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.EXACTLY);
        } else if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //如果是WRAP_CONTENT汗茄,則用父控件能分配的最大寬度和AT_MOST構(gòu)建MeasureSpec。
            toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.AT_MOST);
        } else {
            //否則則是具體的寬度數(shù)值铭若,則用這個(gè)寬度和EXACTLY構(gòu)建MeasureSpec洪碳。
            toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
        }
        //高度同理
        if (lp.height == ViewGroup.LayoutParams.MATCH_PARENT) {
            toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom(), View.MeasureSpec.EXACTLY);
        } else if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom(), View.MeasureSpec.AT_MOST);
        } else {
            toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
        }
        //依次調(diào)用 measure,layout,draw方法,將復(fù)雜頭部顯示在屏幕上叼屠。
        toDrawView.measure(toDrawWidthSpec, toDrawHeightSpec);
        toDrawView.layout(parent.getPaddingLeft(), parent.getPaddingTop(),
                parent.getPaddingLeft() + toDrawView.getMeasuredWidth(), parent.getPaddingTop() + toDrawView.getMeasuredHeight());
        toDrawView.draw(c);

這里還有個(gè)有趣的地方偶宫,某些需要不斷調(diào)用onDraw()更新繪制自己最新?tīng)顟B(tài)的View,例如ProgressBar,由于在屏幕上顯示的并不是真正的View环鲤,只是我們手動(dòng)的調(diào)用了一次draw方法纯趋,進(jìn)而調(diào)用View的onDraw()顯示的一次“殘影”,所以ProgressBar只會(huì)顯示onDraw()當(dāng)時(shí)的樣子冷离,并不會(huì)主動(dòng)刷新了吵冒。
看圖說(shuō)話,還是很容易理解的:

這里寫圖片描述

滑動(dòng)時(shí)西剥,由于會(huì)回調(diào)onDrawOver() 方法痹栖,所以ProgressBar又被手動(dòng)調(diào)用了draw(),開(kāi)始變化瞭空,滑動(dòng)的快的話揪阿,progressBar會(huì)有動(dòng)畫效果。
停止不動(dòng)時(shí)咆畏,ProgressBar也是靜止的南捂,保持draw()時(shí)繪制的狀態(tài)。


四 自定義View實(shí)現(xiàn)右側(cè)索引導(dǎo)航欄IndexBar

不管是自定義ItemDecoration還是實(shí)現(xiàn)右側(cè)索引導(dǎo)航欄旧找,都有大量的自定義View知識(shí)在里面 溺健,這里簡(jiǎn)單復(fù)習(xí)一下。
(步驟1-4是自定義View的必須套路钮蛛,步驟5+是IndexBar特殊定制)
1 自定義View首先要確定這個(gè)View需要在xml里接受哪些屬性鞭缭?
在IndexBar里,我們先需要兩個(gè)屬性魏颓,每個(gè)索引的文字大小和手指按下時(shí)整個(gè)View的背景岭辣,
即在attrs.xml如下定義:

    <attr name="textSize" format="dimension" />
    <declare-styleable name="IndexBar">
        <attr name="textSize" />
        <attr name="pressBackground" format="color" />
    </declare-styleable>

2 在View的構(gòu)造方法中獲得我們自定義的屬性
套路代碼如下,都是套路甸饱,記得使用完最后要將typeArray對(duì)象 recycle()沦童。

        int textSize = (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics());//默認(rèn)的TextSize
        mPressedBackground = Color.BLACK;//默認(rèn)按下是純黑色
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.IndexBar, defStyleAttr, 0);
        int n = typedArray.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.IndexBar_textSize:
                    textSize = typedArray.getDimensionPixelSize(attr, textSize);
                    break;
                case R.styleable.IndexBar_pressBackground:
                    mPressedBackground = typedArray.getColor(attr, mPressedBackground);
                default:
                    break;
            }
        }
        typedArray.recycle();

3 重寫onMesure()方法(可選)
onMeasure()方法里,主要就是遍歷一遍indexDatas,得到index最大寬度和高度搞动。然后根據(jù)三種測(cè)量模式躏精,分配不同的值給View,
EXACLTY就分配具體的測(cè)量值(match_parent,確定數(shù)值)鹦肿,
AT_MOST就分配父控件能給的最大值和自己需要的值之間的最小值矗烛。(保證不超過(guò)父控件限定的值)
UNSPECIFIED則分配自己需要的值。(隨心所欲)

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //取出寬高的MeasureSpec  Mode 和Size
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidth = 0, measureHeight = 0;//最終測(cè)量出來(lái)的寬高

        //得到合適寬度:
        Rect indexBounds = new Rect();//存放每個(gè)繪制的index的Rect區(qū)域
        String index;//每個(gè)要繪制的index內(nèi)容
        for (int i = 0; i < mIndexDatas.size(); i++) {
            index = mIndexDatas.get(i);
            mPaint.getTextBounds(index, 0, index.length(), indexBounds);//測(cè)量計(jì)算文字所在矩形箩溃,可以得到寬高
            measureWidth = Math.max(indexBounds.width(), measureWidth);//循環(huán)結(jié)束后瞭吃,得到index的最大寬度
            measureHeight = Math.max(indexBounds.width(), measureHeight);//循環(huán)結(jié)束后,得到index的最大高度涣旨,然后*size
        }
        measureHeight *= mIndexDatas.size();
        switch (wMode) {
            case MeasureSpec.EXACTLY:
                measureWidth = wSize;
                break;
            case MeasureSpec.AT_MOST:
                measureWidth = Math.min(measureWidth, wSize);//wSize此時(shí)是父控件能給子View分配的最大空間
                break;
            case MeasureSpec.UNSPECIFIED:
                break;
        }

        //得到合適的高度:
        switch (hMode) {
            case MeasureSpec.EXACTLY:
                measureHeight = hSize;
                break;
            case MeasureSpec.AT_MOST:
                measureHeight = Math.min(measureHeight, hSize);//wSize此時(shí)是父控件能給子View分配的最大空間
                break;
            case MeasureSpec.UNSPECIFIED:
                break;
        }

        setMeasuredDimension(measureWidth, measureHeight);
    }

4 重寫onDraw()方法
整理一下需求和思路:
利用index數(shù)據(jù)源的size歪架,和控件可繪制的高度(高度-paddingTop-paddingBottom),求出每個(gè)index區(qū)域的高度mGapHeight霹陡。
每個(gè)index在繪制時(shí)和蚪,都是處于水平居中,豎直方向上在mGapHeight區(qū)域高度內(nèi)居中烹棉。
思路整理清楚攒霹,代碼很簡(jiǎn)單如下:

    public static String[] INDEX_STRING = {"A", "B", "C", "D", "E", "F", "G", "H", "I","J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V","W", "X", "Y", "Z", "#"};//#在最后面(默認(rèn)的數(shù)據(jù)源)
    private List<String> mIndexDatas;//索引數(shù)據(jù)源
    private int mGapHeight;//每個(gè)index區(qū)域的高度
    .....
    mIndexDatas = Arrays.asList(INDEX_STRING);//數(shù)據(jù)源

在onSizeChanged方法里,獲取控件的寬高浆洗,并計(jì)算出mGapHeight:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mGapHeight = (mHeight - getPaddingTop() - getPaddingBottom()) / mIndexDatas.size();
    }

最后在onDraw()方法里繪制,
如果對(duì)于豎直居中baseLine的計(jì)算不太理解可以先放置催束,這塊的確挺繞人,后面應(yīng)該會(huì)寫一篇 canvas.drawText()x y坐標(biāo)計(jì)算的小短文.
可記住重點(diǎn)就是 Paint默認(rèn)的TextAlign是Left伏社,即x方向抠刺,左對(duì)齊,所以x坐標(biāo)決定繪制文字的左邊界摘昌。
y坐標(biāo)是繪制文字的baseLine位置速妖。

    @Override
    protected void onDraw(Canvas canvas) {
        int t = getPaddingTop();//top的基準(zhǔn)點(diǎn)(支持padding)
        Rect indexBounds = new Rect();//存放每個(gè)繪制的index的Rect區(qū)域
        String index;//每個(gè)要繪制的index內(nèi)容
        for (int i = 0; i < mIndexDatas.size(); i++) {
            index = mIndexDatas.get(i);
            mPaint.getTextBounds(index, 0, index.length(), indexBounds);//測(cè)量計(jì)算文字所在矩形,可以得到寬高
            Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();//獲得畫筆的FontMetrics第焰,用來(lái)計(jì)算baseLine买优。因?yàn)閐rawText的y坐標(biāo),代表的是繪制的文字的baseLine的位置
            int baseline = (int) ((mGapHeight - fontMetrics.bottom - fontMetrics.top) / 2);//計(jì)算出在每格index區(qū)域挺举,豎直居中的baseLine值
            canvas.drawText(index, mWidth / 2 - indexBounds.width() / 2, t + mGapHeight * i + baseline, mPaint);//調(diào)用drawText,居中顯示繪制index
        }
    }

以上四步基本完成了IndexBar的繪制工作烘跺,下面我們?yōu)樗砑右恍┬袨榈捻憫?yīng)湘纵。

5 重寫onTouchEvent()方法
我們需要重寫onTouchEvent()方法,
以便處理手指按下時(shí)的View背景變色滤淳,抬起時(shí)恢復(fù)原來(lái)顏色
并根據(jù)手指觸摸的落點(diǎn)坐標(biāo)梧喷,判斷當(dāng)前處于哪個(gè)index區(qū)域汹碱,回調(diào)給相應(yīng)的監(jiān)聽(tīng)器處理(顯示當(dāng)前index的值配猫,滑動(dòng)RecyclerView至相應(yīng)區(qū)域等。。)
代碼如下:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                setBackgroundColor(mPressedBackground);//手指按下時(shí)背景變色
                //注意這里沒(méi)有break屯阀,因?yàn)閐own時(shí),也要計(jì)算落點(diǎn) 回調(diào)監(jiān)聽(tīng)器
            case MotionEvent.ACTION_MOVE:
                float y = event.getY();
                //通過(guò)計(jì)算判斷落點(diǎn)在哪個(gè)區(qū)域:
                int pressI = (int) ((y - getPaddingTop()) / mGapHeight);
                //邊界處理(在手指move時(shí)婶熬,有可能已經(jīng)移出邊界两疚,防止越界)
                if (pressI < 0) {
                    pressI = 0;
                } else if (pressI >= mIndexDatas.size()) {
                    pressI = mIndexDatas.size() - 1;
                }
                //回調(diào)監(jiān)聽(tīng)器
                if (null != mOnIndexPressedListener) {
                    mOnIndexPressedListener.onIndexPressed(pressI, mIndexDatas.get(pressI));
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            default:
                setBackgroundResource(android.R.color.transparent);//手指抬起時(shí)背景恢復(fù)透明
                //回調(diào)監(jiān)聽(tīng)器
                if (null != mOnIndexPressedListener) {
                    mOnIndexPressedListener.onMotionEventEnd();
                }
                break;
        }
        return true;
    }

6 聯(lián)動(dòng)IndexBar和RecyclerView
具體的操作交由監(jiān)聽(tīng)器處理,定義和實(shí)現(xiàn)如下:
值得一提的就是弯囊,滑動(dòng)RecyclerView到指定postion痰哨,我們使用的是LinearLayoutManager的scrollToPositionWithOffset(int position, int offset)方法,offset傳入0匾嘱,postion即目標(biāo)postion即可斤斧。如果使用RecyclerView.scrollToPosition();等方法,滑動(dòng)會(huì)很飄~定位不準(zhǔn)霎烙。
mPressedShowTextView 就是在屏幕中間顯示的當(dāng)前處于哪個(gè)index的TextView撬讽。

    /**
     * 當(dāng)前被按下的index的監(jiān)聽(tīng)器
     */
    public interface onIndexPressedListener {
        void onIndexPressed(int index, String text);//當(dāng)某個(gè)Index被按下

        void onMotionEventEnd();//當(dāng)觸摸事件結(jié)束(UP CANCEL)
    }
    
    private onIndexPressedListener mOnIndexPressedListener;
    
    public void setmOnIndexPressedListener(onIndexPressedListener mOnIndexPressedListener) {
        this.mOnIndexPressedListener = mOnIndexPressedListener;
    }
        //設(shè)置index觸摸監(jiān)聽(tīng)器
        setmOnIndexPressedListener(new onIndexPressedListener() {
            @Override
            public void onIndexPressed(int index, String text) {
                if (mPressedShowTextView != null) { //顯示hintTexView
                    mPressedShowTextView.setVisibility(View.VISIBLE);
                    mPressedShowTextView.setText(text);
                }
                //滑動(dòng)Rv
                if (mLayoutManager != null) {
                    int position = getPosByTag(text);
                    if (position != -1) {
                        mLayoutManager.scrollToPositionWithOffset(position, 0);
                    }
                }
            }

            @Override
            public void onMotionEventEnd() {
                //隱藏hintTextView
                if (mPressedShowTextView != null) {
                    mPressedShowTextView.setVisibility(View.GONE);
                }
            }
        });

五 封裝重復(fù)步驟,方便二次使用悬垃。

在我個(gè)人的理解里游昼,程序過(guò)多的封裝是會(huì)導(dǎo)致擴(kuò)展性的降低(也是因?yàn)槲宜接邢蓿欢覀兘裉煲庋b的這個(gè)IndexBar盗忱,由于使用場(chǎng)景和套路還是挺固定的(城市分組列表酱床,商品分類列表)所以值得將相關(guān)的操作都聚合起來(lái),二次使用更方便趟佃。畢竟扇谣,一個(gè)項(xiàng)目里同樣的代碼寫第二遍的程序員都不是好的圣斗士。(其實(shí)是我的leader不想寫第二遍闲昭,讓我封裝一下給他秒用)
梳理一下固定的操作:
1 都是先對(duì)原始數(shù)據(jù)sourceDatas源按照排序字段拼音排序罐寨。
2 然后將屏幕中hint的TextView ,以及索引數(shù)據(jù)源indexDatas(通過(guò)sourceDatas獲得)序矩,通過(guò)set方法傳給IndexBar鸯绿。
3 聯(lián)動(dòng)IndexBar和RecyclerView,使得觸摸IndexBar相應(yīng)區(qū)域RecyclerView會(huì)滾動(dòng)(借助sourceDatas獲得對(duì)應(yīng)postion)簸淀。
根據(jù)上述瓶蝴,我的設(shè)想在使用時(shí),只需要給IndexBar設(shè)置 原始數(shù)據(jù)sourceDatas租幕,HintTextView舷手,和RecyclerView的LinearLayoutManager,在IndexBar內(nèi)部對(duì)sourceDatas排序劲绪,并獲得索引數(shù)據(jù)源indexDatas男窟,然后設(shè)置一個(gè)默認(rèn)的index觸摸監(jiān)聽(tīng)器盆赤,在手指按下滑動(dòng)時(shí),由于IndexBar持有HintTextView和LayoutManager歉眷,則HintTextView的show hide牺六,以及LayoutManager的滾動(dòng) 都在IndexBar內(nèi)部完成。
最終使用預(yù)覽:

        //使用indexBar
        mTvSideBarHint = (TextView) findViewById(R.id.tvSideBarHint);//HintTextView
        mIndexBar = (IndexBar) findViewById(R.id.indexBar);//IndexBar
        mIndexBar.setmPressedShowTextView(mTvSideBarHint)//設(shè)置HintTextView
                .setNeedRealIndex(true)//設(shè)置需要真實(shí)的索引
                .setmLayoutManager(mManager)//設(shè)置RecyclerView的LayoutManager
                .setmSourceDatas(mDatas);//設(shè)置數(shù)據(jù)源

布局xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </android.support.v7.widget.RecyclerView>

    <mcxtzhang.itemdecorationdemo.IndexBar.widget.IndexBar
        android:id="@+id/indexBar"
        android:layout_width="24dp"
        android:layout_height="match_parent"
        android:layout_gravity="right"
        app:pressBackground="@color/partTranslucent"
        app:textSize="16sp" />

    <TextView
        android:id="@+id/tvSideBarHint"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_gravity="center"
        android:background="@drawable/shape_side_bar_bg"
        android:gravity="center"
        android:textColor="@android:color/white"
        android:textSize="48sp"
        android:visibility="gone"
        tools:text="A"
        tools:visibility="visible" />

</FrameLayout>

其中汗捡,setNeedRealIndex(true)//設(shè)置需要真實(shí)的索引淑际,是指索引欄的數(shù)據(jù)不是固定的A-Z,#凉唐。而是根據(jù)真實(shí)的sourceDatas生成庸追。
因?yàn)殒準(zhǔn)秸{(diào)用用起來(lái)很爽,所以在這些set方法里都return 了 this台囱。

1 抽象兩個(gè)實(shí)體類和一個(gè)接口淡溯。
先把tag抽象出來(lái),放在頂層簿训,這里存放的就是IndexBar顯示的每個(gè)index值(A-Z,#)(本例是城市的漢語(yǔ)拼音首字母)咱娶,而且在聯(lián)動(dòng)滑動(dòng)時(shí),根據(jù)tag獲取postion時(shí)强品,也需要用到tag膘侮。它是導(dǎo)航分組列表的基礎(chǔ)。

public class BaseIndexTagBean {
    private String tag;//所屬的分類(城市的漢語(yǔ)拼音首字母)

    public String getTag() {
        return tag;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }
}

然后抽象一個(gè)接口和一個(gè)實(shí)體類的榛,
接口定義一個(gè)方法getTarget()琼了,它返回 需要被轉(zhuǎn)化成拼音,并取出首字母 索引排序的 字段夫晌。(本例就是城市的名字)
實(shí)體類繼承BaseIndexTagBean雕薪,并實(shí)現(xiàn)以上接口,且額外存放 需要排序的字段的拼音值晓淀,(本例是城市的拼音)所袁。它根據(jù)getTarget()返回的值利用TinyPinyin庫(kù)得到拼音。

public interface IIndexTargetInterface {
    String getTarget();//需要被轉(zhuǎn)化成拼音凶掰,并取出首字母 索引排序的 字段
}
public abstract class BaseIndexPinyinBean extends BaseIndexTagBean implements IIndexTargetInterface {
    private String pyCity;//城市的拼音

    public String getPyCity() {
        return pyCity;
    }

    public void setPyCity(String pyCity) {
        this.pyCity = pyCity;
    }
}

有了以上兩個(gè)類一個(gè)接口燥爷,我們就可以將 對(duì)原始數(shù)據(jù)源sourceDatas按照拼音排序,并取出索引數(shù)據(jù)源indexDatas的操作封裝起來(lái)懦窘。

2 封裝原始數(shù)據(jù)源初始化(利用TinyPinyin獲取全拼音)前翎,取出索引數(shù)據(jù)源indexDatas的操作。
使用時(shí)畅涂,我們先讓具體的實(shí)體bean鱼填,繼承自BaseIndexPinyinBean ,在getTarget()方法返回排序目標(biāo)字段毅戈。本例如下:

public class CityBean extends BaseIndexPinyinBean {

    private String city;//城市名字
    
    public CityBean() {
    }
    public CityBean(String city) {
        this.city = city;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    @Override
    public String getTarget() {
        return city;
    }
}

IndexBar類內(nèi)代碼:
使用時(shí)會(huì)調(diào)用IndexBar.setmSourceDatas()方法傳入原始數(shù)據(jù)源苹丸,在方法內(nèi)對(duì)數(shù)據(jù)源初始化,并取出索引數(shù)據(jù)源苇经。

    private List<? extends BaseIndexPinyinBean> mSourceDatas;//Adapter的數(shù)據(jù)源
    public IndexBar setmSourceDatas(List<? extends BaseIndexPinyinBean> mSourceDatas) {
        this.mSourceDatas = mSourceDatas;
        initSourceDatas();//對(duì)數(shù)據(jù)源進(jìn)行初始化
        return this;
    }
    /**
     * 初始化原始數(shù)據(jù)源赘理,并取出索引數(shù)據(jù)源
     *
     * @return
     */
    private void initSourceDatas() {
        int size = mSourceDatas.size();
        for (int i = 0; i < size; i++) {
            BaseIndexPinyinBean indexPinyinBean = mSourceDatas.get(i);
            StringBuilder pySb = new StringBuilder();
            String target = indexPinyinBean.getTarget();//取出需要被拼音化的字段
            //遍歷target的每個(gè)char得到它的全拼音
            for (int i1 = 0; i1 < target.length(); i1++) {
                //利用TinyPinyin將char轉(zhuǎn)成拼音
                //查看源碼,方法內(nèi) 如果char為漢字扇单,則返回大寫拼音
                //如果c不是漢字商模,則返回String.valueOf(c)
                pySb.append(Pinyin.toPinyin(target.charAt(i1)));
            }
            indexPinyinBean.setPyCity(pySb.toString());//設(shè)置城市名全拼音

            //以下代碼設(shè)置城市拼音首字母
            String tagString = pySb.toString().substring(0, 1);
            if (tagString.matches("[A-Z]")) {//如果是A-Z字母開(kāi)頭
                indexPinyinBean.setTag(tagString);
                if (isNeedRealIndex) {//如果需要真實(shí)的索引數(shù)據(jù)源
                    if (!mIndexDatas.contains(tagString)) {//則判斷是否已經(jīng)將這個(gè)索引添加進(jìn)去,若沒(méi)有則添加
                        mIndexDatas.add(tagString);
                    }
                }
            } else {//特殊字母這里統(tǒng)一用#處理
                indexPinyinBean.setTag("#");
                if (isNeedRealIndex) {//如果需要真實(shí)的索引數(shù)據(jù)源
                    if (!mIndexDatas.contains("#")) {
                        mIndexDatas.add("#");
                    }
                }
            }
        }
        sortData();
    }

3 封裝對(duì)原始數(shù)據(jù)源sourceDatas蜘澜,索引數(shù)據(jù)源indexDatas的排序操作施流。

    /**
     * 對(duì)數(shù)據(jù)源排序
     */
    private void sortData() {
        //對(duì)右側(cè)欄進(jìn)行排序 將 # 丟在最后
        Collections.sort(mIndexDatas, new Comparator<String>() {
            @Override
            public int compare(String lhs, String rhs) {
                if (lhs.equals("#")) {
                    return 1;
                } else if (rhs.equals("#")) {
                    return -1;
                } else {
                    return lhs.compareTo(rhs);
                }
            }
        });

        //對(duì)數(shù)據(jù)源進(jìn)行排序
        Collections.sort(mSourceDatas, new Comparator<BaseIndexPinyinBean>() {
            @Override
            public int compare(BaseIndexPinyinBean lhs, BaseIndexPinyinBean rhs) {
                if (lhs.getTag().equals("#")) {
                    return 1;
                } else if (rhs.getTag().equals("#")) {
                    return -1;
                } else {
                    return lhs.getPyCity().compareTo(rhs.getPyCity());
                }
            }
        });
    }

4 是否需要真實(shí)的索引數(shù)據(jù)源。
相關(guān)變量定義:

    public static String[] INDEX_STRING = {"A", "B", "C", "D", "E", "F", "G", "H", "I","J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "#"};//#在最后面(默認(rèn)的數(shù)據(jù)源)
    private List<String> mIndexDatas;//索引數(shù)據(jù)源
    private boolean isNeedRealIndex;//是否需要根據(jù)實(shí)際的數(shù)據(jù)來(lái)生成索引數(shù)據(jù)源(例如 只有 A B C 三種tag鄙信,那么索引欄就 A B C 三項(xiàng))

初始化init時(shí)瞪醋,判斷不需要真實(shí)的索引數(shù)據(jù)源,就用默認(rèn)值(A-Z,#)

        if (!isNeedRealIndex) {//不需要真實(shí)的索引數(shù)據(jù)源
            mIndexDatas = Arrays.asList(INDEX_STRING);
        }

使用時(shí)装诡,如果如果真實(shí)索引數(shù)據(jù)源银受,調(diào)用這個(gè)方法,傳入true鸦采,一定要在設(shè)置數(shù)據(jù)源setmSourceDatas(List)之前調(diào)用宾巍。

    /**
     * 一定要在設(shè)置數(shù)據(jù)源{@link #setmSourceDatas(List)}之前調(diào)用
     *
     * @param needRealIndex
     * @return
     */
    public IndexBar setNeedRealIndex(boolean needRealIndex) {
        isNeedRealIndex = needRealIndex;
        if (isNeedRealIndex){
            if (mIndexDatas != null) {
                mIndexDatas = new ArrayList<>();
            }
        }
        return this;
    }

在initSourceDatas() 里,會(huì)根據(jù)這個(gè)變量往mIndexDatas里增加index渔伯。

5 IndexBar和外部聯(lián)動(dòng)的相關(guān)(HintTextView顶霞,和RecyclerView的LinearLayoutManager)
set方法很簡(jiǎn)單:

    public IndexBar setmPressedShowTextView(TextView mPressedShowTextView) {
        this.mPressedShowTextView = mPressedShowTextView;
        return this;
    }

    public IndexBar setmLayoutManager(LinearLayoutManager mLayoutManager) {
        this.mLayoutManager = mLayoutManager;
        return this;
    }

它們兩最終都是在index觸摸監(jiān)聽(tīng)器里用到,代碼上文已提及锣吼,只不過(guò)這次挪到IndexBar內(nèi)部init里选浑。
init函數(shù)如下:

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        ...
        if (!isNeedRealIndex) {//不需要真實(shí)的索引數(shù)據(jù)源
            mIndexDatas = Arrays.asList(INDEX_STRING);
        }
        //設(shè)置index觸摸監(jiān)聽(tīng)器
        setmOnIndexPressedListener(new onIndexPressedListener() {
            @Override
            public void onIndexPressed(int index, String text) {
                if (mPressedShowTextView != null) { //顯示hintTexView
                    mPressedShowTextView.setVisibility(View.VISIBLE);
                    mPressedShowTextView.setText(text);
                }
                //滑動(dòng)Rv
                if (mLayoutManager != null) {
                    int position = getPosByTag(text);
                    if (position != -1) {
                        mLayoutManager.scrollToPositionWithOffset(position, 0);
                    }
                }
            }
            @Override
            public void onMotionEventEnd() {
                //隱藏hintTextView
                if (mPressedShowTextView != null) {
                    mPressedShowTextView.setVisibility(View.GONE);
                }
            }
        });
    }
    /**
     * 根據(jù)傳入的pos返回tag
     *
     * @param tag
     * @return
     */
    private int getPosByTag(String tag) {
        if (TextUtils.isEmpty(tag)) {
            return -1;
        }
        for (int i = 0; i < mSourceDatas.size(); i++) {
            if (tag.equals(mSourceDatas.get(i).getTag())) {
                return i;
            }
        }
        return -1;
    }

六 完整代碼

思前想后還是放出來(lái)吧,三百多行有點(diǎn)長(zhǎng)

/**
 * 介紹:索引右側(cè)邊欄
 * 作者:zhangxutong
 * 郵箱:mcxtzhang@163.com
 * CSDN:http://blog.csdn.net/zxt0601
 * 時(shí)間: 16/09/04.
 */

public class IndexBar extends View {
    private static final String TAG = "zxt/IndexBar";
    public static String[] INDEX_STRING = {"A", "B", "C", "D", "E", "F", "G", "H", "I",
            "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
            "W", "X", "Y", "Z", "#"};//#在最后面(默認(rèn)的數(shù)據(jù)源)
    private List<String> mIndexDatas;//索引數(shù)據(jù)源
    private boolean isNeedRealIndex;//是否需要根據(jù)實(shí)際的數(shù)據(jù)來(lái)生成索引數(shù)據(jù)源(例如 只有 A B C 三種tag吐限,那么索引欄就 A B C 三項(xiàng))

    private int mWidth, mHeight;//View的寬高
    private int mGapHeight;//每個(gè)index區(qū)域的高度

    private Paint mPaint;

    private int mPressedBackground;//手指按下時(shí)的背景色

    //以下邊變量是外部set進(jìn)來(lái)的
    private TextView mPressedShowTextView;//用于特寫顯示正在被觸摸的index值
    private List<? extends BaseIndexPinyinBean> mSourceDatas;//Adapter的數(shù)據(jù)源
    private LinearLayoutManager mLayoutManager;

    public IndexBar(Context context) {
        this(context, null);
    }

    public IndexBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public IndexBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        int textSize = (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics());//默認(rèn)的TextSize
        mPressedBackground = Color.BLACK;//默認(rèn)按下是純黑色
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.IndexBar, defStyleAttr, 0);
        int n = typedArray.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.IndexBar_textSize:
                    textSize = typedArray.getDimensionPixelSize(attr, textSize);
                    break;
                case R.styleable.IndexBar_pressBackground:
                    mPressedBackground = typedArray.getColor(attr, mPressedBackground);
                default:
                    break;
            }
        }
        typedArray.recycle();

        if (!isNeedRealIndex) {//不需要真實(shí)的索引數(shù)據(jù)源
            mIndexDatas = Arrays.asList(INDEX_STRING);
        }
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(textSize);
        mPaint.setColor(Color.BLACK);

        //設(shè)置index觸摸監(jiān)聽(tīng)器
        setmOnIndexPressedListener(new onIndexPressedListener() {
            @Override
            public void onIndexPressed(int index, String text) {
                if (mPressedShowTextView != null) { //顯示hintTexView
                    mPressedShowTextView.setVisibility(View.VISIBLE);
                    mPressedShowTextView.setText(text);
                }
                //滑動(dòng)Rv
                if (mLayoutManager != null) {
                    int position = getPosByTag(text);
                    if (position != -1) {
                        mLayoutManager.scrollToPositionWithOffset(position, 0);
                    }
                }
            }

            @Override
            public void onMotionEventEnd() {
                //隱藏hintTextView
                if (mPressedShowTextView != null) {
                    mPressedShowTextView.setVisibility(View.GONE);
                }
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int t = getPaddingTop();//top的基準(zhǔn)點(diǎn)(支持padding)
        Rect indexBounds = new Rect();//存放每個(gè)繪制的index的Rect區(qū)域
        String index;//每個(gè)要繪制的index內(nèi)容
        for (int i = 0; i < mIndexDatas.size(); i++) {
            index = mIndexDatas.get(i);
            mPaint.getTextBounds(index, 0, index.length(), indexBounds);//測(cè)量計(jì)算文字所在矩形鲜侥,可以得到寬高
            Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();//獲得畫筆的FontMetrics,用來(lái)計(jì)算baseLine诸典。因?yàn)閐rawText的y坐標(biāo)描函,代表的是繪制的文字的baseLine的位置
            int baseline = (int) ((mGapHeight - fontMetrics.bottom - fontMetrics.top) / 2);//計(jì)算出在每格index區(qū)域,豎直居中的baseLine值
            canvas.drawText(index, mWidth / 2 - indexBounds.width() / 2, t + mGapHeight * i + baseline, mPaint);//調(diào)用drawText狐粱,居中顯示繪制index
        }
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                setBackgroundColor(mPressedBackground);//手指按下時(shí)背景變色
                //注意這里沒(méi)有break舀寓,因?yàn)閐own時(shí),也要計(jì)算落點(diǎn) 回調(diào)監(jiān)聽(tīng)器
            case MotionEvent.ACTION_MOVE:
                float y = event.getY();
                //通過(guò)計(jì)算判斷落點(diǎn)在哪個(gè)區(qū)域:
                int pressI = (int) ((y - getPaddingTop()) / mGapHeight);
                //邊界處理(在手指move時(shí)肌蜻,有可能已經(jīng)移出邊界互墓,防止越界)
                if (pressI < 0) {
                    pressI = 0;
                } else if (pressI >= mIndexDatas.size()) {
                    pressI = mIndexDatas.size() - 1;
                }
                //回調(diào)監(jiān)聽(tīng)器
                if (null != mOnIndexPressedListener) {
                    mOnIndexPressedListener.onIndexPressed(pressI, mIndexDatas.get(pressI));
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            default:
                setBackgroundResource(android.R.color.transparent);//手指抬起時(shí)背景恢復(fù)透明
                //回調(diào)監(jiān)聽(tīng)器
                if (null != mOnIndexPressedListener) {
                    mOnIndexPressedListener.onMotionEventEnd();
                }
                break;
        }
        return true;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mGapHeight = (mHeight - getPaddingTop() - getPaddingBottom()) / mIndexDatas.size();
    }


    /**
     * 當(dāng)前被按下的index的監(jiān)聽(tīng)器
     */
    public interface onIndexPressedListener {
        void onIndexPressed(int index, String text);//當(dāng)某個(gè)Index被按下

        void onMotionEventEnd();//當(dāng)觸摸事件結(jié)束(UP CANCEL)
    }

    private onIndexPressedListener mOnIndexPressedListener;

    public onIndexPressedListener getmOnIndexPressedListener() {
        return mOnIndexPressedListener;
    }

    public void setmOnIndexPressedListener(onIndexPressedListener mOnIndexPressedListener) {
        this.mOnIndexPressedListener = mOnIndexPressedListener;
    }

    /**
     * 顯示當(dāng)前被按下的index的TextView
     *
     * @return
     */

    public IndexBar setmPressedShowTextView(TextView mPressedShowTextView) {
        this.mPressedShowTextView = mPressedShowTextView;
        return this;
    }

    public IndexBar setmLayoutManager(LinearLayoutManager mLayoutManager) {
        this.mLayoutManager = mLayoutManager;
        return this;
    }

    /**
     * 一定要在設(shè)置數(shù)據(jù)源{@link #setmSourceDatas(List)}之前調(diào)用
     *
     * @param needRealIndex
     * @return
     */
    public IndexBar setNeedRealIndex(boolean needRealIndex) {
        isNeedRealIndex = needRealIndex;
        if (mIndexDatas != null) {
            mIndexDatas = new ArrayList<>();
        }
        return this;
    }

    public IndexBar setmSourceDatas(List<? extends BaseIndexPinyinBean> mSourceDatas) {
        this.mSourceDatas = mSourceDatas;
        initSourceDatas();//對(duì)數(shù)據(jù)源進(jìn)行初始化
        return this;
    }


    /**
     * 初始化原始數(shù)據(jù)源,并取出索引數(shù)據(jù)源
     *
     * @return
     */
    private void initSourceDatas() {
        int size = mSourceDatas.size();
        for (int i = 0; i < size; i++) {
            BaseIndexPinyinBean indexPinyinBean = mSourceDatas.get(i);
            StringBuilder pySb = new StringBuilder();
            String target = indexPinyinBean.getTarget();//取出需要被拼音化的字段
            //遍歷target的每個(gè)char得到它的全拼音
            for (int i1 = 0; i1 < target.length(); i1++) {
                //利用TinyPinyin將char轉(zhuǎn)成拼音
                //查看源碼蒋搜,方法內(nèi) 如果char為漢字篡撵,則返回大寫拼音
                //如果c不是漢字判莉,則返回String.valueOf(c)
                pySb.append(Pinyin.toPinyin(target.charAt(i1)));
            }
            indexPinyinBean.setPyCity(pySb.toString());//設(shè)置城市名全拼音

            //以下代碼設(shè)置城市拼音首字母
            String tagString = pySb.toString().substring(0, 1);
            if (tagString.matches("[A-Z]")) {//如果是A-Z字母開(kāi)頭
                indexPinyinBean.setTag(tagString);
                if (isNeedRealIndex) {//如果需要真實(shí)的索引數(shù)據(jù)源
                    if (!mIndexDatas.contains(tagString)) {//則判斷是否已經(jīng)將這個(gè)索引添加進(jìn)去,若沒(méi)有則添加
                        mIndexDatas.add(tagString);
                    }
                }
            } else {//特殊字母這里統(tǒng)一用#處理
                indexPinyinBean.setTag("#");
                if (isNeedRealIndex) {//如果需要真實(shí)的索引數(shù)據(jù)源
                    if (!mIndexDatas.contains("#")) {
                        mIndexDatas.add("#");
                    }
                }
            }
        }
        sortData();
    }

    /**
     * 對(duì)數(shù)據(jù)源排序
     */
    private void sortData() {
        //對(duì)右側(cè)欄進(jìn)行排序 將 # 丟在最后
        Collections.sort(mIndexDatas, new Comparator<String>() {
            @Override
            public int compare(String lhs, String rhs) {
                if (lhs.equals("#")) {
                    return 1;
                } else if (rhs.equals("#")) {
                    return -1;
                } else {
                    return lhs.compareTo(rhs);
                }
            }
        });

        //對(duì)數(shù)據(jù)源進(jìn)行排序
        Collections.sort(mSourceDatas, new Comparator<BaseIndexPinyinBean>() {
            @Override
            public int compare(BaseIndexPinyinBean lhs, BaseIndexPinyinBean rhs) {
                if (lhs.getTag().equals("#")) {
                    return 1;
                } else if (rhs.getTag().equals("#")) {
                    return -1;
                } else {
                    return lhs.getPyCity().compareTo(rhs.getPyCity());
                }
            }
        });
    }


    /**
     * 根據(jù)傳入的pos返回tag
     *
     * @param tag
     * @return
     */
    private int getPosByTag(String tag) {
        if (TextUtils.isEmpty(tag)) {
            return -1;
        }
        for (int i = 0; i < mSourceDatas.size(); i++) {
            if (tag.equals(mSourceDatas.get(i).getTag())) {
                return i;
            }
        }
        return -1;
    }
}

七 總結(jié)

不管是自定義ItemDecoration還是實(shí)現(xiàn)右側(cè)索引導(dǎo)航欄育谬,其實(shí)大量的自定義View知識(shí)在里面 券盅,
so 要想自定義ItemDecoration玩得好,自定義View少不了膛檀。

對(duì)數(shù)據(jù)源的排序字段按照拼音排序,我們使用TinyPinyin(https://github.com/promeG/TinyPinyin)幫助我們排序锰镀。
它的特性很適合Android平臺(tái)。

  1. 生成的拼音不包含聲調(diào)咖刃,也不處理多音字泳炉,默認(rèn)一個(gè)漢字對(duì)應(yīng)一個(gè)拼音;
  2. 拼音均為大寫嚎杨;
  3. 無(wú)需初始化花鹅,執(zhí)行效率很高(Pinyin4J的4倍);
  4. 很低的內(nèi)存占用(小于30KB)磕潮。
    (介紹來(lái)源于其項(xiàng)目github)

其實(shí)不僅僅是IndexBar以及它和RecyclerView翠胰,HintTextView的聯(lián)動(dòng)可以封裝在一起。
懸停頭部ItemDecoration也可以利用 BaseIndexTagBean 類來(lái)抽象一下自脯,不與具體的實(shí)體類耦合之景,

    private List<CityBean> mDatas;

替換成

    private List<?extends BaseIndexPinyinBean> mDatas;

即可。

本文起筆于9.5晚八點(diǎn)膏潮,項(xiàng)目上線打包期間锻狗,每逢打包是非多~你們懂得,結(jié)果打包期間出現(xiàn)各種問(wèn)題焕参,各種bug緊急修復(fù)通宵到凌晨轻纪,9.6日,兩點(diǎn)起床叠纷,又續(xù)寫后面三節(jié)刻帚。本文篇幅也略長(zhǎng),寫到后面自己也有點(diǎn)懵逼(也可能是通宵還沒(méi)醒導(dǎo)致)涩嚣,總耗時(shí)6小時(shí)+崇众,希望大家看后覺(jué)得有用可以給我刷波66666.


八 代碼地址

我不想吐槽CSDN了,上傳代碼真心慢航厚,
稍后補(bǔ)上CSDN地址顷歌。
csdn傳送門:
http://download.csdn.net/detail/zxt0601/9623621

github地址:歡迎star
https://github.com/mcxtzhang/ItemDecorationIndexBar


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市幔睬,隨后出現(xiàn)的幾起案子眯漩,更是在濱河造成了極大的恐慌,老刑警劉巖麻顶,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赦抖,死亡現(xiàn)場(chǎng)離奇詭異舱卡,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)摹芙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門灼狰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人浮禾,你說(shuō)我怎么就攤上這事》莺梗” “怎么了盈电?”我有些...
    開(kāi)封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)杯活。 經(jīng)常有香客問(wèn)我匆帚,道長(zhǎng),這世上最難降的妖魔是什么旁钧? 我笑而不...
    開(kāi)封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任吸重,我火速辦了婚禮,結(jié)果婚禮上歪今,老公的妹妹穿的比我還像新娘嚎幸。我一直安慰自己,他們只是感情好寄猩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布嫉晶。 她就那樣靜靜地躺著,像睡著了一般田篇。 火紅的嫁衣襯著肌膚如雪替废。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天泊柬,我揣著相機(jī)與錄音椎镣,去河邊找鬼。 笑死兽赁,一個(gè)胖子當(dāng)著我的面吹牛状答,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播闸氮,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼剪况,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蒲跨?” 一聲冷哼從身側(cè)響起译断,我...
    開(kāi)封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎或悲,沒(méi)想到半個(gè)月后孙咪,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體堪唐,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年翎蹈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了淮菠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡荤堪,死狀恐怖合陵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澄阳,我是刑警寧澤拥知,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站碎赢,受9級(jí)特大地震影響低剔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肮塞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一襟齿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧枕赵,春花似錦猜欺、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至装黑,卻和暖如春副瀑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恋谭。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工糠睡, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疚颊。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓狈孔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親材义。 傳聞我的和親對(duì)象是個(gè)殘疾皇子均抽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容