RecyclerView你需要掌握的高级控件

Google 在Android 5.0 提出了design系列的控件,其中RecyclerView就是其中的一员。相对于ListView而言,RecyclerView功能强大的一批。之前总是对这个控件的使用模模糊糊,这里便仔西品味下这个控件。

知识点

在这里插入图片描述

一、基础

1、基本使用

在这里插入图片描述

(1)有关依赖引入

其实RecyclerView就是design库里面的,我们可以引入design库即可,这时你不仅可以使用RecyclerView控件,只要是design系列的控件都可以使用,比如CardView、NavigationView都可以使用。如果你就是仅仅使用RecyclerView那么只引入RecyclerView的依赖也行。
ps:依赖如下,根据自己的需求合理选择即可。

  // design库依赖
  implementation 'com.android.support:design:28.0.0'
  //recyclerview依赖
  implementation 'com.android.support:recyclerview-v7:21.0.0'

(2)Adapter中布局转换为View的注意点

这里主要总结下RecyclerView的Adapter中布局转换为View的注意点,其次吧具体的书写步骤附带下。

[MainActivity.java]

public class MainActivity extends AppCompatActivity {
    
    

    private RecyclerView mRecyclerView;
    private List<String> mList;
    private MyRecyclerAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initData();
    }
    
    /**
     * 初始化数据
     * */
    private void initData() {
    
    
        // recycler view 的数据
        mList = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
    
    
            mList.add("item" + i);
        }
        
        mAdapter = new MyRecyclerAdapter(mList, this);
        //给recycler view 设置布局管理器
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        // 给recycler view 设置Adapter
        mRecyclerView.setAdapter(mAdapter);
    }

    /**
     * 初始化view
     * */
    private void initView() {
    
    
        mRecyclerView = findViewById(R.id.recycle_view);
    }
}

[MyRecyclerAdapter.java]

/**
 * Created by sunnyDay on 2019/9/4 14:58
 */
public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {
    
    
    private List<String> mList;
    private Context mContext;

    public MyRecyclerAdapter(List<String> list, Context context) {
    
    
        mContext = context;
        mList = list;
    }

    public Context getContext() {
    
    
        return mContext;
    }

    /**
     * ViewHolder 创建时,这个方法回调。
     *
     * @param viewGroup 容器
     * @param viewType  条目类型
     * ps:这里可以做布局转换view的操作
     */
    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
    
    
        //重要事说三遍:最后一个参数必须为false、最后一个参数必须为false、最后一个参数必须为false
        View mView = LayoutInflater.from(mContext).inflate(R.layout.layout_recyclerview_item, viewGroup, false);
        return new MyViewHolder(mView);// 吧view传递给ViewHolder
    }

    /**
     * 当绑定ViewHolder 时这个方法回调
     * @param myViewHolder 自定义的ViewHolder
     *  ps:这里可以处理view的一些逻辑                   
     */
    @SuppressLint("SetTextI18n")
    @Override
    public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int position) {
    
    
        myViewHolder.text.setText(mList.get(position));
    }

    /**
     * RecyclerView 的item数目
     */
    @Override
    public int getItemCount() {
    
    
        return mList == null ? 0 : mList.size(); // 三目运算,非空处理,避免空指针。
    }

    /**
     * RecyclerView的条目类型 默认为一种类型
     * */
    @Override
    public int getItemViewType(int position) {
    
    
        return super.getItemViewType(position);
    }

    /**
     * 提供ViewHolder
     * 一般在viewHolder内部就行findViewById操作
     */
    static class MyViewHolder extends RecyclerView.ViewHolder {
    
    

        private final AppCompatTextView text;

        MyViewHolder(@NonNull View itemView) {
    
    
            super(itemView);
            text = itemView.findViewById(R.id.atv_text);
        }
    }
}

如上效果图实现,这里就吧代码实现贴了出来。其实使用还是贼简单的。但是有几点是需要我们注意的,特别是刚从ListView迁移过来的小伙伴。

  • 布局转换View.inflate 的使用
 View mView = View.inflate(mContext, R.layout.layout_recyclerview_item, null);
  return new MyViewHolder(mView);

