本文是
RecyclerView源码分析系列最后一篇文章
, 主要讲一下我我的对于RecycleView
的使用的一些思考以及一些常见的问题怎么解决。先来看一下使用RecycleView
时常见的问题以及一些需求。android
这个每每是由于你没有设置LayoutManger
。 没有LayoutManger
的话RecycleView
是没法布局的,便是没法展现数据,下面是RecycleView
布局的源码:git
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,就不会执行布局操做。github
RecycleView
在滚动过程当中ViewHolder
是会不断复用的,所以就会带着上一次展现的UI信息(也包含滚动状态), 因此在设置一个ViewHolder
的UI时,尽可能要作resetUi()
操做:bash
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.resetUi()
...设置信息UI
}
复制代码
resetUi()
这个方法就是用来把Ui还原为最初的操做。固然若是你的每一次bindData
操做会对每个UI对象从新赋值的话就不须要有这个操做。就不会出现itemView
的UI混乱问题。app
咱们可能会有这样的需求: 当RecycleView
中的特定Item
滚动到某个位置时作一些操做。好比某个Item
滚动到顶部时,展现搜索框。那怎么实现呢?框架
首先要获取的Item确定处于数据源的某个位置而且确定要展现在屏幕。所以咱们能够直接获取这个Item
的ViewHolder
:ide
val holder = recyclerView.findViewHolderForAdapterPosition(speicalItemPos) ?: return
val offsetWithScreenTop = holder.itemview.top
if(offsetWithScreenTop <= 0){ //这个ItemView已经滚动到屏幕顶部
//do something
}
复制代码
smoothScrollToPosition()
你们应该都用过,若是滚动二、3个Item。那么总体的用户体验仍是很是棒的。源码分析
可是,若是你滚动20个Item,那这个体验可能就会就不好了,由于用户看到的多是下面这样子:布局
恩,滚动的时间有点长。所以对于这种case其实我推荐直接使用scrollToPosition(20)
,效果要比这个好。 但是若是你就是想在200ms
内从Item 1
滚到Item 20
怎么办呢?post
能够参考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
中的每一个ItemView
的高度都是不固定的。我数据源中有20条数据,在没有渲染的状况下我想知道这个20条数据被RecycleView
渲染后的总共高度, 好比下面这个图片:
怎么作呢?个人思路是利用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()
}
复制代码
这个异常一般是因为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。
如今不少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)
}
复制代码
为何我认为这是一个不错的封装?
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 }
}
复制代码
为何呢?缘由有两点 :
SimpleStringView
不只能够在RecycleView
中当一个itemView
,也能够在任何地方使用。但其实直接继承自一个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>
固然,若是你不须要对这个View作复用的话你能够不用直接继承自View
,只实现AdapterItemView
接口, inflate的工做交给底层框架便可。这样是不会产生上面这个问题的。
这篇文章就先说这么多吧。欢迎关注个人Android进阶计划。看更多干货。
另外欢迎浏览个人
RecyclerView源码分析系列
的其余文章: