小序
实际上RecyclerView已不是什么新颖的话题了,至少对于用过的前辈们而言是这样的,并且大部分人都会觉得这斯很强大,必须上。而对于刚接触的小伙伴们,难免会遇到各种问题,或是因为陌生,又或是因为项目需求(譬如:侧滑出现删除按钮,拖动与上下拉刷新等等)。虽有云“前人种树,后人乘凉”一说,但树依然是需要后人来灌溉的,至少这么说是为了铺垫。
显示效果
上面图示仅是RecyclerView数据显示的基本使用,包括Adapter、Divider,相信绝大部分人都是在Hongyang前辈的树下乘过凉,由于其还没有考虑到关于RecyclerView的扩展问题,所以当我们在使用的时候难免会遇到以下两个存在的主流问题:
- 列表添加头部(底部)视图,也保留相应的分割线;
- 瀑布流列表Item的高度差导致的位置混乱,以及添加分割线出错;
解决问题与效果实现
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.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.LayoutManager;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
import android.widget.LinearLayout;
/**
* RecyclerView 分割符
*/
public class RecyclerVDivider extends RecyclerView.ItemDecoration {
private Drawable mDividerDraw;
private int mLeftSpace, mRightSpace, mTopSpace, mBottomSpace;
private int mSpaceColor;
/**
* @param context src/main/res/values/styles <item name="android:listDivider"/>
*/
public RecyclerVDivider(Context context) {
TypedArray a = context.obtainStyledAttributes(new int[]{android.R.attr.listDivider});
mDividerDraw = a.getDrawable(0);
a.recycle();
}
/**
* @param drawable src/main/res/drawable
*/
public RecyclerVDivider(Drawable drawable) {
mDividerDraw = drawable;
}
/**
* @param leftSpace 左侧间距
*/
public void setLeftSpace(int leftSpace) {
mLeftSpace = leftSpace;
}
/**
* @param rightSpace 右侧间距
*/
public void setRightSpace(int rightSpace) {
mRightSpace = rightSpace;
}
/**
* @param topSpace 顶部间距
*/
public void setTopSpace(int topSpace) {
mTopSpace = topSpace;
}
/**
* @param bottomSpace 底部间距
*/
public void setBottomSpace(int bottomSpace) {
mBottomSpace = bottomSpace;
}
/**
* @param spaceColor 分割线间距颜色,避免浮现父容器颜色
*/
public void setSpaceColor(int spaceColor) {
mSpaceColor = spaceColor;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (0 != mSpaceColor) c.drawColor(mSpaceColor);
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 + mLeftSpace;
final int right = child.getRight() + params.rightMargin
+ mDividerDraw.getIntrinsicWidth() - mRightSpace;
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDividerDraw.getIntrinsicHeight();
mDividerDraw.setBounds(left, top, right, bottom);
mDividerDraw.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 + mTopSpace;
final int bottom = child.getBottom() + params.bottomMargin - mBottomSpace;
final int left = child.getRight() + params.rightMargin;
final int right = left + mDividerDraw.getIntrinsicWidth();
mDividerDraw.setBounds(left, top, right, bottom);
mDividerDraw.draw(c);
}
}
private boolean isVertical(LayoutManager layoutManager) {
if (layoutManager instanceof GridLayoutManager) {
return ((GridLayoutManager) layoutManager).getOrientation() == LinearLayout.VERTICAL;
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager) layoutManager).getOrientation() == LinearLayout.VERTICAL;
} else if (layoutManager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) layoutManager).getOrientation() == LinearLayout.VERTICAL;
}
return true;
}
/**
* @param adapter RecyclerView.Adapter
* @return 当前遍历Item所处列数
*/
private int getSpanIndex(RecyclerView.Adapter adapter) {
if (adapter instanceof BaseRVAdapter) {
return ((BaseRVAdapter) adapter).mSpanIndex + 1;
}
return 0;
}
/**
* @param adapter RecyclerView.Adapter
* @param spanCount 最大可显示列数
* @return 所需绘制底部分割线的最大行数(除去底部视图以及最后一排数据)
*/
private int getMaxRaw(RecyclerView.Adapter adapter, int spanCount) {
int childCount = adapter.getItemCount();
int maxRawSize = childCount - childCount % spanCount;
if (adapter instanceof BaseRVAdapter) {
BaseRVAdapter baseRVAdapter = (BaseRVAdapter) adapter;
if (null != baseRVAdapter.mFooterViews && null != baseRVAdapter.mHeaderViews) {
childCount = baseRVAdapter.mList.size();
maxRawSize = childCount - childCount % spanCount;
maxRawSize += baseRVAdapter.mHeaderViews.size();
}
}
return maxRawSize;
}
/**
* @param adapter RecyclerView.Adapter
* @return true_表示当前为数据Item,false_标识当前为头部/底部View
*/
private boolean getItemState(RecyclerView.Adapter adapter) {
if (adapter instanceof BaseRVAdapter) {
return ((BaseRVAdapter) adapter).isMainItem;
}
return true;
}
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
LayoutManager layoutManager = parent.getLayoutManager();
int spanCount = getSpanCount(parent);
boolean isLastRaw = isVertical(layoutManager) ?
itemPosition + 1 >= getMaxRaw(parent.getAdapter(), spanCount) :
getSpanIndex(parent.getAdapter()) == spanCount;
boolean isLastColum = isVertical(layoutManager) ?
spanCount == getSpanIndex(parent.getAdapter()) :
itemPosition + 1 >= getMaxRaw(parent.getAdapter(), spanCount);
if (getItemState(parent.getAdapter())) {
outRect.set(0, 0,
isLastColum ? 0 : mDividerDraw.getIntrinsicWidth(),// 如果是最后一列,则不需要绘制右边
isLastRaw ? 0 : mDividerDraw.getIntrinsicHeight());// 如果是最后一行,则不需要绘制底部
}
}
}
可见分割线的绘制判断改动比较大,且与自定义RecyclerView.Adapter的实现类BaseRVAdapter.class耦合在一起了。但为了解决以上存在的两个问题,这也是没有办法中的办法(表示蓝瘦到香菇)。以上简单的判断处理相信大家是能看懂,主要还是对这些判断的数据来源比较感冒些。
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* RecyclerView 适配器
*/
public abstract class BaseRVAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
public static final int TYPE_NORMAL = 0;
public static final int TYPE_HEADER = 1;
public static final int TYPE_FOOTER = -1;
public Context mContext;
public List<T> mList = new ArrayList<>();
public List<View> mHeaderViews = new ArrayList<>();
public List<View> mFooterViews = new ArrayList<>();
public boolean isCloseItemAnim;
public BaseRVAdapter(Context context, @NonNull List<T> list) {
this.mList = list;
this.mContext = context;
}
public T getData(int position) {
return mList.get(position);
}
public void updateAdapter(@NonNull List<T> list) {
mList.clear();
mList.addAll(list);
notifyDataSetChanged();
// notifyItemRangeChanged(startIndex(0), mList.size());
}
public void updateData(int position, T object) {
mList.set(position, object);
if (isCloseItemAnim) {
notifyDataSetChanged();
} else {
int updateIndex = startIndex(position);
notifyItemChanged(updateIndex);
notifyItemRangeChanged(updateIndex, mList.size());
}
}
public void cleanData() {
if (!isCloseItemAnim) {
mList.clear();
notifyDataSetChanged();
} else {
int cleanIndex = mHeaderViews.size();
notifyItemRangeRemoved(cleanIndex, mList.size());
mList.clear();
notifyItemRangeChanged(cleanIndex, mList.size());
}
}
public void removeData(int position) {
if (mList.size() <= position) return;
if (isCloseItemAnim) {
mList.remove(position);
notifyDataSetChanged();
} else {
notifyItemRemoved(startIndex(position));
mList.remove(position);
notifyItemRangeChanged(startIndex(position), mList.size());
}
}
public void addDataLs(final int position, @NonNull List<T> list) {
mList.addAll(position, list);
if (isCloseItemAnim) {
notifyDataSetChanged();
} else {
int addIndex = startIndex(position);
notifyItemRangeInserted(addIndex, list.size());
notifyItemRangeChanged(addIndex, mList.size());
}
}
public void addDataLs(@NonNull List<T> list) {
addDataLs(startIndex(mList.size()), list);
}
public void addData(int position, T object) {
int startIndex = startIndex(position);
mList.add(position, object);
if (isCloseItemAnim) {
notifyDataSetChanged();
} else {
notifyItemInserted(startIndex);
notifyItemRangeChanged(startIndex, mList.size());
}
}
public void addData(T object) {
addData(startIndex(mList.size()), object);
}
public void addHeaderViews(@NonNull List<View> headerViews) {
mHeaderViews.addAll(headerViews);
notifyDataSetChanged();
}
public void addHeaderView(@NonNull View headerView) {
mHeaderViews.add(headerView);
notifyDataSetChanged();
}
public void addFooterViews(@NonNull List<View> footerViews) {
mFooterViews.addAll(footerViews);
notifyDataSetChanged();
}
public void addFooterView(@NonNull View footerView) {
mFooterViews.add(footerView);
notifyDataSetChanged();
}
private int startIndex(int doneIndex) {
return doneIndex + mHeaderViews.size();
}
/**
* {@link #TYPE_HEADER} 头部列表
* {@link #TYPE_NORMAL} 正常列表
* {@link #TYPE_FOOTER} 底部列表
*/
@Override
public int getItemViewType(int position) {
if (!mHeaderViews.isEmpty() && position < mHeaderViews.size()) {
return TYPE_HEADER + position;
} else if (!mFooterViews.isEmpty() && position >= mHeaderViews.size() + mList.size()) {
int beforeSize = mHeaderViews.size() + mList.size();
return TYPE_FOOTER - (position - beforeSize);
} else {
return TYPE_NORMAL;
}
}
@Override
public int getItemCount() {
if ((null == mList || mList.isEmpty())) {
return null == mHeaderViews ? 0 : mHeaderViews.size()
+ (null == mFooterViews ? 0 : mFooterViews.size());
} else {
return mHeaderViews.size() + mList.size() + mFooterViews.size();
}
}
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (!mHeaderViews.isEmpty() && viewType > TYPE_NORMAL) {
return new BaseViewHolder(mHeaderViews.get(viewType - 1));
} else if (!mFooterViews.isEmpty() && viewType <= TYPE_FOOTER) {
return new BaseViewHolder(mFooterViews.get(-viewType - 1));
} else {
View view = LayoutInflater.from(mContext).inflate(getLayoutId(viewType),
parent, false);
return new BaseViewHolder(view);
}
}
public abstract int getLayoutId(int viewType);
@Override
public void onBindViewHolder(final BaseViewHolder holder, int position) {
if (getItemViewType(position) != TYPE_NORMAL) return;
onBind(holder, position - mHeaderViews.size());
if (null != mOnItemClickListener) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mOnItemClickListener.itemSelect(
holder.getAdapterPosition() - mHeaderViews.size());
}
});
}
}
public abstract void onBind(BaseViewHolder holder, int position);
@Override
public void onViewRecycled(final BaseViewHolder holder) {
super.onViewRecycled(holder);
}
@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_NORMAL ? gridManager.getSpanCount() : 1;
}
});
}
}
/**
* @see RecyclerVDivider
* <ul>判断是否添加分割符的标识
* <li>true_添加,false_不添加</li></ul>
* Title:另外使用分割符的情况下可删除该变量
*/
public boolean isMainItem;
/**
* @see RecyclerVDivider
* 获取GridLayoutManager/StaggeredGridLayoutManager当前Item项所处的列数(行数)
* 提示:另外使用分割符的情况下可删除该变量
*/
public int mSpanIndex;
@Override
public void onViewAttachedToWindow(BaseViewHolder holder) {
super.onViewAttachedToWindow(holder);
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (null == lp) return;
isMainItem = getItemViewType(holder.getLayoutPosition()) == TYPE_NORMAL;
if (lp instanceof GridLayoutManager.LayoutParams) {
GridLayoutManager.LayoutParams p = (GridLayoutManager.LayoutParams) lp;
mSpanIndex = p.getSpanIndex();
} else if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
mSpanIndex = p.getSpanIndex();
p.setFullSpan(!isMainItem);
}
}
private OnItemClickListener mOnItemClickListener;
public void addItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
public interface OnItemClickListener {
void itemSelect(int position);
}
}
本来还想着直接贴重点相关的代码块就行了,毕竟这些实现都大同小异,再说就这些也上不了排场。但是我左思右想觉得不成,这个都得贴上,这样若是出现其他问题“怪我咯~”。大伙重点看onViewAttachedToWindow()就可以了,因为关于分割线绘制与否的数据判定基本上就是根据这里劫取的。
自定义RecyclerView
除此之外因RecyclerView在使用前操作过多嫌麻烦,于是自定义了一个RecyclerView的实现类,并且已在RecyclerVDivider.class当中做了相应的关联和处理,若不嫌弃可一并拿去。
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.AttributeSet;
import com.mrv.R;
import com.mrv.utils.UnitConverter;
/**
* 重定义RecyclerView使用
*/
public class MyRecyclerView extends RecyclerView {
public static final int TYPE_LIST = 0;
public static final int TYPE_GRID = 1;
public static final int TYPE_STAGGERED_GRID = 2;
/**
* <ul>列表类型
* <li>同 ListView :{@link #TYPE_LIST}(默认)</li>
* <li>同 GridView :{@link #TYPE_GRID}</li>
* <li>同 StaggeredGridView :{@link #TYPE_STAGGERED_GRID}</li></ul>
*/
private int mType;
public static final int VERTICAL = 1;
public static final int HORIZONTAL = 0;
/**
* <ul>列表类型
* <li>垂直方向 :{@link #VERTICAL}(默认)</li>
* <li>水平方向 :{@link #HORIZONTAL}</li></ul>
*/
private int mOrientation;
/**
* ?固定大小,默认固定
*/
private boolean isFixSize = true;
private static final int DEFAULT_ROW_NUM = 2;
/**
* GRID与STAGGERED_GRID显示列数,默认{@link #DEFAULT_ROW_NUM}列
*/
private int mSpanCount;
/**
* @see RecyclerVDivider
* 分割线,默认从styles中获取
*/
private Drawable mDividerDraw;
/**
* 分割线左侧边距,默认无边距
*/
private int mDividerLeftSpace;
/**
* 分割线右侧边距,默认无边距
*/
private int mDividerRightSpace;
/**
* 分割线顶部边距,默认无边距
*/
private int mDividerTopSpace;
/**
* 分割线底部边距,默认无边距
*/
private int mDividerBottomSpace;
/**
* 分割线间距颜色(即背景色),默认无
*/
private int mDividerSpaceColor;
public MyRecyclerView(Context context) {
super(context);
initView();
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.recycle_view);
isFixSize = typedArray.getBoolean(R.styleable.recycle_view_fixSize, true);
mSpanCount = typedArray.getInteger(R.styleable.recycle_view_spanCount, DEFAULT_ROW_NUM);
mType = typedArray.getInt(R.styleable.recycle_view_type, TYPE_LIST);
mOrientation = typedArray.getInt(R.styleable.recycle_view_orientation, VERTICAL);
mDividerDraw = typedArray.getDrawable(R.styleable.recycle_view_divider);
mDividerLeftSpace = typedArray.getInt(R.styleable.recycle_view_dividerLeftSpace, 0);
mDividerRightSpace = typedArray.getInt(R.styleable.recycle_view_dividerRightSpace, 0);
mDividerTopSpace = typedArray.getInt(R.styleable.recycle_view_dividerTopSpace, 0);
mDividerBottomSpace = typedArray.getInt(R.styleable.recycle_view_dividerBottomSpace, 0);
mDividerSpaceColor = typedArray.getColor(R.styleable.recycle_view_dividerSpaceColor, 0);
typedArray.recycle();
initView();
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private void initView() {
// 布局样式
switch (mType) {
case TYPE_LIST:
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(
getContext(), mOrientation, false);
this.setLayoutManager(linearLayoutManager);
break;
case TYPE_GRID:
GridLayoutManager gridLayoutManager = new GridLayoutManager(
getContext(), mSpanCount, mOrientation, false);
this.setLayoutManager(gridLayoutManager);
break;
case TYPE_STAGGERED_GRID:
StaggeredGridLayoutManager staggeredGridLayoutManager =
new StaggeredGridLayoutManager(mSpanCount, mOrientation);
this.setLayoutManager(staggeredGridLayoutManager);
break;
}
// 分割线
if (null != mDividerDraw) {
RecyclerVDivider divider = new RecyclerVDivider(mDividerDraw);
divider.setLeftSpace(UnitConverter.dip2px(getContext(), mDividerLeftSpace));
divider.setRightSpace(UnitConverter.dip2px(getContext(), mDividerRightSpace));
divider.setTopSpace(UnitConverter.dip2px(getContext(), mDividerTopSpace));
divider.setBottomSpace(UnitConverter.dip2px(getContext(), mDividerBottomSpace));
if (0 != mDividerSpaceColor) divider.setSpaceColor(mDividerSpaceColor);
this.addItemDecoration(divider);
}
// 大小固定
this.setHasFixedSize(isFixSize);
// Item出入动画
this.setItemAnimator(new DefaultItemAnimator());
}
public int getSpanCount() {
return mSpanCount;
}
public int getOrientation() {
return mOrientation;
}
public int getType() {
return mType;
}
}
下面是关于自定义属性的内容与对应实现功能,其中需要注意的是如果启动了分割符的边距设置,这需要相应的设置一下分割符的颜色,从而避免产生的边距露出Item的父容器背景颜色。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="recycle_view">
<attr name="fixSize" format="boolean" />
<attr name="spanCount" format="integer" />
<attr name="divider" format="reference"/>
<attr name="dividerLeftSpace" format="integer" />
<attr name="dividerRightSpace" format="integer" />
<attr name="dividerTopSpace" format="integer" />
<attr name="dividerBottomSpace" format="integer" />
<attr name="dividerSpaceColor" format="color" />
<attr name="type">
<flag name="list" value="0" />
<flag name="grid" value="1" />
<flag name="staggeredGrid" value="2" />
</attr>
<attr name="orientation">
<flag name="vertical" value="1" />
<flag name="horizontal" value="0" />
</attr>
</declare-styleable>
</resources>
Name | Format | Function |
---|---|---|
fixSize | boolean | 设置true提升性能,默认true |
spanCount | integer | 网格以及流布局显示最大列数,默认2 |
divider | reference | 分割符资源Id,默认没有分割符 |
dividerLeftSpace | integer | 分隔符左边距,默认0,单位dp |
dividerRightSpace | integer | 分隔符左边距,默认0,单位dp |
dividerTopSpace | integer | 分隔符顶部边距,默认0,单位dp |
dividerBottomSpace | integer | 分隔符底部边距,默认0,单位dp |
dividerSpaceColor | color | 分隔符底部颜色,默认无 |
dividerSpaceColor | color | 分隔符底部颜色,默认无 |
type | flag | list:列表;grid:网格;staggeredGrid:瀑布流 |
orientation | flag | vertical:垂直方向延伸; horizontal:水平方向延伸 |
结尾助力
以上为本篇重点简述内容,剩下的讲讲何为”助力篇“的实际意义。一方面,个人觉得为RecyclerView添加分割符的形式有些毛躁,因为判断太多直接影响性能。另一方面是自己能力尚浅,没有办法将其优化好,包括逻辑处理以及动画效果的延伸。
如果你觉得上面的gif演示已经比较nice了,那么再请你看看下面的另一个gif演示:
看完上面两者的比较以后不难发现存在的问题了,当然如果用回notifyDataSetChanged();是可以尽量避免这些问题的。而这里不仅是为了将存在的问题表露出来,更多的是为了能让有实力的小伙伴们帮忙解决问题,或者提供一些已有的解决方案。以下为浮现的问题:
- Item进出动画卡顿
- 分割线配合动画使用绘制无刷新(可能页面显示无更新,又或者只是绘制逻辑上出现的错误)
- 有些情况下使用动画会有闪屏的现象,体验较差。如上面gif从底部scrollToPosition()到顶部时候出现闪屏
- …
Fork助力项目地址:https://github.com/gzejia/URecyclerView
分隔符使用参考:Android RecyclerView 使用完全解析 体验艺术般的控件
添加头部/底部视图参考:RecyclerView添加Header的正确方式