1、簡介
之前在仿寫豆瓣詳情頁,以及日常的一些涉及嵌套滾動的需求時,每次都需要新增自定義 View 來實現(xiàn),而在 touch 事件的攔截和處理,滾動和 fling 的處理上,又有著很大的共性,為了減少之后處理類似需求的重復勞動,也為了更進一步學習 Android 提供的嵌套滾動框架,于是打造了 BehaviorScrollView
來解決嵌套滾動的共性問題。
BehaviorScrollView
內(nèi)部實現(xiàn)了對 touch 事件、嵌套滾動和 fling 的攔截和處理的通用邏輯,實現(xiàn)了 NestedScrollingParent3
和 NestedScrollingChild3
接口,支持多級嵌套(Demo 中會有一個四層嵌套的示例),支持水平和垂直方向滾動,外部可以通過實現(xiàn) NestedScrollBehavior
接口來支持不同的嵌套滾動需求。
2、嵌套滾動處理流程
在講 BehaviorScrollView
和 NestedScrollBehavior
怎么使用之前,還是有必要介紹下 Android 是怎么處理嵌套滾動的,這部分的文章就很多了,這里只做簡要介紹。
- 分發(fā) touch 事件,父一級的 View 盡可能不攔截事件,一直向下分發(fā),直到找到處理事件的
Child
-
Child
處理 touch 事件,在手指滑動時產(chǎn)生滾動量,開始滾動自身內(nèi)容 -
Child
在處理滾動量之前,要告訴父 View 自己要開始滾動了pre-scroll
,并一級一級的向上分發(fā) -
Child
此時需要計算下父級 View 還有多少滾動量沒有消耗,然后開始滾動自己,并計算自己消耗了多少滾動量 -
Child
處理完自己后,將滾動量的消耗情況向父 View 分發(fā)after-scroll
我們平時要處理的嵌套滾動問題也是 Grandparent
、Parent
和 Child
三種角色中的一個或多個的組合,接下來分別介紹下這三種角色分別要處理那些問題。
Grandparent
只需要處理子 View 的嵌套滾動事件,實現(xiàn) NestedScrollingParent
(后綴的 2、3 是為了兼容更多情況進行的擴展)接口,然后根據(jù)自身需求在滾動前 pre-scroll
或 滾動后 after-scroll
,執(zhí)行自己的操作。這種類型的 View 有很多,比如 NestedScrollView
、CoordinatorLayout
、SwipeRefreshLayout
等,我們平時需要的大多數(shù)嵌套滾動需求只需要處理子 View 分發(fā)的滾動,也是這種情況。
Child
一般只負責處理 touch 事件,并將產(chǎn)生的滾動量向父 View 分發(fā),實現(xiàn) NestedScrollingChild
接口,在自己處理滾動前分發(fā) pre-scroll
,自己處理后分發(fā) after-scroll
。這種 View 都是些能夠產(chǎn)生滾動的 View,比如 RecyclerView
、NestedScrollView
等。
Parent
相對比較復雜,即負責接收子 View 的嵌套滾動事件,還需要將其分發(fā)給自己的父 View,實現(xiàn) NestedScrollingParent
和 NestedScrollingChild
接口(即當兒子有當?shù)ǔG闆r下還需要處理 touch 事件和 fling、動畫等。常見的有 NestedScrollView
、SwipeRefreshLayout
等,本文介紹的 BehaviorScrollView
就屬于此類。
同方向嵌套滾動最核心的問題是 優(yōu)先級 問題,手指滑動時父 View 可以處理,子 View 也可以處理,那到底需要交給誰呢。比如常見的 SwipeRefreshLayout
嵌套 RecyclerView
。在嵌套滾動的流程中,Parent
收到 Child
的 pre-scroll
時,需要決定自己是否要處理,還要決定是先分發(fā)給 Grandparent
然后自己處理,還是先自己處理,再分發(fā)給 Grandparent
。
當然,BehaviorScrollView
是不會幫你決定這些優(yōu)先級的,它負責處理優(yōu)先級之外的滾動量計算和分發(fā),以及通用的 touch 事件、fling 和動畫的處理,從而是我們能夠更加方便地處理優(yōu)先級問題。
3、使用和原理
BehaviorScrollView
的使用主要是通過 setupBehavior
方法設置不同的 NestedScrollBehavior
,從而實現(xiàn)不同的優(yōu)先級策略。這里就從 NestedScrollBehavior
開始,介紹它在嵌套滾動各個階段發(fā)揮的作用。
interface NestedScrollBehavior {
/**
* 當前的可滾動方向
*/
@ViewCompat.ScrollAxis
val scrollAxis: Int
val prevView: View?
val midView: View
val nextView: View?
/**
* 在 layout 之后的回調(diào)
*
* @param v
*/
fun afterLayout(v: BehavioralScrollView) {
// do nothing
}
/**
* 在 [v] dispatchTouchEvent 時是否處理 touch 事件
*
* @param v
* @param e touch 事件
* @return true -> 處理,會在 dispatchTouchEvent 中直接返回 true,false -> 直接返回 false,null -> 不關心,會執(zhí)行默認邏輯
*/
fun handleDispatchTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? = null
/**
* 在 [v] onTouchEvent 時是否處理 touch 事件
*
* @param v
* @param e touch 事件
* @return true -> 處理,會直接返回 true,false -> 不處理,會直接返回 false,null -> 不關心,會執(zhí)行默認邏輯
*/
fun handleTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? = null
/**
* 在 onNestedPreScroll 時,是否優(yōu)先自己處理
*
* @param v
* @param scroll 滾動量
* @param type 滾動類型
* @return true -> 自己優(yōu)先,false -> 自己不優(yōu)先,null -> 不處理 onNestedPreScroll
*/
fun handleNestedPreScrollFirst(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null
/**
* 在 onNestedScroll 時,是否優(yōu)先自己處理
*
* @param v
* @param scroll 滾動量
* @param type 滾動類型
* @return true -> 自己優(yōu)先,false -> 自己不優(yōu)先,null -> 不處理 onNestedPreScroll
*/
fun handleNestedScrollFirst(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null
/**
* 在需要 [v] 自身滾動時,是否需要處理
*
* @param v
* @param scroll 滾動量
* @param type 滾動類型
* @return 是否處理自身滾動,true -> 處理,false -> 不處理,null -> 不關心,會執(zhí)行默認自身滾動
*/
fun handleScrollSelf(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null
}
復制代碼
3.1 布局
NestedScrollBehavior
提供的 scrollAxis
決定了 BehavioralScrollView
要處理的滾動方向,同時也會決定布局的方向。
BehavioralScrollView
的子 View 是由 NestedScrollBehavior
提供的 prevView
、midView
和 nextView
,會在 onLayout
時形成水平或垂直線性布局。具體來說,BehavioralScrollView
是繼承自 FrameLayout
的,在垂直布局的情況下,midView
的位置不變,prevView
會移動它的上面,nextView
移動到其下面,從而使得 BehavioralScrollView
有一個可以上下滾動的范圍。布局完成后會計算滾動范圍,從 preView.top
到 nextView.bottom
,并且回調(diào) NestedScrollBehavior.afterLayout
方法。
private fun layoutVertical() {
// midView 位置不變
val t = midView?.top ?: 0
val b = midView?.bottom ?: 0
// prevView 移動到 midView 之上,bottom 和 midView 的 top 對齊
prevView?.also {
it.offsetTopAndBottom(t - it.bottom)
minScroll = it.top
}
// nextView 移動到 midView 之下,top 和 midView 的 bottom 對齊
nextView?.also {
it.offsetTopAndBottom(b - it.top)
maxScroll = it.bottom - height
}
}
復制代碼
這里為什么用三個 View
而不是兩個或著更多呢?一方面在我涉及到的場合下,三個 View
足夠用了,實在不夠還可以嵌套,另一方面,三個 View
能夠比較方便地控制一些邊界條件。比如在垂直滾動情況下,會在 scrollY == 0
的邊界處做一些判斷,調(diào)整嵌套滾動的優(yōu)先級策略,判斷 scrollY
是大于 0 還是小于 0,從而判斷是 nextView
滾動出來還是 prevView
滾動出來了。如果增加到了四個以上,這種邊界的判斷就會變得很麻煩。
3.2 Touch 事件處理
首先看下 dispatchTouchEvent
,會先回調(diào) NestedScrollBehavior.handleDispatchTouchEvent
,返回非空的值表示 NestedScrollBehavior
已經(jīng)處理了,會直接返回,空的話會在 ACTION_DOWN
時復位一些標志位,無特殊處理。
這里回調(diào)給 NestedScrollBehavior
是為了可以盡早拿到 touch 事件,這里通常會在 ACTION_UP
抬手時做一些動畫或復位。
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
// behavior 優(yōu)先處理,不處理走默認邏輯
behavior?.handleDispatchTouchEvent(this, e)?.also {
log("handleDispatchTouchEvent $it")
return it
}
// 在 down 時復位一些標志位,停掉 scroller 的動畫
if (e.action == MotionEvent.ACTION_DOWN) {
lastScrollDir = 0
state = NestedScrollState.NONE
scroller.abortAnimation()
}
return super.dispatchTouchEvent(e)
}
復制代碼
onInterceptTouchEvent
沒有回調(diào)給 NestedScrollBehavior
,這里就不貼代碼了,主要邏輯是只有手指在滾動方向上發(fā)生了滑動,且觸點位置沒有可以處理嵌套滑動的 NestedScrollingChild
才去攔截事件自己處理。
onTouchEvent
也會優(yōu)先分發(fā)給 NestedScrollBehavior.handleTouchEvent
處理,默認會 ACTION_MOVE
時計算滾動量,并通過 dispatchScrollInternal
(這個方法后面再講)進行分發(fā),在 ACTION_UP
時進行 fling 的處理。
override fun onTouchEvent(e: MotionEvent): Boolean {
// behavior 優(yōu)先處理,不處理時自己處理 touch 事件
behavior?.handleTouchEvent(this, e)?.also {
return it
}
when (e.action) {
MotionEvent.ACTION_DOWN -> {
// ...
}
MotionEvent.ACTION_MOVE -> {
// ...
dispatchScrollInternal(dx, dy, ViewCompat.TYPE_TOUCH)
}
MotionEvent.ACTION_UP -> {
// ...
if (!dispatchNestedPreFling(vx, vy)) {
dispatchNestedFling(vx, vy, true)
fling(vx, vy)
}
}
}
// ...
}
復制代碼
3.3 嵌套滾動事件處理
BehavioralScrollView
實現(xiàn)的 NestedScrollingParent3
和 NestedScrollingChild3
的大多數(shù)方法都不需要我們做什么特殊處理,用 NestedScrollingParentHelper
和 NestedScrollingChildHelper
兩個幫助類就能解決,可以多參考 NestedScrollView
。這里主要介紹作為 Grandparent
角色的 onNestedPreScroll
和 onNestedScroll
兩個方法,顧名思義,對應上面說的 pre-scroll
和 after-scroll
兩個時機。
onNestedPreScroll
會有兩個重載的方法,第二個比第一個多了 NestedScrollType
參數(shù)用以區(qū)分滾動是否是 touch 事件產(chǎn)生的,這里統(tǒng)一回調(diào)到 dispatchNestedPreScrollInternal
處理。
代碼邏輯比較簡單,首先時回調(diào) NestedScrollBehavior.handleNestedPreScrollFirst
判斷處理的優(yōu)先級,返回值有三種情況:
-
null
:表示不處理pre-scroll
,這時會直接調(diào)用dispatchNestedPreScroll
將滾動量分發(fā)給父 View -
true
:表示自己優(yōu)先處理,這時會先調(diào)用handleScrollSelf
(這個方法后面再講)自己處理,然后計算未消耗的滾動量,再dispatchNestedPreScroll
分發(fā)給父 View -
false
:表示父 View 優(yōu)先處理,這時會先dispatchNestedPreScroll
分發(fā)給父 View,然后計算未消耗的滾動量,再handleScrollSelf
自己處理
/**
* 分發(fā) pre scroll 的滾動量
*/
private fun dispatchNestedPreScrollInternal(
dx: Int,
dy: Int,
consumed: IntArray,
type: Int = ViewCompat.TYPE_TOUCH
) {
when (scrollAxis) {
ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
// ...
}
ViewCompat.SCROLL_AXIS_VERTICAL ->{
val handleFirst = behavior?.handleNestedPreScrollFirst(this, dy, type)
when (handleFirst) {
true -> {
val selfConsumed = handleScrollSelf(dy, type)
dispatchNestedPreScroll(dx, dy - selfConsumed, consumed, null, type)
consumed[1] += selfConsumed
}
false -> {
dispatchNestedPreScroll(dx, dy, consumed, null, type)
val selfConsumed = handleScrollSelf(dy - consumed[1], type)
consumed[1] += selfConsumed
}
null -> dispatchNestedPreScroll(dx, dy, consumed, null, type)
}
}
else -> dispatchNestedPreScroll(dx, dy, consumed, null, type)
}
}
復制代碼
onNestedScroll
會有三個重載方法,依次增加了 NestedScrollType
和父 View 用于記錄消耗滾動量的數(shù)組 consumed
,這里會統(tǒng)一回調(diào)給 dispatchNestedScrollInternal
處理。
處理邏輯和 dispatchNestedPreScrollInternal
類似,先回調(diào) NestedScrollBehavior.handleNestedScrollFirst
得到優(yōu)先級,再進行分發(fā)和處理,這里不再贅述。
/**
* 分發(fā) nested scroll 的滾動量
*/
private fun dispatchNestedScrollInternal(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int = ViewCompat.TYPE_TOUCH,
consumed: IntArray = intArrayOf(0, 0)
) {
when (scrollAxis) {
ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
// ...
}
ViewCompat.SCROLL_AXIS_VERTICAL -> {
val handleFirst = behavior?.handleNestedScrollFirst(this, dyUnconsumed, type)
when (handleFirst) {
true -> {
val selfConsumed = handleScrollSelf(dyUnconsumed, type)
dispatchNestedScroll(dxConsumed, dyConsumed + selfConsumed, dxUnconsumed, dyUnconsumed - selfConsumed, null, type, consumed)
consumed[1] += selfConsumed
}
false -> {
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type, consumed)
val selfConsumed = handleScrollSelf(dyUnconsumed - consumed[1], type)
consumed[1] += selfConsumed
}
null -> dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type, consumed)
}
}
}
}
復制代碼
NestedScrollBehavior
對滾動量分發(fā)的優(yōu)先級控制主要體現(xiàn)在 handleNestedPreScrollFirst
和 handleNestedScrollFirst
兩個方法,通過 BehavioralScrollView
當前狀態(tài)、滾動的距離、滾動類型和不同策略設置不同的優(yōu)先級,從而滿足不同嵌套滾動需求。
3.4 自身滾動的處理
dispatchScrollInternal
用來處理自身產(chǎn)生的來自 touch 事件或者 fling 的滾動量,這里其實是處于 Child
的角色,所以在自身處理的前后都要分發(fā)嵌套滾動事件,這里復用了前面的 dispatchNestedPreScrollInternal
和 dispatchNestedScrollInternal
,在自身滾動時實現(xiàn)精細的優(yōu)先級控制。
/**
* 分發(fā)來自自身 touch 事件或 fling 的滾動量
* -> dispatchNestedPreScrollInternal
* -> handleScrollSelf
* -> dispatchNestedScrollInternal
*/
private fun dispatchScrollInternal(dx: Int, dy: Int, type: Int) {
val consumed = IntArray(2)
when (scrollAxis) {
ViewCompat.SCROLL_AXIS_HORIZONTAL -> {
// ...
}
ViewCompat.SCROLL_AXIS_VERTICAL -> {
var consumedY = 0
dispatchNestedPreScrollInternal(dx, dy, consumed, type)
consumedY += consumed[1]
consumedY += handleScrollSelf(dy - consumedY, type)
val consumedX = consumed[0]
// 復用數(shù)組
consumed[0] = 0
consumed[1] = 0
dispatchNestedScrollInternal(consumedX, consumedY, dx - consumedX, dy - consumedY, type, consumed)
}
}
}
復制代碼
handleScrollSelf
是真正到了自身滾動的時刻,會先回調(diào) NestedScrollBehavior.handleScrollSelf
判斷是否處理該滾動量,同樣的有三種返回值:
-
null
表示NestedScrollBehavior
不做特殊處理,此時BehavioralScrollView
會根據(jù)自身是否可以滾動進行滾動,并返回消耗的滾動量 -
true
表示處理,消耗所有的滾動量 -
false
表示不處理,不消耗滾動量
handleScrollSelf
主要用于在 BehavioralScrollView
自身滾動時做特殊處理,比如下拉刷新等不希望 fling 的 ViewCompat.TYPE_NON_TOUCH
類型滾動造成自身的位移,有些彈性滾動的場合希望自身的滾動帶有阻尼效果等都可以在這里處理。
/**
* 處理自身滾動
*/
private fun handleScrollSelf(scroll: Int, @ViewCompat.NestedScrollType type: Int): Int {
// behavior 優(yōu)先決定是否滾動自身
val handle = behavior?.handleScrollSelf(this, scroll, type)
val consumed = when(handle) {
true -> scroll
false -> 0
else -> if (canScrollSelf(scroll)) {
scrollBy(scroll, scroll)
scroll
} else {
0
}
}
return consumed
}
復制代碼
自身的滾動最終是通過 scrollBy
實現(xiàn)的,通過 getScrollByX/getScrollByY
實現(xiàn)了邊界控制。同時 scrollX/scrollY
在 0 處做了特殊處理,如 scrollY > 0
時,滾動范圍是 從 0 到 maxScroll
,這和「3.1 布局」中說的邊界處的特殊處理有關,需要在 scrollY
小于 0、等于 0 或大于 0 時使用不用的優(yōu)先級策略。
override fun scrollBy(x: Int, y: Int) {
val xx = getScrollByX(x)
val yy = getScrollByY(y)
super.scrollBy(xx, yy)
}
/**
* 根據(jù)方向計算 y 軸的真正滾動量
*/
private fun getScrollByY(dy: Int): Int {
val newY = scrollY + dy
return when {
scrollAxis != ViewCompat.SCROLL_AXIS_VERTICAL -> scrollY
scrollY > 0 -> newY.constrains(0, maxScroll)
scrollY < 0 -> newY.constrains(minScroll, 0)
else -> newY.constrains(minScroll, maxScroll)
} - scrollY
}
復制代碼
3.5 fling 和動畫
fling 和動畫都是通過 Scroller
處理的,fling 需要 VelocityTracker
幫助類在 touch 事件中記錄手指移動速度。
這里需要介紹 BehavioralScrollView
保存當前狀態(tài)的一個屬性 NestedScrollState
,方便嵌套滾動事件的優(yōu)先級判斷。
/**
* 用于描述 [BehavioralScrollView] 正處于的嵌套滾動狀態(tài),和滾動類型 [ViewCompat.NestedScrollType] 共同描述滾動量
*/
@IntDef(NestedScrollState.NONE, NestedScrollState.DRAGGING, NestedScrollState.ANIMATION, NestedScrollState.FLING)
@Retention(AnnotationRetention.SOURCE)
annotation class NestedScrollState {
companion object {
/**
* 無狀態(tài)
*/
const val NONE = 0
/**
* 正在拖拽
*/
const val DRAGGING = 1
/**
* 正在動畫,動畫產(chǎn)生的滾動不會被分發(fā)
*/
const val ANIMATION = 2
/**
* 正在 fling
*/
const val FLING = 3
}
}
復制代碼
fling 和動畫最終都會回調(diào)到 computeScroll
中處理,不同的是動畫產(chǎn)生的滾動不需要進行分發(fā)(因為動畫不是 touch 事件產(chǎn)生的,而是外部明確調(diào)用的),而 fling 的需要 dispatchScrollInternal
進行分發(fā)。
override fun computeScroll() {
when {
scroller.computeScrollOffset() -> {
val dx = (scroller.currX - lastX).toInt()
val dy = (scroller.currY - lastY).toInt()
lastX = scroller.currX.toFloat()
lastY = scroller.currY.toFloat()
// 不分發(fā)來自動畫的滾動
if (state == NestedScrollState.ANIMATION) {
scrollBy(dx, dy)
} else {
dispatchScrollInternal(dx, dy, ViewCompat.TYPE_NON_TOUCH)
}
invalidate()
}
// ...
}
}
復制代碼
4、示例
BehavioralScrollView
已經(jīng)處理了共性的東西,個性化的部分是 NestedScrollBehavior
實現(xiàn)的,因此這里的示例可能不具備通用性。當有特殊需要是,可以很方便地自定義 NestedScrollBehavior
實現(xiàn),這也正是 BehavioralScrollView
希望達到的效果。
這里以底部浮層 BottomSheetBehavior
為例大致介紹下 NestedScrollBehavior
的使用。
構造 BottomSheetBehavior
需要知道內(nèi)容視圖 contentView
以及浮層彈出的范圍和初始位置。
class BottomSheetBehavior(
/**
* 浮層的內(nèi)容視圖
*/
contentView: View,
/**
* 初始位置,最低高度 [POSITION_MIN]、中間高度 [POSITION_MID] 或最大高度 [POSITION_MAX]
*/
private val initPosition: Int,
/**
* 內(nèi)容視圖的最低顯示高度
*/
private val minHeight: Int,
/**
* 內(nèi)容視圖中間停留的顯示高度,默認等于最低高度
*/
private val midHeight: Int = minHeight
)
復制代碼
由于滾動范圍是由 prevView
、midView
和 nextView
確定的,頂部的空白區(qū)域需要設置 prevView
進行占位,通過 topMargin
控制其高度,從而控制滾動的范圍,midView
設置為 contentView
,這里不需要 nextView
設為 null
。
/**
* 用于控制滾動范圍
*/
override val prevView: View? = Space(contentView.context).also {
val lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
lp.topMargin = minHeight
it.layoutParams = lp
}
override val midView: View = contentView
override val nextView: View? = null
復制代碼
在 afterLayout
時計算中間高度時 scrollY
的值,并在第一次 layout 后直接滾動指定的初始位置。
override fun afterLayout(v: BehavioralScrollView) {
// 計算中間高度時的 scrollY
midScroll = v.minScroll + midHeight - minHeight
// 第一次 layout 滾動到初始位置
if (firstLayout) {
firstLayout = false
v.scrollTo(
v.scrollX,
when (initPosition) {
POSITION_MIN -> v.minScroll
POSITION_MAX -> v.maxScroll
else -> midScroll
}
)
}
}
復制代碼
簡單畫了下布局的示意圖
在 handleDispatchTouchEvent
的 up 或 cancel 時,需要根據(jù)當前滾動位置和上次滾動的方向,決定動畫的目標位置。
override fun handleDispatchTouchEvent(
v: BehavioralScrollView,
e: MotionEvent
): Boolean? {
if ((e.action == MotionEvent.ACTION_CANCEL || e.action == MotionEvent.ACTION_UP)
&& v.scrollY != 0) {
// 在 up 或 cancel 時,根據(jù)當前滾動位置和上次滾動的方向,決定動畫的目標位置
v.smoothScrollTo(
if (v.scrollY > midScroll) {
if (v.lastScrollDir > 0) { v.maxScroll } else { midScroll }
} else {
if (v.lastScrollDir > 0) { midScroll } else { v.minScroll }
}
)
return true
}
return super.handleDispatchTouchEvent(v, e)
}
復制代碼
handleTouchEvent
需要在 down 在 prevView
時不進行處理,因為它只是個占位的,這樣不會影響下層視圖對事件的處理。
override fun handleTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? {
// down 事件觸點在 prevView 上時不做處理
return if (e.action == MotionEvent.ACTION_DOWN && prevView?.isUnder(e.rawX, e.rawY) == true) {
false
} else {
null
}
}
復制代碼
嵌套滾動的優(yōu)先級處理比較簡單,handleNestedPreScrollFirst
只在 contentView
沒有完全展開,即 v.scrollY != 0
時處理,而 handleNestedScrollFirst
總是優(yōu)先處理。
override fun handleNestedPreScrollFirst(
v: BehavioralScrollView,
scroll: Int,
@ViewCompat.NestedScrollType type: Int
): Boolean? {
// 只要 contentView 沒有完全展開,就在子 View 滾動前處理
return if (v.scrollY != 0) { true } else { null }
}
override fun handleNestedScrollFirst(
v: BehavioralScrollView,
scroll: Int,
type: Int
): Boolean? {
return true
}
復制代碼
自身的滾動只處理 touch 類型的,其他的過濾掉。
override fun handleScrollSelf(
v: BehavioralScrollView,
scroll: Int,
@ViewCompat.NestedScrollType type: Int
): Boolean? {
// 只允許 touch 類型用于自身的滾動
return if (type == ViewCompat.TYPE_NON_TOUCH) { true } else { null }
}
復制代碼
Demo 中還有其他各種類型的 NestedScrollBehavior
,如實現(xiàn)頂部 TabLayout
懸浮效果的 FloatingHeaderBehavior
,兼容嵌套的下拉刷新 SwipeRefreshBehavior
等。這里簡單說明下為什么 SwipeRefreshLayout
已經(jīng)實現(xiàn)了 NestedScrollingParent
和 NestedScrollingChild
,卻無法適用于嵌套滾動呢?
NestedScrollingChild.dispatchNestedScroll
缺少 NestedScrollingChild3.dispatchNestedScroll
中的 consumed
參數(shù),所以在向父 View 分發(fā)時,無法得知父 View 消耗了多少滾動量,嵌套使用就會存在問題,來看下 SwipeRefreshLayout.onNestedScroll
方法。
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) {
// Dispatch up to the nested parent first
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
// ...
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
moveSpinner(mTotalUnconsumed);
}
}
復制代碼
它在 dispatchNestedScroll
之后是不知道父 View 有沒有消耗滾動量的,而函數(shù)中的 mParentOffsetInWindow
得到的是 SwipeRefreshLayout
在屏幕上的位移,SwipeRefreshLayout
認為的父 View 沒有消耗的滾動量等于 dyUnconsumed + mParentOffsetInWindow[1]
。
這樣看起來沒啥問題,但當父 View 消耗的滾動量不等于其子 View 在屏幕上的位移時(比如增加了阻尼效果,消耗了 n 的滾動量,卻只移動了 n/2)就會出問題,即使?jié)L動量已經(jīng)全部被外部消耗了,SwipeRefreshLayout
還是有下拉效果:
所以為了解決這種問題,就需要實現(xiàn)了 NestedScrollingChild3
的接口,下面是 BehavioralScrollView
+ SwipeRefreshBehavior
的效果:
5、結(jié)束
嵌套滾動的核心問題是優(yōu)先級問題,我們應該專注于優(yōu)先級的策略而不是各種事件的處理和分發(fā)問題,這也真是 BehavioralScrollView
在嘗試做到的,希望這篇文章能夠?qū)δ阌兴鶐椭胁煌悸返囊矚g迎相互探討。