1、刚从ListView迁移过来的小伙伴可能View.inflate的方式使用的多(反正我当年就是这样哈)但是View.inflate的方式在这里是不建议使用的因为你会碰见一个问题内容不居中,确切的说是你指定的layout.layout_recyclerview_item这个布局的根节点容器的属性失效。因为你给他的第三个参数传递了null,当你的layout.layout_recyclerview_item布局转换为view后是不会添加到任何容器中的,所以根节点的属性失效。
2、有的小伙伴该说了:我把onCreateViewHolder方法传递过来的参数viewGroup传过来不就行啦?不好意思回答你的是程序跑的时候直接崩溃了(java.lang.IllegalStateException: ViewHolder views must not be attached when created. Ensure that you are not passing ‘true’ to the attachToRoot parameter of LayoutInflater.inflate(…, boolean attachToRoot))

  • LayoutInflater 的inflate 的使用

有人或许看过View.inflate的源码知道其底层是使用LayoutInflater的。所以你或许会这样使用,如下:

 View mView = LayoutInflater.from(mContext).inflate(R.layout.layout_recyclerview_item, viewGroup, true);
 return new MyViewHolder(mView);

不好意思,跑起来又炸了!!!还是上文同样的崩溃日志,既然看过View.inflate的源码,那么你仔细一想就会发现上文中View.inflate的root不为空时就是调用的LayoutInflater.from(mContext).inflate(R.layout.layout_recyclerview_item, viewGroup, true),这里我们需要坐下来分析下崩溃日志啦!!!

  • 崩溃日志分析
java.lang.IllegalStateException: 
ViewHolder views must not be attached when created. 
Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate
(..., boolean attachToRoot)

这里,ViewHolder 的View一定不能依附于root容器。确认下你的inflate中第三个参数没有传递true。否则就报这个错。
ps:RecyclerView 的ViewHolder要求你不能直接把View添加给RecyclerView,要吧view作为参数,先传递给 ViewHolder 。第三个参数为false就代表不直接加入容器。

  • RecyclerView 中使用inflate 总结

LayoutInflater.from(mContext).inflate(R.layout.layout_recyclerview_item, viewGroup, false)第三个参数传递为false,然后把获得的view传递给ViewHolder即可。

ps:如果对上文inflate有迷惑,可以参考这篇文章: Android的inflate你所需要知道的一切

2、多类型item

Recycler的多条目的实现其实也是蛮简单的:
1、这里你需要重写getItemViewType方法。
2、然后再onCreateViewHolder中根据不同的viewType加载不同的布局。
3、再onBindViewHolder中根据条目所代表的类型处理相关的逻辑即可。
其实这里有一种优雅的写法,在不破坏我们原来的MyRecyclerAdapter代码的基础下添加新的view类型,那就是使用装饰着设计模式(Decorator)

/**
 * Created by sunnyDay on 2019/9/6 20:02
 * recyclerView的多条目实现
 */
