RecyclerView使用

簡單說明

為什么有了ListView還需要RecyclerView?

主要有這幾個(gè)原因:

  1. 只支持豎直方向上的列表形狀排列,不支持橫向、網(wǎng)格(GridView)、瀑布流等其它排列方式,不靈活,適用性不廣。
  2. 在緩存機(jī)制不是很好,還有一些優(yōu)化的空間。

RecyclerView相比于ListView的優(yōu)缺點(diǎn):

  1. 更靈活,適用性更廣。
  2. 更方便添加Item的動(dòng)畫,分割線等
  3. 支持局部刷新和定向刷新
  4. 使用起來沒有ListView簡單
  5. 不支持Item的點(diǎn)擊事件,需要自己處理。

一般使用

RecyclerView的一般使用和ListView在總體上差不多。區(qū)別主要有以下幾點(diǎn):

  1. 必須添加一個(gè)布局管理器來聲明列表中的Item的排列方式
  2. 如果需要分割線,可以單獨(dú)添加,且名字叫Item裝飾:ItemDecoration。
  3. Adapter的創(chuàng)建,相對(duì)于ListView更規(guī)范化,且稍微復(fù)雜一些。
RecyclerView rv = findViewById(R.id.rv_second);
// 設(shè)置布局管理器,這里是最簡單的豎直線性排列的布局
rv.setLayoutManager(new LinearLayoutManager(this));
// 設(shè)置Adapter
rv.setAdapter(adapter);
// 設(shè)置分割線
rv.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

Adapter的使用

1. 整體結(jié)構(gòu)

Adatper需要繼承RecyclerView.Adapter類,且需要額外增加一個(gè)ViewHolder類繼承RecyclerView.ViewHolder。然后將自定義的Holder作為Adapter的泛型類型。關(guān)于Holder的處理,放到后面再說

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.Holder> {
    
    static class Holder extends RecyclerView.ViewHolder {
        
    }
}

2. 復(fù)寫方法

Adapter需要復(fù)寫父類的三個(gè)抽象方法,分別如下。

    // 直接返回?cái)?shù)量,固定式寫法
    @Override
    public int getItemCount() {
        return data == null ? 0 : data.size();
    }

    // 創(chuàng)建ItemView,將ItemView和Holder綁定,當(dāng)然也要綁定itemView中的控件
    onCreateViewHolder()
        
    // 在這里處理數(shù)據(jù),將position對(duì)應(yīng)的JavaBean對(duì)象中的數(shù)據(jù)設(shè)置進(jìn)holder.xx控件中
    onBindViewHolder()

其實(shí)寫法和ListView 的Adapter在優(yōu)化之后的寫法是一樣的。只是將ListView.Adapter中的getView()方法中的代碼分開放到onCreateViewHolder(),Holder類,和onBindViewHolder()方法三部分中去。

ListView.Adapter的getView方法和RecyclerView.Adapter的onCreateViewHolder、onBindViewHolder方法的比較:

ListView的Adapter

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // 對(duì)convertView和view中的控件的復(fù)用
    ViewHolder holder;
    if (convertView == null) {
        convertView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_fruit_first, parent, false);
        holder = new ViewHolder();
        holder.iv = convertView.findViewById(R.id.iv_fruit);
        holder.tv = convertView.findViewById(R.id.tv_fruit);
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    // 下面是將數(shù)據(jù)設(shè)置進(jìn)具體的convertView的控件中
    Fruit fruit = data.get(position);
    holder.iv.setImageResource(fruit.getDrawableResId());
    holder.tv.setText(fruit.getName());
    return convertView;
}

RecyclerView的Adapter

/**
 * 創(chuàng)建ItemView,將ItemView和Holder綁定,當(dāng)然也要綁定itemView中的控件
 *
 * @param parent   就是RecyclerView對(duì)象本身
 * @param viewType 如果有多種布局,根據(jù)這個(gè)viewType的值不同,要加載不同的布局
 * @return 在onBindViewHolder方法中使用的Holder對(duì)象
 */
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.item_fruit_first, parent, false);
    // 這里將view和holder的綁定方式與ListViewAdapter不同,不再是使用Tag的方式了,而是將view作為holder的成員變量
    Holder holder = new Holder(view);
    holder.iv = view.findViewById(R.id.iv_fruit);
    holder.tv = view.findViewById(R.id.tv_fruit);
    return holder;
}

