【Android】使用RecyclerView優(yōu)雅實(shí)現(xiàn)懸浮標(biāo)題通訊錄

ContactList是仿通訊錄制作的一個(gè)app demo

主要技術(shù)點(diǎn)在RecyclerView励两,和自定義view
實(shí)現(xiàn)了懸浮標(biāo)題頭,導(dǎo)航側(cè)欄

項(xiàng)目地址:https://github.com/hgDendi/ContactsList
歡迎star和fork

界面概覽:

ContactsListDemo
ContactsListDemo
ContactsListDemo2
ContactsListDemo2

概要

contactsListStructure
contactsListStructure

如圖慎框,主要簡(jiǎn)單劃分為兩個(gè)部分:

? 數(shù)據(jù)源、與界面組件洞斯。

? 數(shù)據(jù)源主要來自手機(jī)的通訊錄信息掺逼,通過ContentResolver獲取。

? 而界面組件主要有顯示列表和側(cè)邊欄宋税。而重點(diǎn)在于列表的分組欄的繪制與現(xiàn)實(shí)摊崭,這就依靠ItemDecoration來進(jìn)行實(shí)現(xiàn)了,這也是難點(diǎn)杰赛。

復(fù)用方法

FloatingBarItemDecoration傳入需要繪制標(biāo)題欄的position和標(biāo)題String的map,目前只支持豎項(xiàng)矮台、單列的列表乏屯,如需要擴(kuò)展,請(qǐng)讀完此文瘦赫,明白原理后很容易實(shí)現(xiàn)辰晕。

IndexBar傳入Label的List,通過setListener加入勾子确虱。

FloatingBarItemDecoration

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

ItemDecoration主要是用來對(duì)RecyclerView進(jìn)行一些修飾含友,是對(duì)adapter數(shù)據(jù)集中的數(shù)據(jù)視圖增加修飾或空位。經(jīng)常被用來畫分割線校辩、強(qiáng)調(diào)效果窘问、可見的分組邊界等。

getItemOffset()

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position = ((RecyclerView.LayoutParams))
          view.getLayoutParams()).getViewAdapterPosition();
        outRect.set(0, mList.containsKey(position) ? mTitleHeight : 0, 0, 0);
    }

繪制間距宜咒,為繪制標(biāo)題欄空出間隙惠赫。主要邏輯是通過當(dāng)前view的position判斷是否需要在上方空出矩形范圍。

onDraw()

主要是進(jìn)行靜態(tài)標(biāo)題欄等繪制故黑,即在每組view的上方儿咱,即getItemOffset()的區(qū)域進(jìn)行標(biāo)題欄的繪制。

@Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = 
                (RecyclerView.LayoutParams) child.getLayoutParams();
            int position = params.getViewAdapterPosition();
            if (!mList.containsKey(position)) {
                continue;
            }
            drawTitleArea(c, left, right, child, params, position);
        }
    }

onDrawOver

實(shí)現(xiàn)懸浮分組欄场晶,以及懸浮分組欄碰撞效果繪制混埠。

對(duì)于整個(gè)列表的繪制流程,是遵循如下的順序:

? ItemDecoration#onDraw() -> ItemView的繪制 -> ItemDecoration#onDrawOver

故而在onDrawOver中實(shí)現(xiàn)可以滿足“懸浮”诗轻,即在最上層的效果钳宪。

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        final int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
        if (position == RecyclerView.NO_POSITION) {
            return;
        }
        View child = parent.findViewHolderForAdapterPosition(position).itemView;
        String initial = getTag(position);
        if (initial == null) {
            return;
        }

        //flag指示當(dāng)標(biāo)題欄是否發(fā)生碰撞(如開頭gif圖中指示的)
        boolean flag = false;
        if (getTag(position + 1) != null && !initial.equals(getTag(position + 1))) {
            if (child.getHeight() + child.getTop() < mTitleHeight) {
                //與restore()對(duì)應(yīng),表示下面translate平移坐標(biāo)系只對(duì)繪制當(dāng)前標(biāo)題欄生效
                c.save();
                flag = true;
                //translate使發(fā)生碰撞時(shí),兩個(gè)標(biāo)題欄緊貼使套,制造出擠開的效果(dy<0,表示繪制偏下)
                c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);
            }
        }

        c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(),
                parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mBackgroundPaint);
        c.drawText(initial, child.getPaddingLeft() + mTextStartMargin,
                parent.getPaddingTop() + mTitleHeight - (mTitleHeight - mTextHeight) / 2 - mTextBaselineOffset, mTextPaint);

        if (flag) {
            c.restore();
        }
    }

IndexBar

IndexBar是側(cè)邊欄的實(shí)現(xiàn)罐呼,是采用的自定義View的形式。

FontMatrics

在此之前侦高,介紹一個(gè)概念FontMatrics,是表征字體的一個(gè)矩陣嫉柴。

