綜述
上圖是一個(gè)非常常見(jiàn)的嵌套滑動(dòng)UI交互,實(shí)現(xiàn)這樣的效果炬转,大致有如下三種思路:
基于普通的事件分發(fā)機(jī)制
基于NestedScrolling機(jī)制
基于CoordinatorLayout與Behavior
以上三種思路從原理上循序漸進(jìn)蜀撑,逐層封裝矢炼。由于本文主要介紹嵌套滑動(dòng)析藕,會(huì)主要介紹第二種方案及其原理赫蛇,第一種會(huì)稍微講解一下绵患。
Demo布局
<com.threeloe.nestscrolling.nest.ScrollHeaderLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/scrollHeaderLayout"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="Header"/>
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</com.threeloe.nestscrolling.nest.ScrollHeaderLayout>
無(wú)論采用哪種實(shí)現(xiàn)方式,布局都分為上部分的Header和下部分的ViewPager兩部分悟耘。
傳統(tǒng)的事件分發(fā)機(jī)制
優(yōu)點(diǎn):
靈活性最高落蝙。
缺點(diǎn):
處理細(xì)節(jié)多,難度大暂幼,需要對(duì)事件分發(fā)機(jī)制, 多點(diǎn)觸控筏勒,滑動(dòng),fling,以及一些周邊API等都比較清楚旺嬉。
基本思路:
要完成上述效果管行,在豎直滑動(dòng)的情況下,上滑時(shí)先讓外層的父View滾動(dòng)邪媳,到滾動(dòng)的最大距離時(shí)候病瞳,再讓子View開(kāi)始滾動(dòng)揽咕。下滑時(shí)如果子View滑動(dòng)距離不是0的話,先讓子View滑動(dòng)套菜,然后讓父View滑動(dòng)亲善。因此一次滑動(dòng)中的事件需要再父View和子View中切換傳遞。
復(fù)習(xí)一下事件分發(fā)機(jī)制:
事件序列:從手指按下知道抬起逗柴,中間經(jīng)歷一個(gè)ACTION_DONW 蛹头,多個(gè)ACTION_MOVE和一個(gè)ACTION_UP
一般情況下我們處理滑動(dòng)沖突,重寫(xiě)onInterceptTouchEvent方法即可戏溺,但是一旦onInterceptTouchEvent方法返回true渣蜗,那么該事件序列以后的事件都會(huì)直接給父View處理,這種情況在處理滑動(dòng)沖突是是可行的旷祸。但是在我們上面的案例因?yàn)閷?duì)于一個(gè)事件序列需要交替得在子View和父View中傳遞耕拷,如果重寫(xiě)該方法的話,需要我們自己再合適時(shí)機(jī)手動(dòng)派發(fā)一些事件托享。
因此更為簡(jiǎn)單的做法不如直接重寫(xiě)dispatchTouchEvent方法骚烧,以下代碼只是處理了單手指滑動(dòng)的情況,沒(méi)有考慮多點(diǎn)觸控闰围,也沒(méi)有處理fling赃绊。
如上我們需要判斷isInnerScrollViewTop(),即內(nèi)部的View滑動(dòng)距離是否為0羡榴。因此父View需要知道滑動(dòng)的子View到底是誰(shuí)碧查,需要外界告訴。
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
mLastX = ev.x
mLastY = ev.y
}
MotionEvent.ACTION_MOVE -> {
if (!mIsReadyToDragHorizontal) {
var dy = (mLastY - ev.y).toInt()
var dx = (mLastX - ev.x).toInt()
//當(dāng)連續(xù)滑動(dòng)距離達(dá)到TouchSlop時(shí)候校仑,認(rèn)為滑動(dòng)
if (!mIsBeingDragged) {
if (Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop
} else {
dy += mTouchSlop
}
mIsBeingDragged = true
}
if (Math.abs(dx) > mTouchSlop) {
when {
dy == 0 -> mIsReadyToDragHorizontal = true
Math.abs(dx).toFloat() / Math.abs(dy).toFloat() > 30 -> mIsReadyToDragHorizontal = true
else -> {
mIsBeingDragged = true
if (dy > 0) {
dy -= mTouchSlop
} else {
dy += mTouchSlop
}
}
}
}
}
if (mIsBeingDragged) {
mLastY = ev.y
var consumedDy = 0
if (dy == 0) {
//過(guò)濾掉
return true
} else if (dy > 0) {
consumedDy = Math.min(dy, mScrollRange - scrollY)
} else {
if (isInnerScrollViewTop()) {
consumedDy = Math.max(dy, -scrollY)
}
}
if (consumedDy != 0) {
scrollBy(0, consumedDy)
}
}
}
}
MotionEvent.ACTION_UP -> {
mIsBeingDragged = false
mIsReadyToDragHorizontal = false
}
}
//?
super.dispatchTouchEvent(ev)
return true
}
NestedScrolling機(jī)制
Android 5.0之后加入該機(jī)制忠售。
support v4包提供兩個(gè)接口:
NestedScrollingParent,嵌套滑動(dòng)的父View需要實(shí)現(xiàn)迄沫。已有實(shí)現(xiàn)CoordinatorLayout,NestedScroView
NestedScrollingChild档痪, 嵌套滑動(dòng)的子View需要實(shí)現(xiàn)。已有實(shí)現(xiàn)RecyclerView,NestedScroView
Google在給我提供這兩個(gè)接口的時(shí)候邢滑,同時(shí)也給我們提供了實(shí)現(xiàn)這兩個(gè)接口時(shí)一些方法的標(biāo)準(zhǔn)實(shí)現(xiàn)腐螟,
分別是
NestedScrollingChildHelper
NestedScrollingParentHelper
我們?cè)趯?shí)現(xiàn)上面兩個(gè)接口的方法時(shí),只需要調(diào)用相應(yīng)Helper中相同簽名的方法即可困后。
之后由于NestedScrollingParent/NestedScrollingChild功能有些不足乐纸,Google又引入NestedScrollingParent2/NestedScrollingChild2,具體引入原因下文會(huì)說(shuō)摇予。
本文示例代碼主要是NestedScrollingParent2/NestedScrollingChild2
基本原理
對(duì)原始的事件分發(fā)機(jī)制做了一層封裝汽绢,子View實(shí)現(xiàn)NestedScrollingChild接口,父View實(shí)現(xiàn)NestedScrollingParent 接口侧戴。
假設(shè)產(chǎn)生一個(gè)豎直滑動(dòng)宁昭,簡(jiǎn)單來(lái)說(shuō)滑動(dòng)事件會(huì)由NestedScrollingChild先接收到產(chǎn)生一個(gè)dy跌宛,然后詢問(wèn)NestedScrollingParent要消耗多少(dyConsumed),自己再拿dy-dyConsumed來(lái)進(jìn)行滑動(dòng)。當(dāng)然NestedScrollingChild有可能自己本身也并不會(huì)消耗完积仗,此時(shí)會(huì)再向父View報(bào)告情況疆拘。
NestedScrollingParent
NestedScrollingParentHelper 只為我們提供了onNestedScrollAccepted,onStopNestedScroll寂曹,getNestedScrollAxes三個(gè)方法的實(shí)現(xiàn)哎迄,其余的方法我們根據(jù)自身需要自己實(shí)現(xiàn)。NestedScrollingParent的方法基本上都是提供給NestedScrollingChild來(lái)調(diào)用的隆圆,我們自己無(wú)需調(diào)用漱挚。
本例使用的是27.0.0的RecyclerView,實(shí)現(xiàn)了NestedScrollingChild2,下面是本例中NestedScrollingParent2的完整實(shí)現(xiàn)渺氧。
class ScrollHeaderLayout : LinearLayout, NestedScrollingParent2 {
private lateinit var mNestedScrollingParentHelper: NestedScrollingParentHelper
private lateinit var mHeaderView: View
private lateinit var mBottomView: View
private var mScrollRange = 0
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
private fun init() {
orientation = VERTICAL
mNestedScrollingParentHelper = NestedScrollingParentHelper(this)
}
override fun onFinishInflate() {
super.onFinishInflate()
if (childCount != 2) {
throw IllegalStateException("ScrollHeaderLayout must have two children")
}
mHeaderView = getChildAt(0)
mBottomView = getChildAt(1)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
mScrollRange = scrollEvaluator.getScrollRange(mHeaderView)
val bottomHeightSpec = MeasureSpec.makeMeasureSpec(measuredHeight - mHeaderView.measuredHeight + mScrollRange, MeasureSpec.EXACTLY)
measureChild(mBottomView, widthMeasureSpec, bottomHeightSpec)
}
/**
* -----------------------------------------------------------
* NestedScrollingParent
*/
/**
* NestedScrollingChild 未fling之前告訴準(zhǔn)備fling的情況
*
* @param target 具體嵌套滑動(dòng)的那個(gè)子類
* @param velocityX 水平方向速度
* @param velocityY 垂直方向速度
* @return true 父View是否消耗了fling
*/
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
return false
}
/**
* NestedScrollingChild 在fling之后告訴自己fling情況
*
* @param target 具體嵌套滑動(dòng)的那個(gè)子類
* @param velocityX 水平方向速度
* @param velocityY 垂直方向速度
* @param consumed 子view是否fling了
* @return true 父View是否消耗了fling
*/
override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
return false
}
/**
* -----------------------------------------------------------
* NestedScrollingParent2
*/
/**
* 有子View發(fā)起了嵌套滑動(dòng)旨涝,確認(rèn)該父View是否接受嵌套滑動(dòng)
*
* @param child target向上一直尋找NestedScrollingParent,child在這個(gè)路徑上侣背,是NestedScrollingParent的直接子View
* @param target NestedScrollingChild,即發(fā)起NestedScrolling的類
* @param axes 嵌套滑動(dòng)的方向,水平方向白华,垂直方向,或者不指定
* @param type
* @return 是否接受該嵌套滑動(dòng)
*/
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL
}
/**
* 表示該父View已經(jīng)接受了嵌套滑動(dòng)秃踩。onStartNestedScroll 方法返回true后該方法會(huì)調(diào)用。
* NestedScrollingParentHelper為我們提供了該方法的標(biāo)準(zhǔn)實(shí)現(xiàn)业筏。
*
*/
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
}
/**
* NestedScrollingChild在準(zhǔn)備滑動(dòng)前先詢問(wèn)NestedScrollingParent需要消耗多少
*
* @param dx NestedScrollingChild水平方向想要滾動(dòng)的距離
* @param dy 垂直方向嵌套滑動(dòng)的子View豎直方向想要滾動(dòng)的距離
* @param consumed 這個(gè)參數(shù)用于告訴NestedScrollingChild 父View消耗掉的距離
* consumed[0] 水平消耗的距離憔杨,consumed[1] 垂直消耗的距離
*/
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray?, type: Int) {
val headViewHeight = mScrollRange
var consumedDy = 0
if (dy > 0) {
consumedDy = Math.min(dy, headViewHeight - scrollY)
} else {
if (target is RecyclerView) {
if (ScrollHelper.isRecyclerViewTop(target)) {
consumedDy = Math.max(dy, -scrollY)
}
}
}
consumed?.set(1, consumedDy)
scrollBy(0, consumedDy)
}
/**
* NestedScrollingChild自身也不一定消耗完全部距離,因此
* NestedScrollingChild自身滑動(dòng)完成后蒜胖,告訴NestedScrollingParent自己的滑動(dòng)情況
* @param dxConsumed NestedScrollingChild水平方向消耗的距離
* @param dyConsumed NestedScrollingChild豎直方向消耗的距離
* @param dxUnconsumed NestedScrollingChild水平方向未消耗的距離
* @param dyUnconsumed NestedScrollingChild豎直方向未消耗的距離
*/
override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
Log.i(ScrollHeaderLayout::class.java.simpleName, "dyConsumed:$dyConsumed,dyUnconsumed:$dyUnconsumed")
}
/**
* 停止嵌套滑動(dòng)時(shí)
*/
override fun onStopNestedScroll(target: View, type: Int) {
mNestedScrollingParentHelper.onStopNestedScroll(target, type)
}
/**
* ------------------------------------
*/
private var scrollEvaluator: ScrollRangeEvaluator = object : ScrollRangeEvaluator {
override fun getScrollRange(header: View): Int {
return if ((header is ViewGroup) && header.childCount > 0) {
header.getChildAt(0).measuredHeight
} else {
header.measuredHeight
}
}
}
fun setScrollRangeEvaluator(evaluator: ScrollRangeEvaluator) {
this.scrollEvaluator = evaluator
}
interface ScrollRangeEvaluator {
fun getScrollRange(header: View): Int
}
}
NestedScrollingChild
一般情況下我們并不需要自己實(shí)現(xiàn)一個(gè)NestedScrollingChild, 系統(tǒng)已經(jīng)為我們提供了RecyclerView和NestedScrollView大多數(shù)情況下都?jí)蛴昧讼穑@里只是幫助大家更好理解它。
我們自己要實(shí)現(xiàn)一個(gè)NestedScrollingChild分為兩步
1) 實(shí)現(xiàn)NestedScrollingChild里的方法台谢。這一步非常簡(jiǎn)單寻狂,NestedScrollingChildHelper里面已經(jīng)為我們提供了所有NestedScrollingChild所需要的實(shí)現(xiàn)。
2)在合適的實(shí)際調(diào)用相應(yīng)的方法朋沮,大部分都需要在onTouchEvent方法中調(diào)用蛇券。調(diào)用時(shí)機(jī)下文會(huì)以RecyclerView為例來(lái)講解。
class NestedChildView(context: Context, attrs: AttributeSet?) : View(context, attrs), NestedScrollingChild2 {
private var mScrollingChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
init {
isNestedScrollingEnabled = true
}
/**
* 設(shè)置是否開(kāi)啟嵌套滑動(dòng)
* @param enabled
*/
override fun setNestedScrollingEnabled(enabled: Boolean) {
mScrollingChildHelper.isNestedScrollingEnabled = enabled
}
override fun isNestedScrollingEnabled(): Boolean {
return mScrollingChildHelper.isNestedScrollingEnabled
}
/**
* 開(kāi)始嵌套滑動(dòng)流程樊拓,一般ACTION_DOWN里面調(diào)用纠亚。
* 調(diào)用這個(gè)函數(shù)的時(shí)候會(huì)向上尋找NestedScrollingParent,如果找到了并且NestedScrollingParent 說(shuō)可以滑動(dòng)的話就返回true,否則返回false
* @param axes:支持嵌套滾動(dòng)軸筋夏。水平方向蒂胞,垂直方向,或者不指定
* @return true 父控件說(shuō)可以滑動(dòng)条篷,false 父控件說(shuō)不可以滑動(dòng)
*/
override fun startNestedScroll(axes: Int, type: Int): Boolean {
return mScrollingChildHelper.startNestedScroll(axes, type)
}
/**
* 是否有嵌套滑動(dòng)對(duì)應(yīng)的父控件
*/
override fun hasNestedScrollingParent(type: Int): Boolean {
return mScrollingChildHelper.hasNestedScrollingParent(type)
}
/**
* 在嵌套滑動(dòng)的子View滑動(dòng)之前骗随,告訴父View滑動(dòng)的距離蛤织,讓父View做相應(yīng)的處理。
*
* @param dx 告訴父View水平方向需要滑動(dòng)的距離
* @param dy 告訴父View垂直方向需要滑動(dòng)的距離
* @param consumed 出參. 父View通過(guò)這個(gè)參數(shù)告訴子View鸿染,自己對(duì)事件的消耗情況指蚜。consumed[0]父View告訴子View水平方向滑動(dòng)的距離(dx)
* consumed[1]父View告訴子View垂直方向滑動(dòng)的距離(dy).
* @param offsetInWindow 可選 length=2的數(shù)組,如果父View滑動(dòng)導(dǎo)致子View的窗口發(fā)生了變化(子View的位置發(fā)生了變化)
* 該參數(shù)返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化牡昆。 這個(gè)參數(shù)用于對(duì)觸摸事件位置進(jìn)行校準(zhǔn)姚炕。
* 如果你記錄了手指最后的位置,需要根據(jù)參數(shù)offsetInWindow計(jì)算偏移量丢烘,才能保證下一次的touch事件的計(jì)算是正確的柱宦。
*
* 一般在ACTION_MOVE中準(zhǔn)備滑動(dòng)之前
* @return true 父View滑動(dòng)了,false 父View沒(méi)有滑動(dòng)播瞳。
*/
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?,type: Int): Boolean {
return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,type)
}
/**
* 在嵌套滑動(dòng)的子View滑動(dòng)之后再調(diào)用該函數(shù)向父View匯報(bào)滑動(dòng)情況掸刊。
*
* @param dxConsumed 子View水平方向滑動(dòng)的距離
* @param dyConsumed 子View垂直方向滑動(dòng)的距離
* @param dxUnconsumed 子View水平方向沒(méi)有滑動(dòng)的距離
* @param dyUnconsumed 子View垂直方向沒(méi)有滑動(dòng)的距離
*
* 一般在在ACTION_MOVE中調(diào)用,在dispatchNestedPreScroll之后
* @return true 如果父View有滑動(dòng)做了相應(yīng)的處理, false 父View沒(méi)有滑動(dòng).
*/
override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
offsetInWindow: IntArray?,type: Int): Boolean {
return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow,type)
}
/**
* 停止嵌套滑動(dòng)流程(一般ACTION_UP里面調(diào)用)
*/
override fun stopNestedScroll(type: Int) {
mScrollingChildHelper.stopNestedScroll()
}
/**
* 在嵌套滑動(dòng)的子View fling之前告訴父View fling的情況赢乓。
*
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @return 如果父View fling了
*/
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY)
}
/**
* 在嵌套滑動(dòng)的子View fling之后再調(diào)用該函數(shù)向父View匯報(bào)fling情況忧侧。
*
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @param consumed true 如果子View fling了, false 如果子View沒(méi)有fling
* @return true 如果父View fling了
*/
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mScrollingChildHelper.onDetachedFromWindow()
}
}
Why V2
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
return false
}
override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
return false
}
NestedScrollingParent中為我們提供了如上兩個(gè)方法用于處理fling事件,但是由于傳過(guò)來(lái)一個(gè)速度牌芋。對(duì)于速度而言無(wú)法說(shuō)父View消耗一部分蚓炬,子View消耗一部分。因此老版本fling事件只能由父View或者子View中的一個(gè)處理躺屁。這種情況顯然不合理肯夏,比如示例Demo滑動(dòng)速度大,父View滑動(dòng)完犀暑,子View應(yīng)該繼續(xù)滑動(dòng)驯击。
針對(duì)fling無(wú)法在子View和父View之間交替的問(wèn)題,NestedScrollingParent2直接廢棄onNestedPreFling和onNestedFling方法耐亏。 并給原來(lái)的onStartNestedScroll徊都,onNestedScrollAccepted,onNestedPreScroll广辰,onNestedScroll暇矫,onStopNestedScroll方法添加一個(gè)type參數(shù),定義如下
@IntDef({TYPE_TOUCH, TYPE_NON_TOUCH})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP)
public @interface NestedScrollType {}
TYPE_TOUCH表示正常的手指觸摸的滾動(dòng)
TYPE_NON_TOUCH表示的是fling引起的滾動(dòng)
然后再fling時(shí)候也會(huì)重新走一遍嵌套滑動(dòng)的流程择吊,只是type傳的TYPE_NON_TOUCH袱耽。
源碼分析
以RecyclerView為例分析,RecylerView實(shí)現(xiàn)NestedScrollingParent2接口干发,方法的實(shí)現(xiàn)和NestedChildView幾乎一樣朱巨,我們主要是看一下相應(yīng)方法的調(diào)用時(shí)機(jī),以及NestedScrollingChildHelper的標(biāo)準(zhǔn)實(shí)現(xiàn)做了些什么枉长。
@Override
public boolean onTouchEvent(MotionEvent e) {
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
//如果父View發(fā)生了滑動(dòng)等冀续,觸摸事件位置需要偏移
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//1.ACTION_DOWN時(shí)候開(kāi)始嵌套滑動(dòng)
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
//2.RecylcerView沒(méi)開(kāi)始滑動(dòng)琼讽,先問(wèn)一下父View是不是需要滑動(dòng)
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
//減去父View消耗
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// 父View滑動(dòng)的話更新offset
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//3.自身滑動(dòng),并向父View報(bào)告滑動(dòng)情況
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
//fling觸發(fā)調(diào)用
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
//4.停止嵌套滑動(dòng)
resetTouch();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
- ACTION_DOWN時(shí)候開(kāi)始嵌套滑動(dòng)
startNestedScroll的目的就是向上找到NestedScrollParent并詢問(wèn)是否接要嵌套滑動(dòng)
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//循環(huán)往上尋找NestedScrollingParent
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
//為什么判斷
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
如果是NestedScrollingParent2的話直接onStartNestedScroll洪唐,不是的話因?yàn)橹袄习姹镜腘estedScrollingParent只支持TYPE_TOUCH的滑動(dòng)钻蹬,因此需要判斷一下。
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
return false;
}
記錄找到的NestedScrollingParent凭需。
private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
switch (type) {
case TYPE_TOUCH:
mNestedScrollingParentTouch = p;
break;
case TYPE_NON_TOUCH:
mNestedScrollingParentNonTouch = p;
break;
}
}
- ACTION_MOVE,子View未開(kāi)始滑動(dòng)前先詢問(wèn)父View是否消耗
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
//獲取找打startNestedScroll時(shí)候找到的NestedScrollingParent
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
//記錄RecyclerView在滑動(dòng)事件傳給父View前 在窗口上位置
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
//置0
consumed[0] = 0;
consumed[1] = 0;
//調(diào)用NestedScrollingParent的onNestedPreScroll
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
//父View滑動(dòng)后位置減去滑動(dòng)前位置得到一個(gè)偏移量
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//通過(guò)consumed!=0確定父View是否消耗
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
3.NestedScrollingChild完成對(duì)滾動(dòng)事件的消耗问欠,并向NestedScrollingParent報(bào)告
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
if (mAdapter != null) {
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
//RecylerView滑動(dòng),返回自己滑動(dòng)消耗的
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
//獲取未消耗的
unconsumedY = y - consumedY;
}
}
//自己滑動(dòng)消耗完事件后粒蜈,向NestedScrollingParent報(bào)告自己滑動(dòng)的情況顺献,父View此時(shí)還可以進(jìn)行一些滑動(dòng)操作等
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
return consumedX != 0 || consumedY != 0;
}
dispatchNestedScroll的核心就是調(diào)用父View的onNestedScroll,代碼很簡(jiǎn)單
- 停止嵌套滑動(dòng)
ACTION_UP或者ACTION_CANCEL觸發(fā)后枯怖,都會(huì)調(diào)用resetTouch這個(gè)方法注整。
private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll(TYPE_TOUCH);
releaseGlows();
}
調(diào)用NestedScrollingParent的onStopNestedScroll方法,把自己的成員變量置空度硝。
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}
- fling
public boolean fling(int velocityX, int velocityY) {
//這兩個(gè)方法v2的版本其實(shí)不需要了肿轨,這里只是兼容一下
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//1.開(kāi)始嵌套滑動(dòng)
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
//ViewFlinger真正實(shí)現(xiàn)fling
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
class ViewFlinger implements Runnable {
public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
postOnAnimation();
}
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
removeCallbacks(this);
//簡(jiǎn)單認(rèn)為View.post
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}
@Override
public void run() {
final OverScroller scroller = mScroller;
final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
if (scroller.computeScrollOffset()) {
final int[] scrollConsumed = mScrollConsumed;
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
int dx = x - mLastFlingX;
int dy = y - mLastFlingY;
int hresult = 0;
int vresult = 0;
mLastFlingX = x;
mLastFlingY = y;
int overscrollX = 0, overscrollY = 0;
//2.調(diào)用dispatchNestedPreScroll
if (dispatchNestedPreScroll(dx, dy, scrollConsumed, null, TYPE_NON_TOUCH)) {
dx -= scrollConsumed[0];
dy -= scrollConsumed[1];
}
if (mAdapter != null) {
if (dx != 0) {
hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
overscrollX = dx - hresult;
}
if (dy != 0) {
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
}
if (!dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null,
TYPE_NON_TOUCH)
&& (overscrollX != 0 || overscrollY != 0)) {
final int vel = (int) scroller.getCurrVelocity();
int velX = 0;
if (overscrollX != x) {
velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
}
int velY = 0;
if (overscrollY != y) {
velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
}
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
absorbGlows(velX, velY);
}
if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
&& (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
scroller.abortAnimation();
}
}
if (hresult != 0 || vresult != 0) {
dispatchOnScrolled(hresult, vresult);
}
if (!awakenScrollBars()) {
invalidate();
}
final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically()
&& vresult == dy;
final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally()
&& hresult == dx;
final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal
|| fullyConsumedVertical;
//如果滑動(dòng)完成了
if (scroller.isFinished() || (!fullyConsumedAny
&& !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
setScrollState(SCROLL_STATE_IDLE);
if (ALLOW_THREAD_GAP_WORK) {
mPrefetchRegistry.clearPrefetchPositions();
}
//停止嵌套滑動(dòng)
stopNestedScroll(TYPE_NON_TOUCH);
} else {
//滑動(dòng)沒(méi)有完成,繼續(xù)post執(zhí)行run方法
postOnAnimation();
}
}
}
}