RecyclerView使用攻略(助力篇)
来源:互联网 发布:ubuntu 建dns 编辑:程序博客网 时间:2024/06/10 16:19
小序
实际上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>
结尾助力
以上为本篇重点简述内容,剩下的讲讲何为”助力篇“的实际意义。一方面,个人觉得为RecyclerView添加分割符的形式有些毛躁,因为判断太多直接影响性能。另一方面是自己能力尚浅,没有办法将其优化好,包括逻辑处理以及动画效果的延伸。
如果你觉得上面的gif演示已经比较nice了,那么再请你看看下面的另一个gif演示:
看完上面两者的比较以后不难发现存在的问题了,当然如果用回notifyDataSetChanged();是可以尽量避免这些问题的。而这里不仅是为了将存在的问题表露出来,更多的是为了能让有实力的小伙伴们帮忙解决问题,或者提供一些已有的解决方案。以下为浮现的问题:
- Item进出动画卡顿
- 分割线配合动画使用绘制无刷新(可能页面显示无更新,又或者只是绘制逻辑上出现的错误)
- 有些情况下使用动画会有闪屏的现象,体验较差。如上面gif从底部scrollToPosition()到顶部时候出现闪屏
- …
Fork助力项目地址:https://github.com/gzejia/URecyclerView
分隔符使用参考:Android RecyclerView 使用完全解析 体验艺术般的控件
添加头部/底部视图参考:RecyclerView添加Header的正确方式
- RecyclerView使用攻略(助力篇)
- RecyclerView使用攻略(刷新篇)
- RecyclerView的使用全攻略
- RecyclerView的不完全使用攻略+点击单选(未完)
- RecyclerView使用(一)
- RecyclerView全攻略进阶优化
- RecyclerView使用:初步(1)
- RecyclerView使用详解(一)
- RecyclerView使用详解(二)
- RecyclerView使用详解(三)
- RecyclerView使用详解(一)
- RecyclerView使用详解(二)
- RecyclerView使用详解(三)
- RecyclerView使用详解(一)
- RecyclerView使用详解(二)
- RecyclerView使用详解(三)
- RecyclerView使用详解(一)
- RecyclerView使用详解(二)
- 记一个 子线程里跟新ui的便捷操作
- 禁止输入表情
- creatjs 开发工具amimatecc下载地址和破解
- Cocos2d-x3.x游戏开发之旅按钮 一处错误
- 让32位Eclipse和64位Eclipse同时在64的Windows7上运行
- RecyclerView使用攻略(助力篇)
- C++动态内存分配
- kubenetes 1.4 安装后8080端口无法访问
- org.json源码分析及增强(一)——JSONObject对象与Java对象相互转换
- 使用Python的MySQL数据库接口实现简单的银行转账
- Codeforces Round #377 (Div. 2)A,B,C,D【二分】
- NSURLSession基本使用
- Eclipse启动Tomcat错误:Several ports (8080, 8009)端口被占用
- Simple JavaScript Inheritance 源码分析