public class MyRecyclerAdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
    // 1、装饰者与被装饰着继承或实现相同的对象

    private static final int TYPE_NORMAL = 0; //普通类型
    private static final int TYPE_HEAD = 1;  // 类型 头
    private static final int TYPE_FOOT = 2; //  类型 尾

    private MyRecyclerAdapter myRecyclerAdapter;// 2、装饰者持有被装饰者引用


    /**
     * 初始化引用
     */
    public MyRecyclerAdapterWrapper(MyRecyclerAdapter myRecyclerAdapter) {
    
    
        this.myRecyclerAdapter = myRecyclerAdapter;
    }


    /**
     * @param viewType 代表条目类型
     */
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
    
    

        if (viewType == TYPE_HEAD) {
    
    
            return new HeadViewHolder(LayoutInflater.from(myRecyclerAdapter.getContext()).inflate(R.layout.layout_head, viewGroup, false));
        } else if (viewType == TYPE_FOOT) {
    
    
            return new FootViewHolder(LayoutInflater.from(myRecyclerAdapter.getContext()).inflate(R.layout.layout_foot, viewGroup, false));
        } else {
    
    
            return myRecyclerAdapter.onCreateViewHolder(viewGroup, viewType);
        }

    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
    
    

        if (position == 0 || position == myRecyclerAdapter.getItemCount() + 1) {
    
    
            // 索引为 0和最后一个条目时 加载相应的布局
        } else {
    
    
            if (viewHolder instanceof MyRecyclerAdapter.MyViewHolder) {
    
    
                myRecyclerAdapter.onBindViewHolder((MyRecyclerAdapter.MyViewHolder) viewHolder, position - 1);
            }

        }
    }


    @Override
    public int getItemCount() {
    
    
        return myRecyclerAdapter.getItemCount() + 2; // 相当于原来的基础上多添加两个
    }

    /**
     * @param position 条目索引
     * @function 根据条目的索引返回其相应的条目类型。相同类型item返回相同的数值。
     */
    @Override
    public int getItemViewType(int position) {
    
    
        if (position == 0) {
    
    
            return TYPE_HEAD;
        } else if (position == myRecyclerAdapter.getItemCount() + 1) {
    
    
            return TYPE_FOOT;
        } else
            return TYPE_NORMAL;

    }


    class HeadViewHolder extends RecyclerView.ViewHolder {
    
    

        public HeadViewHolder(@NonNull View itemView) {
    
    
            super(itemView);
        }
    }

    class FootViewHolder extends RecyclerView.ViewHolder {
    
    

        public FootViewHolder(@NonNull View itemView) {
    
    
            super(itemView);
        }
    }

}

这里的难点就是对getItemViewType这个方法的理解:根据条目的索引返回其相应的条目类型很简单但是有人可能会迷糊这里就画图说下。

在这里插入图片描述

这里就三种类型,还算简单的,其实如果种类再多时我们只需要指定索引范围为哪种类型即可。例如上文中的普通条目我们还可以写成这样:else if (position == 1 && position < myRecyclerAdapter.getItemCount() + 1) {return TYPE_FOOT; }
ps:阿里开源框架vlayout可以帮我们快速实现电商app首页的多itemType。

3、添加头尾布局

RecyclerView不像Listview那样提供了添加头尾的方法,需要开发者自己实现。其实搞过上文的多条目后我们添加头尾就很方便啦。

4、数据更新

数据的更新一般分为两种,在顶部item时我们可以下拉刷新数据。当画到底部当前可见的最后一个item时,再次上拉(上滑)加载数据。

(1)下拉刷新

下拉刷新的逻辑其实还是有点复杂的,我们需要为RecyclerView添加个头布局,这个头布局里面一般包括刷新图标,这个图标还伴有动画,处理之外我们还要重写触摸事件,监听用户的滑动操作。不同的滑动操作对应刷新图标的动画。反正就是属于自定义拓展view的系列了。还好安卓supportv4给我们提供了这个控件帮助我们快速实现。

简单使用

//1、 要下拉刷新的控件放入这个容器即可
  <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/pull2refresh"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

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


// java 代码中:
        refreshLayout.setOnRefreshListener(this);// 下拉刷新
        refreshLayout.setColorSchemeResources(R.color.colorAccent,R.color.colorPrimary);//刷新时进度条颜色变换,转一圈颜色变化一种
  /**
     * 下拉刷新回调,模拟数据更新
     */
    @Override
    public void onRefresh() {
    
    
        //模拟耗时操作
        new Handler().postDelayed(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                refreshLayout.setRefreshing(false);//取消刷新
            }
        }, 2000);

        mList.add("我是下拉刷新添加的数据");
        mAdapter.notifyDataSetChanged();
        Toast.makeText(this, "刷新数据成功", Toast.LENGTH_SHORT).show();
    }

其实这种下拉刷新就是典型的圆形进度条转鸭转,如果我们想要花里胡哨的操作还是自定义算啦!!!
下拉刷新自定义实现参考:自己动手写RecyclerView的下拉刷新

(2)上拉加载

数据的上拉加载的实现逻辑相对来说是比较简单的,这里就手动总结下。简单实现思路如下:

