RecycleView使用詳解

簡介

RecycleView是一個類似 ListView GridView的控件。但是相比於這兩者Recycleview更加的靈活、強大;使用Recycleview你可以更輕鬆的在開發中實現一些特殊的佈局,並使你的應用更加的酷炫

基本使用

下面我們來看一下最簡單的使用步驟 (No works, show you the code.)
要使用Recycleview 需要在gradle 中依賴下面的包
implementation 'com.android.support:recyclerview-v7:23.4.0'
Activity 中代碼

        itemList.add(new RecycleViewItem("ddd", "This is the 5 description !"));
        itemList.add(new RecycleViewItem("ddd", "This is the 6 description !"));
        
        //步驟 (1)(必要)
        RecyclerView recyclerView = findViewById(R.id.recycleView);
        //步驟 (2)(必要)
        //設置 layoutManager 這個是Recycleview必須的 不然Recycleview 的內容不會顯示。
        //RecyclView提供了 LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager 3 個系統的。當然也可以繼承LayoutManager自定義
        //下面是線性 layoutManager
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        //如果將 orientation 設置爲 HORIZONTAL 可以輕易的實現橫向的 listview 效果
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);
        //步驟 (3)(必要)
        //像listview一樣設置 adapter;
        Adapter adapter = new Adapter(this, itemList);
        recyclerView.setAdapter(adapter);
        //設置分隔線 (需要時設置)
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL));
        //設置增加或刪除條目的動畫(需要時設置)
        recyclerView.setItemAnimator(new DefaultItemAnimator());

從上面的代碼中我們可以看出recycleView 比 listView 等多了一步設置 layoutManager 的步驟,正是這一步驟使得RecycleView變的異常靈活。
Adapter的編碼

//此處 繼承的 RecyclerView.Adapter 中的泛型 指定爲我們自定義的 ViewHOlder的類型
public class Adapter extends RecyclerView.Adapter<Adapter.CzyViewHolder> {
    Context context;
    List<RecycleViewItem> itemList;
    public Adapter(Context context, List<RecycleViewItem> itemList) {
        this.context = context;
        this.itemList = itemList;
    }
    /** 創建 viewHolder 將 xml 傳遞給ViewHolder */
    @NonNull
    @Override
    public CzyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycleview_adapter, viewGroup, false);
        CzyViewHolder czyViewHolder = new CzyViewHolder(view);
        return czyViewHolder;
    }
    /** 這裏是設置item操作的地方 */
    @Override
    public void onBindViewHolder(@NonNull CzyViewHolder czyViewHolder, int i) {
        czyViewHolder.textView.setText(itemList.get(i).getContent());
        czyViewHolder.imageView.setImageResource(R.drawable.dreams_distance);
    }
    /** 跟 BaseAdapter 的 getCount 作用一樣。返回 */
    @Override
    public int getItemCount() {
        return itemList.size();
    }
    /** ViewHolder類,這個類用來初始化控件 */
    class CzyViewHolder extends RecyclerView.ViewHolder{
        TextView textView;
        ImageView imageView;
        public CzyViewHolder(@NonNull View itemView) {
            super(itemView);
            textView  = itemView.findViewById(R.id.tv_description);
            imageView = itemView.findViewById(R.id.imageView);
        }
    }
}

注意: 在onCreateViewHolder中解析視圖的方法必須這樣調用
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycleview_adapter, viewGroup, false);
這樣調用是錯誤
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, null);

分割線 ItemDecoration

recycleView 的分割線設置 recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
這裏系統提供了一個分割線類。上面的 VERTICAL表示分割線在item的底部,如果改爲HORIZONTAL 則分割線在item的右側。這個Decoration類還可以設置 setDrawable()
要實現更加酷炫的分割線效果需要繼承 ItemDecoration 進行自定義。
ItemDecoration源碼:

public abstract static class ItemDecoration {
       public ItemDecoration() {
       }

       public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
           this.onDraw(c, parent);
       }

       public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
           this.onDrawOver(c, parent);
       }

       public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
           this.getItemOffsets(outRect, ((RecyclerView.LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
       }
   }

.當我們設置了recyclerView.addItemDecoration 後,當recycleView繪製的時候,會繪製我們設置的 Decoration。

  • onDraw()方法在drawChildren之前調用
  • onDrawOver()方法在drawChildren之後調用
  • getItemOffsets()方法 中可以利用outRect爲每個item設置一定的偏移量

getItemOffsets() 方法

上面的效果其實只是在`getItemOffsets()`方法中添加了一句代碼
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        //這裏我們設置item(left,top,right,bottom)四周的間隔分別爲(20, 20, 0, 20); 
        outRect.set(20, 20, 0, 20);
    }

