RecyclerView 的基本使用

RecyclerView 的基本使用

2017年1月21日

为了加强理解 RecyclerView 的使用方法,同时让自己的写作能力得到锻炼,所以实践一把,都是一些基本的使用,主要分析下拉刷新与上拉加载部分,细节很重要,以此来激励自己写出高质量的文章。

  • 基本用法
    • 基本介绍
    • 代码实现
  • SwipeRefreshLayout 配合实现下拉刷新
    • 基本介绍
    • 代码实现
  • RecyclerView 实现上拉加载
    • 滚动事件的分析
    • 添加 FooterView 实现上拉加载
  • 参考文章及总结

效果图

基本使用

RecyclerView 的基本介绍

RecyclerView 是谷歌V7包下新增的控件,用来替代 ListView 的使用,在 RecyclerView 标准化了 ViewHolder 类似于 ListView中 convertView 用来做视图缓存.

相信大家对于 ListView 都很熟悉,在我刚学 Android 开发的时候接触就是这个强大的控件,也被称为最难的控件之一。自从 RecyclerView 已经面市很久,也在很多应用中得到广泛的使用,在整个开发者圈子里面也拥有很不错的口碑,那说明 RecyclerView 拥有比 ListView,GridView 之类控件有很多的优点。

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

但是对于点击事件需要自己写回调借口实现,下面会详细介绍,并且没有了 ListView 强大的 addFootView() 的方法,需要自己去实现,不过还好拓展性很强。

代码实现

1.添加库依赖:首先要用这个控件,你需要在gradle文件中添加包的引用(配合官方CardView使用)

compile 'com.android.support:recyclerview-v7:25.0.0'
compile 'com.android.support:cardview-v7:25.0.0'

2.然后在 xml 文件里实现

<android.support.v7.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/recycler_view"
    android:layout_centerVertical="true"
    android:layout_centerHorizontal="true"/>

3.接着就是在 Activity 里面设置

public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private RecyclerAdapter mAdapter;
    private LinearLayoutManager mLayoutManager;
    private List<ShareBean> mData = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();//初始化数据
        initView();//初始化布局
        setListener();//设置监听事件
    }

    private void initData() {
        for (int i = 0; i < 4; i++) {
            mData.add(new ShareBean(R.mipmap.ic_image, "Android vs IOS"));
        }
    }

    private void initView() {
        mRecyclerView = (RecyclerView) findViewById(R.id.recycler);
        //设置布局管理器
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);
        //确保尺寸是一个常数,避免计算每个item的size
        mRecyclerView.setHasFixedSize(true);
        //设置显示动画
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());
        mAdapter = new RecyclerAdapter(this, mData);
        mRecyclerView.setAdapter(mAdapter);
    }

    private void setListener() {
        //设置Items的点击事件
        mAdapter.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(final View view, int position) {
                onClick(view, position);
            }
        });
      }
}

4.适配器 Adapter 的代码

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    public  OnItemClickListener itemClickListener;
    private List<ShareBean> mData = null;
    private LayoutInflater mInflater;

    public RecyclerAdapter(Context context, List<ShareBean> mData) {
        this.mData = mData;
        this.mInflater = LayoutInflater.from(context);
    }

    //将布局转化为 View 并传递给 RecyclerView 封装好的 ViewHolder
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){    
        // 实例化展示的view
        View view = mInflater.inflate(R.layout.item_recyclerview, parent, false);
        // 实例化viewholder
        return new ItemViewHolder(view);
    }

    //将数据与视图进行绑定
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        ((ItemViewHolder) holder).mImageView.setImageResource(mData.get(position).getImage_id());
        ((ItemViewHolder) holder).mTextView.setText(mData.get(position).getName());
    }

    //返回 Item 的数量
    @Override
    public int getItemCount() {
        return mData == null ? 0 : mData.size();;
    }

    public  class ItemViewHolder extends RecyclerView.ViewHolder {

        CardView mCardView;
        CircleImageView mImageView;
        TextView mTextView;

        public ItemViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.textView);
            mCardView = (CardView) itemView.findViewById(R.id.cardView);
            mImageView = (CircleImageView) itemView.findViewById(R.id.image);
            mCardView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (itemClickListener != null) {
                        itemClickListener.onItemClick(view, getPosition());
                    }
                }
            });
        }
    }

    //以下为点击事件的接口回调部分
    public void setOnItemClickListener(OnItemClickListener itemClickListener) {
        this.itemClickListener = itemClickListener;
    }

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