定義BaseLine為Text的起始點(diǎn)(類似英文五線譜的baseline)

drawText傳入的縱坐標(biāo)值也為BaseLine所在的縱坐標(biāo),而非矩形區(qū)域的左下角的縱坐標(biāo)(這點(diǎn)很重要,否則在開發(fā)者模式中開啟布局邊界會(huì)發(fā)現(xiàn)字體和邊界錯(cuò)亂)

主要有以下幾個(gè)屬性:

  • Top (<0)
    • Ascent可能的最小值(絕對(duì)值最大)
  • Ascent (<0)
    • 字體最高處距BaseLine的距離
  • Descent (>0)
    • 字體最低處距BaseLine的距離
  • Bottom (>0)
    • Descent可能的最大值
  • Leading
    • 間距奉呛,用于多行文字顯示時(shí)的距離
fontMatrics
fontMatrics

在此例中我們用來計(jì)算每個(gè)text的高度计螺,以此作為測(cè)量View高度的參數(shù)。很多時(shí)候可以選擇不加leanding值瞧壮, 因?yàn)閱涡卸嘈袝r(shí)候的leading值都為0.(不知道什么時(shí)候可以取到非0的值)

Paint.FontMetrics fm = mPaint.getFontMetrics();
float singleHeight = fm.bottom - fm.top + fm.leading;

onMeasure()

計(jì)算View的長(zhǎng)寬登馒。

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

private int measureWidth(int widthMeasureSpec) {
    int result;
    int specMode = MeasureSpec.getMode(widthMeasureSpec);
    int specSize = MeasureSpec.getSize(widthMeasureSpec);
    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        result = getSuggestedMinWidth();
        if (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
    }
    return result;
}

//獲取建議的最小寬度,盡量保證不會(huì)出現(xiàn)顯示不下的情況(極端情況下仍會(huì)顯示不下)
private int getSuggestedMinWidth() {
    String maxLengthTag = "";
    for (String tag : mNavigators) {
        if (maxLengthTag.length() < tag.length()) {
            maxLengthTag = tag;
        }
    }
    return (int) (mPaint.measureText(maxLengthTag) + 0.5);
}

private int measureHeight(int heightMeasureSpec) {
    int result;
    int specMode = MeasureSpec.getMode(heightMeasureSpec);
    int specSize = MeasureSpec.getSize(heightMeasureSpec);
    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        Paint.FontMetrics fm = mPaint.getFontMetrics();
        float singleHeight = fm.bottom - fm.top + fm.leading;
        //這個(gè)mLetterSpacingExtra是疏密程度咆槽,是自定義屬性陈轿,默認(rèn)1.4
        mBaseLineHeight = fm.bottom * mLetterSpacingExtra;
        result = (int) (mNavigators.size() * singleHeight * mLetterSpacingExtra);
        if (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
    }
    return result;
}

onDraw()

負(fù)責(zé)繪制

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int height = getHeight();
        int width = getWidth();
        //高度為0,可能是因?yàn)閭魅雲(yún)?shù)為空秦忿,則不予顯示
        if (height == 0) {
            return;
        }
        int singleHeight = height / mNavigators.size();

        //遍歷繪制Text
        for (int i = 0; i < mNavigators.size(); i++) {
            float xPos = width / 2 - mPaint.measureText(mNavigators.get(i)) / 2;
            float yPos = singleHeight * (i + 1);
            if (i == mFocusIndex) {
                canvas.drawText(mNavigators.get(i), xPos, yPos - mBaseLineHeight, mFocusPaint);
            } else {
                canvas.drawText(mNavigators.get(i), xPos, yPos - mBaseLineHeight, mPaint);
            }
        }
    }

DispatchTouchEvent()

處理交互事件麦射,主要是監(jiān)聽UP、CANCEL灯谣、DOWN潜秋、MOVE,其中以DOWN做為起點(diǎn)胎许,CANCEL峻呛、UP做為終點(diǎn),其他為中間狀態(tài)辜窑。以TAG的焦點(diǎn)變更和事件的開始钩述、結(jié)束做為重繪的觸發(fā)點(diǎn)。

@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        final float y = event.getY();
        final int formerFocusIndex = mFocusIndex;
        final OnTouchingLetterChangeListener listener = mOnTouchingLetterChangeListener;
        final int c = calculateOnClickItemNum(y);

        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mFocusIndex = -1;
                invalidate();
                listener.onTouchingEnd(mNavigators.get(c));
                break;
            case MotionEvent.ACTION_DOWN:
                listener.onTouchingStart(mNavigators.get(c));
            default:
                if (formerFocusIndex != c) {
                    if (c >= 0 && c < mNavigators.size()) {
                        listener.onTouchingLetterChanged(mNavigators.get(c));
                        mFocusIndex = c;
                        invalidate();
                    }
                }
                break;
        }
        return true;
    }

    /**
     * @param yPos
     * @return the corresponding position in list
     */
    private int calculateOnClickItemNum(float yPos) {
        int result;
        //計(jì)算當(dāng)前觸摸點(diǎn)屬于哪個(gè)TAG,超出邊界按照邊界值返回(尤其在MOVE的時(shí)候很容易滑出邊界)
        result = (int) (yPos / getHeight() * mNavigators.size());
        if (result >= mNavigators.size()) {
            result = mNavigators.size() - 1;
        } else if (result < 0) {
            result = 0;
        }
        return result;
    }

