RecyclerView使用完全指南,是時(shí)候體驗(yàn)新控件了(二)

轉(zhuǎn)載注明出處:http://www.lxweimin.com/p/7c3c549a0ec4

概述

上一篇講解了RecyclerView的基本用法,回顧下上一篇文章講解內(nèi)容

  • 水平列表展示,設(shè)置LayoutManager的方向性
  • 豎直列表展示,設(shè)置LayoutManager的方向性
  • 自定義間隔,RecyclerView.addItemDecoration()
  • Item添加和刪除動(dòng)畫(huà),RecyclerView.setItemAnimator()

關(guān)于網(wǎng)格樣式和瀑布流樣式在本篇會(huì)仔細(xì)的介紹,細(xì)心的同學(xué)會(huì)發(fā)現(xiàn),自定義間隔在上一篇文章中并沒(méi)有太過(guò)深入,只是介紹了方法的調(diào)用時(shí)機(jī),但是關(guān)于更換間隔樣式?jīng)]有太詳細(xì)的介紹,是因?yàn)榱斜順邮降腞ecyclerView自定義間隔比較簡(jiǎn)單,統(tǒng)一放到復(fù)雜一點(diǎn)的網(wǎng)格中來(lái)講解。直接進(jìn)入主題,看看期待已久的網(wǎng)格模式和瀑布流模式的使用吧。

網(wǎng)格樣式

上篇文章中已經(jīng)了解到,RecyclerView展示的樣式是有布局管理器LayoutManager來(lái)控制。網(wǎng)格樣式的管理器是GridLayoutManager,看一下它最常用的兩個(gè)構(gòu)造函數(shù)以及參數(shù)含義。

  • GridLayoutManager(Context context, int spanCount)
    • spanCount,每列或者每行的item個(gè)數(shù),設(shè)置為1,就是列表樣式
    • 該構(gòu)造函數(shù)默認(rèn)是豎直方向的網(wǎng)格樣式
  • GridLayoutManager(Context context, int spanCount, int orientation,boolean reverseLayout)
    • spanCount,每列或者每行的item個(gè)數(shù),設(shè)置為1,就是列表樣式
    • 網(wǎng)格樣式的方向,水平(OrientationHelper.HORIZONTAL)或者豎直(OrientationHelper.VERTICAL)
    • reverseLayout,是否逆向,true:布局逆向展示,false:布局正向顯示

看一下使用。

// 豎直方向的網(wǎng)格樣式,每行四個(gè)Item
mLayoutManager = new GridLayoutManager(this, 4, OrientationHelper.VERTICAL, false);
mRecyclerView.setLayoutManager(mLayoutManager);

看一下運(yùn)行效果。

RecyclerView-網(wǎng)格無(wú)間隔.jpg

網(wǎng)格樣式已經(jīng)顯示出來(lái)了,和之前遇見(jiàn)的問(wèn)題一樣,沒(méi)有間隔線,非常丑,間隔線必須加,而且要使用自定義,不使用系統(tǒng)自帶的。

新建文件md_divider.xml,是一個(gè)灰色的矩形。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle" >
    <solid android:color="@android:color/darker_gray"/>
    <size android:height="4dp" android:width="4dp"/>
</shape>

在styles.xml中的自定義的應(yīng)用主題里替換掉listdivider屬性。

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="android:listDivider">@drawable/md_divider</item>
</style>

然后繼承RecyclerView.ItemDecoration類,在構(gòu)造函數(shù)里獲取自定義的間隔線,復(fù)寫繪制間隔線的方法。

