一、RecycleView 简介
(1)RecycleView是什么
RecyclerView 出现已经有一段时间了,相信大家肯定不陌生了,不过这里还是简单介绍下。
RecylerView是Android L版本中新添加的一个用来取代ListView的SDK,是support-v7包中的新组件,是一个强大的滑动组件,与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字recylerview即“回收view”也可以看得出来,毫无疑问它的灵活性与可替代性比listview更好。
看到这也许有人会问:除此之外还有什么优点呢?我们为什么一定要用RecylerView呢?下面我们详细介绍下RecylerView的诸多优点。
(2)RecyclerView的优点是什么?
根据官方的介绍RecylerView是ListView的升级版,既然如此那么RecylerView必然有它的优点,现就RecylerView相对于ListView的优点罗列如下:
1、RecylerView封装了viewholder的回收复用。
也就是说 RecylerView标准化了ViewHolder,编写Adapter面向的是ViewHolder而不再是View了,复用的逻辑被封装了,写起来更加简单。
2、提供了一种插拔式的体验,高度的解耦,异常的灵活。
针对一个Item的显示,RecylerView专门抽取出了相应的类来控制Item的显示,使其的扩展性非常强。
比如:你想控制横向或者纵向滑动列表效果可以通过LinearLayoutManager这个类来进行控制,与GridView效果对应的是GridLayoutManager,与瀑布流对应的还有StaggeredGridLayoutManager等,也就是说RecylerView不再拘泥于ListView的线性展示方式,它也可以实现ListView、GridView等多种效果。再比如你想控制Item的分隔线,可以通过继承RecylerView的ItemDecoration这个类,然后针对自己的业务需求去写代码。
总之,通过简单改变下LayoutManager,就可以产生很多不同的效果,那么我们可以根据手机屏幕的宽度去动态设置LayoutManager,屏幕宽度一般的,显示为ListView,宽度稍大的显示两列的GridView或者瀑布流,或者横纵屏幕切换时变化,显示的列数和宽度成正比。甚至某些特殊屏幕,让其横向滑动,再选择一个好的动画效果,可以达到前所未有的用户体验。
3、可以控制Item增删的动画。
可以通过ItemAnimator这个类进行控制,当然针对增删的动画,RecylerView也有其自己默认的实现。
(3)RecyclerView项目结构
- Adapter:使用RecyclerView之前,你需要一个继承自RecyclerView.Adapter的适配器,作用是将数据与每一个item的界面进行绑定。
-
LayoutManager:用来控制其显示的方式,比如:确定每一个item如何进行排列摆放,何时展示和隐藏。回收或重用一个View的时候,LayoutManager会向适配器请求新的数据来替换旧的数据,这种机制避免了创建过多的View和频繁的调用findViewById方法(与ListView原理类似)。
目前SDK中提供了三种自带的LayoutManager:- LinearLayoutManager(线性布局管理器)
- GridLayoutManager(网格布局管理器)
- StaggeredGridLayoutManager(瀑布流布局管理器)
二、RecycleView 的简单使用
说了这么多,可能大家最关心的就是RecylerView应该怎么用,我们先来讨论讨论RecylerView的用法的理论知识,然后首先结合一个实例来展示是一个最简单的使用方法,然后介绍更多RecyclerView的用法。
1、添加依赖
在AS的build.gradle中添加依赖,然后同步一下就可以引入依赖包:
dependencies {
...
compile 'com.android.support:appcompat-v7:24.0.0'
}
2、编写代码
添加完依赖之后,就开始写代码了,与ListView用法类似,也是先在xml布局文件中创建一个RecyclerView的布局:
<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"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/my_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
</RelativeLayout>
创建完布局之后在MainActivity中获取这个RecyclerView,并声明LayoutManager与Adapter,代码如下:
mRecyclerView = (RecyclerView)findViewById(R.id.my_recycler_view);
// 创建默认的线性LayoutManager
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
// 如果可以确定每个item的高度是固定的,设置这个选项可以提高性能
mRecyclerView.setHasFixedSize(true);
// 创建并设置Adapter
mAdapter = newMyAdapter(getDummyDatas());
mRecyclerView.setAdapter(mAdapter);
可以看到对RecylerView的设置过程,比ListView要复杂一些,因为ListView可能只需要去设置一个adapter就能正常使用了,这也是RecylerView高度解耦的表现,虽然代码抒写上有点复杂,但它的扩展性是极高的。也就是说RecyclerView只管回收与复用View,其他的你可以自己去设置。这就给予了我们充分定制的自由,所以我们才可以轻松的通过这个控件实现ListView、GirdView、瀑布流等效果。
接下来就是Adapter的创建:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
public String[] datas = null;
public MyAdapter(String[] datas) {
this.datas = datas;
}
// 创建新View,被LayoutManager所调用
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item,viewGroup,false);
ViewHolder vh = new ViewHolder(view);
return vh;
}
// 将数据与界面进行绑定的操作
@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
viewHolder.mTextView.setText(datas[position]);
}
// 获取数据的数量
@Override
public int getItemCount() {
return datas.length;
}
// 自定义的ViewHolder,持有每个Item的的所有界面元素
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView mTextView;
public ViewHolder(View view){
super(view);
mTextView = (TextView) view.findViewById(R.id.text);
}
}
}
在了解了RecyclerView的一些控制之后,我们来看看它的Adapter的写法,RecyclerView的Adapter与ListView的Adapter还是有点区别的,RecyclerView.Adapter需要实现3个方法:
-
onCreateViewHolder()
这个方法主要生成为每个Item inflater出一个View,但是该方法返回的是一个ViewHolder。该方法把View直接封装在ViewHolder中,然后我们面向的是ViewHolder这个实例,当然这个ViewHolder需要我们自己去编写。直接省去了当初的convertView.setTag(holder)和convertView.getTag()这些繁琐的步骤。 -
onBindViewHolder()
这个方法主要用于适配渲染数据到View中。方法提供给你了一个viewHolder,而不是原来的convertView。 -
getItemCount()
这个方法就类似于BaseAdapter的getCount方法了,即总共有多少个条目。
可以看到RecyclerView标准化了ViewHolder,编写 Adapter面向的是ViewHoder而不在是View了,复用的逻辑被封装了,写起来更加简单。其实它的写法与BaseAdapter的写法是差不多的,大家可以对比下它与getView方法写法的区别,在onCreateViewHolder方法中初始化了一个View,然后返回一个ViewHolder,这个返回的ViewHolder类似于之前在getView中的convertView.getTag(),然后在onBindViewHolder方法中去给这个ViewHolder中的控件填充值。其实它的原理跟getView是差不多的,只是做了封装,我们写起来比较简洁。
编译运行效果如下:
三、RecycleView 添加点击和长击事件
上面我们讲了如何使用RecyclerView的Adpater,接下来我们为添加点击监听的功能。
熟悉ListView的小伙伴都知道ListView在使用的时候是可以直接添加点击事件的,就是说ListView给我们提供了一个onItemClickListener监听器,这样当我们点击Item的时候,会回调相关的方法,以便我们方便处理Item点击事件。但是对于RecyclerView来讲,非常可惜的是,该控件没有给我们提供ClickListener和LongClickListener这样的内置监听器方法,所以我们需要自己动手进行改造实现,只不过是会多了些代码而已。
实现的方式比较多,我们可以通过mRecyclerView.addOnItemTouchListener去监听然后去判断手势,
也可以通过adapter中自己去提供回调,这里我们选择最常用的后者来讲解,前者的方式大家有时间有兴趣自己去实现。
(1)首先我们在Adapter中创建一个实现点击接口:
其中view是点击的Item,data是我们的数据,因为我们想知道我点击的区域部分的数据是什么,以便我下一步进行操作:
public static interface OnRecyclerViewItemClickListener {
void onItemClick(View view , DataModel data);
}
(2)定义完接口,添加接口和设置Adapter接口的方法:
private OnRecyclerViewItemClickListener mOnItemClickListener = null;
public void setOnItemClickListener(OnRecyclerViewItemClickListener listener) {
this.mOnItemClickListener = listener;
}
(3)然后为Adapter实现OnClickListener方法:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> implements View.OnClickListener{
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, final int i) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item, viewGroup, false);
ViewHolder vh = new ViewHolder(view);
// 将创建的View注册点击事件
view.setOnClickListener(this);
return vh;
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, final int i) {
viewHolder.mTextView.setText(datas.get(i).title);
// 将数据保存在itemView的Tag中,以便点击时进行获取
viewHolder.itemView.setTag(datas.get(i));
}
...
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
// 注意这里使用getTag方法获取数据
mOnItemClickListener.onItemClick(v,(DataModel)v.getTag());
}
}
...
}
当然我们也可以通过Recyclerview直接获取item位置信息:
@Override
public void onClick(View v) {
int position = mRecyclerview.getChildAdapterPosition(v);
Log.i(TAG, "position : " + position);
}
这样获取位置信息的方法依赖于Recyclerview,所以如果MyAdapter和当前界面不是在同一个界面的话就需要传过来一个Recyclerview实例,这样是比较麻烦的,一般来讲我们在mAdapter设置监听器的地方来获取即可,如下。
(4)最后在Activity或其他地方应用:
mAdapter = new MyAdapter(getDummyDatas());
mRecyclerView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(new MyAdapter.OnRecyclerViewItemClickListener() {
@Override
public void onItemClick(View view, DataModel data) {
// Do something.
}
});
这样我们的点击事件监听器就设置完毕了,同理我们可以为RecyclerView设置长击事件,而且把两个接口融合到一起也是可以的,比如:
public interface OnItemClickLitener {
void onItemClick(View view, int position);
void onItemLongClick(View view, int position);
}
后续的步骤同点击事件是一样的,但是我们却可以在长击事件里面做更多的事情,比如我们熟悉的长按删除,可谓一举两得。
编译运行看效果:
四、RecycleView 添加和删除数据
以前在ListView当中,我们只要修改后数据用Adapter的notifyDatasetChange一下就可以更新界面。然而在RecyclerView中还有一些更高级的用法:
- 添加数据:
public void addItem(DataModel content, int position) {
datas.add(position, content);
notifyItemInserted(position); //Attention!
}
- 删除数据:
public void removeItem(DataModel model) {
int position = datas.indexOf(model);
datas.remove(position);
notifyItemRemoved(position);//Attention!
}
改变数据后我们可以设置RecycleView滚动到相应位置:
mRecyclerview.scrollToPosition(position);
五、RecycleView 增加多样式分隔线
我们可以通过RecyclerView.addItemDecoration(ItemDecoration decoration)这个方法进行设置RecycleView的分割线,但是RecycleView的分隔线是不是强制地需要被设置呢?当然不是,因为我们上面的例子没有设置分隔线也没有报错,但是通过设置分割线可以让我们每一个Item从视觉上面相互分开来,例如ListView中divider哪种非常相似的效果。
可以看见,RecyclerView.addItemDecoration(ItemDecoration decoration)这个方法是一个ItemDecoration,而这个可以是我们自己定义的继承自ItemDecoration的一个对象,所以接下来我们创建一个继承RecyclerView.ItemDecoration类来绘制分隔线。
系统提供的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);
}
(1)增加分隔线
既然我们为RecycleView设置了分隔线,那么 当我们的RecyclerView在进行绘制的时候一定会进行Decoration的绘制,那么就会去调用onDraw和onDrawOver方法,所以我们其实只要去重写onDraw(onDrawOver在drawChildren之后,一般我们选择复写其中一个即可)和getItemOffsets这两个方法就可以实现相应的分隔线绘制了。而在绘制好了之后,LayoutManager会进行Item的布局,这个时候就会去调用getItemOffset方法来计算每个Item的Decoration合适的尺寸,这就是分隔线在绘制的过程中各个方法的调用步骤 ,下面我们来实现一个:
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)的方式来设置分隔线的(使用系统listDivider的好处是方便我们去随意的改变分隔线样式),然后来获取尺寸和位置进行setBound()绘制。接着通过outRect.set()来设置绘制整个区域范围,当然了它是有两种情况的,一种LinearLayoutManager.HORIZONTAL另外一种LinearLayoutManager.VERTICAL,我们需要分别对其进行处理。最后在RecyclerView中设置我们自定义的分割线,即在MainActivity中加上一句recyclerView .addItemDecoration(new DividerItemDecoration(MainActivity.this,LinearLayoutManager.VERTICAL)。
(2)改变样式
正如我们上面提到的,使用系统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>
或者这样:
<?xml version="1.0" encoding= "utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<!-- 填充的颜色 -->
<solid android:color ="@color/color_red"/>
<!-- 线条大小 -->
<size android:height ="1dp" android:width ="1dp"/>
</shape>
六、 两种方式为 RecyclerView 添加动画
(1)添加系统动画和自定义动画
控制RecyclerView增加和删除的动画是通过ItemAnimator这个类来实现的,ItemAnimator这类也是个抽象的类,系统默认给我们提供了一种增加和删除的动画,下面我们就来看看这种动画的效果,我们需要做的修改如下:
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
// 设置布局管理器
recyclerView.setLayoutManager(layoutManager);
// 设置增加或删除条目的动画
recyclerView.setItemAnimator( new DefaultItemAnimator());
值得注意的是:我们这里更新数据集不是用 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);
}
然后在合适的地方调用addData和removeData就能看到相应的动画效果了。
我们这里只提供了一种动画,如果你想要去自定义各种更好玩的动画效果的话,可以自定义实现,也可以到github的许多类似的项目中借鉴经验,比如这里:RecyclerViewItemAnimators,有兴趣的同学可以自行查阅。
(2)使用ItemTouchHelper实现Item的拖拽和滑动删除以及动画效果
简介:
Google官方文档上是这么介绍的:
This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. 这是一个支持RecyclerView滑动删除和拖拽的实用工具类。我们自己的理解:
ItemTouchHelper是一个强大的工具,它是RecyclerView.ItemDecoration的子类,它处理好了关于在RecyclerView上添加拖动排序与滑动删除的所有事情,也就是说它可以轻易的添加到几乎所有的LayoutManager和Adapter中。除此之外,它还可以和现有的item动画一起工作,提供受类型限制的拖放动画等。
首先我们给出itemTouchHelper的整体实现,然后对其中各个方法做出解释。
itemTouchHelper的整体实现:
itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = 0, swipeFlags = 0;
if (recyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) {
dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
} else if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
// 设置侧滑方向为从左到右和从右到左都可以
swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
}
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
Collections.swap(meizis, from, to);
mAdapter.notifyItemMoved(from, to);
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mAdapter.removeItem(viewHolder.getAdapterPosition());
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder.itemView.setBackgroundColor(Color.LTGRAY);
}
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setBackgroundColor(Color.WHITE);
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
viewHolder.itemView.setAlpha(1 - Math.abs(dX) / screenwidth);
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
});
-
getMovementFlags()方法:
ItemTouchHelper让我们可以轻易得到一个事件的方向,我们需要重写getMovementFlags()方法来指定可以支持的拖放和滑动的方向。使用helperItemTouchHelper.makeMovementFlags(int, int)来构造返回的flag,这里我们启用了上下左右两种方向。
注:上下为拖动(drag),左右为滑动(swipe)。 -
isLongPressDragEnabled()方法:
ItemTouchHelper可以用于没有滑动的拖动操作(或者反过来),你必须指明你到底要支持哪一种。要支持长按RecyclerView item进入拖动操作,你必须在isLongPressDragEnabled()方法中返回true。或者,也可以调用ItemTouchHelper.startDrag(RecyclerView.ViewHolder) 方法来开始一个拖动。 -
isItemViewSwipeEnabled()方法:
要在view任意位置触摸事件发生时启用滑动操作,则直接在sItemViewSwipeEnabled()中返回true就可以了。或者,你也主动调用ItemTouchHelper.startSwipe(RecyclerView.ViewHolder) 来开始滑动操作。 -
onMove()和onSwiped()方法:
用于通知底层数据的更新。 -
onSelectedChanged()、clearView()和onChildDraw()方法:
设置拖拽和滑动时的响应动画效果。
七、几种不同的布局管理器
上面我们编写的类似ListView样子的Demo都是通过LinearLayoutManager来实现的,接下来我们介绍几种其它类型的LayoutManager。
系统提供了3个实现类:
- LinearLayoutManager:线性管理器,支持横向、纵向。
- GridLayoutManager:网格布局管理器。
- StaggeredGridLayoutManager:瀑布流式布局管理器。
(1)实现网格布局
如果想把上面的线性布局改成网格布局,可以这样改写:
//mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
但是改为GridLayoutManager以后,对于分割线前面的DividerItemDecoration就不再适用了,主要是因为它在绘制的时候,比如水平线针对每个child的取值为:
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
这样写对于每个Item一行的情况是没问题的,而GridLayoutManager一行有多个childItem,这样就会多次绘制,并且GridLayoutManager的Item如果为最后一列或者为最后一行时,也需要对边界条件进行处理,所以我们编写了DividerGridItemDecoration:
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更方便。
(2)实现瀑布流布局
瀑布流布局其实也可以实现GridLayoutManager一样的功能,我们按照下列代码改写:
// mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
可以看到效果为:
我们可以发现:通过RecyclerView去实现ListView、GridView、瀑布流的效果基本上没有什么区别,仅仅通过设置不同的LayoutManager即可实现不同的效果,这就是RecyclerView结合LayoutManager的强大之处。
感谢优秀的你跋山涉水看到了这里,不如关注下让我们永远在一起!