RecyclerView的使用总结以及常见问题处理方案
本文是
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 <= 0){ //这个ItemView已经滚动到屏幕顶部 //do something }
如何在固定时间内滚动一款距离
smoothScrollToPosition()
大家应该都用过,假如滚动2、3个Item。那么整体的客户体验还是非常棒的。
但是,假如你滚动20个Item,那这个体验可能就会就很差了,由于客户看到的可能是下面这样子:
smoothScroll20.gif
恩,滚动的时间有点长。因而对于这种case其实我推荐直接使用scrollToPosition(20)
,效果要比这个好。 可是假如你就是想在200ms
内从Item 1
滚到Item 20
怎样办呢?
可以参考StackOverflow上的一个答案。大致写法是这样的:
//自己设置 LayoutManager, Hook smoothScrollToPosition 方法recyclerView.layoutManager = object : LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) { override fun smoothScrollToPosition(recyclerView: RecyclerView?, state: RecyclerView.State?, position: Int) { if (recyclerView == null) return val scroller = get200MsScroller(recyclerView.context, position * 500) scroller.targetPosition = position startSmoothScroll(scroller) }}private fun get200MsScroller(context: Context, distance: Int): RecyclerView.SmoothScroller = object : LinearSmoothScroller(context) { override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float { return (200.0f / distance) //表示滚动 distance 花费200ms }}
比方上面我把时间改为10000
,那么就是用10s的时间完成这个滚动操作。
如何测量当前RecyclerView的高度
先形容一下这个需求: RecyclerView
中的每个ItemView
的高度都是不固定的。我数据源中有20条数据,在没有渲染的情况下我想知道这个20条数据被RecycleView
渲染后的总共高度, 比方下面这个图片:
RecyclerView中Item高度各不相同.png
怎样做呢?我的思路是利用LayoutManager
来测量,由于RecycleView
在对子View
进行布局时就是用LayoutManager
来测量子View
来计算还有多少剩余空间可用,源码如下:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); //这个方法会向 recycler要一个View ... measureChildWithMargins(view, 0, 0); //测量这个View的尺寸,方便布局, 这个方法是public ... }
所以我们也可以利用layoutManager.measureChildWithMargins
方法来测量,代码如下:
private fun measureAllItemHeight():Int { val measureTemplateView = SimpleStringView(this) var totalItemHeight = dataSource.forEach { //dataSource当前中的所有数据 measureTemplateView.bindData(it, 0) //设置好UI数据 recyclerView.layoutManager.measureChild(measureTemplateView, 0, 0) //调用源码中的子View的测量方法 currentHeight += measureTemplateView.measuredHeight } return totalItemHeight }
但要注意的是,这个方法要等布局稳固的时候才可以用,假如你在Activity.onCreate
中调用,那么应该post
一下, 即:
recyclerView.post{ val totalHeight = measureAllItemHeight()}
IndexOutOfBoundsException: Inconsistency detected. Invalid item position 5(offset:5).state:9
这个异常通常是因为Adapter的数据源大小
改变没有及时通知RecycleView
做UI刷新导致的,或者者通知的方式有问题。 比方假如数据源变化了(比方数量变少了),而没有调用notifyXXX
, 那么此时滚动RecycleView
就会产生这个异常。
处理办法很简单 : Adapter的数据源
改变时应立即调用adapter.notifyXXX
来刷新RecycleView
。
分析一下这个异常为什么会产生:
在RecycleView刷新机制
一文详情过,RecycleView
的滚动操作是不会走RecycleView
的正常布局过程的,它直接根据滚动的距离来摆放新的子View
。 想象一下这种场景,原来数据源集合中
有8个Item,而后删除了4个后没有调用adapter.notifyXXX()
,这时直接滚动RecycleView
,比方滚动将要出现的是第6个Item,LinearLayoutManager
就会向Recycler
要第6个Item的View:
Recycler.tryGetViewHolderForPositionByDeadline()
:
final int offsetPosition = mAdapterHelper.findPositionOffset(position); //position是6 if (offsetPosition < 0 || offsetPosition >= 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<T>(private val dataSource: List<T>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { 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<T> 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<T> /** * @param T 代表dataSource中的一个data * * @return 返回 显示 T 类型的data的 ItemView的 类型 * */ abstract fun getItemType(item: T): Int /** * Wrapper 的ViewHolder。 业务不必理睬RecyclerView的ViewHolder * */ private class CommonViewHolder<T>(context: Context?, parent: ViewGroup, val adapterItemView: AdapterItemView<T>) //这一点做了特殊解决,假如业务的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<T> { fun getLayoutResId(): Int fun initViews(var1: View) fun bindData(data: T, post: Int)}
为什么我认为这是一个不错的封装?
业务假如写一个新的Adapter的话只要要实现两个方法:
abstract fun createItem(viewType: Int): AdapterItemView<T>abstract 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<String> { 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
中。这其实就有问题了。比方你的布局文件是下面这种:
<FrameLayout>.....</FrameLayout>
这就相当于你可能多加了一层无用的父View
所有假如是直接继承自一个View的话,我推荐这样写:
- 布局文件中尽可能使用
<merge>
标签来消除这层无用的父View, 即上面的<FrameLayout>
改为<merge>
- 很简单的布局的可以直接在代码中写,不要inflate。这样其实也可以减少inflate的耗时,略微提高了一点性能吧。
当然,假如你不需要对这个View做复用的话你可以不用直接继承自View
,只实现AdapterItemView
接口, inflate的工作交给底层框架就可。这样是不会产生上面这个问题的。
这篇文章就先说这么多吧。欢迎关注我的Android进阶计划。看更多干货。
另外欢迎浏览我的RecyclerView源码分析系列
的其余文章:
RecyclerView源码分析系列
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » RecyclerView的使用总结以及常见问题处理方案