這裏 outRect.set()設置的是item四邊的偏移量,我們看到的紅色是我們設置的recycleView的背景色。

onDraw() 方法 和 onDrawOver()方法

  1. 這兩個方法的參數是一樣的。他們的區別在於,onDraw()的繪製在item的繪製之前,而onDrawOver()的繪製在item的繪製之後,我們來看
  2. getItemOffsets方法針對的是每一個item,而onDraw和onDrawOver方法針對的是RecycleView本身。所以,在這兩個方法中要遍歷屏幕上可見的item,分別計算和繪製分割線。
    這三者形成的關係如圖


    可以看到onDraw在item下面,item在中間,onDrawOver是可以蓋住onDraw和item的。

-我們來看三者的代碼-

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        // 這個方法得到的 RecycleView 中 child view 的個數,也就是屏幕中顯示的item的個數。並不是adapter中數據源的大小
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i ++) {
            View child = parent.getChildAt(i);
            int left = child.getLeft() + mDividerW / 2;
            int right = child.getRight() + 60;
            int top = child.getBottom() - mDividerW;
            int bottom = child.getBottom() + mDividerW / 2;
            c.drawRect(left, top, right, bottom, paint);
            c.drawText("onDraw", (right)/2, bottom - 40, paintText);
        }
    }

    @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 left = child.getLeft() + mDividerW / 2;
            int right = child.getRight() + 60;
            int top = child.getTop() - mDividerW / 2;
            int bottom = child.getTop() + mDividerW / 2;
            c.drawRect(left, top, right, bottom, paintOver);
            c.drawText("onDrawOver", (right)/2, bottom - 40, paintText);
        }
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.set(200, 200, 200, 200);
    }

注意: 代碼中註釋的地方,parent.getChildCount()這個方法獲取到的是顯示在屏幕上的item的個數,千萬不要以爲是adapter中數據源的個數。這一點很重要,因爲我們經常要根據數據源中的index對不同的item進行不同的處理,如果這裏搞不清楚就會出現混亂。下面我們看個例子
我們想實現每隔5個數據添加一個頭部
先上代碼吧(這是一個錯誤的代碼,你可以先設想一下這段代碼的效果)

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        // 這個方法得到的 RecycleView 中 child view 的個數,也就是屏幕中顯示的item的個數。並不是adapter中數據源的大小
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i ++) {
           if (i % 5 == 0) {
               View view = parent.getChildAt(i);
               int left = 0;
               int top = view.getTop() - 50;
               int right = view.getRight();
               int bottom = view.getTop();
               c.drawRect(left, top, right, bottom, paintOver);
           }
        }
    }

我們看到這段代碼的效果是這個綠色的分割線總是隨着滑動不同的變換。
那麼我們來看正確的寫法

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        // 這個方法得到的 RecycleView 中 child view 的個數,也就是屏幕中顯示的item的個數。並不是adapter中數據源的大小
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            //獲取這個 view 對應於 adapter 中的數據下標
            int index = parent.getChildAdapterPosition(view);
            if (index % 5 == 0) {
                int left = 0;
                int top = view.getTop() - 50;
                int right = view.getRight();
                int bottom = view.getTop();
                c.drawRect(left, top, right, bottom, paintOver);
            }
        }
    }

LayoutManager 佈局管理器

RecycleView之所以能做到靈活多變的佈局,是因爲提供了一個可供用戶自定義佈局管理抽象類RecycleView.LayoutManager類。當然,系統也爲我們提供了三個實現類

  • LinearLayoutManager 線性管理器, 支持縱向、橫向(可以很輕鬆的實現橫向listview的效果)
  • GridLayoutManager 網格佈局管理器
  • StaggeredGridLayoutManager 瀑布流佈局管理器
    前面我們已經使用過了LinearLayoutManager下面我們來看一下GridLayoutManager
  • GridLayoutManager
    很簡單,我們像LinearLayoutManager一樣設置
    mRecyclerView.setLayoutManager(new GridLayoutManager(this,4));
    很簡單吧。對於GridLayoutManager時添加分割線來說。我們可以添加兩個分割線
        val dividerItemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        dividerItemDecoration.setDrawable(ColorDrawable(Color.parseColor("#ff0000")))
        recycleView.addItemDecoration(dividerItemDecoration)

        val dividerItemDecoration1 = DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL)
        dividerItemDecoration1.setDrawable(ColorDrawable(Color.parseColor("#ff0000")))
        recycleView.addItemDecoration(dividerItemDecoration1)