/**
 * 在這里處理數(shù)據(jù),將position對(duì)應(yīng)的JavaBean對(duì)象中的數(shù)據(jù)設(shè)置進(jìn)holder.xx控件中
 *
 * @param holder   就是或新建,或復(fù)用的Holder對(duì)象
 * @param position item對(duì)應(yīng)的索引
 */
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
    Fruit fruit = data.get(position);
    holder.iv.setImageResource(fruit.getDrawableResId());
    holder.tv.setText(fruit.getName());
}

3. 不要忘記將數(shù)據(jù)設(shè)置進(jìn)Adapter中

private ArrayList<Fruit> data;

public FruitAdapter(ArrayList<Fruit> data) {
    this.data = data;
}

4. ViewHolder

與ListView中的Holder不同,RecyclerViewAdapter中的Holder需要繼承RecyclerView.ViewHolder,且因?yàn)镽ecyclerView.ViewHolder只有一個(gè)要itemView作為參數(shù)的構(gòu)造方法,所以我們自定義的ViewHolder也要必須添加構(gòu)造方法。

/**
 * 因?yàn)楦割愔挥幸粋€(gè)需要view參數(shù)的構(gòu)造方法,所以Holder類必須添加一個(gè)構(gòu)造方法,能夠調(diào)用父類的這個(gè)構(gòu)造方法
 */
static class Holder extends RecyclerView.ViewHolder {

    ImageView iv;
    TextView tv;

    /**
     * 構(gòu)造方法,將itemView與holder對(duì)象綁定,并調(diào)用父類的有itemView作為參數(shù)的構(gòu)造方法
     *
     * @param itemView 就是Adapter的onCreateViewHolder方法創(chuàng)建的View
     */
    public Holder(@NonNull View itemView) {
        super(itemView);
    }
}

5. 完整代碼如下

/**
 * 1. 一般情況下我們同時(shí)需要自定義Holder類繼承Rv中的ViewHolder,然后將Holder類設(shè)置為Adapter中的泛型
 * 2. 繼承Rv.Adapter類之后,需要復(fù)寫3個(gè)抽象方法
 */
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.Holder> {

    private ArrayList<Fruit> data;

    public FruitAdapter(ArrayList<Fruit> data) {
        this.data = data;
    }

    /**
     * 和ListView一樣,也是獲取列表需要渲染加載的數(shù)據(jù)的數(shù)量
     */
    @Override
    public int getItemCount() {
        return data == null ? 0 : data.size();
    }


    /**
     * 創(chuàng)建ItemView,將ItemView和Holder綁定,當(dāng)然也要綁定itemView中的控件
     *
     * @param parent   就是RecyclerView對(duì)象本身
     * @param viewType 如果有多種布局,根據(jù)這個(gè)viewType的值不同,要加載不同的布局
     * @return 在onBindViewHolder方法中使用的Holder對(duì)象
     */
    @NonNull
    @Override
    public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_fruit_first, parent, false);
        Holder holder = new Holder(view);
        holder.iv = view.findViewById(R.id.iv_fruit_first);
        holder.tv = view.findViewById(R.id.tv_fruit_first);
        return holder;
    }

    /**
     * 在這里處理數(shù)據(jù),將position對(duì)應(yīng)的JavaBean對(duì)象中的數(shù)據(jù)設(shè)置進(jìn)holder.xx控件中
     *
     * @param holder   就是或新建,或復(fù)用的Holder對(duì)象
     * @param position item對(duì)應(yīng)的索引
     */
    @Override
    public void onBindViewHolder(@NonNull Holder holder, int position) {
        Fruit fruit = data.get(position);
        holder.iv.setImageResource(fruit.getDrawableResId());
        holder.tv.setText(fruit.getName());
    }

    /**
     * 因?yàn)楦割愔挥幸粋€(gè)需要view參數(shù)的構(gòu)造方法,所以Holder類必須添加一個(gè)構(gòu)造方法,能夠調(diào)用父類的這個(gè)構(gòu)造方法
     */
    static class Holder extends RecyclerView.ViewHolder {

        ImageView iv;
        TextView tv;

        /**
         * 構(gòu)造方法,將itemView與holder對(duì)象綁定,并調(diào)用父類的有itemView作為參數(shù)的構(gòu)造方法
         *
         * @param itemView 就是Adapter的onCreateViewHolder方法創(chuàng)建的View
         */
        public Holder(@NonNull View itemView) {
            super(itemView);
        }
    }
}

其它布局:LayoutManager

常用的布局管理器有2種,分別是線性和網(wǎng)格。可以達(dá)到普通的列表、橫向列表、網(wǎng)格狀、瀑布流布局的效果。

LinearLayoutManager

最常用的就是LinearLayoutManager。

下面就是創(chuàng)建一個(gè)最普通的類似ListView的布局管理器。

LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
rv.setLayoutManager(linearLayoutManager);

LinearLayoutManager還有一個(gè)常用的構(gòu)造方法。

  • 參數(shù)orientation表示排列方向
  • 參數(shù)reverseLayout表示是否倒序展示數(shù)據(jù)
/**
 * @param context       Current context, will be used to access resources.
 * @param orientation   Layout orientation. Should be {@link #HORIZONTAL} or {@link
 *                      #VERTICAL}.
 * @param reverseLayout When set to true, layouts from end to start.
 */
public LinearLayoutManager(Context context, @RecyclerView.Orientation int orientation,
        boolean reverseLayout) {
    setOrientation(orientation);
    setReverseLayout(reverseLayout);
}

因此我們?nèi)绻胍@示水平方向列表,直接使用這個(gè)構(gòu)造方法即可。下面的代碼,再將itemView的寬度不設(shè)置為match_parent,就可以實(shí)現(xiàn)水平方向的排列。當(dāng)然就算itemView的寬度是match_parent,也是水平排列的列表,但是每個(gè)item的寬度就會(huì)都占用屏幕的寬度了。

LinearLayoutManager linearLayoutManager =
        new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
rv.setLayoutManager(linearLayoutManager);

StaggeredGridLayoutManager

如果想要網(wǎng)格狀的布局,就可以使用StaggeredGridLayoutManager來完成。

和LinearLayoutManager不同,我們常用的StaggeredGridLayoutManager構(gòu)造方法是不傳Context,而必須指定方向和行列數(shù)的構(gòu)造方法。

/**
 * Creates a StaggeredGridLayoutManager with given parameters.
 *
 * @param spanCount   If orientation is vertical, spanCount is number of columns. If
 *                    orientation is horizontal, spanCount is number of rows.
 * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
 */
public StaggeredGridLayoutManager(int spanCount, int orientation) {
    mOrientation = orientation;
    setSpanCount(spanCount);
    mLayoutState = new LayoutState();
    createOrientationHelpers();
}

StaggeredGridLayoutManager的使用如下,不過要注意itemView的寬高的設(shè)置。

// 創(chuàng)建一個(gè)豎直方向排列,一共只有2列的網(wǎng)格狀布局管理器
StaggeredGridLayoutManager staggeredGridLayoutManager
        = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
rv.setLayoutManager(staggeredGridLayoutManager);
// 創(chuàng)建一個(gè)水平方向排列,一共只有2行的網(wǎng)格狀布局管理器
StaggeredGridLayoutManager staggeredGridLayoutManager
        = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.HORIZONTAL);
rv.setLayoutManager(staggeredGridLayoutManager);

瀑布流

瀑布流就是控件的寬度或者高度不等的網(wǎng)格型布局。

瀑布流的實(shí)現(xiàn)很簡單,就是在StaggeredGridLayoutManager的基礎(chǔ)之上,更改ItemView的高度或者寬度即可。只是這里注意,要使用控件的LayoutParams來修改寬高等尺寸屬性。

/**
 * 將數(shù)據(jù)設(shè)置進(jìn)itemView中的控件,也就是ViewHolder中的成員變量。
 */
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    // 不能通過view直接設(shè)置它的寬高,需要通過一個(gè)LayoutParams的成員變量來修改寬高。
    ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
    // 想辦法讓itemView的高度在某個(gè)范圍內(nèi)變化
    layoutParams.height = DensityUtil.dip2px(holder.itemView.getContext(), 60) +
                DensityUtil.dip2px(holder.itemView.getContext(), new Random().nextInt(60));
    // 不要忘記設(shè)置數(shù)據(jù)
    Fruit fruit = data.get(position);
    holder.iv.setImageResource(fruit.getDrawableRes());
    holder.tv.setText(fruit.getName());
}

dp轉(zhuǎn)px的工具方法也很簡單。獲取系統(tǒng)的屏幕像素密度,乘以要dp數(shù)值,就是像素值。

public class DensityUtil {

    /**
     * 根據(jù)手機(jī)的分辨率從 dp 的單位 轉(zhuǎn)成為 px(像素)
     */
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 根據(jù)手機(jī)的分辨率從 px(像素) 的單位 轉(zhuǎn)成為 dp
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

}

分割線/裝飾:ItemDecoration

分割線

一般情況下,我們?cè)赗ecyclerView中添加分割線的方式是將線畫在itemView中,然后根據(jù)條件決定線是否顯示。

如果想要itemView之間有間距,我們一般也是用在itemView中添加margin的方式完成。

但是如果有比較復(fù)雜的的對(duì)于itemView分割線、背景樣式等的處理的時(shí)候,我們就需要使用ItemDecoration來完成了。

ItemDecoration

案例完整代碼:

public class FruitDecoration extends RecyclerView.ItemDecoration {

    private final Paint mPaint;

    public FruitDecoration(Context context) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(context.getResources().getColor(R.color.purple_200));
    }

    /**
     * 可以實(shí)現(xiàn)類似于Padding的效果。就是控制各個(gè)Item之間的間距等。
     *
     * @param outRect 就是ItemView的四周的邊距。就是系統(tǒng)會(huì)根據(jù)outRect的值來擴(kuò)展item的區(qū)域。
     * @param view    當(dāng)前ItemView
     * @param parent  就是RecyclerView
     * @param state   存儲(chǔ)一些RecyclerView的狀態(tài)等,用的不多
     */
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        /**
         * 這里有兩個(gè)獲取position的方法。他們一般情況下沒有區(qū)別。
         * 僅在RecyclerView刷新布局后,layoutPosition會(huì)晚adapterPosition大約16ms。
         * 因?yàn)槔L制完成之后,layoutPosition才有正確的值,所以我們一般用adapterPosition就好。
         *
         * 只有在使用findViewHolderForLayoutPosition獲取當(dāng)前item的ViewHolder時(shí),用layoutPosition才更好,因?yàn)榇藭r(shí)layoutPosition 和用戶在屏幕上看到的一定是一樣的
         */
        int position = parent.getChildAdapterPosition(view);
        int layoutPosition = parent.getChildLayoutPosition(view);
        /**
         * 這里注意,下面兩個(gè)count的值的不同。
         * viewCount是當(dāng)前RecyclerView中的itemView的數(shù)量。并不是數(shù)據(jù)的數(shù)量,Adapter中的getItemCount才是
         * 因?yàn)镽ecyclerView不會(huì)一次性創(chuàng)建所以的itemView,而是會(huì)進(jìn)行view的復(fù)用。
         */
        int viewCount = parent.getChildCount();
        int childCount = parent.getAdapter().getItemCount();
        if (position == 0) {
            outRect.bottom = 20;
        } else if (position == childCount - 1) {
            outRect.top = 20;
        } else {
            outRect.top = 20;
            outRect.bottom = 20;
        }
    }

    /**
     * 繪制ItemView的背景。意思就是這里畫出來的圖像,會(huì)顯示在itemView的下面。這個(gè)方法會(huì)在繪制itemView之前調(diào)用.
     * 這里的繪制區(qū)域是根據(jù)上面的getItemOffsets決定的。
     * 注意這里的canvas指的是RecyclerView的布局部分,而不是itemView的界面。如果想在每一個(gè)ItemView的相同位置畫圖案,需要計(jì)算對(duì)應(yīng)的坐標(biāo)。
     *
     * @param c      繪畫的布
     * @param parent 就是RecyclerView對(duì)象本身
     * @param state  state本身
     */
    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);

    }

    /**
     * 繪制ItemView的前景。意思就是這里畫出來的圖像,會(huì)顯示在itemView的上面
     *
     * @param c      繪畫的布
     * @param parent 就是RecyclerView對(duì)象本身
     * @param state  state本身
     */
    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int childCount = parent.getChildCount();

        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            int radius = child.getHeight() / 2;
            int centerX = child.getRight() - 20 - radius;
            int centerY = child.getTop() + child.getHeight() / 2;
            c.drawCircle(centerX, centerY, radius, mPaint);

        }
    }
}

動(dòng)畫:ItemAnimator

點(diǎn)擊事件

主要的實(shí)現(xiàn)方式還是在Adapter的onBindViewHolder中設(shè)置點(diǎn)擊事件。

多種布局

對(duì)應(yīng)的Layout布局文件

getItemViewType

onCreateViewHolder

Holder類

onBindViewHolder

添加頭尾

下拉刷新上拉更多

好用的第三方框架

BRVAH(BaseRecyclerViewAdapterHelper)(RecyclerView使用框架):http://www.recyclerview.org/

SmartRefrshLayout(下拉刷新框架):https://gitee.com/scwang90/SmartRefreshLayout

參考資料

Android RecyclerView 使用完全解析 體驗(yàn)藝術(shù)般的控件

Android 優(yōu)雅的為RecyclerView添加HeaderView和FooterView

【騰訊Bugly干貨分享】Android ListView 與 RecyclerView 對(duì)比淺析—緩存機(jī)制

RecyclerView之ItemDecoration由淺入深

最后編輯于
?著作權(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)容