背景
想必接触 Android 开发不久的开发者应该都听说过类似的经验之谈:RecycleView 放在 ScrollView 内会导致内存溢出、RecycleView 内不能嵌套其他 RecycleView 等等。但其实实际业务场景中这类需求也不是没有的,比如:垂直滑动的列表中,某部分的数据需要支持横向滑动;列表滚动到一定位置后,顶部 Tab 栏自动吸顶;侧边菜单中某部分需要可以展开收起且支持滑动,该部分数据来自接口等等。
以上场景各有处理方案,限于篇幅本文只讨论:当同滚动方向的 RecycleView 嵌套在 ScrollView 内时,处理内存溢出和内外滑动冲突的问题。
注:滑动方向不同的 RecycleView 嵌套属于滑动冲突处理,通常会根据不同方向的滑动距离来判断交互意图进而分配相应组件;NestedScrollView 支持另一套机制更优雅地处理联动(如吸顶效果),后续会放在另外的文章中讨论
方案
以下处理 ScrollView 内嵌套 RecycleView 的思路,同样适用于两个 RecycleView 嵌套的场景
从问题来看,要解决嵌套主要是 2 个问题:(1)控制内部 RecycleView 的 ItemView 复用,不能有多少数据就渲染多少控件,进而防止内存溢出。(2)处理上下滑动到内层 RecycleView 边界时的交互效果:当滑动到内层 RecycleView 时得让内层 RecycleView 响应事件,但当内层 RecycleView 内容滚动完毕后要及时把事件交给外部处理。
问题(1)好解决,只需根据业务需要限制好内层 RecycleView 的尺寸(此处以垂直滚动为例,即为限制高度,下同)即可;问题(2)则需根据具体的业务场景需要来控制事件响应,本文以抽屉菜单列表中嵌套二级可滚动菜单为例(通常这类场景中二级菜单的数据来自网络)
布局
<com.chavin.test.OuterScrollView android:id="@+id/outer">
<LinearLayout>
<TextView android:text="固定菜单项1" />
<TextView android:text="固定菜单项2" />
<TextView android:text="固定菜单项3" />
<!-- 滑动后吸顶部分,高度:50dp -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#F99"
android:gravity="center_vertical|start"
android:text="点击展开收起二级菜单" />
</LinearLayout>
<!-- 内层RecycleView -->
<com.chavin.test.InnerRecyclerView
android:id="@+id/inner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.chavin.test.OuterScrollView>
限制 RecycleView 大小
// 初始化最大高度
val displayMetrics = resources.displayMetrics
mInnerRecyclerView.init(findViewById(R.id.outer), displayMetrics.heightPixels - dp2px(50F))
// 保存最大高度,Measure是做限制
class InnerRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RecyclerView(context, attrs) {
private var mLastY: Float = 0F
private lateinit var mParentView: ViewGroup
private var maxH: Int = 0
fun init(parentView: ViewGroup, maxH: Float) {
this.mParentView = parentView;
this.maxH = maxH.toInt();
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
// 控制高度
val hSpec = MeasureSpec.makeMeasureSpec(maxH, MeasureSpec.AT_MOST);
super.onMeasure(widthSpec, hSpec)
}
}
处理滑动冲突
由于朴素的事件分发有其局限性,在双层滑动边界处有顿挫感,可考虑外层换用
NestedScrollView,利用Nested事件共享机制改进滑动体验
class OuterScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ScrollView(context, attrs) {
private var mLastY: Float = 0F
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_MOVE) {
// 向下滚动,尽量拦截
if (ev.y < mLastY && canScrollVertically(1)) {
super.onInterceptTouchEvent(ev)
mLastY = ev.y
return true
}
}
mLastY = ev.y;
return super.onInterceptTouchEvent(ev)
}
}
class InnerRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RecyclerView(context, attrs) {
private var mLastY: Float = 0F
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_MOVE) {
// 向上滚动,若已到头,直接放弃
if (ev.y > mLastY && !canScrollVertically(-1)) {
mLastY = ev.y
super.dispatchTouchEvent(ev)
return false
}
}
mLastY = ev.y
return super.dispatchTouchEvent(ev)
}
}