public class MDGridRvDividerDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    /**
     * 用于繪制間隔樣式
     */
    private Drawable mDivider;

    public MDGridRvDividerDecoration(Context context) {
        // 獲取默認(rèn)主題的屬性
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
    }


    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        // 繪制間隔,每一個(gè)item,繪制右邊和下方間隔樣式
        int childCount = parent.getChildCount();
        int spanCount = ((GridLayoutManager)parent.getLayoutManager()).getSpanCount();
        int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
        boolean isDrawHorizontalDivider = true;
        boolean isDrawVerticalDivider = true;
        int extra = childCount % spanCount;
        extra = extra == 0 ? spanCount : extra;
        for(int i = 0; i < childCount; i++) {
            isDrawVerticalDivider = true;
            isDrawHorizontalDivider = true;
            // 如果是豎直方向,最右邊一列不繪制豎直方向的間隔
            if(orientation == OrientationHelper.VERTICAL && (i + 1) % spanCount == 0) {
                isDrawVerticalDivider = false;
            }

            // 如果是豎直方向,最后一行不繪制水平方向間隔
            if(orientation == OrientationHelper.VERTICAL && i >= childCount - extra) {
                isDrawHorizontalDivider = false;
            }

            // 如果是水平方向,最下面一行不繪制水平方向的間隔
            if(orientation == OrientationHelper.HORIZONTAL && (i + 1) % spanCount == 0) {
                isDrawHorizontalDivider = false;
            }

            // 如果是水平方向,最后一列不繪制豎直方向間隔
            if(orientation == OrientationHelper.HORIZONTAL && i >= childCount - extra) {
                isDrawVerticalDivider = false;
            }

            if(isDrawHorizontalDivider) {
                drawHorizontalDivider(c, parent, i);
            }

            if(isDrawVerticalDivider) {
                drawVerticalDivider(c, parent, i);
            }
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
        int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
        int position = parent.getChildLayoutPosition(view);
        if(orientation == OrientationHelper.VERTICAL && (position + 1) % spanCount == 0) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            return;
        }

        if(orientation == OrientationHelper.HORIZONTAL && (position + 1) % spanCount == 0) {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            return;
        }

        outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
    }

    /**
     * 繪制豎直間隔線
     * 
     * @param canvas
     * @param parent
     *              父布局,RecyclerView
     * @param position
     *              irem在父布局中所在的位置
     */
    private void drawVerticalDivider(Canvas canvas, RecyclerView parent, int position) {
        final View child = parent.getChildAt(position);
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
        final int top = child.getTop() - params.topMargin;
        final int bottom = child.getBottom() + params.bottomMargin + mDivider.getIntrinsicHeight();
        final int left = child.getRight() + params.rightMargin;
        final int right = left + mDivider.getIntrinsicWidth();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(canvas);
    }

    /**
     * 繪制水平間隔線
     * 
     * @param canvas
     * @param parent
     *              父布局,RecyclerView
     * @param position
     *              item在父布局中所在的位置
     */
    private void drawHorizontalDivider(Canvas canvas, RecyclerView parent, int position) {
        final View child = parent.getChildAt(position);
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
        final int top = child.getBottom() + params.bottomMargin;
        final int bottom = top + mDivider.getIntrinsicHeight();
        final int left = child.getLeft() - params.leftMargin;
        final int right = child.getRight() + params.rightMargin + mDivider.getIntrinsicWidth();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(canvas);
    }
}

設(shè)置RecyclerView的間隔線。

mRecyclerView.addItemDecoration(new MDGridRvDividerDecoration(this));

運(yùn)行效果如下圖。


RecyclerView-網(wǎng)格有間隔.jpg

關(guān)于網(wǎng)格樣式的RecyclerView使用大體和列表樣式相同,主要在于間隔線的實(shí)現(xiàn)上有些不同,來(lái)看一下如果真正的使用自定義的間隔線需要做些什么。

  • 實(shí)現(xiàn)間隔線樣式,可以是xml文件也可以是圖片
  • 覆蓋應(yīng)用主題的listdivider屬性,使用自定義的間隔線樣式
  • 繼承RecyclerView.ItemDecoration類,并實(shí)現(xiàn)其中的繪制間隔線方法
  • 設(shè)置RecyclerView間隔線樣式

關(guān)于第三步,實(shí)現(xiàn)繪制線的方法,上面的代碼提供了一種大體的思路,可以供大家借鑒,下面就讓我們看看期待已久的瀑布流樣式的列表。

瀑布流樣式

RecyclerView的瀑布流布局管理器是taggeredGridLayoutManager,它最常用的構(gòu)造函數(shù)就一個(gè),StaggeredGridLayoutManager(int spanCount, int orientation),spanCount代表每行或每列的Item個(gè)數(shù),orientation代表列表的方向,豎直或者水平。

