1. 程式人生 > >自從用了RecyclerView,腰再也不痛了,手也不酸了

自從用了RecyclerView,腰再也不痛了,手也不酸了

RecyclerView是support:recyclerview-v7中提供的控制元件,最低相容到android 3.0版本。

官方介紹RecyclerView為在有限的視窗展現大量資料的控制元件。擁有類似功能的控制元件有ListView、GridView以及被Google遺棄的Gallery等,為毛已經有了它們,Google還推出RecyclerView呢,那就要說說RecyclerView所具有的一些優勢了。

那RecyclerView到底有啥優勢呢?總結起來六顆字:低耦合高類聚。RecyclerView已經標準化ViewHolder,我們自定義的ViewHoler需要繼承 RecyclerView.ViewHolder,然後在構造方法中初始化控制元件

,後面會有具體介紹。通過設定不同的LayoutManager,以及結合ItemDecoration , ItemAnimator,ItemTouchHelper,可以實現非常炫酷的效果,這些是ListView等控制元件難以企及的。

基本使用:

1.使用前需要在在gradle中新增依賴

implementation 'com.android.support:recyclerview-v7:27.0.2'

2.編寫程式碼,首先我們需要在Xml中寫RecyclerView的佈局,

  <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>  

然後在activity中獲取RecyclerView,並設定LayoutManager以及adapter

//通過findViewById拿到RecyclerView例項
mRecyclerView =   findViewById(R.id.recyclerView);
//設定RecyclerView管理器
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
//初始化介面卡
mAdapter = new MyRecyclerViewAdapter(list); 
//設定新增或刪除item時的動畫,這裡使用預設動畫
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//設定介面卡
mRecyclerView.setAdapter(mAdapter);
下面是MyRecyclerViewAdapter的程式碼:
package com.sharejoys.recyclerviewdemo.actvity;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.sharejoys.recyclerviewdemo.R;

import java.util.List;

/**
 * Created by 青青-子衿 on 2018/1/15.
 */


public class MyRecyclerViewAdapterextends RecyclerView.Adapter<MyAdapter.ViewHolder> {
    private List<String> list;
    
    public MyAdapter(List<String> list) {
        this.list = list;
    }

    @Override
    public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_base_use, parent, false);
        MyAdapter.ViewHolder viewHolder = new MyAdapter.ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(MyAdapter.ViewHolder holder, int position) {
        holder.mText.setText(list.get(position));
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        TextView mText;
        ViewHolder(View itemView) {
            super(itemView);
            mText = itemView.findViewById(R.id.item_tx);
        }
    }
}

這裡item_normal的佈局也非常簡單

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/item_tx"
        android:layout_width="match_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="10dp"
        android:layout_gravity="center_horizontal"
        android:text="Item"/>

</LinearLayout>

然後我們執行效果如下


從例子也可以看出來,RecyclerView的用法並不比ListView複雜,反而更靈活好用,它將資料、排列方式、資料的展示方式都分割開來,因此可定製型,自定義的形式也非常多,非常靈活。

設定橫向佈局:

 mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));


設定網格佈局:
 mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));


設定瀑布流:

 mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL));


如果第二個引數可以設定為橫向的,則效果如下:

從以上可知,我們可以通過設定不同的管理器,實現不同的效果
LinearLayoutManager:以線性佈局展示,可以設定橫向和縱向
GridLayoutManager:以網格形式展示,類似GridView效果
StaggeredGridLayoutManager:以瀑布流形式的效果

RecyclerView條目之間預設沒有分割線,那是否可以像ListView一樣設定divider以及dividerHight搞一條分割線出來呢,答案是不可以的,google並沒有提供這樣的屬性。但是谷歌為我們提供了可以定製的解決辦法,那就是以下要說ItemDecoration

利用ItemDecoration實現條目分割線

ItemDecoration是谷歌定義的可用於畫分割線的類, 是抽象的,需要我們自己去實現

  /**
     * 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.
     *
     * <p>All ItemDecorations are drawn in the order they were added, before the item
     * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
     * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
     * RecyclerView.State)}.</p>
     */
    public abstract static class ItemDecoration {
        public void onDraw(Canvas c, RecyclerView parent, State state) {
            onDraw(c, parent);
        }
        @Deprecated
        public void onDraw(Canvas c, RecyclerView parent) {
        }

        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
            onDrawOver(c, parent);
        }

        @Deprecated
        public void onDrawOver(Canvas c, RecyclerView parent) {
        }


        @Deprecated
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }
當我們通過
mRecyclerView.addItemDecoration();
    onDraw: 該方法可以在RecyclerView的畫布上畫任何裝飾,且是在 the item views 被繪製之前回調
    onDrawOver:該方法可以在RecyclerView的畫布上畫任何裝飾,且是在 the item views 被繪製之後回撥
    getItemOffsets :可以在該方法中為the item views新增偏移量

