Android—RecyclerView進(jìn)階(2)—ItemDecoration與城市列表

我的CSDN: ListerCi
我的簡書: 東方未曦

寫在前面

本系列博客的demo都上傳到了github:RecyclerViewDemo
如果有幫助到你的話不妨給我點(diǎn)個(gè)star~

在介紹ItemDecoration之前我們不妨先看下它能實(shí)現(xiàn)什么功能。
這是一個(gè)國內(nèi)大部分城市的列表,通過城市拼音對(duì)其排序,通過拼音首字母對(duì)其分組。在滑動(dòng)到某一組的城市時(shí),它的Header會(huì)在頂部保持不動(dòng),下一組滑動(dòng)上來時(shí),新的Header會(huì)把上一組“頂”上去,這個(gè)效果就是ItemDecoration實(shí)現(xiàn)的。當(dāng)然,為了功能的完整性,我還添加了側(cè)邊欄用于搜索查找。

gif-城市列表demo.gif

看完效果后是不是對(duì)ItemDecoration充滿了興趣?現(xiàn)在讓我們一步一步去認(rèn)識(shí)它并實(shí)現(xiàn)這個(gè)功能吧。

一、ItemDecoration簡介

1.1 API介紹

ItemDecoration是定義在RecyclerView內(nèi)部的抽象類,排除過時(shí)的方法,它提供了3個(gè)可重寫的方法,代碼如下。

public abstract static class ItemDecoration {
        /**
         * 繪制提供給RecyclerView的裝飾
         * 任何由此方法繪制的內(nèi)容都會(huì)在item繪制之前就繪制完畢,因此它是處于item下層的
         */
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
            onDraw(c, parent);
        }

        /**
         * 此方法與onDraw相對(duì),方法中的內(nèi)容是在item繪制完畢后開始繪制的
         * 因此會(huì)顯示在item上層
         */
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                @NonNull State state) {
            onDrawOver(c, parent);
        }

        /**
         * 通過outRect表示當(dāng)前item距離left、top、right、bottom 4個(gè)方向的距離
         */
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                @NonNull RecyclerView parent, @NonNull State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

其中onDraw(...)onDrawOver(...)方法的參數(shù)中傳入了畫布Canvas,通過畫布我們可以在任何坐標(biāo)繪制任何事物。
getItemOffsets(...)方法用于指定每個(gè)item距離左上右下4個(gè)方向的距離,效果同margin。參數(shù)中傳入的view表示當(dāng)前的item,如果你想根據(jù)item的數(shù)據(jù)設(shè)置不同margin的話,可以通過RecyclerView的getChildAdapterPosition(View)得到該item在Adapter中的position。

1.2 DividerItemDecoration分析

ItemDecoration最簡單的用法就是添加分隔線,如果使用DividerItemDecoration,之后你會(huì)發(fā)現(xiàn)item之間多了一根細(xì)細(xì)的分隔線。

RecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

那么這個(gè)細(xì)線是怎么畫出來的呢?回憶一下ItemDecoration中方法的作用,步驟應(yīng)該是這樣的:首先我們通過getItemOffsets()在item之間添加間隔,然后通過onDraw()或者onDrawOver()在這段間隔內(nèi)繪制線段。

為了驗(yàn)證我們的想法,來看一下DividerItemDecoration的源碼。我們使用的時(shí)候是垂直方向,代碼中水平方向的代碼我就省略掉了。

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    
    private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
    private Drawable mDivider;
    // 垂直或水平方向
    private int mOrientation;

    private final Rect mBounds = new Rect();

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0); // 默認(rèn)的分隔線
        a.recycle();
        setOrientation(orientation);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        }
    }

    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        // 根據(jù)RecyclerView是否有padding獲取線段的left和right的坐標(biāo)
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }
        // 得到當(dāng)前顯示的每個(gè)child的線段的top和bottom坐標(biāo)
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            // 設(shè)置每個(gè)item與下方的間隔為mDivider.getIntrinsicHeight()
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

DividerItemDecoration的實(shí)現(xiàn)邏輯果然與我們想的一樣,先在getItemOffsets()中設(shè)置每個(gè)item與下方的間隔,隨后在onDraw(...)中調(diào)用drawVertical(Canvas canvas, RecyclerView parent)得到線段的邊界坐標(biāo)并繪制。

二、城市列表實(shí)現(xiàn)

