美文网首页Android TechAndroid开发学习学习中的Android
RecyclerView使用完全指南,是时候体验新控件了(一)

RecyclerView使用完全指南,是时候体验新控件了(一)

作者: 王三的猫阿德 | 来源:发表于2016-07-05 16:27 被阅读62240次

    转载注明出处:http://www.jianshu.com/p/4fc6164e4709

    概述

    官方介绍,RecyclerView用于在有限的窗口展现大量的数据,其实早已经有了类似的控件,如ListView、GridView,那么相比它们,RecyclerView有什么样优势呢?
    RecyclerView标准化了ViewHolder,而且异常的灵活,可以轻松实现ListView实现不了的样式和功能,通过布局管理器LayoutManager可控制Item的布局方式,通过设置Item操作动画自定义Item添加和删除的动画,通过设置Item之间的间隔样式,自定义间隔。

    • 设置布局管理器以控制Item的布局方式,横向、竖向以及瀑布流方式。
    • 可设置Item操作的动画(删除或者添加等)
    • 可设置Item的间隔样式(可绘制)

    但是关于Item的点击和长按事件,需要用户自己去实现。

    在使用RecyclerView时候,必须指定一个适配器Adapter和一个布局管理器LayoutManager。适配器继承RecyclerView.Adapter类,具体实现类似ListView的适配器,取决于数据信息以及展示的UI。布局管理器用于确定RecyclerView中Item的展示方式以及决定何时复用已经不可见的Item,避免重复创建以及执行高成本的findViewById()方法。

    来看一下用法。

    mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
    // 设置布局管理器
    mRecyclerView.setLayoutManager(mLayoutManager);
    // 设置adapter
    mRecyclerView.setAdapter(mAdapter);
    // 设置Item添加和移除的动画
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());
    // 设置Item之间间隔样式
    mRecyclerView.addItemDecoration(mDividerItemDecoration);
    

    可以看见RecyclerView相比ListView会多出许多操作,这也是RecyclerView灵活的地方,它将许多动能暴露出来,用户可以选择性的自定义属性以满足需求。

    RecyclerView提供了三种布局管理器:

    • LinerLayoutManager 以垂直或者水平列表方式展示Item
    • GridLayoutManager 以网格方式展示Item
    • StaggeredGridLayoutManager 以瀑布流方式展示Item

    基本使用

    在build.gradle文件中引入该类。

        compile 'com.android.support:recyclerview-v7:23.4.0'
    

    Activity代码

    public class MDRvActivity extends MDBaseActivity {
    
        private RecyclerView mRecyclerView;
    
        private RecyclerView.Adapter mAdapter;
    
        private RecyclerView.LayoutManager mLayoutManager;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_rv);
            initData();
            initView();
        }
    
        private void initData() {
            mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
            mAdapter = new MyAdapter(getData());
        }
    
        private void initView() {
            mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
            // 设置布局管理器
            mRecyclerView.setLayoutManager(mLayoutManager);
            // 设置adapter
            mRecyclerView.setAdapter(mAdapter);
        }
    
        private ArrayList<String> getData() {
            ArrayList<String> data = new ArrayList<>();
            String temp = " item";
            for(int i = 0; i < 20; i++) {
                data.add(i + temp);
            }
    
            return data;
        }
    }
    

    Activity布局文件activity_rv.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:orientation="vertical"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/my_recycler_view"
            android:scrollbars="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>
    

    RecyclerView适配器Adapter代码

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
    
        private ArrayList<String> mData;
    
        public MyAdapter(ArrayList<String> data) {
            this.mData = data;
        }
    
        public void updateData(ArrayList<String> data) {
            this.mData = data;
            notifyDataSetChanged();
        }
    
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            // 实例化展示的view
            View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);
            // 实例化viewholder
            ViewHolder viewHolder = new ViewHolder(v);
            return viewHolder;
        }
    
        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            // 绑定数据
            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);
            }
        }
    }
    

    Item的布局文件view_rv_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">
        <TextView
            android:id="@+id/item_tv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            tools:text="item"/>
    </LinearLayout>
    

    运行结果如下:


    RecyclerView-无间隔.jpg

    可以看见展示效果和ListView基本上无差别,但是Item之间并没有分割线,在xml去找divider属性的时候,发现RecyclerView没有divider属性,当然也可以在Item布局中加上分割线,但是这样做并不是很优雅。前文说过,RecyclerView可是支持自定义间隔样式的。通过mRecyclerView.addItemDecoration()来设置我们定义好的间隔样式。

    间隔样式

    自定义间隔样式需要继承RecyclerView.ItemDecoration类,该类是个抽象类,主要有三个方法。

    • onDraw(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式
    • onDrawOver(Canvas c, RecyclerView parent, State state),在Item绘制之后被调用,该方法主要用于绘制间隔样式
    • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state),设置item的偏移量,偏移的部分用于填充间隔样式,在RecyclerView的onMesure()中会调用该方法

    onDraw()onDrawOver()这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。直接来看一下自定义的间隔样式的实现吧,参考官方实例

    public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {
    
        private static final int[] ATTRS = new int[]{
                android.R.attr.listDivider
        };
        public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
        public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
        /**
         * 用于绘制间隔样式
         */
        private Drawable mDivider;
        /**
         * 列表的方向,水平/竖直
         */
        private int mOrientation;
    
    
        public MyDividerItemDecoration(Context context, int orientation) {
            // 获取默认主题的属性
            final TypedArray a = context.obtainStyledAttributes(ATTRS);
            mDivider = a.getDrawable(0);
            a.recycle();
            setOrientation(orientation);
        }
    
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            // 绘制间隔
            if (mOrientation == VERTICAL_LIST) {
                drawVertical(c, parent);
            } else {
                drawHorizontal(c, parent);
            }
        }
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            if (mOrientation == VERTICAL_LIST) {
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            } else {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            }
        }
    
        private void setOrientation(int orientation) {
            if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
                throw new IllegalArgumentException("invalid orientation");
            }
            mOrientation = orientation;
        }
    
        /**
         * 绘制间隔
         */
        private void drawVertical(Canvas c, RecyclerView parent) {
            final int left = parent.getPaddingLeft();
            final int right = parent.getWidth() - parent.getPaddingRight();
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                        .getLayoutParams();
                final int top = child.getBottom() + params.bottomMargin +
                        Math.round(ViewCompat.getTranslationY(child));
                final int bottom = top + mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    
        /**
         * 绘制间隔
         */
        private void drawHorizontal(Canvas c, RecyclerView parent) {
            final int top = parent.getPaddingTop();
            final int bottom = parent.getHeight() - parent.getPaddingBottom();
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                        .getLayoutParams();
                final int left = child.getRight() + params.rightMargin +
                        Math.round(ViewCompat.getTranslationX(child));
                final int right = left + mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    }
    

    然后在代码中设置RecyclerView的间隔样式。

    mRecyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.VERTICAL));
    

    来看一下展示效果。

    RecyclerView-有间隔.jpg

    既然RecyclerView还支持水平列表,简单改一下属性,看看水平列表的显示效果。

    修改Item的布局文件view_rv_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="@dimen/md_common_view_width"
                  android:layout_height="match_parent">
        <TextView
            android:id="@+id/item_tv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
    
            tools:text="item"/>
    </LinearLayout>
    

    修改LayoutManager的初始化和间隔样式初始化。

    mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
    // 设置Item之间间隔样式
    mRecyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.HORIZONTAL));
    

    看一下水平列表效果。


    RecyclerView-水平列表.jpg

    动画

    前面说过,RecyclerView可以设置列表中Item删除和添加的动画,在v7包中给我们提供了一种默认的Item删除和添加的动画,如果没有特殊的需求,默认使用这个动画即可。

    // 设置Item添加和移除的动画
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());
    

    下面就添加一下删除和添加Item的动作。在Adapter里面添加方法。

    public void addNewItem() {
        if(mData == null) {
            mData = new ArrayList<>();
        }
        mData.add(0, "new Item");
        notifyItemInserted(0);
    }
    
    public void deleteItem() {
        if(mData == null || mData.isEmpty()) {
            return;
        }
        mData.remove(0);
        notifyItemRemoved(0);
    }
    

    添加事件的处理。

    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.rv_add_item_btn) {
            mAdapter.addNewItem();
            // 由于Adapter内部是直接在首个Item位置做增加操作,增加完毕后列表移动到首个Item位置
            mLayoutManager.scrollToPosition(0);
        } else if(id == R.id.rv_del_item_btn){
            mAdapter.deleteItem();
            // 由于Adapter内部是直接在首个Item位置做删除操作,删除完毕后列表移动到首个Item位置
            mLayoutManager.scrollToPosition(0);
        }
    }
    

    准备工作完毕后,来看一下运行的效果。

    RecyclerView-动画.gif

    点击事件

    RecyclerView并没有像ListView一样暴露出Item点击事件或者长按事件处理的api,也就是说使用RecyclerView时候,需要我们自己来实现Item的点击和长按等事件的处理。实现方法有很多,可以监听RecyclerView的Touch事件然后判断手势做相应的处理,也可以通过在绑定ViewHolder的时候设置监听,然后通过Apater回调出去,我们选择第二种方法,更加直观和简单。

    看一下Adapter的完整代码。

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
    
        /**
         * 展示数据
         */
        private ArrayList<String> mData;
    
        /**
         * 事件回调监听
         */
        private MyAdapter.OnItemClickListener onItemClickListener;
    
        public MyAdapter(ArrayList<String> data) {
            this.mData = data;
        }
    
        public void updateData(ArrayList<String> data) {
            this.mData = data;
            notifyDataSetChanged();
        }
    
        /**
         * 添加新的Item
         */
        public void addNewItem() {
            if(mData == null) {
                mData = new ArrayList<>();
            }
            mData.add(0, "new Item");
            notifyItemInserted(0);
        }
    
        /**
         * 删除Item
         */
        public void deleteItem() {
            if(mData == null || mData.isEmpty()) {
                return;
            }
            mData.remove(0);
            notifyItemRemoved(0);
        }
    
        /**
         * 设置回调监听
         * 
         * @param listener
         */
        public void setOnItemClickListener(MyAdapter.OnItemClickListener listener) {
            this.onItemClickListener = listener;
        }
    
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            // 实例化展示的view
            View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);
            // 实例化viewholder
            ViewHolder viewHolder = new ViewHolder(v);
            return viewHolder;
        }
    
        @Override
        public void onBindViewHolder(final ViewHolder holder, int position) {
            // 绑定数据
            holder.mTv.setText(mData.get(position));
    
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(final View v) {
                    if(onItemClickListener != null) {
                        int pos = holder.getLayoutPosition();
                        onItemClickListener.onItemClick(holder.itemView, pos);
                    }
                }
            });
    
            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    if(onItemClickListener != null) {
                        int pos = holder.getLayoutPosition();
                        onItemClickListener.onItemLongClick(holder.itemView, pos);
                    }
                    //表示此事件已经消费,不会触发单击事件
                    return true;
                }
            });
        }
    
        @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);
            }
        }
    
        public interface OnItemClickListener {
            void onItemClick(View view, int position);
            void onItemLongClick(View view, int position);
        }
    }
    

    设置Adapter的事件监听。

    mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
        @Override
        public void onItemClick(View view, int position) {
            Toast.makeText(MDRvActivity.this,"click " + position + " item", Toast.LENGTH_SHORT).show();
        }
    
        @Override
        public void onItemLongClick(View view, int position) {
            Toast.makeText(MDRvActivity.this,"long click " + position + " item", Toast.LENGTH_SHORT).show();
        }
    });
    

    最后的实现效果。


    RecyclerView-点击.gif

    总结

    可以看见相比于ListView,RecyclerView非常灵活,但其实这篇文章只是介绍了RecyclerView的基本使用,并没有深入,比如像网格展示和瀑布流展示都没有介绍,而且这篇文章为了详细的介绍使用方法,贴了大量的源代码,导致篇幅过长,不得以要将RecyclerView的使用分好几篇来介绍。就目前而言,我们已经知道RecyclerView的一些功能如下。

    • 水平列表展示,设置LayoutManager的方向性
    • 竖直列表展示,设置LayoutManager的方向性
    • 自定义间隔,RecyclerView.addItemDecoration()
    • Item添加和删除动画,RecyclerView.setItemAnimator()

    所以在项目中如果再遇见列表类的布局,就可以优先考虑使用RecyclerView,更灵活更快捷的使用方式会给编码带来不一样的体验。如果你以为这些就是RecyclerView相比ListView/GridView优势的话,那就大错特错了,关于RecyclerView还有更多灵活的功能,在后面文章会慢慢介绍。

    Demo地址

    下一篇会主要介绍RecyclerView的其他两种展示方式,网格和瀑布流。

    相关文章

      网友评论

      • cwwei:文章写的不错,研究很全面+仔细,非常有参考价值
      • 4d226614f116:大佬我想问一下最后一行的分割线怎么 处理?让它不显示。
      • nodzhang:onDrawOver是在绘制之后调用,你写错了吧
        王三的猫阿德:嗯,写错了,谢谢指正
      • 96899601ea34:你好,如果在item中 有2种相同的View,然后中间View是不同的,比如Image或者Gif,之后,在Holder里面应该怎么写,不然每一种不同的Item的View都要实例化相同的View,
        王三的猫阿德:1. 复写Adapter中的getItemViewType方法,根据postion返回不同的type,然后在onCreateViewHolder方法总根据不同的type,实例化不同的view,和listView差不多
        2. 如果按照你的举例,写一个view同时支持image和gif就可以了,就不用做区分
      • 还没爬出坑的程序猿:RecyclerView: No adapter attached; skipping layout
        数据都正常显示出来了,为什么会报这个错误呢
        还没爬出坑的程序猿:@王三的猫阿德 那在Fragment里呢,我在onCreateView设置,等获得数据后更新,设置了layoutmanager,还是会出现这个错误
        王三的猫阿德:@还没爬出坑的程序猿 试一下再oncreate里面把adapter设置到RecyclerView里面,等更新数据后调用notify,第二个看你是否设置了layoutmanager
      • 328588e72ea3:@dimen/md_common_view_height这个值是自己定义的么?
        王三的猫阿德:是的,自定义的值
      • 0e045a341393:你好,我想请教一下,如果在recyclerview的item中根据item类别(例如设置头像之类的),在item上再加一个imageview或者其他的,应该如何做?
        0e045a341393:@王三的猫阿德 谢谢~:blush:
        王三的猫阿德:复写Adapter中的getItemViewType方法,根据postion返回不同的type,然后在onCreateViewHolder方法总根据不同的type,实例化不同的view,和listView差不多

      本文标题:RecyclerView使用完全指南,是时候体验新控件了(一)

      本文链接:https://www.haomeiwen.com/subject/bcwhjttx.html