下面我們可以就通過繼承ItemDecoration為RecyclerView新增分割線。
DividerItemDecoration的程式碼如下:
package com.sharejoys.mvpdemo.ui.customview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntDef;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

/**
 * Date: 2018/1/14
 *
 * @author 青青-子衿
 * @since 1.0
 */

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    @OrientationType
    private int mOrientation = LinearLayoutManager.VERTICAL;
    private Drawable mDivider;

    private int[] attrs = new int[]{
            android.R.attr.listDivider
    };

    public DividerItemDecoration(Context context, @OrientationType int orientation) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs);
        mDivider = typedArray.getDrawable(0);
        typedArray.recycle();
        setOrientation(orientation);
    }

    private void setOrientation(@OrientationType int orientation) {
        if (orientation != LinearLayoutManager.VERTICAL && orientation != LinearLayoutManager.HORIZONTAL) {
            throw new IllegalArgumentException("傳入的佈局型別不合法");
        }
        this.mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        //呼叫這個繪製方法,RecyclerView會回撥該繪製方法,需要我們自己去繪製條目的間隔線
        if (mOrientation == LinearLayoutManager.VERTICAL) {
            //垂直
            drawVertical(c, parent);
        } else {
            //水平
            drawHorizontal(c, parent);
        }
    }

    private void drawVertical(Canvas c, RecyclerView parent) {
        // 畫水平線
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);

            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child));
            int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    private void drawHorizontal(Canvas c, RecyclerView parent) {
        int top = parent.getPaddingTop();
        int bottom = parent.getHeight() - parent.getPaddingBottom();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);

            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child));
            int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        //獲得條目的偏移量(所有的條目都會回撥一次該方法)
        if (mOrientation == LinearLayoutManager.VERTICAL) {
            //垂直
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            //水平
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }

    @IntDef({LinearLayoutManager.VERTICAL, LinearLayoutManager.HORIZONTAL})
    public @interface OrientationType {
    }
}
然後在activity設定水平方向
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); 
mRecyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));

豎直方向:
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONAL, false); 
mRecyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.HORIZONAL))


這裡的分割線是預設的,我們可以在主題中去設定分割線的顏色

   <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:listDivider">@drawable/bg_recyclerview_divider</item>
    </style>
bg_recyclerview_divider.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

    <gradient
        android:centerColor="#ff00ff00"
        android:endColor="#ff0000ff"
        android:startColor="#ffff0000"
        android:type="linear"/>

    <size
        android:width="10dp"
        android:height="10dp"/>

</shape>

執行後效果如下


以上的分割線只適用在LinearLayoutManager的相關佈局中。
對於GridLayoutManager佈局是不適用的。需要我們單獨寫一個。

以下是對於GridLayoutManager佈局的分割線程式碼

package com.sharejoys.recyclerviewdemo.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

/**
 * Date: 2018/1/14 
 *
 * @author 青青-子衿
 * @since 1.0
 */

public class DividerGridViewItemDecoration extends RecyclerView.ItemDecoration {
    private Drawable mDivider;
    private int[] attrs = new int[]{
            android.R.attr.listDivider};

    public DividerGridViewItemDecoration(Context context) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs);
        mDivider = typedArray.getDrawable(0);
        typedArray.recycle();
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        drawVertical(c, parent);
        drawHorizontal(c, parent);
    }

    private void drawVertical(Canvas c, RecyclerView parent) {
        //繪製垂直間隔線(垂直的矩形)
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin;
            int right = left + mDivider.getIntrinsicWidth();
            int top = child.getTop() - params.topMargin;
            int bottom = child.getBottom() + params.bottomMargin;

            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    private void drawHorizontal(Canvas c, RecyclerView parent) {
        //繪製水平分割線
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getLeft() - params.leftMargin;
            int right = child.getRight() + params.rightMargin + mDivider.getIntrinsicWidth();
            int top = child.getBottom() + params.bottomMargin;
            int bottom = top + mDivider.getIntrinsicHeight();

            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        // 四個方向的偏移值
        int right = mDivider.getIntrinsicWidth();
        int bottom = mDivider.getIntrinsicHeight();

        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        int itemPosition = params.getViewAdapterPosition();
        if (isLastColum(itemPosition, parent)) {
            right = 0;
        }

        if (isLastRow(itemPosition, parent)) {
            bottom = 0;
        }
        outRect.set(0, 0, right, bottom);
    }

    /**
     * 是否最後一行
     */
    private boolean isLastRow(int itemPosition, RecyclerView parent) {
        int spanCount = getSpanCount(parent);
        if (spanCount != -1) {
            int childCount = parent.getAdapter().getItemCount();
            int lastRowCount = childCount % spanCount;
            //最後一行的數量小於spanCount
            if (lastRowCount == 0 || lastRowCount < spanCount) {
                return true;
            }
        }

        return false;
    }


    /**
     * 根據parent獲取到列數
     */
    private int getSpanCount(RecyclerView parent) {
        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            GridLayoutManager lm = (GridLayoutManager) layoutManager;
            int spanCount = lm.getSpanCount();
            return spanCount;
        }
        return -1;
    }

    /**
     * 判斷是否是最後一列
     */
    private boolean isLastColum(int itemPosition, RecyclerView parent) {
        int spanCount = getSpanCount(parent);
        if (spanCount != -1) {
            if ((itemPosition + 1) % spanCount == 0) {
                return true;
            }
        }
        return false;
    }
}
我們在activity中使用該分割線
        mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        mRecyclerView.addItemDecoration(new DividerGridViewItemDecoration(this));


