以前在仿寫豆瓣詳情頁,以及平常的一些涉及嵌套滾動的需求時,每次都須要新增自定義 View 來實現,而在 touch 事件的攔截和處理,滾動和 fling 的處理上,又有着很大的共性,爲了減小以後處理相似需求的重複勞動,也爲了更進一步學習 Android 提供的嵌套滾動框架,因而打造了 BehaviorScrollView
來解決嵌套滾動的共性問題。java
BehaviorScrollView
內部實現了對 touch 事件、嵌套滾動和 fling 的攔截和處理的通用邏輯,實現了 NestedScrollingParent3
和 NestedScrollingChild3
接口,支持多級嵌套(Demo 中會有一個四層嵌套的示例),支持水平和垂直方向滾動,外部能夠經過實現 NestedScrollBehavior
接口來支持不一樣的嵌套滾動需求。git
在講 BehaviorScrollView
和 NestedScrollBehavior
怎麼使用以前,仍是有必要介紹下 Android 是怎麼處理嵌套滾動的,這部分的文章就不少了,這裏只作簡要介紹。github
Child
Child
處理 touch 事件,在手指滑動時產生滾動量,開始滾動自身內容Child
在處理滾動量以前,要告訴父 View 本身要開始滾動了 pre-scroll
,並一級一級的向上分發Child
此時須要計算下父級 View 還有多少滾動量沒有消耗,而後開始滾動本身,並計算本身消耗了多少滾動量Child
處理完本身後,將滾動量的消耗狀況向父 View 分發 after-scroll
咱們平時要處理的嵌套滾動問題也是 Grandparent
、Parent
和 Child
三種角色中的一個或多個的組合,接下來分別介紹下這三種角色分別要處理那些問題。數組
Grandparent
只須要處理子 View 的嵌套滾動事件,實現 NestedScrollingParent
(後綴的 二、3 是爲了兼容更多狀況進行的擴展)接口,而後根據自身需求在滾動前 pre-scroll
或 滾動後 after-scroll
,執行本身的操做。這種類型的 View 有不少,好比 NestedScrollView
、CoordinatorLayout
、SwipeRefreshLayout
等,咱們平時須要的大多數嵌套滾動需求只須要處理子 View 分發的滾動,也是這種狀況。框架
Child
通常只負責處理 touch 事件,並將產生的滾動量向父 View 分發,實現 NestedScrollingChild
接口,在本身處理滾動前分發 pre-scroll
,本身處理後分發 after-scroll
。這種 View 都是些可以產生滾動的 View,好比 RecyclerView
、NestedScrollView
等。ide
Parent
相對比較複雜,即負責接收子 View 的嵌套滾動事件,還須要將其分發給本身的父 View,實現 NestedScrollingParent
和 NestedScrollingChild
接口(即當兒子有當爹),一般狀況下還須要處理 touch 事件和 fling、動畫等。常見的有 NestedScrollView
、SwipeRefreshLayout
等,本文介紹的 BehaviorScrollView
就屬於此類。函數
同方向嵌套滾動最核心的問題是 優先級 問題,手指滑動時父 View 能夠處理,子 View 也能夠處理,那到底須要交給誰呢。好比常見的 SwipeRefreshLayout
嵌套 RecyclerView
。在嵌套滾動的流程中,Parent
收到 Child
的 pre-scroll
時,須要決定本身是否要處理,還要決定是先分發給 Grandparent
而後本身處理,仍是先本身處理,再分發給 Grandparent
。佈局
固然,BehaviorScrollView
是不會幫你決定這些優先級的,它負責處理優先級以外的滾動量計算和分發,以及通用的 touch 事件、fling 和動畫的處理,從而是咱們可以更加方便地處理優先級問題。學習
BehaviorScrollView
的使用主要是經過 setupBehavior
方法設置不一樣的 NestedScrollBehavior
,從而實現不一樣的優先級策略。這裏就從 NestedScrollBehavior
開始,介紹它在嵌套滾動各個階段發揮的做用。動畫
interface NestedScrollBehavior {
/** * 當前的可滾動方向 */
@ViewCompat.ScrollAxis
val scrollAxis: Int
val prevView: View?
val midView: View
val nextView: View?
/** * 在 layout 以後的回調 * * @param v */
fun afterLayout(v: BehavioralScrollView) {
// do nothing
}
/** * 在 [v] dispatchTouchEvent 時是否處理 touch 事件 * * @param v * @param e touch 事件 * @return true -> 處理,會在 dispatchTouchEvent 中直接返回 true,false -> 直接返回 false,null -> 不關心,會執行默認邏輯 */
fun handleDispatchTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? = null
/** * 在 [v] onTouchEvent 時是否處理 touch 事件 * * @param v * @param e touch 事件 * @return true -> 處理,會直接返回 true,false -> 不處理,會直接返回 false,null -> 不關心,會執行默認邏輯 */
fun handleTouchEvent(v: BehavioralScrollView, e: MotionEvent): Boolean? = null
/** * 在 onNestedPreScroll 時,是否優先本身處理 * * @param v * @param scroll 滾動量 * @param type 滾動類型 * @return true -> 本身優先,false -> 本身不優先,null -> 不處理 onNestedPreScroll */
fun handleNestedPreScrollFirst(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null
/** * 在 onNestedScroll 時,是否優先本身處理 * * @param v * @param scroll 滾動量 * @param type 滾動類型 * @return true -> 本身優先,false -> 本身不優先,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 -> 不關心,會執行默認自身滾動 */
fun handleScrollSelf(v: BehavioralScrollView, scroll: Int, @ViewCompat.NestedScrollType type: Int): Boolean? = null
}
複製代碼
NestedScrollBehavior
提供的 scrollAxis
決定了 BehavioralScrollView
要處理的滾動方向,同時也會決定佈局的方向。
BehavioralScrollView
的子 View 是由 NestedScrollBehavior
提供的 prevView
、midView
和 nextView
,會在 onLayout
時造成水平或垂直線性佈局。具體來講,BehavioralScrollView
是繼承自 FrameLayout
的,在垂直佈局的狀況下,midView
的位置不變,prevView
會移動它的上面,nextView
移動到其下面,從而使得 BehavioralScrollView
有一個能夠上下滾動的範圍。佈局完成後會計算滾動範圍,從 preView.top
到 nextView.bottom
,而且回調 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
的邊界處作一些判斷,調整嵌套滾動的優先級策略,判斷 scrollY
是大於 0 仍是小於 0,從而判斷是 nextView
滾動出來仍是 prevView
滾動出來了。若是增長到了四個以上,這種邊界的判斷就會變得很麻煩。
首先看下 dispatchTouchEvent
,會先回調 NestedScrollBehavior.handleDispatchTouchEvent
,返回非空的值表示 NestedScrollBehavior
已經處理了,會直接返回,空的話會在 ACTION_DOWN
時復位一些標誌位,無特殊處理。
這裏回調給 NestedScrollBehavior
是爲了能夠儘早拿到 touch 事件,這裏一般會在 ACTION_UP
擡手時作一些動畫或復位。
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
// behavior 優先處理,不處理走默認邏輯
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
沒有回調給 NestedScrollBehavior
,這裏就不貼代碼了,主要邏輯是隻有手指在滾動方向上發生了滑動,且觸點位置沒有能夠處理嵌套滑動的 NestedScrollingChild
纔去攔截事件本身處理。
onTouchEvent
也會優先分發給 NestedScrollBehavior.handleTouchEvent
處理,默認會 ACTION_MOVE
時計算滾動量,並經過 dispatchScrollInternal
(這個方法後面再講)進行分發,在 ACTION_UP
時進行 fling 的處理。
override fun onTouchEvent(e: MotionEvent): Boolean {
// behavior 優先處理,不處理時本身處理 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)
}
}
}
// ...
}
複製代碼
BehavioralScrollView
實現的 NestedScrollingParent3
和 NestedScrollingChild3
的大多數方法都不須要咱們作什麼特殊處理,用 NestedScrollingParentHelper
和 NestedScrollingChildHelper
兩個幫助類就能解決,能夠多參考 NestedScrollView
。這裏主要介紹做爲 Grandparent
角色的 onNestedPreScroll
和 onNestedScroll
兩個方法,顧名思義,對應上面說的 pre-scroll
和 after-scroll
兩個時機。
onNestedPreScroll
會有兩個重載的方法,第二個比第一個多了 NestedScrollType
參數用以區分滾動是不是 touch 事件產生的,這裏統一回調到 dispatchNestedPreScrollInternal
處理。
代碼邏輯比較簡單,首先時回調 NestedScrollBehavior.handleNestedPreScrollFirst
判斷處理的優先級,返回值有三種狀況:
null
:表示不處理 pre-scroll
,這時會直接調用 dispatchNestedPreScroll
將滾動量分發給父 Viewtrue
:表示本身優先處理,這時會先調用 handleScrollSelf
(這個方法後面再講)本身處理,而後計算未消耗的滾動量,再 dispatchNestedPreScroll
分發給父 Viewfalse
:表示父 View 優先處理,這時會先 dispatchNestedPreScroll
分發給父 View,而後計算未消耗的滾動量,再 handleScrollSelf
本身處理/** * 分發 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 用於記錄消耗滾動量的數組 consumed
,這裏會統一回調給 dispatchNestedScrollInternal
處理。
處理邏輯和 dispatchNestedPreScrollInternal
相似,先回調 NestedScrollBehavior.handleNestedScrollFirst
獲得優先級,再進行分發和處理,這裏再也不贅述。
/** * 分發 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
對滾動量分發的優先級控制主要體如今 handleNestedPreScrollFirst
和 handleNestedScrollFirst
兩個方法,經過 BehavioralScrollView
當前狀態、滾動的距離、滾動類型和不一樣策略設置不一樣的優先級,從而知足不一樣嵌套滾動需求。
dispatchScrollInternal
用來處理自身產生的來自 touch 事件或者 fling 的滾動量,這裏實際上是處於 Child
的角色,因此在自身處理的先後都要分發嵌套滾動事件,這裏複用了前面的 dispatchNestedPreScrollInternal
和 dispatchNestedScrollInternal
,在自身滾動時實現精細的優先級控制。
/** * 分發來自自身 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]
// 複用數組
consumed[0] = 0
consumed[1] = 0
dispatchNestedScrollInternal(consumedX, consumedY, dx - consumedX, dy - consumedY, type, consumed)
}
}
}
複製代碼
handleScrollSelf
是真正到了自身滾動的時刻,會先回調 NestedScrollBehavior.handleScrollSelf
判斷是否處理該滾動量,一樣的有三種返回值:
null
表示 NestedScrollBehavior
不作特殊處理,此時 BehavioralScrollView
會根據自身是否能夠滾動進行滾動,並返回消耗的滾動量true
表示處理,消耗全部的滾動量false
表示不處理,不消耗滾動量handleScrollSelf
主要用於在 BehavioralScrollView
自身滾動時作特殊處理,好比下拉刷新等不但願 fling 的 ViewCompat.TYPE_NON_TOUCH
類型滾動形成自身的位移,有些彈性滾動的場合但願自身的滾動帶有阻尼效果等均可以在這裏處理。
/** * 處理自身滾動 */
private fun handleScrollSelf(scroll: Int, @ViewCompat.NestedScrollType type: Int): Int {
// behavior 優先決定是否滾動自身
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
實現的,經過 getScrollByX/getScrollByY
實現了邊界控制。同時 scrollX/scrollY
在 0 處作了特殊處理,如 scrollY > 0
時,滾動範圍是 從 0 到 maxScroll
,這和「3.1 佈局」中說的邊界處的特殊處理有關,須要在 scrollY
小於 0、等於 0 或大於 0 時使用不用的優先級策略。
override fun scrollBy(x: Int, y: Int) {
val xx = getScrollByX(x)
val yy = getScrollByY(y)
super.scrollBy(xx, yy)
}
/** * 根據方向計算 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
}
複製代碼
fling 和動畫都是經過 Scroller
處理的,fling 須要 VelocityTracker
幫助類在 touch 事件中記錄手指移動速度。
這裏須要介紹 BehavioralScrollView
保存當前狀態的一個屬性 NestedScrollState
,方便嵌套滾動事件的優先級判斷。
/** * 用於描述 [BehavioralScrollView] 正處於的嵌套滾動狀態,和滾動類型 [ViewCompat.NestedScrollType] 共同描述滾動量 */
@IntDef(NestedScrollState.NONE, NestedScrollState.DRAGGING, NestedScrollState.ANIMATION, NestedScrollState.FLING)
@Retention(AnnotationRetention.SOURCE)
annotation class NestedScrollState {
companion object {
/** * 無狀態 */
const val NONE = 0
/** * 正在拖拽 */
const val DRAGGING = 1
/** * 正在動畫,動畫產生的滾動不會被分發 */
const val ANIMATION = 2
/** * 正在 fling */
const val FLING = 3
}
}
複製代碼
fling 和動畫最終都會回調到 computeScroll
中處理,不一樣的是動畫產生的滾動不須要進行分發(由於動畫不是 touch 事件產生的,而是外部明確調用的),而 fling 的須要 dispatchScrollInternal
進行分發。
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()
// 不分發來自動畫的滾動
if (state == NestedScrollState.ANIMATION) {
scrollBy(dx, dy)
} else {
dispatchScrollInternal(dx, dy, ViewCompat.TYPE_NON_TOUCH)
}
invalidate()
}
// ...
}
}
複製代碼
BehavioralScrollView
已經處理了共性的東西,個性化的部分是 NestedScrollBehavior
實現的,所以這裏的示例可能不具有通用性。當有特殊須要是,能夠很方便地自定義 NestedScrollBehavior
實現,這也正是 BehavioralScrollView
但願達到的效果。
這裏以底部浮層 BottomSheetBehavior
爲例大體介紹下 NestedScrollBehavior
的使用。
構造 BottomSheetBehavior
須要知道內容視圖 contentView
以及浮層彈出的範圍和初始位置。
class BottomSheetBehavior(
/**
* 浮層的內容視圖
*/
contentView: View,
/**
* 初始位置,最低高度 [POSITION_MIN]、中間高度 [POSITION_MID] 或最大高度 [POSITION_MAX]
*/
private val initPosition: Int,
/**
* 內容視圖的最低顯示高度
*/
private val minHeight: Int,
/**
* 內容視圖中間停留的顯示高度,默認等於最低高度
*/
private val midHeight: Int = minHeight
)
複製代碼
因爲滾動範圍是由 prevView
、midView
和 nextView
肯定的,頂部的空白區域須要設置 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 時,須要根據當前滾動位置和上次滾動的方向,決定動畫的目標位置。
override fun handleDispatchTouchEvent( v: BehavioralScrollView, e: MotionEvent ): Boolean? {
if ((e.action == MotionEvent.ACTION_CANCEL || e.action == MotionEvent.ACTION_UP)
&& v.scrollY != 0) {
// 在 up 或 cancel 時,根據當前滾動位置和上次滾動的方向,決定動畫的目標位置
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
}
}
複製代碼
嵌套滾動的優先級處理比較簡單,handleNestedPreScrollFirst
只在 contentView
沒有徹底展開,即 v.scrollY != 0
時處理,而 handleNestedScrollFirst
老是優先處理。
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
,如實現頂部 TabLayout
懸浮效果的 FloatingHeaderBehavior
,兼容嵌套的下拉刷新 SwipeRefreshBehavior
等。這裏簡單說明下爲何 SwipeRefreshLayout
已經實現了 NestedScrollingParent
和 NestedScrollingChild
,卻沒法適用於嵌套滾動呢?
NestedScrollingChild.dispatchNestedScroll
缺乏 NestedScrollingChild3.dispatchNestedScroll
中的 consumed
參數,因此在向父 View 分發時,沒法得知父 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 有沒有消耗滾動量的,而函數中的 mParentOffsetInWindow
獲得的是 SwipeRefreshLayout
在屏幕上的位移,SwipeRefreshLayout
認爲的父 View 沒有消耗的滾動量等於 dyUnconsumed + mParentOffsetInWindow[1]
。
這樣看起來沒啥問題,但當父 View 消耗的滾動量不等於其子 View 在屏幕上的位移時(好比增長了阻尼效果,消耗了 n 的滾動量,卻只移動了 n/2)就會出問題,即便滾動量已經所有被外部消耗了,SwipeRefreshLayout
仍是有下拉效果:
因此爲了解決這種問題,就須要實現了 NestedScrollingChild3
的接口,下面是 BehavioralScrollView
+ SwipeRefreshBehavior
的效果:
嵌套滾動的核心問題是優先級問題,咱們應該專一於優先級的策略而不是各類事件的處理和分發問題,這也真是 BehavioralScrollView
在嘗試作到的,但願這篇文章可以對你有所幫助,有不一樣思路的也歡迎相互探討。