Android RecyclerView艺术般的控件使用完全解析
recyclerview出现已经有一段时间了,相信大家肯定不陌生了,大家可以通过导入support-v7对其进行使用。
据官方的介绍,该控件用于在有限的窗口中展示大量数据集,其实这样功能的控件我们并不陌生,例如:listview、gridview。
那么有了listview、gridview为什么还需要recyclerview这样的控件呢?整体上看recyclerview架构,提供了一种插拔式的体验,高度的解耦,异常的灵活,通过设置它提供的不同layoutmanager,itemdecoration , itemanimator实现令人瞠目的效果。
你想要控制其显示的方式,请通过布局管理器layoutmanager你想要控制item间的间隔(可绘制),请通过itemdecoration你想要控制item增删的动画,请通过itemanimator你想要控制点击、长按事件,请自己写(擦,这点尼玛。)
基本使用
鉴于我们对于listview的使用特别的熟悉,对比下recyclerview的使用代码:
mrecyclerview = findview(r.id.id_recyclerview); //设置布局管理器 mrecyclerview.setlayoutmanager(layout); //设置adapter mrecyclerview.setadapter(adapter) //设置item增加、移除动画 mrecyclerview.setitemanimator(new defaultitemanimator()); //添加分割线 mrecyclerview.additemdecoration(new divideritemdecoration( getactivity(), divideritemdecoration.horizontal_list));
ok,相比较于listview的代码,listview可能只需要去设置一个adapter就能正常使用了。而recyclerview基本需要上面一系列的步骤,那么为什么会添加这么多的步骤呢?
那么就必须解释下recyclerview的这个名字了,从它类名上看,recyclerview代表的意义是,我只管recycler view,也就是说recyclerview只管回收与复用view,其他的你可以自己去设置。可以看出其高度的解耦,给予你充分的定制*(所以你才可以轻松的通过这个控件实现listview,girdview,瀑布流等效果)。
just like listviewactivity
package com.zhy.sample.demo_recyclerview; import java.util.arraylist; import java.util.list; import android.os.bundle; import android.support.v7.app.actionbaractivity; import android.support.v7.widget.linearlayoutmanager; 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.textview; public class homeactivity extends actionbaractivity { private recyclerview mrecyclerview; private list<string> mdatas; private homeadapter madapter; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_single_recyclerview); initdata(); mrecyclerview = (recyclerview) findviewbyid(r.id.id_recyclerview); mrecyclerview.setlayoutmanager(new linearlayoutmanager(this)); mrecyclerview.setadapter(madapter = new homeadapter()); } protected void initdata() { mdatas = new arraylist<string>(); for (int i = 'a'; i < 'z'; i++) { mdatas.add("" + (char) i); } } class homeadapter extends recyclerview.adapter<homeadapter.myviewholder> { @override public myviewholder oncreateviewholder(viewgroup parent, int viewtype) { myviewholder holder = new myviewholder(layoutinflater.from( homeactivity.this).inflate(r.layout.item_home, parent, false)); return holder; } @override public void onbindviewholder(myviewholder holder, int position) { holder.tv.settext(mdatas.get(position)); } @override public int getitemcount() { return mdatas.size(); } class myviewholder extends viewholder { textview tv; public myviewholder(view view) { super(view); tv = (textview) view.findviewbyid(r.id.id_num); } } } }
activity的布局文件
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <android.support.v7.widget.recyclerview android:id="@+id/id_recyclerview" android:divider="#ffff0000" android:dividerheight="10dp" android:layout_width="match_parent" android:layout_height="match_parent" /> </relativelayout>
item的布局文件
<?xml version="1.0" encoding="utf-8"?> <framelayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:background="#44ff0000" android:layout_height="wrap_content" > <textview android:id="@+id/id_num" android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:text="1" /> </framelayout>
这么看起来用法与listview的代码基本一致哈~~
看下效果图:
看起来好丑,item间应该有个分割线,当你去找时,你会发现recyclerview并没有支持divider这样的属性。那么怎么办,你可以给item的布局去设置margin,当然了这种方式不够优雅,我们文章开始说了,我们可以*的去定制它,当然我们的分割线也是可以定制的。
itemdecoration
我们可以通过该方法添加分割线:
mrecyclerview.additemdecoration()
该方法的参数为recyclerview.itemdecoration,该类为抽象类,官方目前并没有提供默认的实现类(我觉得最好能提供几个)。
该类的源码:
public static abstract class itemdecoration { public void ondraw(canvas c, recyclerview parent, state state) { ondraw(c, parent); } public void ondrawover(canvas c, recyclerview parent, state state) { ondrawover(c, parent); } public void getitemoffsets(rect outrect, view view, recyclerview parent, state state) { getitemoffsets(outrect, ((layoutparams) view.getlayoutparams()).getviewlayoutposition(), parent); } @deprecated public void getitemoffsets(rect outrect, int itemposition, recyclerview parent) { outrect.set(0, 0, 0, 0); }
当我们调用mrecyclerview.additemdecoration()
方法添加decoration的时候,recyclerview在绘制的时候,去会绘制decorator,即调用该类的ondraw和ondrawover方法,
ondraw方法先于drawchildrenondrawover在drawchildren之后,一般我们选择复写其中一个即可。getitemoffsets 可以通过outrect.set()为每个item设置一定的偏移量,主要用于绘制decorator。
接下来我们看一个recyclerview.itemdecoration
的实现类,该类很好的实现了recyclerview添加分割线(当使用layoutmanager为linearlayoutmanager时)。
该类参考自:divideritemdecoration
package com.zhy.sample.demo_recyclerview; /* * copyright (c) 2014 the android open source project * * licensed under the apache license, version 2.0 (the "license"); * limitations under the license. */ 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.linearlayoutmanager; import android.support.v7.widget.recyclerview; import android.support.v7.widget.recyclerview.state; import android.util.log; import android.view.view; /** * this class is from the v7 samples of the android sdk. it's not by me! * <p/> * see the license above for details. */ public class divideritemdecoration 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 divideritemdecoration(context context, int orientation) { final typedarray a = context.obtainstyledattributes(attrs); mdivider = a.getdrawable(0); a.recycle(); setorientation(orientation); } public void setorientation(int orientation) { if (orientation != horizontal_list && orientation != vertical_list) { throw new illegalargumentexception("invalid orientation"); } morientation = orientation; } @override public void ondraw(canvas c, recyclerview parent) { log.v("recyclerview - itemdecoration", "ondraw()"); if (morientation == vertical_list) { drawvertical(c, parent); } else { drawhorizontal(c, parent); } } public 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); android.support.v7.widget.recyclerview v = new android.support.v7.widget.recyclerview(parent.getcontext()); final recyclerview.layoutparams params = (recyclerview.layoutparams) child .getlayoutparams(); final int top = child.getbottom() + params.bottommargin; final int bottom = top + mdivider.getintrinsicheight(); mdivider.setbounds(left, top, right, bottom); mdivider.draw(c); } } public 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; final int right = left + mdivider.getintrinsicheight(); mdivider.setbounds(left, top, right, bottom); mdivider.draw(c); } } @override public void getitemoffsets(rect outrect, int itemposition, recyclerview parent) { if (morientation == vertical_list) { outrect.set(0, 0, 0, mdivider.getintrinsicheight()); } else { outrect.set(0, 0, mdivider.getintrinsicwidth(), 0); } } }
该实现类可以看到通过读取系统主题中的 android.r.attr.listdivider
作为item间的分割线,并且支持横向和纵向。如果你不清楚它是怎么做到的读取系统的属性用于自身,请参考我的另一篇博文:android 深入理解android中的自定义属性
获取到listdivider以后,该属性的值是个drawable,在getitemoffsets
中,outrect去设置了绘制的范围。ondraw中实现了真正的绘制。
我们在原来的代码中添加一句:
mrecyclerview.additemdecoration(new divideritemdecoration(this, divideritemdecoration.vertical_list));
ok,现在再运行,就可以看到分割线的效果了。
该分割线是系统默认的,你可以在theme.xml中找到该属性的使用情况。那么,使用系统的listdivider有什么好处呢?就是方便我们去随意的改变,该属性我们可以直接声明在:
<!-- application theme. --> <style name="apptheme" parent="appbasetheme"> <item name="android:listdivider">@drawable/divider_bg</item> </style>
然后自己写个drawable即可,下面我们换一种分隔符:
<?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:height="4dp"/> </shape>
现在的样子是:
当然了,你可以根据自己的需求,去随意的绘制,反正是画出来的,随便玩~~
ok,看到这,你可能觉得,这玩意真尼玛麻烦,完全不能比拟的心爱的listview。那么继续看。
layoutmanager
好了,上面实现了类似listview样子的demo,通过使用其默认的linearlayoutmanager。
recyclerview.layoutmanager吧,这是一个抽象类,好在系统提供了3个实现类:
linearlayoutmanager 现行管理器,支持横向、纵向。gridlayoutmanager 网格布局管理器staggeredgridlayoutmanager 瀑布就式布局管理器
上面我们已经初步体验了下linearlayoutmanager,接下来看gridlayoutmanager。
gridlayoutmanager
我们尝试去实现类似gridview,秒秒钟的事情:
//mrecyclerview.setlayoutmanager(new linearlayoutmanager(this)); mrecyclerview.setlayoutmanager(new gridlayoutmanager(this,4));
只需要修改layoutmanager即可,还是很nice的。
当然了,改为gridlayoutmanager以后,对于分割线,前面的divideritemdecoration就不适用了,主要是因为它在绘制的时候,比如水平线,针对每个child的取值为:
final int left = parent.getpaddingleft(); final int right = parent.getwidth() - parent.getpaddingright();
因为每个item一行,这样是没问题的。而gridlayoutmanager时,一行有多个childitem,这样就多次绘制了,并且gridlayoutmanager时,item如果为最后一列(则右边无间隔线)或者为最后一行(底部无分割线)。
针对上述,我们编写了dividergriditemdecoration
。
package com.zhy.sample.demo_recyclerview; 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.support.v7.widget.recyclerview.layoutmanager; import android.support.v7.widget.recyclerview.state; import android.support.v7.widget.staggeredgridlayoutmanager; import android.view.view; /** * * @author zhy * */ public class dividergriditemdecoration extends recyclerview.itemdecoration { private static final int[] attrs = new int[] { android.r.attr.listdivider }; private drawable mdivider; public dividergriditemdecoration(context context) { final typedarray a = context.obtainstyledattributes(attrs); mdivider = a.getdrawable(0); a.recycle(); } @override public void ondraw(canvas c, recyclerview parent, state state) { drawhorizontal(c, parent); drawvertical(c, parent); } private int getspancount(recyclerview parent) { // 列数 int spancount = -1; layoutmanager layoutmanager = parent.getlayoutmanager(); if (layoutmanager instanceof gridlayoutmanager) { spancount = ((gridlayoutmanager) layoutmanager).getspancount(); } else if (layoutmanager instanceof staggeredgridlayoutmanager) { spancount = ((staggeredgridlayoutmanager) layoutmanager) .getspancount(); } return spancount; } public void drawhorizontal(canvas c, recyclerview parent) { 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.getleft() - params.leftmargin; final int right = child.getright() + params.rightmargin + mdivider.getintrinsicwidth(); final int top = child.getbottom() + params.bottommargin; final int bottom = top + mdivider.getintrinsicheight(); mdivider.setbounds(left, top, right, bottom); mdivider.draw(c); } } public void drawvertical(canvas c, recyclerview parent) { 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.gettop() - params.topmargin; final int bottom = child.getbottom() + params.bottommargin; final int left = child.getright() + params.rightmargin; final int right = left + mdivider.getintrinsicwidth(); mdivider.setbounds(left, top, right, bottom); mdivider.draw(c); } } private boolean islastcolum(recyclerview parent, int pos, int spancount, int childcount) { layoutmanager layoutmanager = parent.getlayoutmanager(); if (layoutmanager instanceof gridlayoutmanager) { if ((pos + 1) % spancount == 0)// 如果是最后一列,则不需要绘制右边 { return true; } } else if (layoutmanager instanceof staggeredgridlayoutmanager) { int orientation = ((staggeredgridlayoutmanager) layoutmanager) .getorientation(); if (orientation == staggeredgridlayoutmanager.vertical) { if ((pos + 1) % spancount == 0)// 如果是最后一列,则不需要绘制右边 { return true; } } else { childcount = childcount - childcount % spancount; if (pos >= childcount)// 如果是最后一列,则不需要绘制右边 return true; } } return false; } private boolean islastraw(recyclerview parent, int pos, int spancount, int childcount) { layoutmanager layoutmanager = parent.getlayoutmanager(); if (layoutmanager instanceof gridlayoutmanager) { childcount = childcount - childcount % spancount; if (pos >= childcount)// 如果是最后一行,则不需要绘制底部 return true; } else if (layoutmanager instanceof staggeredgridlayoutmanager) { int orientation = ((staggeredgridlayoutmanager) layoutmanager) .getorientation(); // staggeredgridlayoutmanager 且纵向滚动 if (orientation == staggeredgridlayoutmanager.vertical) { childcount = childcount - childcount % spancount; // 如果是最后一行,则不需要绘制底部 if (pos >= childcount) return true; } else // staggeredgridlayoutmanager 且横向滚动 { // 如果是最后一行,则不需要绘制底部 if ((pos + 1) % spancount == 0) { return true; } } } return false; } @override public void getitemoffsets(rect outrect, int itemposition, recyclerview parent) { int spancount = getspancount(parent); int childcount = parent.getadapter().getitemcount(); if (islastraw(parent, itemposition, spancount, childcount))// 如果是最后一行,则不需要绘制底部 { outrect.set(0, 0, mdivider.getintrinsicwidth(), 0); } else if (islastcolum(parent, itemposition, spancount, childcount))// 如果是最后一列,则不需要绘制右边 { outrect.set(0, 0, 0, mdivider.getintrinsicheight()); } else { outrect.set(0, 0, mdivider.getintrinsicwidth(), mdivider.getintrinsicheight()); } } }
主要在getitemoffsets
方法中,去判断如果是最后一行,则不需要绘制底部;如果是最后一列,则不需要绘制右边,整个判断也考虑到了staggeredgridlayoutmanager
的横向和纵向,所以稍稍有些复杂。最重要还是去理解,如何绘制什么的不重要。一般如果仅仅是希望有空隙,还是去设置item的margin方便。
最后的效果是:
ok,看到这,你可能还觉得recyclerview不够强大?
但是如果我们有这么个需求,纵屏的时候显示为listview,横屏的时候显示两列的gridview,我们recyclerview可以轻松搞定,而如果使用listview去实现还是需要点功夫的~~~
当然了,这只是皮毛,下面让你心服口服。
staggeredgridlayoutmanager
瀑布流式的布局,其实他可以实现gridlayoutmanager
一样的功能,仅仅按照下列代码:
// mrecyclerview.setlayoutmanager(new gridlayoutmanager(this,4)); mrecyclerview.setlayoutmanager(new staggeredgridlayoutmanager(4, staggeredgridlayoutmanager.vertical));
这两种写法显示的效果是一致的,但是注意staggeredgridlayoutmanager构造的第二个参数传一个orientation,如果传入的是staggeredgridlayoutmanager.vertical
代表有多少列;那么传入的如果是staggeredgridlayoutmanager.horizontal
就代表有多少行,比如本例如果改为:
mrecyclerview.setlayoutmanager(new staggeredgridlayoutmanager(4, staggeredgridlayoutmanager.horizontal));
那么效果为:
可以看到,固定为4行,变成了左右滑动。有一点需要注意,如果是横向的时候,item的宽度需要注意去设置,毕竟横向的宽度没有约束了,应为控件可以横向滚动了。
如果你需要一样横向滚动的gridview,那么恭喜你。
ok,接下来准备看大招,如果让你去实现个瀑布流,最起码不是那么随意就可以实现的吧?但是,如果使用recyclerview,分分钟的事。
那么如何实现?其实你什么都不用做,只要使用staggeredgridlayoutmanager
我们就已经实现了,只是上面的item布局我们使用了固定的高度,下面我们仅仅在适配器的onbindviewholder
方法中为我们的item设置个随机的高度(代码就不贴了,最后会给出源码下载地址),看看效果图:
是不是棒棒哒,通过recyclerview去实现listview、gridview、瀑布流的效果基本上没有什么区别,而且可以仅仅通过设置不同的layoutmanager即可实现。
还有更nice的地方,就在于item增加、删除的动画也是可配置的。接下来看一下itemanimator。
itemanimator
itemanimator也是一个抽象类,好在系统为我们提供了一种默认的实现类,期待系统多
添加些默认的实现。
借助默认的实现,当item添加和移除的时候,添加动画效果很简单:
// 设置item动画 mrecyclerview.setitemanimator(new defaultitemanimator());
系统为我们提供了一个默认的实现,我们为我们的瀑布流添加以上一行代码,效果为:
如果是gridlayoutmanager呢?动画效果为:
注意,这里更新数据集不是用adapter.notifydatasetchanged()
而是 notifyiteminserted(position)
与notifyitemremoved(position)
否则没有动画效果。
上述为adapter中添加了两个方法:
public void adddata(int position) { mdatas.add(position, "insert one"); notifyiteminserted(position); } public void removedata(int position) { mdatas.remove(position); notifyitemremoved(position); }
activity中点击menuitem触发:
@override public boolean oncreateoptionsmenu(menu menu) { getmenuinflater().inflate(r.menu.main, menu); return super.oncreateoptionsmenu(menu); } @override public boolean onoptionsitemselected(menuitem item) { switch (item.getitemid()) { case r.id.id_action_add: madapter.adddata(1); break; case r.id.id_action_delete: madapter.removedata(1); break; } return true; }
好了,到这我对这个控件已经不是一般的喜欢了~~~
当然了只提供了一种动画,那么我们肯定可以去自定义各种nice的动画效果。
高兴的是,github上已经有很多类似的项目了,这里我们直接引用下:recyclerviewitemanimators,大家自己下载查看。
提供了slideinoutleftitemanimator
,slideinoutrightitemanimator
,slideinouttopitemanimator
,slideinoutbottomitemanimator
等动画效果。
click and longclick
不过一个挺郁闷的地方就是,系统没有提供clicklistener和longclicklistener。
不过我们也可以自己去添加,只是会多了些代码而已。
实现的方式比较多,你可以通过mrecyclerview.addonitemtouchlistener去监听然后去判断手势,
当然你也可以通过adapter中自己去提供回调,这里我们选择后者,前者的方式,大家有兴趣自己去实现。
那么代码也比较简单:
class homeadapter extends recyclerview.adapter<homeadapter.myviewholder> { //... public interface onitemclicklitener { void onitemclick(view view, int position); void onitemlongclick(view view , int position); } private onitemclicklitener monitemclicklitener; public void setonitemclicklitener(onitemclicklitener monitemclicklitener) { this.monitemclicklitener = monitemclicklitener; } @override public void onbindviewholder(final myviewholder holder, final int position) { holder.tv.settext(mdatas.get(position)); // 如果设置了回调,则设置点击事件 if (monitemclicklitener != null) { holder.itemview.setonclicklistener(new onclicklistener() { @override public void onclick(view v) { int pos = holder.getlayoutposition(); monitemclicklitener.onitemclick(holder.itemview, pos); } }); holder.itemview.setonlongclicklistener(new onlongclicklistener() { @override public boolean onlongclick(view v) { int pos = holder.getlayoutposition(); monitemclicklitener.onitemlongclick(holder.itemview, pos); return false; } }); } } //... }
adapter中自己定义了个接口,然后在onbindviewholder中去为holder.itemview去设置相应
的监听最后回调我们设置的监听。
最后别忘了给item添加一个drawable:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" > <item android:state_pressed="true" android:drawable="@color/color_item_press"></item> <item android:drawable="@color/color_item_normal"></item> </selector>
activity中去设置监听:
madapter.setonitemclicklitener(new onitemclicklitener() { @override public void onitemclick(view view, int position) { toast.maketext(homeactivity.this, position + " click", toast.length_short).show(); } @override public void onitemlongclick(view view, int position) { toast.maketext(homeactivity.this, position + " long click", toast.length_short).show(); madapter.removedata(position); } });
测试效果:
ok,到此我们基本介绍了recylerview常见用法,包含了:
系统提供了几种layoutmanager的使用;如何通过自定义itemdecoration去设置分割线,或者一些你想作为分隔的drawable,注意这里巧妙的使用了系统的listdivider属性,你可以尝试添加使用divider和dividerheight属性。如何使用itemanimator为recylerview去添加item移除、添加的动画效果。介绍了如何添加itemclicklistener与itemlongclicklistener。
可以看到recyclerview可以实现:
listview的功能gridview的功能横向listview的功能,参考android 自定义recyclerview 实现真正的gallery效果横向scrollview的功能瀑布流效果便于添加item增加和移除动画
整个体验下来,感觉这种插拔式的设计太棒了,如果系统再能提供一些常用的分隔符,多添加些动画效果就更好了。
通过简单改变下layoutmanager,就可以产生不同的效果,那么我们可以根据手机屏幕的宽度去动态设置layoutmanager,屏幕宽度一般的,显示为listview;宽度稍大的显示两列的gridview或者瀑布流(或者横纵屏幕切换时变化,有点意思~);显示的列数和宽度成正比。甚至某些特殊屏幕,让其横向滑动~~再选择一个nice的动画效果,相信这种插件式的编码体验一定会让你迅速爱上recyclerview。