2.1 分析實(shí)現(xiàn)方式

雖然demo中的效果是下一組的Header將上一組的Header頂了上去,但實(shí)現(xiàn)的邏輯并非如此,如果把Header的背景色調(diào)整為半透明,效果是這樣的。

gif-半透明Header.gif

Header半透明之后,它就露出了馬腳,仔細(xì)觀察后我們可以總結(jié)實(shí)現(xiàn)這個(gè)效果所需的步驟:
① 每個(gè)分組的第一個(gè)城市item的上方都有一個(gè)Header,例如“阿壩”和“白城”的上方都有一個(gè)Header
② 當(dāng)前RecyclerView所展示的第一個(gè)item的分類會(huì)顯示在RecyclerView的最上方,例如當(dāng)前RecyclerView第一個(gè)item是“阿克蘇”、“安慶”等城市時(shí),RecyclerView最上方會(huì)漂浮一個(gè)"A"類Header
③ 當(dāng)某個(gè)分組的最后一個(gè)item滑出RecyclerView時(shí),Header會(huì)隨著這個(gè)item一起滑走,這也是“頂上去”效果的由來。例如當(dāng)“澳門”即將滑出RecyclerView時(shí),"A"類Header會(huì)隨著“澳門”item一起滑走,并且我們很容易得到他們坐標(biāo)之間的關(guān)系:item.bottom = Header.bottom

2.2 具體實(shí)現(xiàn)

分析完步驟,即可開始實(shí)現(xiàn)這個(gè)效果。項(xiàng)目中的城市數(shù)據(jù)保存在arrays文件中,數(shù)據(jù)格式如下所示,我已經(jīng)先為每個(gè)城市添加了拼音并為所有城市進(jìn)行了排序,數(shù)據(jù)中還包括城市ID,完整數(shù)據(jù)請(qǐng)去博客開頭的github下載。

<string-array name="city">
        <item>阿壩</item>
        <item>aba</item>
        <item>101271901</item>
        <item>阿克蘇</item>
        <item>akesu</item>
        <item>101130801</item>
        <item>阿勒泰</item>
        <item>aletai</item>
        <item>101131401</item>
        <item>阿里</item>
        <item>ali</item>
        <item>101140701</item>
        <item>安康</item>
        <item>ankang</item>
        <item>101110701</item>
        <item>安慶</item>
        <item>anqing</item>
        <item>101220601</item>
        <item>鞍山</item>
        <item>anshan</item>
        <item>101070301</item>
        ......
</string-array>

在繪制Header時(shí),我們需要知道一個(gè)item是不是它分組的第一個(gè)或者是最后一個(gè),那么需要構(gòu)建這樣的一個(gè)實(shí)體類:

public class CityInfo {

    private String mCityName;
    private String mPinYin;
    private String mGroup;
    private String mCityID;
    private boolean mIsFirstInGroup;
    private boolean mIsLastInGroup;

    public CityInfo(String cityName, String pinYin, String cityID,
                    boolean isFirstInGroup, boolean isLastInGroup) {
        this.mCityName = cityName;
        this.mPinYin = pinYin;
        this.mGroup = mPinYin.substring(0, 1).toUpperCase();
        this.mCityID = cityID;
        this.mIsFirstInGroup = isFirstInGroup;
        this.mIsLastInGroup = isLastInGroup;
    }

    // setters and getters...
}

再來將數(shù)據(jù)都解析成實(shí)體類。由于數(shù)據(jù)是已經(jīng)排好序的,那么判斷一個(gè)item是不是它分組的第一個(gè)或最后一個(gè)可以用這種方式:如果第i個(gè)數(shù)據(jù)與第i-1個(gè)的group不相同,那么i就是i的分組的第一個(gè)item;而i-1就是i-1的分組的最后一個(gè)item。

    private void prepareCityInfo() {
        mCityInfoList = new ArrayList<>();
        String[] cityArray = getResources().getStringArray(R.array.city);
        String curGroup = "0";
        // 每 3 個(gè)String構(gòu)成一個(gè)CityInfo
        for (int i = 0; i < cityArray.length; i += 3) {
            CityInfo cityInfo = new CityInfo(cityArray[i], cityArray[i + 1], cityArray[i + 2],
                    false, false);
            if (!cityInfo.getGroup().equals(curGroup)) {
                // 如果當(dāng)前城市的 group 信息與保存的不一致, 那么就是該 group 的第一個(gè)
                cityInfo.setIsFirstInGroup(true);
                // 同時(shí)將該 group 信息添加到索引中
                indexList.add(cityInfo.getGroup());
                // 它的上一個(gè)城市就是上一個(gè) group 的最后一個(gè)
                if (i > 0) {
                    mCityInfoList.get(mCityInfoList.size() - 1).setIsLastInGroup(true);
                }
                curGroup = cityInfo.getGroup();
            }
            mCityInfoList.add(cityInfo);
        }
    }