1、滑动事件监听
2、判断条目是否是最后一个、可见的条目,且用户正在向上滑。
3、满足2时进行加载数据,更新UI。

        // 滑动监听
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    
    
            boolean isSlide2Up = false;

            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
    
    
                super.onScrollStateChanged(recyclerView, newState);

                LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
    
    //状态为静止没有滑动时
                    if (manager != null) {
    
    
                        int lastItemIndex = manager.findLastCompletelyVisibleItemPosition();//获取最后一个可见的条目索引
                        int itemCount = manager.getItemCount();//获取item的数量

                        // 当滑动到最后一个可见条目,且上拉时。加载数据
                        if (lastItemIndex == itemCount - 1 && isSlide2Up) {
    
    
                            mList.add("我是上拉加载的数据");
                            mAdapter.notifyDataSetChanged();
                            Toast.makeText(getApplicationContext(), "刷新数据成功", Toast.LENGTH_SHORT).show();
                        }
                    }

                }

            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
    
    
                super.onScrolled(recyclerView, dx, dy);
                // 大于0表示正在向上滑动,小于等于0表示停止或向下滑动
                isSlide2Up = dy > 0;
            }
        });

这里也是简要的实现下逻辑,还是那句话想要花里胡哨那就自定义吧!!!
newState的三种状态:

1、SCROLL_STATE_IDLE :静止没有滚动
2、SCROLL_STATE_DRAGGING :正在被外部拖拽,一般为用户正在用手指滚动
3、SCROLL_STATE_SETTLING :用户画动后,手离开屏幕,条目自动滚动、滑动。

注意滑动到最后一个可见条目,且上拉时的判断条件

注意向上向下滑动的判断(dy,左右时使用dx)
ps:参考 RecyclerView的滚动事件OnScrollListener研究

二、中级-RecyclerView的四大组成

1、打造万能的Adapter

RecyclerView的Adapter写多了你就会发现存在着一些重复的工作,这时我们便思考能不能像ListView的adapter一样有个BaseAdapter就好啦。于是开始设计。
思路:想要打造万能的adapter必须先搞个通用的ViewHolder,有了通用的ViewHolder后再设计万能的adapter就方便多啦。具体实现如下:

(1)普通的通用Adapter(适合一种类型的条目)

/**
 * Created by sunnyDay on 2019/9/16 16:36
 * 通用的Adapter封装
 */
public abstract class BaseAdapter<T> extends RecyclerView.Adapter<BaseAdapter.BaseViewHolder> {
    
    

    protected Context mContext;
    protected int mLayoutId;
    protected List<T> mData;
    protected LayoutInflater mLayoutInflate;

    public BaseAdapter(Context context, int layoutId, List<T> data) {
    
    
        mContext = context;
        mLayoutId = layoutId;
        mData = data;
        mLayoutInflate = LayoutInflater.from(context);
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
    
    

        return BaseViewHolder.getViewHolder(mContext, viewGroup, mLayoutId);

    }

    // 有点小bug(用户使用时,可以重写RecyclerView.Adapter的onBindViewHolder)
    @Override
    public final void onBindViewHolder(@NonNull BaseViewHolder baseViewHolder, int position) {
    
    
        convert(baseViewHolder, position);
    }


    public abstract void convert(BaseViewHolder baseViewHolder, int position);


    /**
     * 返回条目个数(进行啦非空处理)
     */
    public int getItemCount() {
    
    
        return mData == null ? 0 : mData.size();
    }

    /**
     * 通用的ViewHolder,内部使用SparseArray来缓存View对象
     */
    public static class BaseViewHolder extends RecyclerView.ViewHolder {
    
    
        private SparseArray<View> mViews;//键值对为int类型,存储相对于hashMap高效
        private View mConvertView;
        private Context mContext;


        BaseViewHolder(Context context, @NonNull View itemView, ViewGroup parent) {
    
    
            super(itemView);
            mContext = context;
            mConvertView = itemView;
            mViews = new SparseArray<>();
        }

        /**
         * 获取ViewHolder
         */
        public static BaseViewHolder getViewHolder(Context context, ViewGroup parent, int layoutId) {
    
    
            View view = LayoutInflater.from(context).inflate(layoutId, parent, false);
            return new BaseViewHolder(context, view, parent);
        }


