Android NestedScrolling 嵌套滚动原理解析
一.原有问题
众所周知,android的触摸事件传递有局限性,当比较复杂的可滚动控件嵌套在一起的时候,总会有各种各样的滑动问题,这与android的触摸事件传递机制(View触摸事件机制)密不可分:
android的触摸事件传递是从上至下的递归传递,如果某次DOWN事件,有子view消费了,则之后的所有事件都只可能交由该子view处理,其父view没有机会再去处理(只能拦截);
并且很多ViewGroup,比如ViewPager、ScrollView,直接在onInterceptTouchEvent方法中,将move事件拦截,为的是交由自己处理,而没有兼顾到其子view可能的滑动;
所以原有的触摸传递问题的局限性就是:一旦子view处理事件后,父view就不能处理了,并且因此导致许多ViewGroup为了自己处理,直接拦截事件并没有交由子view处理事件,导致滑动效果的局限性很大
二.解决方案
1.实现原理
android在5.0后,对这块的问题做了很大的改善,其根本解决办法就是:既然原有问题是子view处理事件后父view就处理不了,那么就想办法在子view处理前,给父view一个处理的机会就好了。
这样有三个好处:
-
不影响核心的触摸事件传递机制,还是从上至下递归执行;一个view处理后不会交由其他view处理(onTouchEvent)
-
父view不用为了自己处理而武断的拦截事件,因为自己也会有一个处理事件的机会
-
可以让一次触摸事件在多层view中都应用上去,即可以实现滚动view内部嵌套滚动view的效果
2.方案设计
(1)android的support-v4包提供了两个接口来实现NestedScroll框架
NestedScrollingChild:提供了作为内部嵌套类应该实现的方法
public interface NestedScrollingChild {
//是否可以嵌套滚动
public void setNestedScrollingEnabled(boolean enabled);
//自身是否支持嵌套滚动
public boolean isNestedScrollingEnabled();
//开始内部嵌套滚动,返回值为是否可以内部嵌套滚动,参数为滚动方向
public boolean startNestedScroll(int axes);
//停止内部嵌套滚动
public void stopNestedScroll();
//是否已经有支持其内部嵌套滚动的父view
public boolean hasNestedScrollingParent();
//在内部嵌套滚动时,派发给支持其嵌套滚动的parent,使其有机会做一些滚动的处理
//参数为x/y轴已消费的和未消费的距离
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
//在内部嵌套滚动前,派发给支持其嵌套滚动的parent,使其有机会做一些滚动的预处理
//dx,dy为可以消耗的距离,consumed为已经消耗的距离
//返回值为支持其嵌套滚动的parent是否消费了部分距离
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
//在内部嵌套自由滑动时,派发给支持其嵌套滚动的parent,使其有机会做一些自由滑动的处理
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
//在内部嵌套自由滑动前,派发给支持其嵌套滚动的parent,使其有机会做一些自由滑动的预处理
//返回值为支持其嵌套滚动的parent是否消费了fling事件
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
NestedScrollingParent:提供了作为支持内部嵌套滚动的view的方法
public interface NestedScrollingParent {
//是否接受此次的内部嵌套滚动
//target是想要内部滚动的view,child是包含target的parent的直接子view
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);
//内部嵌套滚动开始前做一些预处理,主要是根据dx,dy,将自己要消费的距离计算出来,告知target(通过consumed一层层记录实现)
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
//内部嵌套滑动开始,consumed参数为target是否消费了fling事件,parent可以根据此来做出自己的选择
//返回值为parent自己是否消费了fling事件
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
//内部嵌套滑动开始前做一些预处理
//返回值为parent自己是否消费此次fling事件
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
}
(2)android的support-v4包也提供了两个相应的Helper类来实现通用功能
NestedScrollingChildHelper:内部嵌套滚动view实现NestedScrollingChild的一些通用实现
public class NestedScrollingChildHelper {
private final View mView;
private ViewParent mNestedScrollingParent;
private boolean mIsNestedScrollingEnabled;
//view作为内部嵌套滚动的view
public NestedScrollingChildHelper(View view) {
mView = view;
}
//开始嵌套滚动的方法
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
//之前已经找到了支持嵌套滚动的parent,说明正在进行嵌套滚动,则返回true即可
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
//向上找到支持内部嵌套滚动的parent,并记录下来
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;
}
//派发滚动事件
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
//已经消费部分距离或者还有未消费距离时,需要派发给parent进行处理
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
//派发,即调用到NestedScrollingParent的onNestedScroll中进行处理
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
mView.