以上是适配器基本的写法,这个自定义 Adapter 和我们在使用 Listview 时候的 Adapter 相比还是有点不太一样的,首先这边我们需要继承 RecyclerView.Adaper 类,然后实现两个重要的方法 onBindViewHodler() 以及 onCreateViewHolder() ,这边我们看出来区别,使用 RecyclerView 控件我们就可以把 ItemView 视图创建和数据绑定这两步进行分来进行管理,用法就更加方便而且灵活了,并且我们可以定制打造千变万化的布局。同时这边我们还需要创建一个 ViewHolder 类,该类必须继承自 RecyclerView.ViewHolder 类,现在 Google 也要求我们必须要实现 ViewHolder 来承载 Item 的视图。

特别注意一下最下面的点击事件的回调函数,主要是对接口的熟悉程度,Java 是硬伤 55555.

同时 RecyclerView 有三种实现方式(上面介绍有),通过一下代码设置

mRecyclerView.setLayoutManager(new LinearLayoutManager(this));//这里用线性显示 类似于listview
mRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));//这里用线性宫格显示 类似于grid view
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL));//这里用线性宫格显示 类似于瀑布流


SwipeRefreshLayout 配合实现下拉刷新

基本介绍

SwipeRefrshLayout 是 Google 官方更新的一个 Widget ,可以实现下拉刷新的效果。该控件集成自 ViewGroup 在 support-v4 兼容包下,不过我们需要升级 supportlibrary 的版本到19.1以上。基本使用的方法如下:

  • setOnRefreshListener(OnRefreshListener):添加下拉刷新监听器
  • setRefreshing(boolean):显示或者隐藏刷新进度条
  • isRefreshing():检查是否处于刷新状态
  • setColorSchemeResources():设置进度条的颜色主题,最多设置四种,以前的setColorScheme()方法已经弃用了
代码实现

1.首先先看一下 xml 布局,在 RecyclerView 布局外部嵌套一层 SwipeRefreshLayout 布局即可。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

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

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="visible"/>

    </android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

2.在 Activity 中获取 SwipeRefreshLayout 控件并且设置 OnRefreshListener 监听器,同时实现里边的 onRefresh() 方法,在该方法中进行网络请求最新数据,然后刷新 RecyclerView 列表同时设置 SwipeRefreshLayout 的进度Bar的隐藏或者显示效果。具体代码如下:

mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
          @Override
          public void onRefresh() {
              addTopData();
          }
      });

  private void addTopData() {
           new Handler().postDelayed(new Runnable() {
               @Override
               public void run() {
                   for (int i = 0; i < 5; i++) {
                       mData.add(i, new ShareBean(R.mipmap.ic_image_h, "下拉刷新数据" + i));
                   }
                   mAdapter.notifyDataSetChanged();
                   mSwipeRefreshLayout.setRefreshing(false);
               }
           }, 1000);
       }

RecyclerView 实现上拉加载

滚动事件的分析

1.RecyclerView 本身已经提供了滑动的监听接口,OnScrollListener,这个接口包含了以下的方法,代码注释介绍。

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
      //当recycleView的滑动状态改变时回调
       @Override
       public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
           super.onScrollStateChanged(recyclerView, newState);
       }

      //当RecycleView滑动之后被回调
       @Override
       public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
           super.onScrolled(recyclerView, dx, dy);
      }
});