        /**
         * @param viewId view的id
         * @function通过View Id 找到该控件
         */
        @SuppressWarnings("unchecked")
        public <T extends View> T getView(int viewId) {
    
    
            View view = mViews.get(viewId);
            if (view == null) {
    
    
                view = mConvertView.findViewById(viewId);
                mViews.put(viewId, view);
            }
            return (T) view;
        }
    }
}

简单使用

  mRecyclerView.setAdapter(new BaseAdapter(this,R.layout.layout_recyclerview_item,mList) {
    
    
            @Override
            public void convert(BaseAdapter.BaseViewHolder baseViewHolder, int position) {
    
    
               AppCompatTextView text = baseViewHolder.getView(R.id.atv_text);
               text.setText("万能适配器:"+position);
            }
        });

哈哈,是不是6的一笔,真快鸭!!!直接给个RecyclerView的item布局,在给个数据集合完事。
ps:注意这里只重写convert即可不要重写onCreateViewHolder,否则,convert不生效。
参考:java的继承机制。

在这里插入图片描述
(2)多Item万能Adapter的实现

/**
 * Created by sunnyDay on 2019/9/16 17:48
 */
public abstract class MultiItemBaseAdapter<T> extends BaseAdapter<T> {
    
    

    protected MultiItemTypeSupport<T> mMultiItemTypeSupport;

    public MultiItemBaseAdapter(Context context, List<T> data, MultiItemTypeSupport<T> multiItemTypeSupport) {
    
    
        super(context, -1, data);
        this.mMultiItemTypeSupport = multiItemTypeSupport;
    }

    @Override
    public int getItemViewType(int position) {
    
    
        return mMultiItemTypeSupport.getItemViewType(position, mData.get(position));
    }


    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
    
    
        int layoutId = mMultiItemTypeSupport.getLayoutId(viewType);
        return BaseViewHolder.getViewHolder(mContext, viewGroup, layoutId);
    }


/**
 *
 * 我们的ViewHolder是通用的,唯一依赖的就是个layoutId。那么上述第二条就变成,
 * 根据不同的itemView告诉我用哪个layoutId即可,生成viewholder这种事我们通用adapter来做。
 * */
    public interface MultiItemTypeSupport<T> {
    
    
        int getLayoutId(int itemType);
        int getItemViewType(int position, T t);
    }
}

2、LayoutManager

LayoutManager负责RecyclerView的布局,其中包含了Item View的获取与回收。

(1)常见的实现类:

  • LinearLayoutManager:线性布局管理器,管理item的显示效果(垂直或者水平)。
        // 参数:上下文,布局方向(垂直或者水平),布局是否翻转
        // 默认为垂直效果
        RecyclerView.LayoutManager manager = new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false);
       
  • GridLayoutManager:表格布局管理器,可以实现表格样式效果。
    // 参数:上下文、列数目(指定显示几列)、布局方向(指定可以水平或者垂直滑动)、是否翻转布局(翻转item)
    manager = new GridLayoutManager(this,3);//两个参数的
    manager = new GridLayoutManager(this,3,LinearLayoutManager.VERTICAL,false);//四个参数的
  • StaggeredGridLayoutManager:可以实现瀑布流效果
// 参数:显示列,布局走向
 manager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);

ps:这里条目的高度我们要手动设置下,否则和Grid的效果一致。

  • FlexboxLayoutManager 实现条目可伸缩

使用参考:
1、官方库
2、Android可伸缩布局-FlexboxLayout(支持RecyclerView集成)

(2)LinearLayoutManager的常用方法

canScrollHorizontally();//能否横向滚动
canScrollVertically();//能否纵向滚动
scrollToPosition(int position);//滚动到指定位置

setOrientation(int orientation);//设置滚动的方向
getOrientation();//获取滚动方向

findViewByPosition(int position);//获取指定位置的Item View
findFirstCompletelyVisibleItemPosition();//获取第一个完全可见的Item位置
findFirstVisibleItemPosition();//获取第一个可见Item的位置
findLastCompletelyVisibleItemPosition();//获取最后一个完全可见的Item位置
findLastVisibleItemPosition();//获取最后一个可见Item的位置
3、Item Decoration

