自Android 5.0之后,Google推出了RecylerView控件,大家可以通过导入support-v7对其进行使用。根据官方介绍,该控件用于在有限的窗口中展示大量数据集,其实这样功能的控件我们使用过很多了,例如:ListView、GridView。那么RecyclerView相对于ListView、GridView有什么优势呢? RecylerView是一个强大的滑动组件,与ListView、GridView相比,同样拥有item回收复用的功能,这一点从它的名字RecyclerView也可以看出,而且它提供了一种插拔式的体验,高度的解耦,给予你充分的定制自由,异常的灵活。下面我们一起来学习一下RecyclerView的用法吧。
RecyclerView相关类:
eclipse用户使用前需要先添加android-support-v7-recyclerview.jar包。
问题:我添加v7后编译成功运行时却报错Didn’t find class “android.support.v4.util.Pools$SimplePool” on path: DexPathList。
原因:其实这是因为RecyclerView控件所依赖的android-support-v4包版本不一致造成的,即项目本身使用了一个appcompat的依赖(appcompat中包含了android-support-v4.jar),而这个support-v4和RecyclerView依赖的support-v4不是一个版本。
解决:防范这种问题发生的手段主要是所有兼容包都是用同一个版本的。这里我上传了自己的兼容包(里面包含recyclerview包和v4包)下载链接。
和listview一样,我们只需要获取RecyclerView,添加Adapter即可:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
mRecyclerView = (RecyclerView) findViewById(R.id.id_recyclerview);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
recycleAdapter = new RecyclerAdapter(this, mDatas);
mRecyclerView.setAdapter(recycleAdapter);
}
protected void initData() {
mDatas = new ArrayList();
for (int i = '0'; i < 'z'; i++) {
mDatas.add("" + (char) i);
}
}
Adapter写法如下,也很简单:
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.MyViewHolder> {
private List mDatas;
private Context mContext;
private LayoutInflater inflater;
public RecyclerAdapter(Context context, List datas){
this.mContext=context;
this.mDatas=datas;
inflater=LayoutInflater.from(mContext);
}
@Override
public int getItemCount() {
return mDatas.size();
}
//填充onCreateViewHolder方法返回的holder中的控件
@Override
public void onBindViewHolder(MyViewHolder holder, final int position) {
holder.tv.setText(mDatas.get(position));
}
//重写onCreateViewHolder方法,返回一个自定义的ViewHolder
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = inflater.inflate(R.layout.recycler_item,parent,false);
MyViewHolder holder=new MyViewHolder(view);
return holder;
}
class MyViewHolder extends ViewHolder{ //承载Item视图的子布局
TextView tv;
public MyViewHolder(View view) {
super(view);
tv=(TextView) view.findViewById(R.id.id_num);
}
}
}
最后是两个xml文件:
"http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
.support.v7.widget.RecyclerView
android:id="@+id/id_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#00EEEE"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/id_num"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"/>
RelativeLayout>
上面的获取View添加Adapter看起来和listview没啥不同啊,只是多了mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 这句话,这个是设置LayoutManager,那么LayoutManager又是什么东西呢?
RecyclerView.LayoutManager是一个抽象类,目前系统提供了3个实现类:
(1)设置LinearLayoutManager :
垂直方向:
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
//设置为垂直布局,这也是默认的,可以不设
layoutManager.setOrientation(OrientationHelper.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
效果就是我们上面所示。
水平方向:
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
//设置为水平布局
layoutManager.setOrientation(OrientationHelper.HORIZONTAL);
recyclerView.setLayoutManager(layoutManager);
同时修改一下Item布局:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#00EEEE" >
<TextView
android:id="@+id/id_num"
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center"/>
RelativeLayout>
(2)设置GridLayoutManager :
//第二个参数表示有几列
GridLayoutManager layoutManager = new GridLayoutManager(this,4);
mRecyclerView.setLayoutManager(layoutManager);
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00EEEE" >
<TextView
android:id="@+id/id_num"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"/>
RelativeLayout>
(3)设置StaggeredGridLayoutManager:
上面设置的GridView是纵向的,如果我们想设置一个横向的GridView,怎么办呢?好办,利用StaggeredGridLayoutManager。
//第一个参数表示有几行或几列,第二个参数表示方向
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(4, OrientationHelper.HORIZONTAL);
mRecyclerView.setLayoutManager(layoutManager);
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#00EEEE" >
<TextView
android:id="@+id/id_num"
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center"/>
RelativeLayout>
之前的博客我也写过瀑布流模型,最起码不是那么随意就可以实现的吧?但是,如果使用RecyclerView,几秒钟的事。
//第一个参数表示有几行或几列,第二个参数表示瀑布流方向
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(4, OrientationHelper.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
上面的item布局我们使用了固定的高度,对于瀑布流我们仅仅需要在适配器的onBindViewHolder方法中为我们的item设置个随机的高度
...
LayoutParams lp = holder.tv.getLayoutParams();
lp.height = (int)(100 + Math.random() * 300);
holder.tv.setLayoutParams(lp);
...
看看效果图:
是不是很简单呢?后面我还会介绍item分割线,让瀑布流看起来更加清晰。
上面的例子中Item间是没有分割线的,当你尝试去添加时,会发现RecyclerView并没有divider这样的属性。那怎么办呢?当然你可以在Item的布局文件中自己手动添加,但这种方式不够优雅。其实RecyclerView的分割线我们是可以去定制的。
我们可以通过该方法添加分割线:
mRecyclerView.addItemDecoration(ItemDecoration decoration);
该方法的参数就是我们自己定义的继承自ItemDecoration的一个对象。我们可以创建一个继承RecyclerView.ItemDecoration类来绘制分隔线,通过ItemDecoration可以让我们每一个Item从视觉上面相互分开来。当然像上面的例子我们也可以不设置,说明ItemDecoration并不是强制需要使用的。
系统提供的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方法先于drawChildren,onDrawOver在drawChildren之后,一般我们选择复写其中一个即可。getItemOffsets 可以通过outRect.set()为每个Item设置一定的偏移量,主要用于绘制Decorator。
下面我们来具体实现两个Decoration:
DividerItemDecoration
该实现类可以通过读取系统Theme中的 Android.R.attr.listDivider作为Item间的分割线,并且支持横向和纵向。获取到listDivider以后,该属性的值是个Drawable,在getItemOffsets中,outRect去设置了绘制的范围。onDraw中实现了真正的绘制。
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);
}
}
}
我们在原来的代码中添加一句:
mRecyclerView.addItemDecoration(new DividerItemDecoration(this,
DividerItemDecoration.VERTICAL_LIST));
该分割线是系统默认的,你也可以在styles.xml中重写listDivider属性实现自己想要的效果。下面我们重写一个:
<style name="AppTheme" parent="AppBaseTheme">
<item name="android:listDivider">@drawable/divider_bg
style>
然后自己写个drawable(divider_bg.xml)即可:
<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>
DividerGridItemDecoration
如果为GridLayoutManager添加分割线,前面的DividerItemDecoration就不适用了,例如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;
// StaggeredGridLayoutManager 且横向滚动
} else {
// 如果是最后一行,则不需要绘制底部
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的横向和纵向,最重要还是去理解,如何绘制什么的不重要。
添加分割线:
mRecyclerView.addItemDecoration(new DividerGridItemDecoration(this));
Theme中listDivider添加背景图片(divider_bg.xml):
<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"
//注意这里要增加线宽,否则竖直分割线画不出来
android:width="4dp"/>
shape>
注:一般如果仅仅是希望有空隙,还是去设置item的margin更方便,如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/id_num"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#00EEEE"
android:gravity="center"
android:layout_margin="3dp"/>
RelativeLayout>
RecyclerView中的item增加、删除动画也是可配置的。RecyclerView.ItemAnimator也是一个抽象类,系统为我们提供了一种默认的实现类,我们也可以自定义自己的实现动画。
借助默认的实现,当Item添加和移除的时候,实现动画效果很简单:
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
同时定义添加和移除动作:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.id_action_add:
recycleAdapter.addData(1);
break;
case R.id.id_action_delete:
recycleAdapter.removeData(1);
break;
}
return true;
}
public void addData(int position) {
mDatas.add(position, "Insert item");
notifyItemInserted(position); //通知Item添加
}
public void removeData(int position) {
mDatas.remove(position);
notifyItemRemoved(position); //通知Item删除
}
注意这里更新数据集不是用notifyDataSetChanged()而是 notifyItemInserted(position)与notifyItemRemoved(position) ,否则没有动画效果。
注:
1. 这里使用的是系统的默认动画,其实github上已经有很多类似的项目了,这里我们直接引用下:RecyclerViewItemAnimators,大家自己下载查看。提供了SlideInOutLeftItemAnimator,SlideInOutRightItemAnimator,SlideInOutTopItemAnimator,SlideInOutBottomItemAnimator等动画效果。
2. 这里说一下RecyclerView.Adapter中刷新数据的几个方法,一共有这么几个方法
public final void notifyDataSetChanged()
public final void notifyItemChanged(int position)
public final void notifyItemRangeChanged(int positionStart, int itemCount)
public final void notifyItemInserted(int position)
public final void notifyItemMoved(int fromPosition, int toPosition)
public final void notifyItemRangeInserted(int positionStart, int itemCount)
public final void notifyItemRemoved(int position)
public final void notifyItemRangeRemoved(int positionStart, int itemCount)
notifyDataSetChanged()这个方法跟我们平时用到的ListView的Adapter的方法一样,这里就不多做描述了。
notifyItemChanged(int position),当position位置的数据发生了改变时就会调用这个方法,就会回调对应position的onBindViewHolder()方法了,当然,因为ViewHolder是复用的,所以如果position在当前屏幕以外,也就不会回调了,因为没有意义,下次position滚动会当前屏幕以内的时候同样会调用onBindViewHolder()方法刷新数据了。其他的方法也是同样的道理。
notifyItemRangeChanged(int positionStart, int itemCount),顾名思义,可以刷新从positionStart开始itemCount数量的item了(这里的刷新指回调onBindViewHolder()方法)。
notifyItemMoved(int fromPosition, int toPosition),这个方法是从fromPosition移动到toPosition为止的时候可以使用这个方法刷新。
notifyItemInserted(int position),这个方法是在第position位置被插入了一条数据的时候可以使用这个方法刷新,注意这个方法调用后会有插入的动画,这个动画可以使用默认的,也可以自己定义。
public final void notifyItemRangeInserted(int positionStart, int itemCount),显然是批量添加。
notifyItemRemoved(int position),第position个被删除的时候刷新,同样会有动画。
notifyItemRangeRemoved(int positionStart, int itemCount),批量删除。
我们知道在ListView使用的时候,该控件给我们提供一个onItemClickListener监听器,这样当我们的item发生点击事件的时候,会回调相关的方法,以便我们方便处理Item点击事件。对于RecyclerView来讲,非常可惜的时候,该控件没有给我们提供这样的内置监听器方法,不过我们可以进行改造实现。
首先需要自定义一个类似于onItemClickListener()的监听器来处理点击事件,可以将它定义为adapter的内部接口,然后在onBindViewHolder中监听holder.itemView的点击事件,最后回调到我们设置的监听器方法。
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.MyViewHolder> {
private List mDatas;
private Context mContext;
private LayoutInflater inflater;
private OnItemClickListener mOnItemClickListener;
//自定义onItemClickListener监听器,为adapter的内部接口
public interface OnItemClickListener{
void onClick(int position);
void onLongClick(int position);
}
//为adapter设置监听器
public void setOnItemClickListener(OnItemClickListener onItemClickListener ){
this.mOnItemClickListener=onItemClickListener;
}
public RecyclerAdapter(Context context, List datas){
this.mContext=context;
this.mDatas=datas;
inflater=LayoutInflater.from(mContext);
}
@Override
public int getItemCount() {
return mDatas.size();
}
@Override
public void onBindViewHolder(MyViewHolder holder, final int position) {
holder.tv.setText(mDatas.get(position));
//监听holder.itemView的点击事件
if(mOnItemClickListener!=null){
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//回调到我们设置的监听器的onClick方法
mOnItemClickListener.onClick(position);
}
});
holder.itemView.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
//回调到我们设置的监听器的onLongClick方法
mOnItemClickListener.onLongClick(position);
return false;
}
});
}
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = inflater.inflate(R.layout.recycler_item,parent,false);
MyViewHolder holder=new MyViewHolder(view);
return holder;
}
class MyViewHolder extends ViewHolder{
TextView tv;
public MyViewHolder(View view) {
super(view);
tv=(TextView) view.findViewById(R.id.id_num);
}
}
}
在Activity中去设置监听:
recycleAdapter.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onClick(int position) {
Toast.makeText(MainActivity.this, position + " click", Toast.LENGTH_SHORT).show();
}
@Override
public void onLongClick(int position) {
Toast.makeText(MainActivity.this, position + " long click", Toast.LENGTH_SHORT).show();
}
});
Item背景在点击状态下改变颜色:
<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>
众所周知,listView可以使用addHeaderView、addFooterView等方法添加Header和Footer,但是RecyclerView和各种LayoutManager都没有提供方法添加Header和Footer,这个时候我们就开始思考如何为RecyclerView添加Header和Footer了。这里的解决方案是通过控制Adapter的itemType来设置的,思路就是根据不同的itemType去加载不同的布局。
public class HeaderAndFooterAdapter extends RecyclerView.Adapter {
private List mDatas;
private Context mContext;
private LayoutInflater inflater;
//定义头部、底部和普通Item的类型常量
public static final int TYPE_HEADER = 0;
public static final int TYPE_FOOTER = 1;
public static final int TYPE_NORMAL = 2;
//头部和底部Item
private View mHeaderView;
private View mFooterView;
public HeaderAndFooterAdapter(Context context, List datas){
this.mContext=context;
this.mDatas=datas;
inflater=LayoutInflater.from(mContext);
}
//设置头部Item
public void setHeaderView(View headerView) {
mHeaderView = headerView;
notifyItemInserted(0);
}
//获取头部Item
public View getHeaderView() {
return mHeaderView;
}
//设置底部Item
public void setFooterView(View footerView) {
mFooterView = footerView;
notifyItemInserted(getItemCount() - 1);
}
//获取底部Item
public View getFooterView() {
return mFooterView;
}
//根据position位置获取Item的类型
@Override
public int getItemViewType(int position) {
if (position == 0) {
if (mHeaderView != null) {
return TYPE_HEADER;
} else {
return TYPE_NORMAL;
}
} else if (position == getItemCount() - 1) {
if (mFooterView != null) {
return TYPE_FOOTER;
} else {
return TYPE_NORMAL;
}
} else {
return TYPE_NORMAL;
}
}
//Item总数量
@Override
public int getItemCount() {
int hCount = mHeaderView != null ? 1 : 0;
int fCount = mFooterView != null ? 1 : 0;
return mDatas.size() + hCount + fCount;
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
if(getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER) return;
int pos = mHeaderView == null ? position : position - 1;
holder.tv.setText(mDatas.get(pos));
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//根据Item类型,创建view
if(mHeaderView != null && viewType == TYPE_HEADER) return new MyViewHolder(mHeaderView);
if(mFooterView != null && viewType == TYPE_FOOTER) return new MyViewHolder(mFooterView);
View view = inflater.inflate(R.layout.recycler_item,parent,false);
MyViewHolder holder=new MyViewHolder(view);
return holder;
}
class MyViewHolder extends ViewHolder{
TextView tv;
public MyViewHolder(View view) {
super(view);
if (itemView == mHeaderView || itemView == mFooterView) return;
tv = (TextView) view.findViewById(R.id.id_num);
}
}
}
这里我们重写了getItemViewType方法,并根据位置来返回不同的type,这个type是我们预先商定好的常量,接在onCreateViewHolder方法中来判断itemType,如果是header或footer,则返回我们设置的headerView或footerView,否则正常加载item布局,相信大家对于上面的代码不会有任何疑问,接下来我们就在Activity中用一下试试看
...
case R.id.id_action_headerfootergridview:
HeaderAndFooterAdapter adapter = new HeaderAndFooterAdapter(this, mDatas);
mRecyclerView.setAdapter(adapter);
setHeaderAndFooter(adapter, mRecyclerView);
break;
...
private void setHeaderAndFooter(HeaderAndFooterAdapter adapter, RecyclerView view) {
View header = LayoutInflater.from(this).inflate(R.layout.header, view, false);
View footer = LayoutInflater.from(this).inflate(R.layout.footer, view, false);
adapter.setHeaderView(header);
adapter.setFooterView(footer);
}
这里LayoutManager我们使用默认的LinearLayoutManager,并且给Adapter设置了一个header和footer,运行一下
上面LayoutManager使用的是默认的LinearLayoutManager,如果改成GridLayoutManager会怎样呢?
GridLayoutManager layoutManager5 = new GridLayoutManager(this,4);
mRecyclerView.setLayoutManager(layoutManager5);
这里确实也看到了headerView和footerView,但GridLayoutManager把它们当成的普通的Item来显示了。
这时候我们就该引入一个不太常用的方法了:
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER ? gridManager.getSpanCount() : 1;
}
});
我们解释一下这段代码,首先我们设置了一个SpanSizeLookup,这个类是一个抽象类,而且仅有一个抽象方法getSpanSize,这个方法的返回值决定了我们每个position上的item占据的单元格个数,而我们这段代码综合起来就是为GridLayoutManager设置每行的单元格个数, 当当前位置是header或footer的位置,那么该item占据一行所有的单元格,正常情况下占据1个单元格。那这段代码放哪呢? 为了以后的封装,我们还是在Adapter中找方法放吧。
我们在Adapter中再重写一个方法onAttachedToRecyclerView
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if(manager instanceof GridLayoutManager) {
final GridLayoutManager gridManager = ((GridLayoutManager) manager);
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER ? gridManager.getSpanCount() : 1;
}
});
}
}
这个时候我们再来看一下效果
这次达到我们的要求了,不过对于StaggeredGridLayoutManager我们还没做处理,而且我们还发现StaggeredGridLayoutManager中并没有像GridLayoutManager中这样的方法,我们还需要为StaggeredGridLayoutManager单独处理一下。
我们继续重写Adapter中另外一个方法
@Override
public void onViewAttachedToWindow(HeaderAndFooterAdapter.MyViewHolder holder) {
super.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER) {
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
这里的处理方式是用通过LayoutParams,而且这里更简单,StaggeredGridLayoutManager.LayoutParams为我们提供了一个setFullSpan方法来设置占领全部空间,看一下StaggeredGridLayoutManager的效果
上面我们用的是Item的margin作为分割线,如果使用ItemDecoration效果如何呢?
可以看到Header也被添加了分割线,header对于RecyclerView来说还是一个普普通通的item,这时候我们添加分割线,肯定也会对header产生影响,这不是我们希望的。所以我们需要修改DividerGridItemDecoration
public class DividerGridItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[] { android.R.attr.listDivider};
private Drawable mDivider;
private boolean hasHeader;
private boolean hasFooter;
public DividerGridItemDecoration(Context context, Boolean hasHeader, Boolean hasFooter) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
this.hasHeader = hasHeader;
this.hasFooter = hasFooter;
}
@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;
// StaggeredGridLayoutManager 且横向滚动
} else {
// 如果是最后一行,则不需要绘制底部
if ((pos + 1) % spanCount == 0) {
return true;
}
}
}
return false;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
int spanCount = getSpanCount(parent);
int childCount = parent.getAdapter().getItemCount();
int pos = position;
if (hasHeader) {
if (position == 0) {
// 仅仅绘制底部的分割线,其他的地方不绘制
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
return;
} else {
// position减1,因为我们认为的第1个item其实是第2个
pos = position - 1;
}
}
if (hasFooter) {
// 不绘制分割线
if (position == childCount - 1) {
outRect.set(0, 0, 0, 0);
return;
}
}
// 如果是最后一行,则不需要绘制底部
if (isLastRaw(parent, pos, spanCount, childCount)) {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
// 如果是最后一列,则不需要绘制右边
} else if (isLastColum(parent, pos, spanCount, childCount)) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
}
}
}
改造的地方是获取偏移量的方法我们换了一个,因为原来的那个已经过时了,而且,这里我们还加了一个boolean类型的hasHeader和hasFooter变量来表示是不是有header和footer,如果hasHeader并且position为0,那么我们仅仅绘制底部的分割线,其他的地方不绘制,在有header的情况下,我们还需要将position减1,因为我们认为的第1个item其实是第2个。如果hasFooter并且position是最后一个,不绘制分割线。这个时候我们再来看看有分割线的效果。
这下好了,以上基本上完美的处理好了所有功能,可是难道我们对于不同的Adapter都需要写那么多代码吗? 对于一个懒程序员来说,这肯定是一个可怕的事情,所以,我们还需要对我们的Adapter进行封装,目的就是可以轻轻松松的写代码。
public abstract class BaseRecyclerAdapter extends RecyclerView.Adapter {
private List mDatas = new ArrayList();
//定义头部、底部和普通Item的类型常量
public static final int TYPE_HEADER = 0;
public static final int TYPE_FOOTER = 1;
public static final int TYPE_NORMAL = 2;
//头部和底部Item
private View mHeaderView;
private View mFooterView;
public interface OnItemClickListener{
void onClick(int position, T data);
void onLongClick(int position, T data);
}
private OnItemClickListener mOnItemClickListener;
public void setOnItemClickListener(OnItemClickListener onItemClickListener ){
this.mOnItemClickListener=onItemClickListener;
}
//设置头部Item
public void setHeaderView(View headerView) {
mHeaderView = headerView;
notifyItemInserted(0);
}
//获取头部Item
public View getHeaderView() {
return mHeaderView;
}
//设置底部Item
public void setFooterView(View footerView) {
mFooterView = footerView;
notifyItemInserted(getItemCount() - 1);
}
//获取底部Item
public View getFooterView() {
return mFooterView;
}
//添加数据
public void addDatas(List datas) {
mDatas.addAll(datas);
notifyDataSetChanged();
}
//根据position位置获取Item的类型
@Override
public int getItemViewType(int position) {
if (position == 0) {
if (mHeaderView != null) {
return TYPE_HEADER;
} else {
return TYPE_NORMAL;
}
} else if (position == getItemCount() - 1) {
if (mFooterView != null) {
return TYPE_FOOTER;
} else {
return TYPE_NORMAL;
}
} else {
return TYPE_NORMAL;
}
}
//Item总数量
@Override
public int getItemCount() {
int hCount = mHeaderView != null ? 1 : 0;
int fCount = mFooterView != null ? 1 : 0;
return mDatas.size() + hCount + fCount;
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if(manager instanceof GridLayoutManager) {
final GridLayoutManager gridManager = ((GridLayoutManager) manager);
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER ? gridManager.getSpanCount() : 1;
}
});
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER) {
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//根据Item类型,创建view
if(mHeaderView != null && viewType == TYPE_HEADER) return new Holder(mHeaderView);
if(mFooterView != null && viewType == TYPE_FOOTER) return new Holder(mFooterView);
return onCreate(parent, viewType);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if(getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER) return;
final int pos = mHeaderView == null ? position : position - 1;
final T data = mDatas.get(pos);
onBind(holder, pos, data);
if(mOnItemClickListener!=null){
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mOnItemClickListener.onClick(pos, data);
}
});
holder.itemView.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mOnItemClickListener.onLongClick(pos, data);
return false;
}
});
}
}
public abstract RecyclerView.ViewHolder onCreate(ViewGroup parent, final int viewType);
public abstract void onBind(RecyclerView.ViewHolder viewHolder, int RealPosition, T data);
public class Holder extends RecyclerView.ViewHolder {
public Holder(View itemView) {
super(itemView);
}
}
}
我们将BaseRecyclerAdapter抽象起来,并且提供两个抽象方法onCreate和onBind用来创建holder和绑定数据,而对于header和footer做的一系列工作,我们都放到了BaseRecyclerAdapter中,而继承BaseRecyclerAdapter后,我们仅仅关心我们的holder怎么创建和数据怎么绑定就ok。例如下面代码:
//自定义的MyAdapter继承自BaseRecyclerAdapter
public class MyAdapter extends BaseRecyclerAdapter<String> {
@Override
public RecyclerView.ViewHolder onCreate(ViewGroup parent, int viewType) {
View layout = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false);
return new MyHolder(layout);
}
@Override
public void onBind(RecyclerView.ViewHolder viewHolder, int RealPosition, String data) {
if(viewHolder instanceof MyHolder) {
((MyHolder) viewHolder).text.setText(data);
}
}
class MyHolder extends BaseRecyclerAdapter.Holder {
TextView text;
public MyHolder(View itemView) {
super(itemView);
text = (TextView) itemView.findViewById(R.id.id_num);
}
}
}
...
case R.id.id_action_baseRecyclerAdapter:
//使用GridLayoutManager
GridLayoutManager layoutManager6 = new GridLayoutManager(this,3);
mRecyclerView.setLayoutManager(layoutManager6);
//创建自定义adapter实例
MyAdapter mAdapter = new MyAdapter();
//加载数据
mAdapter.addDatas(mDatas);
mRecyclerView.setAdapter(mAdapter);
//添加头部和底部
setHeaderAndFooter(mAdapter, mRecyclerView);
break;
Demo下载地址