版权声明:本文为openXu原创文章【openXu的博客】,未经博主允许不得以任何形式转载
目录:
由于之前在项目中使用到RecyclerView的时候只做了简单的学习,以满足项目需求,最近得空将RecyclerView相关知识整理整理,发表此篇文章仅作为个人知识梳理,也希望能帮助到需要的人。
RecyclerView用于在有限的窗口展现大量的数据,相比ListView、GridView,RecyclerView标准化了ViewHolder,而且用法非常的灵活,可以实现很多ListView实现不了的样式和功能,接下来我们通过示例逐步探索RecyclerView的各种使用。
①、在build.gradle
中引入RecyclerView:
implementation 'com.android.support:recyclerview-v7:26.1.0'
②、布局文件
<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.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
LinearLayout>
③、设置布局管理器和adapter
// 设置布局管理器
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(this,
LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(mLayoutManager);
// 设置adapter
adapter = new CommandRecyclerAdapter(this, R.layout.item_vertical, dataList) {
@Override
public void convert(ViewHolder holder, String str) {
holder.setText(R.id.tv_item, str);
}
@Override
public void onItemClick(String str, int position) {
}
};
recyclerView.setAdapter(adapter);
//③. 设置Item之间间隔样式
recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
在上面的示例中,我们只需要3步就能从无到有的将RecyclerView展示出来,但实际上并没有那么简单,上面使用的Adapter是通过封装后的一个通用的Adapter,能适用于日常开发绝大多数的要求,下面我们看看里面的结构是怎样的。
在此之前,我们先对比一下之前我们使用的Adapter(比如ListView的),创建Adapter时,需要重写几个重要的方法:getCount()、getView()…,通常说到优化就是使用ViewHolder,但是这并不是强制的,如果不用也可以,那就每次都findView去。RecyclerView中规范了ViewHolder,变成了强制性的,在RecyclerView.Adapter的onBindViewHolder(ViewHolder holder, int position)方法中(相当于ListView Adapter的getView()方法),ViewHolder作为参数传递过来,绑定数据时就必须使用此viewHolder对象了。
那说到底ViewHolder到底是个什么东西呢?它是用来维护Item中控件的引用的,如果复用Item,就能避免很多findView的操作从而优化内存开销,RecyclerView之所以这样,就是为了规范化这个东西,说到底ViewHolder就是一个简单的类,里面维护了item控件的引用。
Adapter是个什么东西就不用说了吧?数据适配器,告诉RecyclerView有多少个条目,然后给每个条目绑定数据。值得注意的是RecyclerView并没有暴露Item的点击事件,我们可以在绑定数据的时候为Item设置点击和长点击事件。
下面看看相关代码:
public class ViewHolder extends RecyclerView.ViewHolder {
private SparseArray mViews; //用于维护item中子控件的引用
private View mConvertView;
private Context mContext;
public ViewHolder(Context context, View itemView, ViewGroup parent) {
super(itemView);
mContext = context;
mConvertView = itemView;
mViews = new SparseArray<>();
}
/**根据item布局id创建一个ViewHolder,Adapter中调用*/
public static ViewHolder get(Context context, ViewGroup parent, int layoutId) {
View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
ViewHolder holder = new ViewHolder(context, itemView, parent);
return holder;
}
/**
* 通过viewId获取控件
* @param viewId
* @return
*/
private T getView(int viewId) {
View view = mViews.get(viewId);
if (view == null) {
view = mConvertView.findViewById(viewId);
mViews.put(viewId, view);
}
return (T) view;
}
/**为条目或者指定控件设置点击和长点击事件,在adapter中调用*/
public ViewHolder setOnClickListener(int viewId, View.OnClickListener listener) {
if(viewId==-1){
mConvertView.setOnClickListener(listener);
}else{
View view = getView(viewId);
view.setOnClickListener(listener);
}
return this;
}
public ViewHolder setOnLongClickListener(int viewId, View.OnLongClickListener listener) {
if(viewId==-1){
mConvertView.setOnLongClickListener(listener);
}else{
View view = getView(viewId);
view.setOnLongClickListener(listener);
}
return this;
}
/**下面的方法暴露出去,用于绑定数据,如果不够用可以扩充*/
public ViewHolder setVisible(int viewId, int visible) {
getView(viewId).setVisibility(visible);
return this;
}
public ViewHolder setTextColor(int viewId, int color) {
TextView tv = getView(viewId);
tv.setTextColor(color);
return this;
}
public ViewHolder setText(int viewId, CharSequence text) {
TextView tv = getView(viewId);
tv.setText(text);
return this;
}
@SuppressLint("NewApi")
public ViewHolder setBackgroundResource(int viewId, int id) {
View view = getView(viewId);
if(0==id)
view.setBackground(null);
else
view.setBackgroundResource(id);
return this;
}
public ViewHolder setImageResource(int viewId, int resId) {
ImageView view = getView(viewId);
view.setImageResource(resId);
return this;
}
public ViewHolder setCheckBoxChecked(int viewId, boolean check) {
CheckBox cb = getView(viewId);
cb.setChecked(check);
return this;
}
public ViewHolder setLinearLayoutBgIcon(int viewId, int iconResourse) {
LinearLayout ll = getView(viewId);
ll.setBackgroundResource(iconResourse);
return this;
}
}
public abstract class CommandRecyclerAdapter extends RecyclerView.Adapter {
protected Context mContext;
protected int mLayoutId;
protected List mDatas;
protected LayoutInflater mInflater;
public CommandRecyclerAdapter(Context context, int layoutId, List datas) {
mDatas = new ArrayList<>();
mContext = context;
mInflater = LayoutInflater.from(context);
mLayoutId = layoutId;
if(datas!=null)
mDatas.addAll(datas);
}
/**Adapter最重要的三个方法getItemCount()、onCreateViewHolder()、onBindViewHolder()*/
@Override
public int getItemCount(){
return mDatas.size();
}
@Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType){
ViewHolder viewHolder = ViewHolder.get(mContext, parent, mLayoutId);
return viewHolder;
}
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
convert(holder, mDatas.get(position));
/**为item设置点击和长点击事件*/
holder.setOnClickListener(-1, new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemClick(mDatas.get(position), position);
}
});
holder.setOnLongClickListener(-1, new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return onItemLongClick(mDatas.get(position), position);
}
});
}
/**数据设置、添加、删除相关方法*/
public void setData(List datas){
mDatas.clear();
if(datas!=null)
mDatas.addAll(datas);
notifyDataSetChanged();
}
public void addData(int position,T t){
if(null!=mDatas)
mDatas.add(position, t);
notifyItemRemoved(position);
}
public void addDatas(int position, List datas){
if(datas!=null)
mDatas.addAll(position, datas);
notifyItemRemoved(position);
}
public void removeData(int position){
if(null!=mDatas && mDatas.size()>position) {
mDatas.remove(position);
notifyItemRemoved(position);
}
}
public void removeDatas(List positions){
if(null!=mDatas){
for(int position:positions){
if(mDatas.size()>position) {
mDatas.remove(position);
notifyItemRemoved(position);
}
}
}
}
/**重写此方法,将数据绑定到控件上*/
public abstract void convert(ViewHolder holder, T t);
/**item点击和长点击事件*/
public abstract void onItemClick(T data, int position);
public boolean onItemLongClick(T data, int position){
return true;
}
}
使用方法:
创建adapter时,需要传入Context、item布局id、数据集合,然后实现convert()、onItemClick()方法,convert()方法用来为item控件绑定数据,拿着holder就能获取到item中的所有控件对象,而item点击事件使用还是非常多的,所以抽象了一下,使用时必须实现;相比长点击事件就使用没那么多,如果要为item设置长点击事件,就重写onItemLongClick()方法即可。数据类型可以是任意Object的子类,这样就能使用各种数据类型的绑定。
adapter = new CommandRecyclerAdapter
(this, R.layout.item_vertical, dataList) {
@Override
public void convert(ViewHolder holder, String str) {
holder.setText(R.id.tv_item, str);
}
@Override
public void onItemClick(String str, int position) {
}
@Override
public boolean onItemLongClick(String data, int position) {
return super.onItemLongClick(data, position);
}
};
recyclerView.setAdapter(adapter);
上面的效果看起来非常不和谐,整个一大块,老的ListView可以设置divider属性就能将item之间隔开,而RecyclerView并没有提供divider属性,因为它的间隔样式更加灵活,需要我们自己去实现。
添加间隔样式使用recyclerView.addItemDecoration(RecyclerView.ItemDecoration)
,传递一个RecyclerView.ItemDecoration
的子类,RecyclerView提供了默认的实现类DividerItemDecoration
,我们看看效果:
//③. 设置Item之间间隔样式
recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
默认的间隔样式的实现就是一条分割线,这条分割线的高度、颜色是不能设置的,查看DividerItemDecoration的源码,发现绘制分割线的高度和颜色是从一个android.R.attr.listDivider的属性中获取的,RecyclerView并没有提供这个属性,所以可以想象使用的是系统默认的属性值。其实一条分割线在绝大多数的场景中已经足够使用,但如果我们有其他的需求(分割线高度或者颜色变一下)怎么办呢?那就实现RecyclerView.ItemDecoration,定义自己的间隔样式,ItemDecoration不是一个抽象类,默认的实现都是空,所以直接使用是没有任何效果的。
首先我们看看RecyclerView.ItemDecoration中都有那些方法,分别有什么用途:
这个方法用于设置item的偏移量,偏移的部分用于填充间隔样式。跟踪源码RecyclerView中只有有一个getItemDecorInsetsForChild()
方法调用了getItemOffsets()
,而getItemDecorInsetsForChild()
方法返回了一个insets,这里面包含了所有item的间隔的left、top、right、bottom
之和:
Rect getItemDecorInsetsForChild(View child) {
...
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
而继续跟踪发现measureChild()
方法会调用getItemDecorInsetsForChild()
,measureChild()
方法的意思就是测量item的高度,之前我们学习onMeasure
方法的时候会说父控件调用子控件的measure()
方法时会传入父控件的测量建议(父控件剩余的宽高和测量模式),看下面的代码可以发现,剩余的宽高计算时会去除RecyclerView的Padding值和所有item间隔所占用的空间,所以RecyclerView的总宽高=Padding+所有item间隔+所有item的宽高,而getItemOffsets()
中为outRect设置设置的left、top、right、bottom值将会加到item对应的padding中去,下面我们看看重写getItemOffsets()
后的效果。
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(20, 40, 60, 80);
} else {
outRect.set(20, 40, 60, 80);
}
}
ItemDecoration的onDraw()会在item的onDraw()方法之前执行,ItemDecoration的onDrawOver()会在item的onDraw()方法之后执行。这样的关系会导致绘制层级,ItemDecoration的onDraw()绘制在最底层,item在中间,onDrawOver()绘制在最上面。下面看看全部代码及效果图:
public class MyDividerItemDecoration 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;
private Paint paint;
private Bitmap bitmap;
public MyDividerItemDecoration(Context context, int orientation) {
// 获取默认主题的属性
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
paint = new Paint();
paint.setAntiAlias(true);
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.cat);
}
private void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(20, 40, 60, 80);
} else {
outRect.set(20, 40, 60, 80);
}
}
/**onDraw方法会在item绘制之前调用,这里用来绘制item的间隔,可能会被item遮挡 */
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
//获取分隔线左右坐标
final int left = parent.getPaddingLeft(); //RecyclerView的左padding值
final int right = parent.getWidth() - parent.getPaddingRight(); //RecyclerView减去right padding值后右边坐标
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();
//分割线绘制的top位置=item底端+item底部margin值+item偏移量 - 20
final int top = child.getBottom() + params.bottomMargin +
Math.round(ViewCompat.getTranslationY(child))-20;
//分割线底部 = top + 20的overdraw + 底部80偏移值
final int bottom = top + 20 + 80;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
} else {
//此处绘制水平方向排列时的间隔样式,省略
}
}
/**onDrawOver方法会在item绘制之后调用,这层绘制将会显示在最上层 */
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
//在item的头部画一只猫
c.drawBitmap(bitmap, new Rect(0, 0,bitmap.getWidth(), bitmap.getHeight()),
new Rect(child.getLeft()+20, child.getTop(),
child.getLeft()+20+(child.getBottom()-child.getTop()),
child.getBottom()),paint);
}
} else {
//此处绘制水平方向排列时的遮罩,省略
}
}
}
上面的实现效果基本上将三个方法的作用及使用场景都说明白了,效果图看起来很丑,不过这都没关系,只要弄明白其原理,想弄成什么样子都可以的。
通过RecyclerView.setItemAnimator()
可以设置item的动画(item添加、删除、修改变换的动画),该方法接受一个RecyclerView.ItemAnimator
对象,同样系统为我们提供了一个默认的实现类DefaultItemAnimator,DefaultItemAnimator中实现了add、remove、move、change四种动画,下面我们看看怎么使用:
①、设置Item动画
recyclerView.setItemAnimator(new DefaultItemAnimator());
②、adapter中编写添加或者删除item的方法
public void addData(int position,T t){
if(null!=mDatas) {
mDatas.add(position, t);
notifyItemInserted(position);
if (position != mDatas.size()) {
//刷新改变位置item下方的所有Item的位置,避免索引错乱
notifyItemRangeChanged(position, mDatas.size() - position);
}
}
}
public void removeData(int position){
if(null!=mDatas && mDatas.size()>position) {
mDatas.remove(position);
notifyItemRemoved(position);
if (position != mDatas.size()) {
//刷新改变位置item下方的所有Item的位置,避免索引错乱
notifyItemRangeChanged(position, mDatas.size() - position);
}
}
}
③、点击新增、长按删除
adapter = new CommandRecyclerAdapter
(this, R.layout.item_vertical, dataList) {
@Override
public void convert(ViewHolder holder, String str) {
holder.setText(R.id.tv_item, str);
}
//点击事件,在下方添加一个item
@Override
public void onItemClick(String str, int position) {
Log.i(getClass().getSimpleName(), "添加item"+position);
adapter.addData(position+1, "新添加的item--"+(position+1));
}
//长按事件,删除item
@Override
public boolean onItemLongClick(String data, int position) {
adapter.removeData(position);
return true;
}
};
recyclerView.setAdapter(adapter);
当数据集合发生变化后,不能用Adapter.notifyDataSetChanged()刷新,否则会导致动画不执行,要使用下面四个方法:
Adapter.notifyItemInserted(int position)
Adapter.notifyItemRemoved(int position)
Adapter.notifyItemChanged(int position)
Adapter.notifyItemMoved(int fromPosition, int toPosition)
使用这四个方法只是视图上的变化,item的索引及ViewHolder的引用并没有刷新,这会导致位置错乱,调用上面方法后,需要使用下面的代码刷新变化的item下方所有item的位置,避免错乱。
if (position != mDatas.size()) {
//刷新改变位置item下方的所有Item的位置,避免索引错乱
notifyItemRangeChanged(position, mDatas.size() - position);
}
对于自定义的动画效果,官方没有给出相应的示例,我们看看ItemAnimator
的代码何其子类的代码量也都不少,自己实现是不太可能,那能不能直接修改默认的动画效果呢?当然是可以的,此处我们创建一个类MyItemAnim复制DefaultItemAnimator
的源码,稍作修改。DefaultItemAnimator
中涉及到动画的关键方法有下面八个,分别对应添加、删除、移动和修改动画:
animateAdd()
animateAddImpl()
animateRemove()
animateRemoveImpl()
animateMove()
animateMoveImpl()
animateChange()
animateChangeImpl()
我们挑选add动画修改一下(在之前的源码上添加平移的动画):
@Override
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
//重置动画,将itemView置为准备添加的状态
resetAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
//在透明度变化的基础上添加一个平移动画,初始位置为左侧不可见
ViewCompat.setTranslationX(holder.itemView,-holder.itemView.getWidth());
mPendingAdditions.add(holder);
return true;
}
void animateAddImpl(final RecyclerView.ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimator animation = view.animate();
mAddAnimations.add(holder);
//在之前基础上添加平移动画,从-item.getWidth移动到0的位置
animation.alpha(1).translationX(0).setDuration(getAddDuration())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(Animator animator) {
view.setAlpha(1);
}
@Override
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
使用自定义的动画:
// 设置Item添加和移除的动画
MyItemAnim anim = new MyItemAnim();
//设置动画持续时间
anim.setAddDuration(1500);
recyclerView.setItemAnimator(anim);
RecyclerView提供了三种布局管理器:
上面的示例中都使用的是LinerLayoutManager,接下来我们看看GridLayoutManager和StaggeredGridLayoutManager。
GridLayoutManager是网格样式的管理器,其效果与GridView差不多,它有三个构造方法,其中常用的有两个:
//默认竖直排列,spanCount为列数
GridLayoutManager(Context context, int spanCount)
//指定排列方向HORIZONTAL or VERTICAL,reverseLayout表示是否倒序,如果设置为true,将从数据集合的最后一个开始拍
GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout)
第一个构造方法相当于GridLayoutManager(context, spanCount, VERTICAL, false),我们使用第二个构造方法分别看看
竖直排列和水平排列的情况:
// 设置布局管理器
GridLayoutManager mGridtManager = new GridLayoutManager(this, 3, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(mGridtManager);
// 设置自定义间隔样式
recyclerView.addItemDecoration(new GridDividerDecoration(this));
// 设置布局管理器
GridLayoutManager mGridtManager = new GridLayoutManager(this, 3, LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(mGridtManager);
// 设置自定义间隔样式
recyclerView.addItemDecoration(new GridDividerDecoration(this));
使用起来还是很方便,但是发现系统没有提供默认的item分隔样式,如果使用DividerItemDecoration,只会在每行下面有一条分割线,所以我们要自己定义分割线。原理上面都说过,对于网格,我们只需要每个item的右下方留分割线就行了,但这样会产生一个问题,最后一列的右侧会有间隔,有两种方法解决:
水平排列的情况也是一样的,就不多说。我们利用第二种思路定义自己的间隔格式,核心代码如下:
/**
* 由于grid网格特性,需要相邻的item之间隔开,所以只需要在右下方留间隔
* 但是特殊item需要特殊对待,比如最后一列、最后一行的情况
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
int position = parent.getChildLayoutPosition(view);
boolean rightSpace = true; //是否在右侧留间隔
boolean bottomSpace = true; //是否在下方留间隔
if(orientation == OrientationHelper.VERTICAL){ //竖直排列的情况
if ((position + 1) % spanCount == 0)
rightSpace = false; //最后一列只需要bottom方向留间隔
else { //前面的列在right和bottom方向留间隔
}
}else if(orientation == OrientationHelper.HORIZONTAL){ //水平排列的情况
if ((position + 1) % spanCount == 0)
bottomSpace = false; //最后一排只需要右边留间隔
else {
//前面的排在right和bottom方向留间隔
}
}
outRect.set(0, 0, rightSpace?mDivider.getIntrinsicWidth() : 0, bottomSpace?mDivider.getIntrinsicHeight() : 0);
}
@Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
// 绘制间隔,每一个item,绘制右边和下方间隔样式
int childCount = parent.getChildCount(); //注意childCount是一屏显示的总数,而不是数据的总数
int spanCount = ((GridLayoutManager)parent.getLayoutManager()).getSpanCount();
int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
boolean drawRight;
boolean drawBottom;
for(int i = 0; i < childCount; i++) {
drawRight = drawBottom = true;
if(orientation == OrientationHelper.VERTICAL){ //竖直排列的情况
if ((i + 1) % spanCount == 0)
drawRight = false; //最后一列只需要bottom方向留间隔
else { //前面的列在right和bottom方向留间隔
}
}else if(orientation == OrientationHelper.HORIZONTAL){ //水平排列的情况
if ((i + 1) % spanCount == 0)
drawBottom = false; //最后一排只需要右边留间隔
else {
//前面的排在right和bottom方向留间隔
}
}
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
if(drawRight) {
int left = child.getRight() + params.rightMargin;
int top = child.getTop() - params.topMargin;
int right = left + mDivider.getIntrinsicWidth();
int bottom = child.getBottom() + params.bottomMargin + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
if(drawBottom){
int left = child.getLeft() - params.leftMargin;
int top = child.getBottom() + params.bottomMargin;
int right = child.getRight() + params.rightMargin;
int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
Log.w(getClass().getSimpleName(),
"第"+i+"个item "+" drawRight="+drawRight+" drawBottom="+drawBottom);
}
}
使用StaggeredGridLayoutManager
可以很方便的实现瀑布流布局,其构造方法StaggeredGridLayoutManager(int spanCount, int orientation)
,spanCount代表每行或每列的Item个数,orientation代表列表的方向,竖直或者水平,限于篇幅我们就看看竖直方向的情况。
瀑布流和网格略有不同,每个item的高度不一定是一样的,所以我们需要准备多种高度的item,还需要在adapter中根据item类型分别加载:
item_staggered_1.xml 和 item_staggered_2.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="85dip"
android:background="#aa000000"
android:paddingBottom="5dp"
android:paddingRight="5dp">
<TextView
android:id="@+id/tv_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:gravity="center"/>
LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="120dip"
android:paddingBottom="5dp"
android:paddingRight="5dp"
android:background="#aa000000">
<TextView
android:id="@+id/tv_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:gravity="center"/>
LinearLayout>
Adapter中相关代码:
@Override
public int getItemViewType(int position) {
// 瀑布流样式外部设置spanCount为2,在这列设置两个不同的item type,以区分不同的布局
return position % 2;
}
@Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType){
ViewHolder viewHolder = ViewHolder.get(mContext, parent, mLayoutIds.get(viewType));
return viewHolder;
}
Activity中:
List itemLayouts = new ArrayList<>();
itemLayouts.add(R.layout.item_staggered_1);
itemLayouts.add(R.layout.item_staggered_2);
recyclerView = findViewById(R.id.recyclerView);
StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager
(2, OrientationHelper.VERTICAL);
recyclerView.setLayoutManager(manager);
adapter = new CommandRecyclerStaggeredAdapter
(this, itemLayouts, dataList) {
@Override
public void convert(ViewHolder holder, String str) {
holder.setText(R.id.tv_item, str);
}
@Override
public void onItemClick(String str, int position) {
}
};
recyclerView.setAdapter(adapter);
我们上面的代码中并没有设置分隔线样式,为什么会有分割线呢?瀑布流的分割线和列表、网格有所不同,瀑布流中item的位置不是死的,相对灵活,比如图中的item8按道理应该在右边,为什么跑到左边了呢?瀑布流在大体方向上是从上往下、从左到右的顺序,但还有一个高度低洼优先排列的规律,item8在安排位置的时候发现左边高度比右边低,则会在左边添加item8,如果有多列就会在高度最低的那列摆放下一个item。
这样我们就不能根据position确定item的具体位置,就无法通过上面的方式自定义间隔样式了,不过我们可以想其他办法,比如上面的item布局中,我们直接为item的paddingBottom和paddingRight,这样每个item之间就有间隔了。但是问题产生了,这样的话左边没有间隔,最右边有间隔,我们可以在RecyclerView上设置paddingLeft就能解决了。
到此RecyclerView的常见使用就差不多都讲完了,后面如果遇到什么问题,我相信通过上面的知识也能很快得到解决。有同学会发现效果图中的分割线颜色有些不一致,那是因为我用的半透明的色值#aa000000,这样我们能清楚的知道那个是分割线的位置,那个是ResyclerView的背景,哪里是item的区域,更方便理解。后面有些部分写得比较匆忙,不太细致,但重要的东西应该都没落下,如果后面有相关高级使用,我会继续总结。谢谢
欢迎关注,希望在这里有你想要的,博主会持续更新高(di)质(shui)量(ping)的文章和大家交流学习
喜欢请点赞,no爱请勿喷~O(∩_∩)O谢谢
##源码下载:
https://github.com/openXu/RecyclerViewDemo