2.RecyclerView 的滑动状态

  • 当前的recycleView不滑动(滑动已经停止时):
    public static final int SCROLL_STATE_IDLE = 0;

  • 当前的recycleView被拖动滑动:
    public static final int SCROLL_STATE_DRAGGING = 1;

  • 当前的recycleView在滚动到某个位置的动画过程,但没有被触摸滚动.调用 scrollToPosition(int) 应该会触发这个状态:
    public static final int SCROLL_STATE_SETTLING = 2;

3.滑动位置的监听
3.1 以下是最简单的顶部/底部的判断方式:

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
      //当recycleView的滑动状态改变时回调
       @Override
       public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
           super.onScrollStateChanged(recyclerView, newState);

          //获取最后一个可见view的位置
          int lastItemPosition = linearManager.findLastVisibleItemPosition();
          //获取第一个可见view的位置
          int firstItemPosition =linearManager.findFirstVisibleItemPosition();

          if (newState == RecyclerView.SCROLL_STATE_IDLE && lastItemPosition + 1 == adapter.getItemCount()) {
          //最后一个itemView的position为adapter中最后一个数据时,说明该itemView就是底部的view了
          //需要注意position从0开始索引,adapter.getItemCount()是数据量总数
          }

          //同理检测是否为顶部itemView时,只需要判断其位置是否为0即可
          if (newState == RecyclerView.SCROLL_STATE_IDLE && firstItemPosition == 0) {}
       }
});

以上的实现方式存在诸多问题,比如:

  • itemView会在滑动过程中只显示一部分或者一半
  • 检测数据不够时的RecycleView

3.2 对于显示不全的问题
也就是说:当某一个itemView只显示一部分的时候,此时已经算是一个position了,此时很可能去触发判断条件。所以官方 Api 里有一下两个方法:

  • findFirstCompletlyVisibleItemPosition()
  • findLastCompletlyVisibleItemPosition()

就可以尽可能的避免这种问题。

3.3 检测数据不够时的状态

如果为了避免这种情况,我们就需要对在检测时多进行一步,若当前的RecycleView显示的itemView不满屏的情况下,其实并不存在滑动的说法(没有加载更多,因为根本数据还没有满屏显示),至于下拉刷新的,还是可以的,但一般都会使用SwipeRefreshLayout实现下拉刷新了.因此这里主要考虑的是关于滑动到底部加载更多的问题.

3.4 上拉加载更多代码实现

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (dy > 0) {//向下滚动
            int visibleItemCount = mLayoutManager.getChildCount();
            int totalItemCount = mLayoutManager.getItemCount();
            int pastVisiblesItems = mLayoutManager.findFirstVisibleItemPosition();

            if (!loading && (visibleItemCount + pastVisiblesItems) == totalItemCount) {
                loading = true;
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 5; i++) {
                            mData.add(new ShareBean(R.mipmap.ic_image_s, "上拉加载数据" + i));
                        }
                        mAdapter.notifyDataSetChanged();
                        loading = false;
                    }
                }, 3000);
            }
        }
    }
});
添加 FooterView 实现上拉加载

