深入解析Android中的RecyclerView组件
前些日子,组里为了在目前的android程序里实现基于listview子项的动画效果,希望将最新的recyclerview引入到程序中,于是我便花了一些时间研究了一下recyclerview的基本情况。本文算是对这些日子里了解的内容做一些汇总。
在网上关于recyclerview的基本使用方式已经有了比较详细介绍,而且其设计结构也类似于listview,所以本文将不重点介绍如何使用,在文末的引用中都可以相关内容。这里主要是介绍recyclerview的基本功能、设计理念,以及系统提供api的情况。
什么是recyclerview
recyclerview是在android l(也就是后来的lollipop)中新加入的一种viewgroup。但因为它使以support-v7库的形式加入到android系统中,所以不仅仅是lollipop版本以后的android系统可以使用,只要系开发项目中引入这个库就在任意api级别中使用。不过查阅其文档,可以感受到recyclerview是如此的强烈的“还在完善中”的感觉,因为对它的介绍只有短短的一句话:
a flexible view for providing a limited window into a large data set.
单看这句话,其实已经被经常使用的listview和gridview基本也是满足这句描述的。而且从单呈现效果来看recyclerview和listview、gridview并没有大多的差别。
不过它的“flexible”并不单单指它可以在listview和gridview之间随意互相切换,更在于它可以创造出更多的复杂的可滚动的视图,比如水平方向的listview,或者是web上很流行的瀑布流式布局(masonry)。只是大部分布局系统都没有提供,必须由开发者自己实现。
所以recyclerview的“flexible”:什么都可以做,但什么都要自己做。
recyclerview和listview的主要区别
只要用android提供的lieanerlayoutmanager并配以vertical模式,recyclerview就可以完美达到listview的基本效果。两者的设计结构也都是数据(dataset)与视图(view)分离,然后通过适配器(adapter)来连接的方式。
但recyclerview相对listview来说有以下几大提升:
强制使用viewholder
在listview性能优化方面,android就推荐使用viewholder来减少findviewbyid()的使用以提高效率。不过对于listview上的viewholder,android只是建议而非强制使用。不过因为使用viewholder模式太有意义了,所以在recyclerview中viewholder就变成了必须使用的模式,adapter要求返回的也从普通的view变成了viewholder。不过如果实现时没有自定义的一些view实际变量,viewholder也依然失去其意义。
没有onitemclicklistener
listview从它的父类adapterview直接继承了对子项目点击的响应,开发者可以定义自己的onitemclicklistner来接受点击事件。但这个设计也造成了一些问题,比如子项内部视图如果设置了onclicklistener,那么子项目视图本身并不会知道,从而可能会导致视图点击状态没有同步等问题。reyclerview没有提供简便的响应子项目被点击的监听器,虽然它有一个onitemtouchlistener,但在这个接口方法中没有任何关于那个子项目被点击的信息,该接口只是帮助开发者截获触摸事件,对于如何处理,检测被触摸目标对象都留给了开发者去完成
视图与布局分离
listview做到了数据和视图的分离,recyclerview在视图和布局之间再进一步分离,于是便有了layoutmanager。recyclerview负责管理视图的重复利用,然后将布局方式全权交给了layoutmanager,通过配置或者切换layoutmanager就可以获得不同的布局效果。不像listview被限制在垂直滚动布局。同时recyclerview还提供了itemdecoration,在已有的子视图基础上还可以添加额外的视图。比如做一条分割线,在listview就需要额外占用一个viewtype来提供视图,现在则不需要在adapter中加入这些与实际逻辑业务无关的辅助内容。
支持子项目层次的动画效果
listview也可以支持子项目层次的动画效果,在android developers的devbytes频道里有很多关于这方面的介绍,不过在看过其实现之后就会发现其解决方案是多么的丑陋而冗长。很多时候都是在计算和分析子视图的位置状态。recyclerview则带来了非常简洁的itemanimator接口。当adapter中的数据发生“增删改移”变化,通过调用adapter相应的方法就可以激活动画的产生。当然开发者还需要自己实现具体的itemanimator对象来完成所需的动画效果,但是其清晰的结构和接口已比listview有极大的进步。
recyclerview的现状
上边提到recyclerview的套件已经加入了豪华support library v7,而且是以单独的库放入,所以只要在android studio项目的gradle编译文件的dependencies下加入下边的这句就可以开始使用recyclerview了:
compile 'com.android.support:recyclerview-v7:21.0.+'
不过看过库中提供的那些自带对象实现,体现基本操作流程,就会发现recyclerview能做的看起来很多,但是已经做到的实在太少。
recyclerview.adapter
recyclerview提供了一个抽象adapter类,然后就没有了。没有任何可以直接使用的子类,像listadapter那样的arrayadapter、simplecursoradapter现成的类都没有。一切都留给开发者自己去实现定义。
仔细想想这也很挺正常,相信应该很少在实际产品中有使用arrayadapter的,因为大部分列表都不会是简单的一行文字。对于cursoradapter使用也往往会实现不同的继承类来提供子视图。再者recyclerview的adapter和listadapter在理念上还是一样的,所以想实现个recyclerviewcursoradapter,直接从cursoradapter取材便可。
recyclerview的adapter相对listadapter在接口上有几处变化也值得注意。
首先其将getview()方法分拆成了createviewholder()和bindviewholder()两个。不过这个没有什么好紧张的,在cursoradapter里就已经有见到过这个更加合理的设计。另外返回对象也从view变成了viewholder只需提一下。
最关键的注意点在于createviewholder(viewgroup parent, int viewtype)第二参数虽然是整形,但是它并不是以往的当前子项的位置(position),而是调用getitemviewtype()获得的的子项的类别。似乎在创建viewholder时,recyclerview故意在隐藏子项的详细信息,希望开发者完全只依赖其类别来创建相应的view以及viewholder。
其次,recyclerview的adapter除了和listadapter一样有notifydatasetchanged()方法外,还有一堆会触发动画效果的通知数据改变的方法:
- final void notifyitemchanged(int position);
- final void notifyiteminserted(int position);
- final void notifyitemmoved(int fromposition, int toposition);
- final void notifyitemrangechanged(int positionstart, int itemcount);
- final void notifyitemrangeinserted(int positionstart, int itemcount);
- final void notifyitemrangeremoved(int positionstart, int itemcount);
- final void notifyitemremoved(int position);
调用这些方法就会激发itemanimator上对应的用于产生动画的方法。
recyclerview.viewholder
在listview的年代里,其实已经在使用viewholder,只是那时的方法看起来比较讨巧,要隐藏在view的tag里。现在recyclerview强制使用viewholder,并且viewholder除了有对子视图的引用,还有诸如itemviewtype和position这些信息。
recyclerview.layoutmanager
layoutmanager相对于listview来说是一个新部分,通过继承该类来实现自定义的布局方式,而不像listview只有固定的布局方式。support库提供了两个现成的子类:linearlayoutmanager和staggeredgridlayoutmanager。前者可以获得和listview一样的布局,还可以是水平方向的;后者则提供了形如gridview的布局。所以应用程序中的基本日常所以都可以被满足。
如果需要实现自定义的layoutmanager,就比较麻烦了,需要理解recycle、scrap、dirty这些关于子项目视图状态的概念。本人还没有尝试过创建一个自定义的layoutmanager,但在文末的引用文档中有部分介绍实现方式的。
recyclerview.itemanimator
通过实现继承实现itemanimator,然后创建对象设置到recyclerview上就可以得到基于子项目的动画效果。不过如何正确合理地创建一个itemanimator子类,却没有详细的描述指南。
窥探库中提供的唯一一个可用的子类defaultitemanimator,可以看出它的动画效果是简单的alpha渐变。同时也会发现其实现是如此的复杂,有很多对于动画步骤的操作,还得注意动画在中途被打断的处理,在结束时也要重置视图状态以便重用。这也反过来说明itemanimator基本没有提供任何关于如何实现和管理动画的信息。另一方面因为defaultitemanimator的实现过于具体,因此它并不是合适作为自定义itemanimator的父类。
相信当recyclerview越来越多的被应用到程序中时,更多关于这方面的合理设计会被提出来。目前在github上也有不少参考了defaultitemanimator的实现方式,比如这个,还有这个。
关于itemanimator的使用,有几点值得提醒的是:如果没有提供itemanimator,recyclerview默认会创建一个defaultitemanimator用于动画,所以不需要显示地设置defaultitemanimator对象到recyclerview上;添加(add)和删除(remove)是默认起效的,但是修改(change)的效果需要调用setsupportschangeanimations(boolean)来指定是否启用,其默认是没有修改的动画。
总体而言,recyclerview的功能非常强大,其结构设计也十分开放,这也造成它的上手使用相对比较困难。随着越来越多的人开始尝试使用这个部件,也会有越来越深刻的理解和设计实现。另外,阅读recyclerview的源码可以帮助对其设计思想的了解,在以后设计其它的复用视图时可以有更好的参照。
demo
说了这么多,我们来看一个recyclerview实现图片文字按钮的混排的demo:
首先还是看我的工程结构吧。
首先还是贴出我的main_acitivy.xml
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <!-- a recyclerview with some commonly used attributes --> <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>
其他几个xml就不用贴了,很简单的放了写textview,imgeview之类。
然后我们就来看看代码,首先是bean里面的代码。
package com.androidl.bob; public class bean { public static final int y_type = 0; //view类型0 public static final int x_type = 1; //view类型2 public static final int z_type = 2;//view 类型3 private int type; private string text; public bean(int type, string text) { super(); this.type = type; this.text = text; } public int gettype() { return type; } public void settype(int type) { this.type = type; } public string gettext() { return text; } public void settext(string text) { this.text = text; } }
然后是adapter里面的代码:
package com.androidl.bob; import java.util.list; import com.example.androidl.r; import android.support.v7.widget.recyclerview; import android.support.v7.widget.recyclerview.viewholder; import android.view.layoutinflater; import android.view.view; import android.view.viewgroup; import android.widget.button; import android.widget.imagebutton; import android.widget.imageview; import android.widget.textview; import android.widget.toast; /** * date : 2014/7/15 * * @author edsheng * */ public class recycleadapter extends recyclerview.adapter<viewholder> { private list<bean> beans; public recycleadapter(list<bean> beans) { super(); this.beans = beans; } /** * 内部textholer * * @author edsheng * */ public class textholer extends recyclerview.viewholder { public textview textview; public textholer(view textview) { super(textview); this.textview = (textview) textview.findviewbyid(r.id.mytext); } } /** * iamgeholder * * @author edsheng * */ public class imageholer extends recyclerview.viewholder { public imageview imageview; public imageholer(view textview) { super(textview); this.imageview = (imageview) textview.findviewbyid(r.id.myiamge); } } /** * 按钮的holder * * @author edsheng * */ public class buttonholder extends recyclerview.viewholder { public button button; public buttonholder(view textview) { super(textview); this.button = (button) textview.findviewbyid(r.id.mybutton); } } @override public int getitemcount() { // todo auto-generated method stub return beans.size(); } /** * 获取消息的类型 */ @override public int getitemviewtype(int position) { // todo auto-generated method stub return beans.get(position).gettype(); } /** * 创建viewholder */ @override public viewholder oncreateviewholder(viewgroup parent, int viewtype) { // todo auto-generated method stub view v = null; viewholder holer = null; switch (viewtype) { case bean.x_type: v = layoutinflater.from(parent.getcontext()).inflate( r.layout.recylce_item_x, null); holer = new textholer(v); break; case bean.y_type: v = layoutinflater.from(parent.getcontext()).inflate( r.layout.recylce_item_y, null); holer = new buttonholder(v); break; case bean.z_type: v = layoutinflater.from(parent.getcontext()).inflate( r.layout.recylce_item_z, null); holer = new imageholer(v); break; } return holer; } /** * 绑定viewholder */ @override public void onbindviewholder(viewholder holder, int position) { // todo auto-generated method stub switch (getitemviewtype(position)) { case bean.x_type: textholer textholer = (textholer) holder; textholer.textview.settext(beans.get(position).gettext()); break; case bean.y_type: buttonholder buttonholder = (buttonholder) holder; buttonholder.button.settext(beans.get(position).gettext()); break; case bean.z_type: imageholer imageholer = (imageholer) holder; // imageholer.imageview.setimageresource(android.r.drawable.checkbox_on_background); break; } } }
最后是activity的代码。
package com.androidl.bob; import java.util.arraylist; import java.util.list; import android.app.activity; import android.os.bundle; import android.support.v7.widget.linearlayoutmanager; import android.support.v7.widget.recyclerview; import com.example.androidl.r; public class mainactivity extends activity { @override protected void oncreate(bundle savedinstancestate) { // todo auto-generated method stub super.oncreate(savedinstancestate); setcontentview(r.layout.main_activity); recyclerview mrecyclerview = (recyclerview) findviewbyid(r.id.my_recycler_view); // // improve performance if you know that changes in content // // do not change the size of the recyclerview // mrecyclerview.sethasfixedsize(true); //创建布局管理器 linearlayoutmanager mlayoutmanager = new linearlayoutmanager(this); mlayoutmanager.setorientation(linearlayoutmanager.vertical); mrecyclerview.setlayoutmanager(mlayoutmanager); //初始化数据 list<bean> mydataset = new arraylist<bean>(); mydataset.add(new bean(bean.z_type, "图片")); mydataset.add(new bean(bean.x_type, "文字")); mydataset.add(new bean(bean.y_type, "按钮")); mydataset.add(new bean(bean.z_type, "图片")); mydataset.add(new bean(bean.x_type, "shit")); mydataset.add(new bean(bean.x_type, "我擦")); mydataset.add(new bean(bean.z_type, "图片")); mydataset.add(new bean(bean.y_type, "按钮")); mydataset.add(new bean(bean.y_type, "按钮")); mydataset.add(new bean(bean.x_type, "文字")); //创建adapter recycleadapter madapter = new recycleadapter(mydataset); mrecyclerview.setadapter(madapter); } }
推荐阅读
-
Android开发中RecyclerView组件使用的一些进阶技讲解
-
深入解析Android中的RecyclerView组件
-
Android中ViewPager带来的滑动卡顿问题解决要点解析
-
Android中的RecyclerView新组件初步上手指南
-
Android RecyclerView艺术般的控件使用完全解析
-
深入理解Android中的建造者模式
-
Android中RecyclerView布局代替GridView实现类似支付宝的界面
-
Android中的RecyclerView新组件初步上手指南
-
Android中ViewPager带来的滑动卡顿问题解决要点解析
-
深入解析Android中的RecyclerView组件