我們來看一下效果

看我們可以利用添加一個橫向分割線和一個縱向分割線來實現網格效果

注意: 這裏有一點不得不吐槽一下,那就是系統提供的這個分割線是在ItemDecoration的onDraw方法中實現的。那麼造成的一個後果就是,當我們給Item設置了背景色之後。這個分割線是無法顯示出來的。但是,作爲一個程序猿很明顯這是難不倒我們的。同樣也難不倒你,所以自己去實現吧。

  • StaggeredGridLayoutManager
    上面說到的 LinearLayout和GridLayoutManager看起來並沒有什麼厲害的,貌似整出來的東西和listView、GridView也沒有太大的區別。那麼下面我們來看些真正厲害 的東西–StaggeredGridLayoutManager。
  1. 最簡單的實現和GridLayoutManager一樣的效果
    recyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
    這句代碼實現的效果和GridLayoutManager是一樣的。和GridLayoutManager一樣StaggeredGridLayoutManager也是可以設置VERTICAL或者HORIZONTAL;相應的設置的列數也就變成了行數。
  2. 這,好像也很平常的東西呀。下面我們來看一下厲害的東西。
    我們想一下以前我們要實現一個瀑布流,就是那個像淘寶首頁一樣的錯落有致的很好看的瀑布流呀。是不是很麻煩?現在用RecycleView就很easy了。

我們來看一下效果

要實現的代碼很簡單,只要在adapter中的onBindViewHolder方法中給Item設置一個隨機的高度就可以了。下面是代碼

//此處 繼承的 RecyclerView.Adapter 中的泛型 指定爲我們自定義的 ViewHOlder的類型
public class Adapter extends RecyclerView.Adapter<Adapter.CzyViewHolder> {

    Context context;
    List<RecycleViewItem> itemList;
    //存儲Item的高度
    List<Integer> heights = new ArrayList<>();

    public Adapter(Context context, List<RecycleViewItem> itemList) {
        this.context = context;
        this.itemList = itemList;
    }

    public void update(List<RecycleViewItem> items) {
        itemList = items;
        notifyDataSetChanged();
    }    
    /**
     *  創建 viewHolder 將 xml 傳遞給ViewHolder
     * @param viewGroup
     * @param i
     * @return
     */
    @NonNull
    @Override
    public CzyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        //此處 映射 Layout 的方法如下
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycleview_adapter, viewGroup, false);
        //映射方法,View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false); 是錯誤的。
        CzyViewHolder czyViewHolder = new CzyViewHolder(view);
        return czyViewHolder;
    }

    /**
     * 這裏是設置item操作的地方
     * @param czyViewHolder
     * @param position
     */
    @Override
    public void onBindViewHolder(@NonNull CzyViewHolder czyViewHolder, int position) {
        //高度默認值
        int height = 300;
        //當heights中有高度時我們取heights中的高度。
        if (heights != null && heights.size() > position) {
            height = heights.get(position);
        }else {
            //heights中沒有時我們生成一個隨機的高度並存儲在heights中
            height = new Random().nextInt(200) + 100;//[100,300)的隨機數
        }
        heights.add(height);
        //設置Item的高度
        ViewGroup.LayoutParams params = czyViewHolder.textView.getLayoutParams();
        params.height = heights.get(position);
        czyViewHolder.textView.setLayoutParams(params);

        czyViewHolder.textView.setText(itemList.get(position).getImgUrl());
    }
    
    /**
     * 跟 BaseAdapter 的 getCount 作用一樣。返回
     * @return
     */
    @Override
    public int getItemCount() {
        return itemList.size();
    }
    
    /**
     * ViewHolder類,這個類用來初始化控件
     */
    class CzyViewHolder extends RecyclerView.ViewHolder{

        TextView textView;
        public CzyViewHolder(@NonNull View itemView) {
            super(itemView);
            textView  = itemView.findViewById(R.id.tv_description);
        }
    }
}

