Android嵌套滑动及CoordinatorLayout源码分析
问题分析
嵌套滑动⼀直是Android中⽐较棘⼿的问题,根本原因是Android的事件分发机制导致的:当⼦控件消费了事件, 那么⽗控件就不会再有机会处理这个事件了。 所以⼀旦内部的滑动控件消费了滑动操作, 外部的滑动控件就再也没机会响应这个滑动操作了。
如何解决?
不过这个问题终于在LOLLIPOP(SDK21)之后终于有了官⽅的解决⽅法,就是嵌套滑动机制。嵌套滑动的基本原理是在⼦控件接收到滑动⼀段距离的请求时,先询问⽗控件是否要滑动,如果需要滑动就通知⼦控件它消耗了⼀部分滑动距离,⼦控件就只处理剩下的滑动距离,然后⼦控件滑动完毕后再把剩余的滑动距离传给⽗控件。
关于兼容
SDK21之后,嵌套滑动的相关逻辑作为普通⽅法直接写进了最新的View和ViewGroup类中;
SDK21之前,官⽅在android.support.v4兼容包中提供了两个接⼝NestedScrollingChild和NestedScrollin
gParent,还有两个辅助类NestedScrollingChildHelper和NestedScrollingParentHelper来帮助控件实现嵌套滑动。简单来说就是,在接⼝⽅法内对应调⽤辅助类的⽅法就可以兼容嵌套滑动了。
所以为了兼容低版本, 处理嵌套滑动更常⽤到的是后者调⽤接⼝⽅法的⽅式。
相关⽅法
NestedScrollingChild
startNestedScroll : 起始⽅法,主要作⽤是到接收滑动距离信息的外控件。
dispatchNestedPreScroll : 在内控件处理滑动前把滑动信息分发给外控件。
dispatchNestedScroll : 在内控件处理完滑动后把剩下的滑动距离信息分发给外控件。
stopNestedScroll : 结束⽅法, 主要作⽤就是清空嵌套滑动的相关状态。
setNestedScrollingEnabled和isNestedScrollingEnabled : ⼀对get&set⽅法,⽤来判断控件是否⽀持嵌套滑动。 dispatchNestedPreFling和dispatchNestedFling : 跟Scroll的对应⽅法作⽤类似,不过分发的不是滑动信息⽽是Fling信息。本⽂主要关注滑动的处理, 所以后续不分析这两个⽅法。
从上⾯⽅法可以看出,内控件是嵌套滑动的发起者.。
NestedScrollingParent
onStartNestedScroll : 对应startNestedScroll,内控件通过调⽤外控件的这个⽅法来确定外控件是否接收滑动信息。 onNestedScrollAccepted : 当外控件确定接收滑动信息后该⽅法被回调,可以让外控件针对嵌套滑动做⼀些前期⼯作。 onNestedPreScroll : 关键⽅法,接收内控件处理滑动前的滑动距离信息,在这⾥外控件可以优先响应滑动操作,消耗部分或者全部滑动距离。
onNestedScroll : 关键⽅法,接收内控件处理完滑动后的滑动距离信息,在这⾥外控件可以选择是否处理剩余的滑动距离。 onStopNestedScroll : 对应stopNestedScroll,⽤来做⼀些收尾⼯作。
getNestedScrollAxes : 返回嵌套滑动的⽅向,区分横向滑动和竖向滑动。
onNestedPreFling和onNestedFling : 同上略。
从上⾯⽅法可以看出,外控件的⼤部分⽅法都是被内控件的对应⽅法回调的。内控件是发起者,外控件是回调者。
通过CoordinatorLayout看嵌套机制
注意:下⽂所指的CoordinatorLayout(⽗控件)、RecyclerView(内控件)以及ImageView(⼦控件)均为中控件。
CoordinatorLayout是android support design推出的新布局,主要作为视图根布局,⽤于协调⼦控件之间的交互。
这⾥将通过CoordinatorLayout、RecyclerView以及⼀个CoordinatorLayout的直接⼦控件ImageView实现的动画效果(),对CoordinatorLayout进⾏源码分析的同时,探索嵌套滑动机制的实现原理。
上⾯已经说了嵌套滑动是从startNestedScroll开始,所以在RecyclerView出调⽤这个⽅法的地⽅。
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
case MotionEvent.ACTION_DOWN: {
...
startNestedScroll(nestedScrollAxis);
} break;
...
}
...
return true;
}
因为ACTION_DOWN是滑动操作的开始事件,所以当接收到这个事件的时候尝试对应的⽗控件。只有到了⽗控件才有后续的嵌套滑动的逻辑发⽣。
接着我们看startNestedScroll是如何对应的⽗控件的,因为RecyclerView#startNestedScroll调⽤了辅助⽅法的startNestedScroll,所以下⾯直接贴NestedScrollingChildHelper#startNestedScroll。
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
//是否⽀持嵌套滑动
if (isNestedScrollingEnabled()) {
ViewParent p = Parent();
View child = mView;
//遍历寻⽗控件
while (p != null) {
//调⽤外控件的onStartNestedScroll⽅法来确定外控件是否接收滑动信息
if (StartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
//外控件确定接收滑动信息后onNestedScrollAccepted被回调
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
遍历⽗控件,调⽤⽗控件的onStartNestedScroll,返回true表⽰到了对应的⽗控件,到⽗控件后马上调⽤onNestedScrollAccepted。那么问题来了,CoordinatorLayout作为⽗控件,它的onStartNestedScroll⽅法什么时候会返回true?
以上是CoordinatorLayout#onStartNestedScroll⽅法的源码。可以看到,只有当⼦控件Behavior的onStartNestedScroll⽅法返回为
true时,CoordinatorLayout#onStartNestedScroll才会返回true。那么问题⼜来了,Behavior⼜是什么⿁?知之为知之,不知官⽹知:可以看到Behavior 是针对 CoordinatorLayout 中 child 的交互插件。记住这个词:插件。插件也就代表如果⼀个 child 需要某种交互,它就需要加载对应的 Behavior,否则它就是不具备这种交互能⼒的。⽽ Behavior 本⾝是⼀个抽象类,它的实现类都是为了能够让⽤户作⽤在⼀个 View 上进⾏拖拽、滑动、快速滑动等⼿势。如果⾃⼰要定制某个交互动作,就需要⾃⼰实现⼀个 Behavior。再来看Behavior源码:
@Override
public  boolean  onStartNestedScroll (View child, View target, int  nestedScrollAxes) {
boolean  handled = false ;
final  int  childCount = getChildCount();
for  (int  i = 0; i < childCount; i++) {
final  View view = getChildAt(i);
if  (Visibility() == View.GONE) {
// If it's GONE, don't dispatch
continue ;
}
final  LayoutParams lp = (LayoutParams) LayoutParams();
final  Behavior viewBehavior = lp.getBehavior();
//如果⼦控件的Behavior 不为空,则触发⼦控件Behavior 的onStartNestedScroll ⽅法
if  (viewBehavior != null ) {
final  boolean  accepted = StartNestedScroll(this , view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else  {
lp.acceptNestedScroll(false );
}
}
return  handled;
}
public static abstract class Behavior<V extends View> {
public Behavior() { }
public Behavior(Context context, AttributeSet attrs) {}
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  return false; }
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) {
return false;
}
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
View directTargetChild, View target, int nestedScrollAxes) {
// Do nothing
}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
// Do nothing
}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
// Do nothing
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dx, int dy, int[] consumed) {
// Do nothing
}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY) {
return false;
}
}
Behavior 其实是 CoordinatorLayout 中的⼀个静态内部类,并且是个泛型,接受任何 View 类型。
⼀般我们⾃定义⼀个 Behavior,⽬的有两个。
⼀是根据某些依赖的 View 的位置进⾏相应的操作(本⽂主要分析嵌套滑动的处理,所以View之间的依赖关系不再具体分析)。 相关⽅法:
layoutDependsOn
onDependentViewChanged
onDependentViewRemoved
另外⼀个就是响应 CoordinatorLayout 中某些组件的滑动事件。
相关⽅法:
onStartNestedScroll
onNestedScrollAccepted
onStopNestedScroll
onNestedScroll
onNestedPreScroll
onNestedFling
onNestedPreFling
有⽊有很眼熟的感觉?没错,和开始提到的NestedScrollingParent相关⽅法名字⼀模⼀样。所以这⾥就解决了刚才的疑问。当CoordinatorLayout⼦控件Behavior的onStartNestedScroll⽅法返回为true时,CoordinatorLayout的onStartNestedScroll⽅法才返回true。⾄于⼦控件Behavior的onStartNestedScroll⽅法返回true还是false,就要看你如何实现嵌套滑动的逻辑了。在中,我对ImageView的Behavior#onStartNestedScroll⽅法返回值的定义是,只要竖直⽅向滑动就返回true。
再回到刚刚的研究中,这时候调⽤了⽗控件的onStartNestedScroll⽅法返回true,内控件RecyclerView到⽗控件CoordinatorLayout 后马上调⽤CoordinatorLayout#onNestedScrollAccepted⽅法,其源码为:
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mNestedScrollingDirectChild = child;
mNestedScrollingTarget = target;
final int childCount = getChildCount();android最新版
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) LayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
}
}
}
这次就简单了,和onStartNestedScroll⽅法⼀个尿性,还是调⽤⼦控件Behavior#onNestedScrollAccepted呗,这⾥就不再过多分析,只需知道该⽅法是做⼀些前期的准备⼯作,可有可⽆。
到了⽗控件后ACTION_DOWN事件就没嵌套滑动的事了,要滑动肯定会在onTouchEvent中处理ACTION_MOVE事件,接着我们看RecyclerView的ACTION_MOVE事件具体是怎样处理的。