SDK21
以後,嵌套滑動相關的邏輯被寫入了View
和ViewGroup
類。android.support.v4
中提供了接口NestedScrollingChild
和NestedScrollingParent
,他們分別定義了View
和ViewParent
中新增的方法,還有兩個相關輔助類NestedScrollingChildHelper
和NestedScrollingParentHelper
。SDK21
以前,那麼就會判斷控件是否實現了接口,而後調用接口的方法,若是是SDK21
以後,那麼就能夠直接調用對應的方法。雖然View
和ViewGroup
自己就具備嵌套滑動的相關方法,可是默認狀況是不會調用,由於View
和ViewGroup
自己不支持滑動,即自己不支持滑動的控件即便有嵌套滑動的相關方法也不能進行嵌套滑動。 所以,要讓控件支持嵌套滑動,那麼要知足:android
21
以後的版本,要麼實現對應的接口。NestedScrollingChild
startNestedScroll
:起始方法,主要做用是找到接收滑動距離信息的外控件。dispatchNestedPreScroll
:在內控件處理滑動前把滑動信息分發給外控件。dispatchNestedScroll
:在內控件處理完滑動後把剩下的距離信息分發給外控件。stopNestedScroll
:結束方法,主要做用是清空嵌套滑動的相關狀態。setNestedScrollingEnabled
和isNestedScrollingEnabled
:用來判斷控件是否支持嵌套滑動。dispatchNestedPreFling
和dispatchNestedFling
:和Scroll
的對應方法相似,可是分發的是Fling
信息。NestedScrollingParent
由於內控件是發起者,因此外控件的大部分方法都是被內控件的對應方法所回調的。數組
onStartNestedScroll
:對應startNestedScroll
,內控件經過調用外控件的這個方法來肯定外控件是否接收滑動信息。onNestedScrollAccepted
:當外控件肯定接收滑動信息後該方法被回調,可讓外控件作一些前期工做。onNestedPreScroll
:關鍵方法,接收內控件處理滑動前的距離信息,在這裏外控件能夠優先響應滑動操做,消耗部分或者所有滑動距離。onNestedScroll
:關鍵方法,接收內控件處理完滑動後的距離信息,在這裏外控件能夠選擇是否處理剩餘的滑動信息。onStopNestedScroll
:對應stopNestedScroll
,用來作一些收尾工做。getNestedScrollAxes
:返回嵌套滑動的方向。onNestedPreFling
和onNestedFling
:同上。NestedScrollView
down
事件,尋找外控件NestedScrollView
其實是一個FrameLayout
,同時它實現了NestedScrollingParent、NestedScrollingChild、ScrollingView
這三個接口,它既能夠用來做爲外控件,也能夠用來做爲內控件。bash
咱們先從入口函數startNestedScroll
方法看起,它在NestedScrollView
中調用的地方有如下三處:app
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent ev)
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)
而在startNestedScroll
又會調用mChildHelper/View
的startNestedScroll
方法,下面咱們來看一下它的實現,它遍歷它全部的祖先節點,並調用每一個節點的onStartNestedScroll(child, this,axes)
方法,若是該方法返回了true
,那麼就將他做爲嵌套滑動的外控件記錄下來,以後全部和外控件的交互都是經過mNestedScrollingParent
來實現的,接下來調用它的onNestedScrollAccepted(child, this, axes)
方法,並中止遍歷,返回true
。若是它全部的祖先結點都不知足嵌套滑動的條件,那麼最終返回false
。ide
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
} catch (AbstractMethodError e) {
Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
"method onStartNestedScroll", e);
// Allow the search upward to continue
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
複製代碼
接下來,咱們看一下mParentHelper/ViewGroup
的public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
,它在ViewGroup
默認值是返回false
:函數
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return false;
}
複製代碼
而在NestedScrollView
中的條件是:佈局
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
複製代碼
在接着調用的onNestedScrollAccepted
中,ViewGroup
記錄下axes
的值:ui
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollAxes = axes;
}
複製代碼
而NestedScrollView
則會繼續調用startNestedScroll
來尋找它的外控件:this
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}
複製代碼
總結:第一個階段主要是爲了尋找到嵌套滑動的外控件,並肯定滑動的方向。spa
move
事件,交給外控件處理一部分的滑動距離以後的滑動就須要經過public boolean onTouchEvent(MotionEvent ev)
中的ACTION_MOVE
來處理了,咱們來看一下NestedScrollView
的處理邏輯:
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
//1.得到當前的y座標
final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
//2.記錄該次滑動的距離
int deltaY = mLastMotionY - y;
//3.若是有外控件,那麼交給它先處理滑動事件,這裏傳入了3個參數:
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
//.....
複製代碼
在View
的dispatchNestedPreScroll
,它經過先前保存下來的外控件變量,把當前滑動的距離傳給它來處理,在ViewGroup
中這個函數什麼事情也沒有作,若是咱們要實現本身的嵌套滑動邏輯,那麼就要在這裏面進行處理:
public boolean dispatchNestedPreScroll(int dx, int dy,
@Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
//調用父控件的接口,詢問它是否要消耗滑動事件.
mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
複製代碼
這個階段的過程,能夠理解爲:
y
座標的值y
座標的值計算出此次滑動的距離deltaY
deltaY
值交給外控件處理mScrollConsumed
表示該階段外控件消耗的距離,mScrollOffset
表示本次交給外控件以後,內控件窗口變更的座標值,若是消耗的x
或y
值不爲0,那麼該函數返回true
。deltaY - mScrollConsumed[1]
獲得內控件接下來要處理的距離。if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = getScrollY();
final int range = getScrollRange();
final int overscrollMode = ViewCompat.getOverScrollMode(this);
boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
(overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
range > 0);
// Calling overScrollByCompat will call onOverScrolled, which
// calls onScrollChanged if applicable.
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
//.....
}
複製代碼
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
//..
}
複製代碼
這裏調用了mChildHelper/View
的dispatchNestedScroll
方法,它裏面會經過mNestedScrollingParent
來通知外控件來處理剩餘的距離,在ViewGroup
的onNestedScroll
方法中,什麼也沒有作:
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable @Size(2) int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
複製代碼
up
事件,中止嵌套滑動經過調用stopNestedScroll
方法來中止滑動:
public boolean onInterceptTouchEvent(MotionEvent ev)
的ACTION_UP
public boolean onTouchEvent(MotionEvent ev)
的ACTION_UP
和ACTION_CANCEL
在View
的stopNestedScroll
方法中,調用外控件的onStopNestedScroll
方法來通知它整個滑動結束:
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
mNestedScrollingParent.onStopNestedScroll(this);
mNestedScrollingParent = null;
}
}
複製代碼
NestedScrollView
下面,咱們再經過一個簡單的例子,來看一下使用NestedScrollView
的效果,佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 標題部分 -->
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_height="wrap_content"
android:layout_width="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
app:layout_scrollFlags="scroll|enterAlways"
android:background="@android:color/holo_blue_dark"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- 內容部分 -->
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="1"
android:layout_width="match_parent"
android:layout_height="200dp"/>
<TextView
android:text="2"
android:layout_width="match_parent"
android:layout_height="200dp"/>
<TextView
android:text="3"
android:layout_width="match_parent"
android:layout_height="200dp"/>
<TextView
android:text="4"
android:layout_width="match_parent"
android:layout_height="200dp"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
複製代碼
咱們經過CoordinatorLayout
把標題部分和內容部分包裹起來,這樣再滑動下面的NestedScrollView
時,能夠實現標題欄的隱藏和顯示。