看在代碼中的使用。

// 初始化布局管理器
mLayoutManager = new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL);
// 設(shè)置布局管理器
mRecyclerView.setLayoutManager(mLayoutManager);
// 設(shè)置adapter
mRecyclerView.setAdapter(mAdapter);
// 設(shè)置間隔樣式
mRecyclerView.addItemDecoration(new MDStaggeredRvDividerDecotation(this));

要實(shí)現(xiàn)瀑布流效果(僅討論豎直方向的瀑布流樣式),每一個(gè)Item的高度要有所差別,如果所有的item的高度相同,就和網(wǎng)格樣式是一樣的展示效果。示例中就實(shí)現(xiàn)兩中不同高度的Item,一個(gè)高度為80dp,一個(gè)高度為100dp。

view_rv_staggered_item.xml布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="80dp">
    <TextView
        android:id="@+id/item_tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        tools:text="item"/>
</LinearLayout>

view_rv_staggered_item_two.xml布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="100dp">
    <TextView
        android:id="@+id/item_tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        tools:text="item"/>
</LinearLayout>

Item不同的布局是在Adapter里面綁定的,看一下Adapter的實(shí)現(xiàn)。

public class MDStaggeredRvAdapter extends RecyclerView.Adapter<MDStaggeredRvAdapter.ViewHolder> {

    /**
     * 展示數(shù)據(jù)
     */
    private ArrayList<String> mData;

    public MDStaggeredRvAdapter(ArrayList<String> data) {
        this.mData = data;
    }

    public void updateData(ArrayList<String> data) {
        this.mData = data;
        notifyDataSetChanged();
    }

    @Override
    public int getItemViewType(int position) {
        // 瀑布流樣式外部設(shè)置spanCount為2,在這列設(shè)置兩個(gè)不同的item type,以區(qū)分不同的布局
        return position % 2;
    }

    @Override
    public MDStaggeredRvAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 實(shí)例化展示的view
        View v;
        if(viewType == 1) {
            v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_staggered_item, parent, false);
        } else {
            v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_staggered_item_two, parent, false);
        }
        // 實(shí)例化viewholder
        ViewHolder viewHolder = new ViewHolder(v);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(MDStaggeredRvAdapter.ViewHolder holder, int position) {
        // 綁定數(shù)據(jù)
        holder.mTv.setText(mData.get(position));
    }

    @Override
    public int getItemCount() {
        return mData == null ? 0 : mData.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {

        TextView mTv;

        public ViewHolder(View itemView) {
            super(itemView);
            mTv = (TextView) itemView.findViewById(R.id.item_tv);
        }
    }
}

接下來(lái)是設(shè)置瀑布流樣式的間隔線樣式的,上面代碼中使用的是MDStaggeredRvDividerDecotation類,其實(shí)是直接拷貝的網(wǎng)格樣式的間隔線繪制類。看一下運(yùn)行效果。

RecyclerView-瀑布流2列.jpg

很奇怪,間隔線并沒(méi)有按照我們想象中的方式繪制,仔細(xì)看瀑布流中Item的分布,發(fā)現(xiàn)瀑布流樣式的Item分布和網(wǎng)格樣式的Item分布有些不同。對(duì)比一下兩者Item的分布,如下圖。

RecyclerView-對(duì)比.png

網(wǎng)格樣式的Item分布規(guī)律很明顯,豎直方向的網(wǎng)格,Item是從左向右從上到下依次按順序排列分布。

瀑布流樣式的Item分布也是從上到下,從左到右的順序排列,但是有一個(gè)高度的優(yōu)先級(jí),如果某一列中有一個(gè)高度最低的位置為空,最優(yōu)先在此處添加Item。看第三張圖的3 item,因?yàn)樵撐恢米畹停瑑?yōu)先在此處添加Item。