RecyclerView的条目默认是没有分割线的,但是google 工程师却暴露了一个接口让用户自定义实现。

(1)接口

public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor)

(2)探究下RecyclerView.ItemDecoration

 public abstract static class 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);
        }
    }

如上,吧弃用的方法去掉后就没有几个方法啦!!! 核心方法如下。

  • onDraw(): 绘制分割线。
  • getItemOffsets(): 设置分割线的宽、高。

(3)想法

既然让我们自己实现,我们只需继承此类,重写这两个方法即可。

(4)参考

让我们自己实现总要给些栗子吧。。。那不错栗子是有的就是google 给的simple: DividerItemDecoration,并且高版本的sdk中已经添加了,直接就可以看这个类的源码。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class DividerItemDecoration extends ItemDecoration {
    
    
    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;
    private static final String TAG = "DividerItem";
    private static final int[] ATTRS = new int[]{
    
    16843284};
    private Drawable mDivider;
    private int mOrientation;
    private final Rect mBounds = new Rect();

    public DividerItemDecoration(Context context, int orientation) {
    
    
        TypedArray a = context.obtainStyledAttributes(ATTRS);
        this.mDivider = a.getDrawable(0);
        if (this.mDivider == null) {
    
    
            Log.w("DividerItem", "@android:attr/listDivider was not set in the theme used for this DividerItemDecoration. Please set that attribute all call setDrawable()");
        }

        a.recycle();
        this.setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
    
    
        if (orientation != 0 && orientation != 1) {
    
    
            throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
        } else {
    
    
            this.mOrientation = orientation;
        }
    }

    public void setDrawable(@NonNull Drawable drawable) {
    
    
        if (drawable == null) {
    
    
            throw new IllegalArgumentException("Drawable cannot be null.");
        } else {
    
    
            this.mDivider = drawable;
        }
    }

    public void onDraw(Canvas c, RecyclerView parent, State state) {
    
    
        if (parent.getLayoutManager() != null && this.mDivider != null) {
    
    
            if (this.mOrientation == 1) {
    
    
                this.drawVertical(c, parent);
            } else {
    
    
                this.drawHorizontal(c, parent);
            }

        }
    }

    private void drawVertical(Canvas canvas, RecyclerView parent) {
    
    
        canvas.save();
        int left;
        int right;
        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();
        }

        int childCount = parent.getChildCount();

        for(int i = 0; i < childCount; ++i) {
    
    
            View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, this.mBounds);
            int bottom = this.mBounds.bottom + Math.round(child.getTranslationY());
            int top = bottom - this.mDivider.getIntrinsicHeight();
            this.mDivider.setBounds(left, top, right, bottom);
            this.mDivider.draw(canvas);
        }

        canvas.restore();
    }

    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
    
    
        canvas.save();
        int top;
        int bottom;
        if (parent.getClipToPadding()) {
    
    
            top = parent.getPaddingTop();
            bottom = parent.getHeight() - parent.getPaddingBottom();
            canvas.clipRect(parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
        } else {
    
    
            top = 0;
            bottom = parent.getHeight();
        }

        int childCount = parent.getChildCount();

        for(int i = 0; i < childCount; ++i) {
    
    
            View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, this.mBounds);
            int right = this.mBounds.right + Math.round(child.getTranslationX());
            int left = right - this.mDivider.getIntrinsicWidth();
            this.mDivider.setBounds(left, top, right, bottom);
            this.mDivider.draw(canvas);
        }

        canvas.restore();
    }

    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
    
    
        if (this.mDivider == null) {
    
    
            outRect.set(0, 0, 0, 0);
        } else {
    
    
            if (this.mOrientation == 1) {
    
    
                outRect.set(0, 0, 0, this.mDivider.getIntrinsicHeight());
            } else {
    
    
                outRect.set(0, 0, this.mDivider.getIntrinsicWidth(), 0);
            }

        }
    }
}

