产生原因
由于在Android上进行滑动的控件在手机性能越来越好的情况下,人们已经习惯于常用的手势进行操作,出现许多控件滑动时需要去协调同一个界面的滑动的情况。例如在同一个方向内外(上下)的嵌套,不同方向(上下与左右方向)的嵌套等。解决这类嵌套可以通过Android开发艺术书上讲的内部拦截法
和外部拦截法
去解决,但是,在处理多个View的协调时使用外部拦截法,特别是一些第三方库,在使用时就必须去修改源码里的onTouch()等方法。并且在处理如下图所示,当滑动到一定距离又需要拦截的View去响应滑动,这种情况是需要自己去手动处理事件分发,相对就复杂不少。于是Google在5.0后推出了NestedScrolling解决方式。
这是Google官方从5.0后引入的滑动嵌套解决方案,同时能够向后兼容。最有代表性的就是Handling Scrolls with CoordinatorLayout中的效果,
下面的效果就是从5.0后常见的效果。那我们需要去了解背后的原因。再举一反三去扩展使用到自己想要的效果。
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"></android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
上面知道了AppBarLayout是LinearLayout并且注解说明使用AppBarLayout.Behavior.class
来处理协调滑动。
具体
看看这个Behavior,可以看到几个关键的NestedScroll相关方法,后面会具体说明。
再了解一下CoordinatorLayout,实现了NestedScrollingParent接口
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent
从上面了解的一些,我们接下来就全面来了解以下几个类。
先看看NestedScrollingChild的细节。
public interface NestedScrollingChild {
/**
* 设置嵌套滑动是否能用
*
* @param enabled true to enable nested scrolling, false to disable
*/
public void setNestedScrollingEnabled(boolean enabled);
/**
* 判断嵌套滑动是否可用
*
* @return true if nested scrolling is enabled
*/
public boolean isNestedScrollingEnabled();
/**
* 开始嵌套滑动
*
* @param axes 表示方向轴,有横向和竖向
*/
public boolean startNestedScroll(int axes);
/**
* 停止嵌套滑动
*/
public void stopNestedScroll();
/**
* 判断是否有父View 支持嵌套滑动
* @return whether this view has a nested scrolling parent
*/
public boolean hasNestedScrollingParent();
/**
* 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
*
* @param dx x轴上滑动的距离
* @param dy y轴上滑动的距离
* @param consumed 父view消费掉的scroll长度
* @param offsetInWindow 子View的窗体偏移量
* @return 支持的嵌套的父View 是否处理了 滑动事件
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
* 子view处理scroll后调用
*
* @param dxConsumed x轴上被消费的距离(横向)
* @param dyConsumed y轴上被消费的距离(竖向)
* @param dxUnconsumed x轴上未被消费的距离
* @param dyUnconsumed y轴上未被消费的距离
* @param offsetInWindow 子View的窗体偏移量
* @return true if the event was dispatched, false if it could not be dispatched.
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
* 滑行时调用
*
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @param consumed 是否被消费
* @return true if the nested scrolling parent consumed or otherwise reacted to the fling
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 进行滑行前调用
*
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @return true if a nested scrolling parent consumed the fling
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
CoordinatorLayout
里嵌套着RecyclerView
和Toolbar
,我们上下滑动RecyclerView
的时候,Toolbar
会随之显现隐藏,这是典型的嵌套滑动机制情景。这里,RecyclerView
作为嵌套的子View,我们猜测,它一定实现了NestedScrollingChild
接口。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
}
所以RecyclerView 实现了NestedScrollingChild
接口里的方法,我们在跟进去看看各个方法是怎么实现的?
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return getScrollingChildHelper().isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
getScrollingChildHelper().stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return getScrollingChildHelper().hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
从上面的代码可以看出,全部都交给getScrollingChildHelper()
这个方法的返回对象处理了,看看这个方法是怎么实现的。
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}
NestedScrollingChild
接口的方法都交给NestedScrollingChildHelper
这个代理对象处理了。现在我们继续深入,随意挑个,分析下NestedScrollingChildHelper
中开始嵌套滑动startNestedScroll(int axes)
方法是怎么实现的。
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
以上方法主要做了:
-
判断是否有嵌套滑动的父View,返回值 true 表示找到了嵌套滑动的父View和同意一起处理 Scroll
事件。
-
用While的方式寻找最近嵌套滑动的父View
,如果找到调用父view的onNestedScrollAccepted.
从这里至少可以得出
子view在调用某个方法都会回调嵌套父view相应的方法,比如子view开始了startNestedScroll,如果嵌套父view存在,就会回调父view的onStartNestedScroll、onNestedScrollAccepted方法。
NestedScrollingChildHelper#dispatchNestedPreScroll
NestedScrollingChildHelper#dispatchNestedScroll
NestedScrollingChildHelper#stopNestedScroll
以上Helper的实现也是这个思路。
public interface NestedScrollingParent {
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX,
float velocityY,boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
其实和子view差不多的方法,大致一一对应关系,而且它的具体实现也交给了NestedScrollingParentHelper
这个代理类,这和我们上文的方式是一样的,就不再重复了
调用流程
-
当 NestedScrollingChild(下文用Child代替) 要开始滑动的时候会调用
onStartNestedScroll
,然后交给代理类NestedScrollingChildHelper(下文ChildHelper代替)的onStartNestedScroll请求给最近的NestedScrollingParent(下文Parent代替).
-
当ChildHelper的onStartNestedScroll方法 返回 true 表示同意一起处理
Scroll 事件的时候时候,ChildHelper会通知Parent回调onNestedScrollAccepted
做一些准备动作
-
当Child
要开始滑动的时候,会先发送onNestedPreScroll,交给ChildHelper的onNestedPreScroll 请求给Parent
,告诉它我现在要滑动多少距离,你觉得行不行,这时候Parent 根据实际情况告诉Child 现在只允许你滑动多少距离.然后 ChildHelper根据
onNestedPreScroll 中回调回来的信息对滑动距离做相对应的调整.
-
在滑动的过程中 Child
会发送onNestedScroll通知ChildeHelpaer的onNestedScroll告知Parent 当前 Child
的滑动情况.
-
当要进行滑行的时候,会先发送onNestedFling 请求给Parent,告诉它
我现在要滑行了,你说行不行, 这时候Parent会根据情况告诉 Child 你是否可以滑行.
-
Child 通过onNestedFling 返回的 Boolean
值来觉得是否进行滑行.如果要滑行的话,会在滑行的时候发送onNestedFling 通知告知 Parent 滑行情况.
-
当滑动事件结束就会child发送onStopNestedScroll通知 Parent
去做相关操作.
我做一个图,做得不好不要见笑。
再来个Material Design风格的动态效果
参考链接
Handling Scrolls with
CoordinatorLayout
NestedScrolling事件机制源码解析
NestedScrollingChild
NestedScrollingChildHelper
NestedScrollingParent
NestedScrollingParentHelper
SwipeRefreshLayout 解析
Android NestedScrolling机制完全解析 带你玩转嵌套滑动
从源码角度分析嵌套滑动机制NestedScrolling