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
时,能够实现标题栏的隐藏和显示。