如上:
1、想要使用这个类提供的默认分割线直接
mRecyclerView.addItemDecoration(new DividerItemDecoration(this, 1));
2、想要指定drawable作为分割线直接调用setDrawable(@NonNull Drawable drawable)给个drawable对象即可。
DividerItemDecoration官方文档
ps:其实还有一种方法就是在xml布局里面,条目的底部添加个分割线即可。最后一个条目的底部分割线隐藏即可。

4、item Animation

RecyclerView能够通过mRecyclerView.setItemAnimator(ItemAnimator animator)设置添加、删除、移动、改变的动画效果。RecyclerView提供了默认的ItemAnimator实现类:DefaultItemAnimator。

(1)提供的默认的动画类及其继承关系图

 mRecyclerView.setItemAnimator(new DefaultItemAnimator());

在这里插入图片描述
(2)类分析

  • ItemAnimator 内部提供了一系列条目变化而触发动画的方法,条目动画的基类。
  • SimpleItemAnimator:实现类,该类提供了一系列更易懂的API,在自定义Item Animator时只需要继承SimpleItemAnimator即可。

SimpleItemAnimator中的重要方法:
animateAdd(ViewHolder holder): 当Item添加时被调用。
animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY): 当Item移动时被调用。
animateRemove(ViewHolder holder): 当Item删除时被调用。
animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop): 当显式调用notifyItemChanged()或notifyDataSetChanged()时被调用。

  • DefaultItemAnimator 继承了SimpleItemAnimator然后实现一堆方法,还是比较麻烦的。

(3)简单方式 实现自定义动画

使用第三方库:recyclerview-animators
而且自定义item动画使用这个也简单多啦,可以玩一些花里胡哨的操作啦。。。

//栗子:

 mRecyclerView.setItemAnimator(new ScaleInAnimator());//使用第三方
 ...
 public void doClick(View view) {
    
    
        mList.remove(0);
        mAdapter.notifyItemRemoved(0); // 刷新方法的使用需要留意
    }

三、拓展

1、点击事件、长摁时间

rv相比listview的事件点击、添加头尾还是麻烦点的。事件,这个使用接口回调的方式即可实现,下面就举个点击事件的栗子。。。

public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.MyViewHolder> {
    
    
    private OnClickListener mOnClickListener;

     //外部设置条目点击事件时回调 view的点击事件
    public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, final int position) {
    
    
        myViewHolder.text.setText(mList.get(position));
        myViewHolder.layout.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                mOnClickListener.clicked(v, position);
            }
        });
    }

 public void setOnClickListener(OnClickListener onClickListener) {
    
    
        mOnClickListener = onClickListener;
    }

    public interface OnClickListener {
    
    
        void clicked(View view, int position);
    }
}
2、结合ItemTouchHelper

安卓系统提供了一个强大的工具类ItemTouchHelper,这个类处理了有关rv的拖拽、侧滑的相关的事情。
简单的使用步骤:
1、实现 ItemTouchHelper.Callback 回调
2、把 ItemTouchHelper 绑定到 RecyclerView 上

(1)实现接口

简单的拖拽到指定位置、侧滑删除效果

/**
 * Created by sunnyDay on 2019/9/20 17:21
 * <p>
 * 侧滑 拖拽的实现
 * 1、实现侧滑删除
 * 2、实现拖动到指定位置
 */
public class MyTouchHelper extends ItemTouchHelper.Callback {
    
    

    private MyRecyclerAdapter mRecyclerAdapter;

    public MyTouchHelper(MyRecyclerAdapter recyclerAdapter) {
    
    
        mRecyclerAdapter = recyclerAdapter;
    }

