PersistentRecyclerView
仿京东首页,整体是个长列表(ParentRecyclerView),内嵌子列表 - 商品 feeds 流(ChildRecyclerView),且商品流可以左右滑动。
实现效果
点击可查看截屏视频:
使用方法
- 外部的长列表使用 ParentRecyclerView;
- 内嵌的子列表使用 ChildRecyclerView;
Adapter 及 ViewHolder 跟官方 Recyclerview 一样,ViewPager 和 ViewPager2 可随意选用,均已内部兼容;
实现原理
通过uiautomatorviewer观察京东首页的 View 层级,会发现其长列表总体是个 RecyclerView,设为ParentRecyclerView;而底部的商品 feeds 流是另一个 Recyclerview,设为ChildRecyclerView。关键要解决这 2 个问题:
问题一:ParentRecyclerView 触底时,Fling 速率传递给 ChildRecyclerView;
问题二:ChildRecyclerView 触顶时,Fling 速率传递给 ParentRecyclerView;
这两个问题,都避不开一个问题,即:如何获取当前 RecyclerView 的 Fling 速率?
在阅读 RecyclerView 源码后,发现 RecyclerView 内部保存了一个 mViewFlinger 对象,而 mViewFlinger 内部持有 OverScroller。于是,获取当前 RecyclerView 的 Fling 速率便迎刃而解:
private val overScroller: OverScroller
init {
// 1. mViewFlinger 对象获取
val viewFlingField = RecyclerView::class.java.getDeclaredField("mViewFlinger")
viewFlingField.isAccessible = true
var viewFlingObj = viewFlingField.get(this)
// 2. overScroller 对象获取
val overScrollerFiled = viewFlingObj.javaClass.getDeclaredField("mOverScroller")
overScrollerFiled.isAccessible = true
overScroller = overScrollerFiled.get(viewFlingObj) as OverScroller
}
/**
* 获取垂直方向的速率
*/
fun getVelocityY(): Int = (overScroller.currVelocity).toInt()
拿到当前 RecyclerView 的 Fling 速率之后,接下来就是将 Fling 速率传递给另一个 RecyclerView 了!这个比较简单,因为 RecyclerView 对外开放了 fling()方法,可直接调用:
/**
* Begin a standard fling with an initial velocity along each axis in pixels per second.
* If the velocity given is below the system-defined minimum this method will return false
* and no fling will occur.
*
* @param velocityX Initial horizontal velocity in pixels per second
* @param velocityY Initial vertical velocity in pixels per second
* @return true if the fling was started, false if the velocity was too low to fling or
* LayoutManager does not support scrolling in the axis fling is issued.
*/
public boolean fling(int velocityX, int velocityY)
看起来好简单,就这么结束了?
当然不是!
上面的问题一还要解决另一个问题:ParentRecyclerView 如何找到 ViewPager.currentItem 对应的 ChildRecyclerView?
ChildRecyclerView 可以通过 getParent()找到 ParentRecyclerView,但是 ParentRecyclerView 如何找到 ChildRecyclerView 呢?现在摆在我们面前的是,Parent 和 Child 之间至少还隔了一层 ViewPager(或 ViewPager2)!如果布局再复杂一些,他们中间可能还隔着若干层其它的 ViewGroup!
我们都知道,ParentRecyclerView、ViewPager/ViewPager2、ChildRecyclerView 三者的关系是 1:1:N,于是可以想到这两点:
- ParentRecyclerView 寻找 ChildRecyclerView 是不是可以通过 ViewPager 来代理?
- ViewPager/ViewPager2 如何找到当前 currentItem 对应的子 View?子 View 如何找到下面的 ChildRecyclerView?
于是乎,ParentRecyclerView 寻找 ChildRecyclerView 的方案就来了:
/**
* ParentRecyclerView 获取当前的 ChildRecyclerView(只贴出了 ViewPager2 对应的代码)
*/
private fun findCurrentChildRecyclerView(): ChildRecyclerView? {
if (innerViewPager2 != null) {
// 1. 获取当前的子 View
val layoutManagerFiled = ViewPager2::class.java.getDeclaredField("mLayoutManager")
layoutManagerFiled.isAccessible = true
val pagerLayoutManager = layoutManagerFiled.get(innerViewPager2) as LinearLayoutManager
var currentChild = pagerLayoutManager.findViewByPosition(innerViewPager2!!.currentItem)
// 2. 从子 View 中获取 ChildRecyclerView
if (currentChild is ChildRecyclerView) {
return currentChild
} else {
// 这个 tag 是 ChildRecyclerView 保存的
val tagView = currentChild?.getTag(R.id.tag_saved_child_recycler_view)
if (tagView is ChildRecyclerView) {
return tagView
}
}
}
}
// ChildRecyclerView 相关代码略
然后?这样就可以了是么?
当然不是!
上述的种种,仅仅处理了 Fling 传导的情形,我们还需要让 ParentRecyclerView 实现 NestedScrollingParent3,借力安卓官方的思路,实现内联滑动:
/**
* ParentRecyclerView 消费多少 dy?
**/
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
if (target is ChildRecyclerView) {
// 根据当前滑动位置及状态,判断自己需要消费多少 dy
// 详细代码略
}
}
RecyclerView 嵌套子列表,原理大体如此,内部做了很友好的封装,调用侧的约束特别少!当然,代码中还有一些其它的巧妙设定,比如 stickyHeight、childPagerContainer 等,限于篇幅问题,此处就不再赘述了!
另一种方案
对于长列表内嵌 ViewPager 以及 ChildRecyclerView,官方控件中最接近这种效果的是 CoordinatorLayout。所以,CoordinatorLayout 改造之后,也能实现这样的效果,感兴趣的同学可去瞅瞅:传送门