以上三種是系統提供的LayoutManager,當然我們也可以繼承LayoutManger自定義我們自己的LayoutManager。這個和佈局的自定義差不多。細緻的問題篇幅限制就不講了。可以網上找幾篇文章參考下,很簡單

ItemAnimator(動畫)

爲RecycleView設置動畫,只要調用recyclerView.setItemAnimator(new DefaultItemAnimator());方法即可
這也是一個抽象類,我們可以繼承這個類自定義動畫。
這裏要注意呈現ItemAnimator需要調用RecycleView.Adapter中notifyItemRemoved(position)/notifyItemInserted();/notifyItemChanged等方法,調用notifyDataSetChanged();方法動畫是無效的。
系統爲我們提供了一個默認動畫DefaultItemAnimator
我們來看一下DefaultItemAnimator的效果

看起來還是不錯的吧;
我們再來看一下代碼

public class Adapter extends RecyclerView.Adapter<Adapter.CzyViewHolder> {

    Context context;
    List<RecycleViewItem> itemList;
    //存儲Item的高度
    List<Integer> heights = new ArrayList<>();

    public Adapter(Context context, List<RecycleViewItem> itemList) {
        this.context = context;
        this.itemList = itemList;
    }

    public void update(List<RecycleViewItem> items) {
        itemList = items;
        notifyDataSetChanged();
    }
    /**
     *  創建 viewHolder 將 xml 傳遞給ViewHolder
     * @param viewGroup
     * @param i
     * @return
     */
    @NonNull
    @Override
    public CzyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        //此處 映射 Layout 的方法如下
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycleview_adapter, viewGroup, false);
        //映射方法,View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false); 是錯誤的。
        CzyViewHolder czyViewHolder = new CzyViewHolder(view);
        return czyViewHolder;
    }

    /**
     * 這裏是設置item操作的地方
     * @param czyViewHolder
     * @param position
     */
    @Override
    public void onBindViewHolder(@NonNull final CzyViewHolder czyViewHolder, int position) {
        //高度默認值
        int height = 300;
        //當heights中有高度時我們取heights中的高度。
        if (heights != null && heights.size() > position) {
            height = heights.get(position);
        }else {
            //heights中沒有時我們生成一個隨機的高度並存儲在heights中
            height = new Random().nextInt(200) + 100;//[100,300)的隨機數
        }
        heights.add(height);
        //設置Item的高度
        ViewGroup.LayoutParams params = czyViewHolder.textView.getLayoutParams();
        params.height = heights.get(position);
        czyViewHolder.textView.setLayoutParams(params);
        czyViewHolder.textView.setText(itemList.get(position).getImgUrl());

        czyViewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                //刪除數據源中的數據
                itemList.remove(czyViewHolder.getAdapterPosition());
                //刪除這一項的高度
                heights.remove(czyViewHolder.getAdapterPosition());
                //adapter中移除這一項
                notifyItemRemoved(czyViewHolder.getAdapterPosition());
                return false;
            }
        });
    }

    /**
     * 跟 BaseAdapter 的 getCount 作用一樣。返回
     * @return
     */
    @Override
    public int getItemCount() {
        return itemList.size();
    }

    /**
     * ViewHolder類,這個類用來初始化控件
     */
    class CzyViewHolder extends RecyclerView.ViewHolder{

        TextView textView;
        public CzyViewHolder(@NonNull View itemView) {
            super(itemView);
            textView  = itemView.findViewById(R.id.tv_description);
        }
    }
}

動畫相關的代碼在 onBindViewHolder方法中,

                //刪除數據源中的數據
                itemList.remove(czyViewHolder.getAdapterPosition());
                //刪除這一項的高度
                heights.remove(czyViewHolder.getAdapterPosition());
                //adapter中移除這一項
                notifyItemRemoved(czyViewHolder.getAdapterPosition());

注意: 這三句代碼中很多人會在這裏使用onBindViewHolder的第二個參數position作爲remove、notifyItemRemoved方法的參數,但是這樣會引起position混亂的問題。這是因爲如果在這裏使用onBindViewHolder的傳入參數,那麼需要將這個參數變成final類型的參數,因此造成混亂。所以,這裏要用viewHolder.getAdapterPosition()獲取我們需要的position

到這裏RecycleView 的基本使用就講完了。從以上的講解我們看到RecycleView相交於listview等更靈活。使用RecycleView可以更輕鬆的實現較爲複雜酷炫的佈局。
相信你很快就會對RecycleView欲罷不能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章