接下来完善UI,让用户知道确实在上拉加载的过程。用RecyclerView实现,使用 getItemType() 方法,就与 ListView 差不多了。不说多了,直接上完整代码(注释很清晰):

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    public  OnItemClickListener itemClickListener;
    private List<ShareBean> mData = null;
    private LayoutInflater mInflater;

    //1.加入布局状态标志-用来判断此时加载是普通Item还是footView
    //没有更多了
    public static final int NO_DATA_MORE = 0;
    //上拉加载更多
    public static final int PULLUP_LOAD_MORE = 1;
    //正在加载中
    public static final int LOADING_MORE = 2;

    private int load_more_status = 0;

    private static final int TYPE_ITEM = 0;  //普通Item View
    private static final int TYPE_FOOTER = 1;  //顶部FootView

    public RecyclerAdapter(Context context, List<ShareBean> mData) {
        this.mData = mData;
        this.mInflater = LayoutInflater.from(context);
    }

    //4.接着onCreateViewHolder(ViewGroup parent,int viewType)加载布局的时候根据viewType的类型来选择指定的布局创建,返回即可:
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//        将布局转化为View并传递给Recycler封装好的ViewHolder
        if (viewType == TYPE_ITEM) {
            View view = mInflater.inflate(R.layout.item_recyclerview, parent, false);
            return new ItemViewHolder(view);
        } else if (viewType == TYPE_FOOTER) {
            View foot_view = mInflater.inflate(R.layout.part_footer, parent, false);
            return new BottomViewHolder(foot_view);
        }
        return null;
    }

    //5.最后进行判断数据的时候(onBindViewHolder),判断holder的类型来进行判定数据即可.
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof ItemViewHolder) {
            ((ItemViewHolder) holder).mImageView.setImageResource(mData.get(position).getImage_id());
            ((ItemViewHolder) holder).mTextView.setText(mData.get(position).getName());
        } else if (holder instanceof BottomViewHolder) {
            BottomViewHolder footViewHolder = (BottomViewHolder) holder;
            switch (load_more_status) {
                case PULLUP_LOAD_MORE:
                    footViewHolder.mFooterTv.setText("上拉加载更多...");
                    footViewHolder.mProgressBar.setVisibility(View.VISIBLE);
                    break;
                case LOADING_MORE:
                    footViewHolder.mFooterTv.setText("正在加载中...");
                    footViewHolder.mProgressBar.setVisibility(View.VISIBLE);
                    break;
                case NO_DATA_MORE:
                    footViewHolder.mFooterTv.setText("----我是有底线的----");
                    footViewHolder.mProgressBar.setVisibility(View.GONE);
                    break;
            }
        }
    }

    //3.重写getItemViewType方法来判断返回加载的布局的类型
    @Override
    public int getItemViewType(int position) {
        if (position + 1 == getItemCount()) {
            return TYPE_FOOTER;
        } else {
            return TYPE_ITEM;
        }
    }

    //2.返回item数量
    @Override
    public int getItemCount() {
        return mData.size() + 1;
    }

    /**
     * //上拉加载更多
     * PULLUP_LOAD_MORE=0;
     * //正在加载中
     * LOADING_MORE=1;
     * //加载完成已经没有更多数据了
     * NO_MORE_DATA=2;
     *
     * @param status
     */
    public void changeMoreStatus(int status) {
        load_more_status = status;
        notifyDataSetChanged();
    }

    public  class ItemViewHolder extends RecyclerView.ViewHolder {

        CardView mCardView;
        CircleImageView mImageView;
        TextView mTextView;

        public ItemViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.textView);
            mCardView = (CardView) itemView.findViewById(R.id.cardView);
            mImageView = (CircleImageView) itemView.findViewById(R.id.image);
            mCardView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (itemClickListener != null) {
                        itemClickListener.onItemClick(view, getPosition());
                    }
                }
            });
        }
    }

     /**
     * 底部FootView布局
     */
    public class BottomViewHolder extends RecyclerView.ViewHolder {

        ProgressBar mProgressBar;
        TextView mFooterTv;

        public BottomViewHolder(View itemView) {
            super(itemView);
            mProgressBar = (ProgressBar) itemView.findViewById(R.id.progress_bar);
            mFooterTv = (TextView) itemView.findViewById(R.id.footer_tv);
        }
    }

    public void setOnItemClickListener(OnItemClickListener itemClickListener) {
        this.itemClickListener = itemClickListener;
    }

}

终于分析完了,感觉滑动需要继续探究,,,嗯嗯,先就这样吧。


参考文章及总结

以上说对基础知识的一些梳理,存在诸多不足,请多指正。

主要参考文章:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容