點選事件


RecyclerView並沒有像ListView的那樣可以設定點選事件以及長按點選事件,這個需要我們可以在adapter中去設定回撥的方式實現,具體程式碼如下:
MyRecyclerViewAdapter的程式碼如下:
package com.sharejoys.recyclerviewdemo.actvity;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.sharejoys.recyclerviewdemo.R;

import java.util.List;

/**
 * Created by 青青-子衿 on 2018/1/15.
 */


public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
    private List<String> list;
    private OnItemClickListener onItemClickListener;
    private OnItemLongClickListener onItemLongClickListener;

    /**
     * 設定點選事件
     */
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    /**
     * 設定長按點選事件
     */
    public void setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener) {
        this.onItemLongClickListener = onItemLongClickListener;
    }

    public MyRecyclerViewAdapter(List<String> list) {
        this.list = list;
    }

    @Override
    public MyRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_base_use, parent, false);
        MyRecyclerViewAdapter.ViewHolder viewHolder = new MyRecyclerViewAdapter.ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(MyRecyclerViewAdapter.ViewHolder holder, int position) {
        holder.mText.setText(list.get(position));
        int adapterPosition = holder.getAdapterPosition();
        if (onItemClickListener != null) {
            holder.itemView.setOnClickListener(new MyOnClickListener(position, list.get(adapterPosition)));
        }
        if (onItemLongClickListener != null) {
            holder.itemView.setOnLongClickListener(new MyOnLongClickListener(position, list.get(adapterPosition)));
        }
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        TextView mText;

        ViewHolder(View itemView) {
            super(itemView);
            mText = itemView.findViewById(R.id.item_tx);
        }
    }

    private class MyOnLongClickListener implements View.OnLongClickListener {
        private int position;
        private String data;

        public MyOnLongClickListener(int position, String data) {
            this.position = position;
            this.data = data;
        }

        @Override
        public boolean onLongClick(View v) {
            onItemLongClickListener.onItemLongClick(v, position, data);
            return true;
        }
    }

    private class MyOnClickListener implements View.OnClickListener {
        private int position;
        private String data;

        public MyOnClickListener(int position, String data) {
            this.position = position;
            this.data = data;
        }

        @Override
        public void onClick(View v) {
            onItemClickListener.onItemClick(v, position, data);
        }
    }


    public interface OnItemClickListener {
        void onItemClick(View view, int position, String data);
    }

    public interface OnItemLongClickListener {
        void onItemLongClick(View view, int position, String data);
    }

}
activity中設定監聽:
    mAdapter.setOnItemClickListener(new MyRecyclerViewAdapter.OnItemClickListener() {
            @Override
         public void onItemClick(View view, int position, String data) {
                Toast.makeText(MainActivity.this, "您點選了:  " + data, Toast.LENGTH_SHORT).show();
            }
        });
        mAdapter.setOnItemLongClickListener(new MyRecyclerViewAdapter.OnItemLongClickListener() {
            @Override
            public void onItemLongClick(View view, int position, String data) {
                Toast.makeText(MainActivity.this, "您長按點選了:  " + data, Toast.LENGTH_SHORT).show();
            }
        });

執行後效果如下:


ItemAnimator

我們可以為RecyclerView設定增加和刪除動畫,這裡我們可以使用預設動畫

//設定新增或刪除item時的動畫,這裡使用預設動畫
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
然後在Adapter中增加刪除和新增資料的方法
   /**
     * 插入一條資料
     *
     * @param index 下標
     * @param s     資料
     */
    public void addItem(int index, String s) {
        list.add(index, s);
        notifyItemInserted(index);
    }

    /**
     * 刪除一條資料
     *
     * @param index 下標
     */
    public void deleteItem(int index) {
        list.remove(index);
        notifyItemRemoved(index);
    }
activty呼叫刪除和新增方法後效果如下


RecycleView還有一些其他用法,比如結合ItemTouchHelper實現item的拖拽效果,可以自定義增加header和footer(類似Listview),有時間我就會補上!