ContactsUtils

主要是負(fù)責(zé)獲得縮寫谬擦,其中英文字符就直接獲得英文字符切距,中文字符通過比對(duì)GB2312得到英文縮寫

對(duì)于中文獲得縮寫的核心思想如下,是通過比對(duì)GB2312值得到中文中聲母惨远,繼而獲得縮寫情況谜悟。

    //GB2312中簡(jiǎn)體中文的起止,判斷范圍
    private static int BEGIN = 45217;
    private static int END = 63486;

    /**
     * 各聲母第一個(gè)漢字
     * {i北秽、u葡幸、v} 不做聲母
     */
    private static char[] chartable = {'啊', '芭', '擦', '搭', '蛾', '發(fā)', '噶', '哈', '擊', '喀', '垃','媽', '拿', '哦', '啪', '期', '然', '撒', '塌', '挖', '昔', '壓', '匝'};

    private static char[] initialtable = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K','L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z'};

    //此table是各聲母對(duì)應(yīng)的起始GB值,與initialtable對(duì)應(yīng)
    private static int[] table = new int[chartable.length + 1];

    static {
        for (int i = 0; i < chartable.length; i++) {
            table[i] = gbValue(chartable[i]);
        }
        table[chartable.length] = END;
    }
    
    //計(jì)算char對(duì)應(yīng)的gb值
    private static int gbValue(char ch) {
        String str = "" + ch;
        try {
            byte[] bytes = str.getBytes("GB2312");
            if (bytes.length < 2) {
                return 0;
            }
            return (bytes[0] << 8 & 0xff00) + (bytes[1] & 0xff);
        } catch (Exception e) {
            return 0;
        }
    }

ContactsManager

負(fù)責(zé)通訊錄信息的獲取贺氓,此處只取了電話號(hào)碼和聯(lián)系人名稱蔚叨,使用的是ContentResolver進(jìn)行查詢

    @NonNull
    public static ArrayList<ShareContactsBean> getPhoneContacts(Context mContext) {
        ArrayList<ShareContactsBean> result = new ArrayList<>(0);
        ContentResolver resolver = mContext.getContentResolver();
        Cursor phoneCursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME}, null, null, null);
        if (phoneCursor != null) {
            while (phoneCursor.moveToNext()) {
                String phoneNumber = phoneCursor.getString(0).replace(" ", "");
                String contactName = phoneCursor.getString(1);
                result.add(new ShareContactsBean(contactName, phoneNumber));
            }
            phoneCursor.close();
        }
        //對(duì)結(jié)果進(jìn)行排序,這個(gè)排序方法寫在bean中
        Collections.sort(result, new Comparator<ShareContactsBean>() {
            @Override
            public int compare(ShareContactsBean l, ShareContactsBean r) {
                return l.compareTo(r);
            }
        });
        return result;
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蔑水,隨后出現(xiàn)的幾起案子邢锯,更是在濱河造成了極大的恐慌,老刑警劉巖搀别,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丹擎,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡歇父,警方通過查閱死者的電腦和手機(jī)蒂培,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來榜苫,“玉大人护戳,你說我怎么就攤上這事〈共牵” “怎么了媳荒?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)驹饺。 經(jīng)常有香客問我肺樟,道長(zhǎng),這世上最難降的妖魔是什么逻淌? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮疟暖,結(jié)果婚禮上卡儒,老公的妹妹穿的比我還像新娘。我一直安慰自己俐巴,他們只是感情好骨望,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著欣舵,像睡著了一般擎鸠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缘圈,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天劣光,我揣著相機(jī)與錄音,去河邊找鬼糟把。 笑死绢涡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的遣疯。 我是一名探鬼主播雄可,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了数苫?” 一聲冷哼從身側(cè)響起聪舒,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虐急,沒想到半個(gè)月后箱残,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡戏仓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年疚宇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赏殃。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡敷待,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仁热,到底是詐尸還是另有隱情榜揖,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布抗蠢,位于F島的核電站举哟,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏迅矛。R本人自食惡果不足惜妨猩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望秽褒。 院中可真熱鬧壶硅,春花似錦、人聲如沸销斟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蚂踊。三九已至约谈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間犁钟,已是汗流浹背棱诱。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留特纤,地道東北人军俊。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像捧存,于是被迫代替她去往敵國(guó)和親粪躬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子担败,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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