RecyclerView的使用总结以及常见问题处理方案

您所在的位置:网站首页 recyclerview报错 RecyclerView的使用总结以及常见问题处理方案

RecyclerView的使用总结以及常见问题处理方案

#RecyclerView的使用总结以及常见问题处理方案| 来源: 网络整理| 查看: 265

本文是RecyclerView源码分析系列最后一篇文章, 主要讲一下我个人对于RecycleView的使用的少量思考以及少量常见的问题怎样处理。先来看一下使用RecycleView时常见的问题以及少量需求。

RecyclerView使用常见的问题和需求RecycleView设置了数据不显示

这个往往是由于你没有设置LayoutManger。 没有LayoutManger的话RecycleView是无法布局的,即是无法展现数据,下面是RecycleView布局的源码:

void dispatchLayout() { //没有设置 Adapter 和 LayoutManager, 都不可能有内容 if (mAdapter == null) { Log.e(TAG, "No adapter attached; skipping layout"); // leave the state in START return; } if (mLayout == null) { Log.e(TAG, "No layout manager attached; skipping layout"); // leave the state in START return; }}

即Adapter或者Layout任意一个为null,就不会执行布局操作。

RecyclerView数据屡次滚动后出现混乱

RecycleView在滚动过程中ViewHolder是会不断复用的,因而就会带着上一次展现的UI信息(也包含滚动状态), 所以在设置一个ViewHolder的UI时,尽量要做resetUi()操作:

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { holder.itemView.resetUi() ...设置信息UI }

resetUi()这个方法就是用来把Ui复原为最初的操作。当然假如你的每一次bindData操作会对每一个UI对象重新赋值的话就不需要有这个操作。就不会出现itemView的UI混乱问题。

如何获取当前 ItemView展现的位置

我们可能会有这样的需求: 当RecycleView中的特定Item滚动到某个位置时做少量操作。比方某个Item滚动到顶部时,展现搜索框。那怎样实现呢?

首先要获取的Item一定处于数据源的某个位置并且一定要展现在屏幕。因而我们可以直接获取这个Item的ViewHolder:

val holder = recyclerView.findViewHolderForAdapterPosition(speicalItemPos) ?: return val offsetWithScreenTop = holder.itemview.top if(offsetWithScreenTop = mAdapter.getItemCount()) { //但此时 mAdapter.getItemCount() = 5 throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + "position " + position + "(offset:" + offsetPosition + ")." + "state:" + mState.getItemCount() + exceptionLabel());}

即这时就会抛出异常。假如调用了adapter.notifyXXX的话,RecycleView就会进行一次完全的布局操作,就不会有这个异常的产生。

其实还有很多异常和这个起因差不多,比方:IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false(很多情况也是因为没有及时同步UI和数据)

所以在使用RecycleView时肯定要注意保证数据和UI的同步,数据变化,及时刷新RecyclerView, 这样就能避免很多crash。

如何对RecyclerView进行封装

现在很多app都会使用RecyclerView来构建一个页面,这个页面中有各种卡片类型。为了支持快速开发我们通常会对RecycleView的Adapter做一层封装来方便我们写各种类型的卡片,下面这种封装是我认为一种比较好的封装:

/** * 对 RecyclerView.Adapter 的封装。方便业务书写。 业务只要要解决 (UI Bean) -> (UI View) 的映射逻辑就可 */abstract class CommonRvAdapter(private val dataSource: List) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val item = createItem(viewType) return CommonViewHolder(parent.context, parent, item) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val commonViewHolder = holder as CommonViewHolder commonViewHolder.adapterItemView.bindData(dataSource[position], position) } override fun getItemCount() = dataSource.size override fun getItemViewType(position: Int): Int { return getItemType(dataSource[position]) } /** * @param viewType 需要创立的ItemView的viewType, 由 {@link getItemType(item: T)} 根据数据产生 * @return 返回这个 viewType 对应的 AdapterItemView * */ abstract fun createItem(viewType: Int): AdapterItemView /** * @param T 代表dataSource中的一个data * * @return 返回 显示 T 类型的data的 ItemView的 类型 * */ abstract fun getItemType(item: T): Int /** * Wrapper 的ViewHolder。 业务不必理睬RecyclerView的ViewHolder * */ private class CommonViewHolder(context: Context?, parent: ViewGroup, val adapterItemView: AdapterItemView) //这一点做了特殊解决,假如业务的AdapterItemView本身就是一个View,那么直接当做ViewHolder的itemView。 否则inflate出一个view来当做ViewHolder的itemView : RecyclerView.ViewHolder(if (adapterItemView is View) adapterItemView else LayoutInflater.from(context).inflate(adapterItemView.getLayoutResId(), parent, false)) { init { adapterItemView.initViews(itemView) } }}/** * 能被 CommonRvAdapter 识别的一个 ItemView 。 业务写一个RecyclerView中的ItemView,只要要实现这个接口就可。 * */interface AdapterItemView { fun getLayoutResId(): Int fun initViews(var1: View) fun bindData(data: T, post: Int)}

为什么我认为这是一个不错的封装?

业务假如写一个新的Adapter的话只要要实现两个方法:abstract fun createItem(viewType: Int): AdapterItemViewabstract fun getItemType(item: T): Int

即业务写一个Adapter只要要对 UI 数据 -> UI View 做映射就可, 不需要关心RecycleView.ViewHolder的逻辑。

由于笼统了AdapterItemView, ItemView足够灵活

因为封装了RecycleView.ViewHolder的逻辑,因而对于UI item view业务方只要要返回一个实现了AdapterItemView的对象就可。可以是一个View,也可以不是一个View, 这是由于CommonViewHolder在构造的时候对它做了兼容:

val view : View = if (adapterItemView is View) adapterItemView else LayoutInflater.from(context).inflate(adapterItemView.getLayoutResId(), parent, false)

即假如实现了AdapterItemView的对象本身就是一个View,那么直接把它当做ViewHolder的itemview,否则就inflate出一个View作为ViewHolder的itemview。

其实这里我比较推荐实现AdapterItemView的同时直接实现一个View,即不要把inflate的工作交给底层框架。比方这样:

private class SimpleStringView(context: Context) : FrameLayout(context), AdapterItemView { init { LayoutInflater.from(context).inflate(getLayoutResId, this) //自己去负责inflate工作 } override fun getLayoutResId() = R.layout.view_test override fun initViews(var1: View) {} override fun bindData(data: String, post: Int) { simpleTextView.text = data }}

为什么呢?起因有两点 :

继承自一个View可复用性很高,封装性很高。即这个SimpleStringView不仅可以在RecycleView中当一个itemView,也可以在任何地方使用。方便单元测试,直接new这个View就好了。

但其实直接继承自一个View是有坑的,即上面那行inflate代码LayoutInflater.from(context).inflate(getLayoutResId, this)

它其实是把xml文件inflate成一个View。而后add到你ViewGroup中。由于SimpleStringView就是一个FrameLayout,所有相当于add到这个FrameLayout中。这其实就有问题了。比方你的布局文件是下面这种:

.....

这就相当于你可能多加了一层无用的父View

所有假如是直接继承自一个View的话,我推荐这样写:

布局文件中尽可能使用标签来消除这层无用的父View, 即上面的改为很简单的布局的可以直接在代码中写,不要inflate。这样其实也可以减少inflate的耗时,略微提高了一点性能吧。

当然,假如你不需要对这个View做复用的话你可以不用直接继承自View,只实现AdapterItemView接口, inflate的工作交给底层框架就可。这样是不会产生上面这个问题的。

这篇文章就先说这么多吧。欢迎关注我的Android进阶计划。看更多干货。

另外欢迎浏览我的RecyclerView源码分析系列的其余文章:

RecyclerView源码分析系列



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3