    /**
     * 设置支持拖拽滑动的方向(内部通过makeMovementFlags方法设置)
     * 规定条目滑动为:左右
     * 规定条目拖拽为:上下
     */
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
    
    
        int slideFlag = ItemTouchHelper.START | ItemTouchHelper.END; // 左右滑动
        int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; //上下拖拽
        return makeMovementFlags(dragFlag, slideFlag);
    }

    /**
     * 拖拽时回调
     */
    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder viewHolder1) {
    
    
        int fromItem = viewHolder.getAdapterPosition();
        int toItem = viewHolder1.getAdapterPosition();

        String prev = mRecyclerAdapter.getmList().remove(fromItem);//删除长摁位置的条目,保存下删除的条目。
        mRecyclerAdapter.getmList().add(toItem > fromItem ? toItem - 1 : toItem, prev);// 吧删除的条目添加到拖动停止的位置
        mRecyclerAdapter.notifyItemMoved(fromItem, toItem);// 通知局部刷新
        return true;
    }

    /**
     * 滑动时回调
     */
    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
    
    
        int position = viewHolder.getAdapterPosition();
        mRecyclerAdapter.getmList().remove(position);
        mRecyclerAdapter.notifyItemRemoved(position);
    }

    /**
     * 状态改变时回调
     */
    @Override
    public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
    
    
        super.onSelectedChanged(viewHolder, actionState);
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
    
    
            viewHolder.itemView.setBackgroundColor(Color.parseColor("#ff0000")); //设置拖拽和侧滑时的背景色
        }
    }

    /**
     * 拖拽滑动完成之后回调
     */
    @Override
    public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
    
    
        super.clearView(recyclerView, viewHolder);
        viewHolder.itemView.setBackgroundColor(Color.parseColor("#FFFFFF"));
    }

    /**
     * 如果想自定义动画,可以重写这个方法
     * 根据偏移量来设置
     */
    @Override
    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    
    
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }

    /**
     * 是否支持长摁拖拽,默认为 true,设置false为关闭。
     */
    @Override
    public boolean isLongPressDragEnabled() {
    
    
        return super.isLongPressDragEnabled();
    }
}

(2)绑定rv

  // rv的拖拽侧滑
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new MyTouchHelper(mAdapter));
        itemTouchHelper.attachToRecyclerView(mRecyclerView);
3、结合SnapHelper实现特殊条目滑动效果

SnapHelp 能够辅助 RecyclerView 在滚动结束时将 Item 对齐到某个位置。
SnapHelp 是一个抽象类,Android 提供了 LinearSnapHelper,可以让 RecyclerView 滚动停止时 Item 停留在中间位置,又提供了 PagerSnapHelper,可以让 RecyclerView 像 ViewPager 一样的效果,一次只能滑动一个,并且 Item 居中显示,和 LinearSnapHelper 的区别在于 LinearSnapHelper 支持惯性滑动,所以一次能滑动多个。

(1)使用贼简单

     // 支持惯性滑动
        LinearSnapHelper linearSnapHelper = new LinearSnapHelper();
        linearSnapHelper.attachToRecyclerView(mRecyclerView);

        // 类似vp效果
        PagerSnapHelper pagerSnapHelper = new PagerSnapHelper();
        pagerSnapHelper.attachToRecyclerView(mRecyclerView);

四、高阶

1、源码浅析

后续熟悉了再补。。。。。

2、更多特效

优秀文章参考

其他

1、嵌套滑动问题

出现方式:
(1)和CoordinatorLayout结合时

Android 5.0推出了嵌套滑动机制,在之前,一旦子View处理了触摸事件,父View就没有机会再处理这次的触摸事件,而嵌套滑动机制解决了这个问题.为了支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口,而RecyclerView实现了NestedScrollingChild接口,而CoordinatorLayout实现了NestedScrollingParent接口
ps:结合第一行代码的5.0UI章节理解。

(2)其他场景及其解决方案

参考:View的事件体系(四)view滑动冲突

2、和listview的对比

(1)ListView的相对优点

  • addHeaderView(), addFooterView()添加头视图和尾视图。
  • 通过”android:divider”设置自定义分割线。
  • setOnItemClickListener()和setOnItemLongClickListener()设置点击事件和长按事件。

(2)Rv的优势

  • 默认已经实现了View的复用,不需要类似if(convertView == null)的实现,而且回收机制更加完善。
  • 默认支持局部刷新。
  • 容易实现添加item、删除item的动画效果。
  • 容易实现拖拽、侧滑删除等功能。
  • RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。

小结

只是把重要知识点过了一遍,懂了些基础、RecyclerView的设计原理、内部的设计模式啥的还没探讨。。。任重而道远。

end

参考文章:

猜你喜欢

转载自blog.csdn.net/qq_38350635/article/details/100582477