下面重點(diǎn)來看下怎么自定義ItemDecoration,我們根據(jù)之前總結(jié)的步驟來,首先為每組的第一個(gè)item繪制Header,在繪制Header之前需要通過getItemOffsets()方法為Header預(yù)留空間。

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
                               @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position = parent.getChildAdapterPosition(view);
        CityInfo cityInfo = mCityInfoList.get(position);
        if (cityInfo.isFirstInGroup()) {
            outRect.top = GROUP_ITEM_TOP;
        }
    }

再通過onDraw()繪制,這里parent.getChildCount()獲取到的是RecyclerView中當(dāng)前可見的所有item的數(shù)量。

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            CityInfo cityInfo = mCityInfoList.get(position);
            if (cityInfo.isFirstInGroup()) {
                int left = parent.getPaddingLeft();
                int top = view.getTop() - GROUP_ITEM_TOP;
                int right = parent.getRight() - parent.getPaddingRight();
                int bottom = view.getTop();
                // 繪制背景
                c.drawRect(left, top, right, bottom, mBackGroundPaint);
                // 繪制文字
                drawText(c, left, top, bottom, cityInfo.getGroup());
            }
        }
    }

再來繪制固定于RecyclerView頂端的Header,這里只要得到RecyclerView所展示的第一個(gè)item的group并將其繪制即可。由于這個(gè)Header是在RecyclerView上層的,因此需要在onDrawOver()中繪制。

public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        // 得到當(dāng)前所展示的第一個(gè)View
        View view = parent.getChildAt(0);
        int position = parent.getChildAdapterPosition(view);
        CityInfo cityInfo = mCityInfoList.get(position);
            
        int left = parent.getPaddingLeft();
        int top = parent.getPaddingTop();
        int right = parent.getRight() - parent.getPaddingRight();
        int bottom = top + GROUP_ITEM_TOP;
        c.drawRect(left, top, right, bottom, mBackGroundPaint);

        drawText(c, left, top, bottom, cityInfo.getGroup());
    }

最后需要實(shí)現(xiàn)就是Header隨著當(dāng)前group的最后一個(gè)item移動(dòng)的效果,移動(dòng)時(shí)Header的bottom與item的bottom一致即可。那什么時(shí)候開始移動(dòng)呢?很顯然就是當(dāng)這個(gè)item的bottom與RecyclerView頂部的距離小于Header的高度時(shí)開始移動(dòng)。我們修改onDrawOver()方法如下即可。

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        View view = parent.getChildAt(0);
        int position = parent.getChildAdapterPosition(view);
        CityInfo cityInfo = mCityInfoList.get(position);
        // 當(dāng)前第一個(gè)item是它group的最后一個(gè)
        // 且view的bottom距離RecyclerView頂端小于Header的高度
        if (cityInfo.isLastInGroup() && view.getBottom() < GROUP_ITEM_TOP) {
            int left = parent.getPaddingLeft();
            int top = view.getBottom() - GROUP_ITEM_TOP;
            int right = parent.getRight() - parent.getPaddingRight();
            int bottom = view.getBottom();
            c.drawRect(left, top, right, bottom, mBackGroundPaint);

            drawText(c, left, top, bottom, cityInfo.getGroup());
        } else {
            int left = parent.getPaddingLeft();
            int top = parent.getPaddingTop();
            int right = parent.getRight() - parent.getPaddingRight();
            int bottom = top + GROUP_ITEM_TOP;
            c.drawRect(left, top, right, bottom, mBackGroundPaint);

            drawText(c, left, top, bottom, cityInfo.getGroup());
        }
    }

代碼介紹到這里就結(jié)束了,如果你對(duì)整體的程序感興趣,歡迎去github下載。

三、參考

RecyclerView探索之通過ItemDecoration實(shí)現(xiàn)StickyHeader效果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容