分析出了瀑布流樣式的Item的分布規(guī)律,就會(huì)發(fā)現(xiàn),按照以往列表樣式或者網(wǎng)格樣式去設(shè)置間隔線是有問(wèn)題的,因?yàn)椴恢繧tem具體的位置,上下左右間隔線是否需要繪制不確定,參考第二張圖,其實(shí)第三張圖的間隔線也有問(wèn)題,向上滑動(dòng)就會(huì)展示出來(lái)。

目前能考慮到的瀑布流添加間隔線的思路:

  • Item布局中設(shè)置四周間隔padding/margin
  • 代碼中動(dòng)態(tài)修改ItemView的間隔padding/margin

設(shè)置間隔有兩個(gè)方法:

  • 上下左右都設(shè)置間隔
  • 相鄰兩邊設(shè)置間隔(左上/左下/右上/右下)

第一種設(shè)置間隔的方法會(huì)導(dǎo)致相鄰的Item間距是間隔的兩倍,第二種設(shè)置間隔的方法會(huì)導(dǎo)致Item某一個(gè)方向上的與父布局邊緣無(wú)間隔,但是另一個(gè)方向與父布局邊緣有間隔,例如左上相鄰兩邊設(shè)置了間隔,最左邊一列的Item左邊與父布局邊緣有間隔,但是最右邊一列Item右邊與父布局無(wú)間隔,第一行和最后一行的Item也會(huì)出現(xiàn)這種情況。

要解決上面的問(wèn)題,父布局RecyclerView也需要根據(jù)相應(yīng)的情況設(shè)置padding讓整個(gè)布局的間隔都一致。下面的例子是選擇在Item布局中設(shè)置間隔,因?yàn)榭梢宰约涸诓季治募锌刂祁伾容^方便,選擇右下兩邊設(shè)置間隔。

首先修改兩個(gè)Item的布局文件。
view_rv_staggered_item.xml修改背景色和外層間距背景色。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="@dimen/md_common_view_height"
              android:background="@color/md_divider"
              android:paddingBottom="5dp"
              android:paddingRight="5dp">
    <TextView
        android:id="@+id/item_tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:background="@color/md_white"
        tools:text="item"/>
</LinearLayout>

同樣修改view_rv_staggered_item_two.xml。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="100dp"
              android:paddingBottom="5dp"
              android:paddingRight="5dp"
              android:background="@color/md_divider">
    <TextView
        android:id="@+id/item_tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:background="@color/md_white"
        tools:text="item"/>
</LinearLayout>

最后修改RecyclerView的一些屬性。

<android.support.v7.widget.RecyclerView
        android:id="@+id/my_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/md_divider"
        android:paddingLeft="5dp"
        android:paddingTop="5dp"
        android:fadeScrollbars="true"/>

運(yùn)行一下,看看最后的效果。

RecyclerView瀑布流.gif

差不多完美的解決了間隔線的問(wèn)題,有細(xì)心的同學(xué)可能發(fā)現(xiàn),在RecyclerView滑動(dòng)的時(shí)候上面一直有一條灰色的間隔線,這個(gè)可以通過(guò)取消xml布局文件中RecyclerView的paddingTop屬性去掉頂部灰色的間隔線。

總結(jié)

本篇文章主要介紹網(wǎng)格樣式和瀑布流樣式的RecyclerView,列表樣式、網(wǎng)格樣式和瀑布流樣式在某種程度上是可以轉(zhuǎn)換的。

  • 網(wǎng)格樣式的布局管理器的spanCount設(shè)置為1,就是列表樣式
  • 瀑布流樣式如果Item的布局文件是等高,豎直方向,就是豎直方向的網(wǎng)格樣式;如果Item是等寬,水平方向,那就是水平方向的網(wǎng)絡(luò)樣式
  • 如果瀑布流樣式的布局管理器spanCount設(shè)置為1,豎直方向,是豎直方向的列表;水平方向,就是水平方向的列表

Demo地址

目前為止關(guān)于RecyclerView的基本使用的介紹可以告一段落了,但其實(shí)關(guān)于RecyclerView深入使用可不止著一些,比如說(shuō)單個(gè)Item橫滑,拖動(dòng)Item之間轉(zhuǎn)換位置等等,官方都有提供,當(dāng)然這些使用會(huì)在后面依次介紹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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