Android在发布5.0以后加入了嵌套滑动机制NestedScrolling,为嵌套滑动提供了更方便的处理方案。在此对嵌套滑动机制进行详细的分析。javascript
嵌套滑动的常见用法好比在滑动列表的时候隐藏相关的TopBar和BottomBar,增长列表的信息展现范围,让用户聚焦于App想展现的内容上等。官方出的Design包里也有不少支持该机制的炫酷控件,好比CoordinatorLayout,AppBarLayout等,在用户体验上有很大的进步。java
说道嵌套滑动,离不开如下几个内容:android
在具体说明以前,先来看看咱们的Sample,这是一个仿携程机票首页的Demo
spring
这里用到了一个实现了NestedScrollingParent的CollaspingLayout做为父View和一个实现了NestedScrollingChild的NestedScrollView做为子View进行嵌套滑动,布局能够简单的描述成:
app
<com.lycc.flight.fastproject.widget.search.CollaspingLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/pl_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="160dp">
<com.yyydjk.library.BannerLayout
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="160dp"
app:autoPlayDuration="5000"
app:indicatorMargin="50dp"
app:indicatorPosition="centerBottom"
app:indicatorShape="oval"
app:indicatorSpace="3dp"
app:scrollDuration="1100"
app:defaultImage="@mipmap/ic_launcher"
app:selectedIndicatorColor="?attr/colorPrimary"
app:selectedIndicatorHeight="6dp"
app:selectedIndicatorWidth="6dp"
app:unSelectedIndicatorColor="#99ffffff"
app:unSelectedIndicatorHeight="6dp"
app:unSelectedIndicatorWidth="6dp"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.7"/>
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/gradient" />
<FrameLayout
android:id="@+id/search_tab_container"
android:layout_width="match_parent"
android:layout_height="43dp"
android:layout_marginBottom="-4dp"
android:layout_alignParentBottom="true">
<View
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="#5a000000"
android:layout_marginLeft="5dp"
android:layout_marginTop="3dp"
android:layout_marginBottom="-4dp"
android:layout_marginRight="5dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom"
android:layout_marginBottom="-4dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:orientation="horizontal">
<View
android:id="@+id/slide_bg"
android:layout_width="120dp"
android:layout_height="43dp"
android:background="@drawable/ctrip_slide_tab"/>
</LinearLayout>
<RadioGroup
android:id="@+id/rg_slide"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center"
android:layout_centerInParent="true">
<RadioButton
android:id="@+id/rb_left"
android:background="@null"
android:textColor="@color/top_layout_sliide_text_color_selector"
android:gravity="center"
android:button="@null"
android:textSize="16dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:checked="false"
android:text="单程" />
<RadioButton
android:id="@+id/rb_center"
android:background="@null"
android:textColor="@color/top_layout_sliide_text_color_selector"
android:gravity="center"
android:textSize="16dp"
android:button="@null"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="往返" />
<RadioButton
android:id="@+id/rb_right"
android:background="@null"
android:button="@null"
android:textColor="@color/top_layout_sliide_text_color_selector"
android:gravity="center"
android:textSize="16dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="多程"
android:visibility="visible" />
</RadioGroup>
</FrameLayout>
<LinearLayout
android:id="@+id/top_container"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:minHeight="?attr/actionBarSize"
android:orientation="horizontal"
android:visibility="gone"
android:gravity="center"
android:layout_alignParentTop="true"
app:layout_collapseMode="pin"
android:background="@color/ctirp_color_primary">
<RadioGroup
android:layout_width="261dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_centerInParent="true">
<RadioButton
android:background="@drawable/title_left_shape"
android:padding="6dp"
android:textColor="@color/top_layout_text_color_selector"
android:gravity="center"
android:button="@null"
android:textSize="16dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:checked="true"
android:text="单程" />
<RadioButton
android:background="@drawable/title_center_shape"
android:padding="6dp"
android:textColor="@color/top_layout_text_color_selector"
android:gravity="center"
android:textSize="16dp"
android:button="@null"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="-1dp"
android:layout_marginRight="-1dp"
android:text="往返" />
<RadioButton
android:background="@drawable/title_right_shape"
android:padding="6dp"
android:button="@null"
android:textColor="@color/top_layout_text_color_selector"
android:gravity="center"
android:textSize="16dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="多程"
android:visibility="visible" />
</RadioGroup>
</LinearLayout>
</RelativeLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/search_bg"
android:scaleType="fitStart"/>
</android.support.v4.widget.NestedScrollView>
</com.lycc.flight.fastproject.widget.search.CollaspingLayout>复制代码
从布局能够看到其实在实现了NestedScrollingParent以后就能很方便的完成子View和父View的嵌套滑动,下面就来简单看看上面的四个类是如何使用的,在系统为咱们提供的控件中,NestedScrollView是实现了这个机制的控件,以它的实现为例,首先看做为嵌套滑动的子View:ide
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}复制代码
再来看看一样做为嵌套滑动父View的CollaspingLayout的实现工具
// NestedScrollingParent
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
if(mHeaderController.getScrollPercentage() == 1.0f){
mHeaderState = STATE_IDLE_TOP;
}else if(mHeaderController.getScrollPercentage() == 0.0f){
mHeaderState = STATE_IDLE_BOTTOM;
}
computeScroll();
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
final int myConsumed = moveBy(dyUnconsumed);
final int myUnconsumed = dyUnconsumed - myConsumed;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (dy > 0 && mHeaderController.canScrollUp()) {
final int delta = moveBy(dy);
consumed[0] = 0;
consumed[1] = delta;
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed) {
flingWithNestedDispatch((int) velocityY);
return true;
}
return false;
}复制代码
从上面的实现能够看出,基本上都是经过mParentHelper和mChildHelper来完成滑动的,没接触过这方面的同窗看着确定以为很难理解,的确有些跳跃性,在说清楚这个问题以前必须先把这几个类之间的交互逻辑理清楚才能不至于不知所云。
先来梳理一会儿View和父View的接中都有哪些方法。这种套路通常都是子View发起的而后父View进行回调从而完成配合。布局
子View | 父View |
---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedScroll | onNestedScroll |
stopNestedScroll | onStopNestedScroll |
这里的子View指的是实现了NestedScrollingChild的View,例如咱们的NestedScrollView,父View指的是实现了NestedScrollingParent的View,好比咱们上面写的CollaspingLayout。post
首先在子View滑动还未开始以前将调用startNestedScroll,对应NestedScrollView中的ACTION_DOWN:动画
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_DOWN: {
......
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);//在接到点击事件之初调用
break;
}
}复制代码
那么调用 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)寓意何在?跟进去看到实际上是调用mChildHelper.startNestedScroll(axes)的实现
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
//重点在这-------> 在子View开始滑动前通知父View,回调到父View的onStartNestedScroll(),
//父View须要滑动则返回true:
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
//---------> 若是父View决定要和子View一块滑动,调用父ViewonNestedScrollAccepted()
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}复制代码
你们仔细看我在代码里加的注释,须要关心的就是父View在此时须要决定是否跟随子View滑动,看看父View的实现:
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}复制代码
ViewCompat.SCROLL_AXIS_VERTICAL的值是2(10),因此当nestedScrollAxes 也为2的时候,返回true,回到上面能够看到只要是竖直方向的 滑动,父View就会和子View进行嵌套滑动。而在父View的
onNestedScrollAccepted中,则把滑动的方向给保存下来了。这样父View和子View的第一次合做关系就结束了,再看看接下来是如何配合的。
当子View在滑动的Move事件中,又开始了嵌套滑动
@Override
public boolean onTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_MOVE:
final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
}复制代码
在子View决定滑动的时候,再次在进行本身的滑动前调用dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
//--------->重点在这,首先把consume封装好,consumed[0]表示X方向父View消耗的距离,
// consumed[1]表示Y方向上父View消耗的距离,在父View处理前固然都是0
consumed[0] = 0;
consumed[1] = 0;
//而后调用父View的onNestedPreScroll并把当前的dx,dy以及消耗距离的consumed传递过去
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
if (offsetInWindow != null) {
mView.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;
}复制代码
看看父View是怎么处理的,也是实现了这套机制的,看看他是怎么处理的:
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (dy > 0 && mHeaderController.canScrollUp()) {
final int delta = moveBy(dy);
consumed[0] = 0;
consumed[1] = delta;
}
}复制代码
经过moveby计算父View滑动的距离,并将父ViewY方向消耗的距离记录下来
继续来看子View,在通知了父View而且父View消耗了滑动距离以后,剩下的就是本身进行滑动了
@Override
public boolean onTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_MOVE:
final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
//重点在这:-------->父View滑动以后调整本身的Offset为父View滑动的距离
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
.........
if(mIsBeingDragged){
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.
//重点在这:-------->父View消耗了部分滑动距离后,子View本身开始滑动,经过overScrollByCompat
//把滑动距离的参数传给mScroller进行弹性滑动
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
}
......
//重点在这:-------->本身滑动完以后再调用dispatchNestedScroll通知父View滑动结束
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
break;
}复制代码
接下来又是父View的回调了,来看看父View的处理:
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
final int myConsumed = moveBy(dyUnconsumed);
final int myUnconsumed = dyUnconsumed - myConsumed;
}复制代码
父View在这里将最后子View滑动完后剩余的距离进行收尾处理,自我调整后第二轮的嵌套滑动也结束了。
那么再看看最后一轮滑动:
@Override
public boolean onTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
stopNestedScroll();
break;
}复制代码
在触控事件的最后一个阶段,也就是ACTION_UP时,调用stopNestedScroll(),这时会通知父View的onStopNestedScroll()来对整个系列的滑动来收尾
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
mNestedScrollingParent = null;
}
}复制代码
父类最后在本身的onStopNestedScroll()实现相关的收尾处理,好比重置滑动状态标记,完成动画操做,通知滑动结束等。这样,整个滑动嵌套流程就完成了。
最后来总结一下整个流程,分为三个步骤:
这样,子View和父View的一系列嵌套滑动就完成了,能够看出来整个嵌套滑动仍是靠子View来推进父View进行滑动的,这也解决了在传统的滑动事件中一旦事件被子View处理了就很难再分享给父View共同处理的问题,这也是嵌套滑动的一个特色。
嵌套滑动做为官方推出的一套更加方便的处理滑动的工具,能够说是很大程度上减小了咱们在出来这方面问题上的复杂性,固然,上面提到的仅仅是原理,真正的实现你们能够仔细地去看Design包一些控件的源码来进一步深刻了解。同时,下一次还将继续分享如何用三大利器:CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout来实现携程机票首页的交互,敬请期待。
附上仿携程机票首页交互:
三分钟带你仿携程机票首页炫酷交互