CoordinatorLayout
在CoordinatorLayout出现之前,为了处理嵌套滑动逻辑,一般需要继承一个ViewGroup,重写onInterceptTouchEvent和onTouchEvent等方法并实现相应的逻辑,然后在布局中直接引用。这样做有一点非常不好的地方就是:代码冗余,同时处理逻辑也比较繁琐。为此,CoordinatorLayout提供了一套非常完美的解决方案,具体来说:
解耦
为了给开发者提供足够的可扩展性,CoordinatorLayout以一个代理者的身份被设计的,具体来说,在嵌套滑动过程中,CoordinatorLayout尽可能的只做事件的转发,将事件转发给具体的处理逻辑,即:Behavior处理。因此,对于不同的嵌套处理方案编写不同的Behavior,同时可以不考虑其直接子view类型时实现功能,保证该子view的完整性和封装性。
Behavior
简要介绍常用api的用途:
- layoutDependsOn:view之间依赖关系判断接口
- onDependentViewChanged:依赖view变化时回调接口,在这里可以做跟随依赖view变化而变化的逻辑
- onLayoutChild:可以劫持并处理CoordinatorLayout布局child的接口,在这里可以实现自己的布局方式
- onMeasureChild:和onLayoutChild类似,可以劫持CoordinatorLayout测量child的逻辑
- onStartNestedScroll:判断是否需要处理滑动事件的接口,很重要、很重要、很重要。
- onNestedPreScroll:作为CoordinatorLayout劫持直接子view滑动时的接口,可以优先处理滑动,保证父view优先于子view处理滑动,主要是为了实现滑动的顺序性,即谁先滑动谁后滑动。
- onNestedScroll:当子view不能滑动时,CoordinatorLayout如何处理后续滑动的接口,如SwipeLayout的拦截原理
- onNestedPreFling:与onNestedPreScroll类似,CoordinatorLayout可以提前劫持直接子view的惯性滑动事件
- onNestedFling:与onNestedScroll类似,继续处理惯性滑动
- onStopNestedScroll:滑动停止时的回调,在这里可以清理数据状态、做滚动到指定位置等等。注意在一个滚动事件的生命周期内被回调次数
注意点:
- onStartNestedScroll:拦截事件点需要认真考虑清楚,例如:RecycleView跟随headerView连续滑动的场景下,当headerview已经滑到底时,若RecycleView继续向上滑动就不需要拦截事件,使用RecycleView自身的处理逻辑更合理。
- onStopNestedScroll:在一个滚动事件的生命周期内至少会被调用两次(谜一样的设计),若在这里处理动画时,需要判断处理时机,否则可能出现如抖动等异常效果。
基本上处理好上述两个api的使用,嵌套滑动不是难事
原理
上面说CoordinatorLayout主要是将事件转发给具体的处理逻辑进行处理,那么如何转发的呢?这里分析几个主要api的源码:
onDependentViewChanged – 如何监听 View 的状态
在onMeasure方法中调用:
void ensurePreDrawListener() {
boolean hasDependencies = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (hasDependencies(child)) {
hasDependencies = true;
break;
}
}
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
addPreDrawListener();
} else {
removePreDrawListener();
}
}
}
void addPreDrawListener() {
if (mIsAttachedToWindow) {
// Add the listener
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
// 重点:给ViewTreeObserver 添加OnPreDrawListener,在draw方法中进行判断是否依赖view有状态变化
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
mNeedsPreDrawListener = true;
}
在 OnPreDrawListener 监听里面会调用 onChildViewsChanged 方法,在该方法里面会根据 View的状态回调 onDependentViewRemoved 或者 onDependentViewChanged 方法。
onStartNestedScroll – 嵌套滑动事件处理的开关
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//回调Behavior#onStartNestedScroll方法
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,target, axes, type);
handled |= accepted;
//标记后续事件是否传递给该view,可以查看下面onNestedPreScroll的源码
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
主要思路:遍历所有直接子view,交给某直接子view的Behavior处理嵌套滑动,并根据处理结果标记是否继续转发后续事件。
onNestedPreScroll
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
//是否转发事件开关
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
//这里是劫持子view优先滑动的关键,主要思路:
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
// 1、优先滑动
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
// 2、计算未消费完的滑动距离
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
// 3、将未消费完的滑动距离返还给子view,子view根据该距离进行后续滑动
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
从改段源码中可以看出:任何一个子view的Behavior都可以引起父view优先滑动,具体思路看代码注解。
performIntercept
onInterceptTouchEvent 和 onTouchEvent最终都走到改方法中
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
final List<View> topmostChildList = mTempList1;
// 1、 对view进行排序,排序规则是:Android5.0以上系统,按照z属性来排序;其它,按照添加顺序或者自定义的绘制顺序来排列。
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
// 3、若已有view拦截了事件,则其它view不能进行拦截处理,按照view事件处理流程需要给其它view发送cancle事件
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
// 2、遍历子view,依次调用子view的Behavior进行事件拦截与否的处理
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
}
这里的处理逻辑遵循了view事件处理规则,在第2步骤中是优先处理被依赖的view的。如view A依赖view B,则B有优先处理拦截事件与处理事件的权利,具体可以看CoordinatorLayout的layout源码,这里不做分析。其他源码分析可以看前人的博客:一步步带你读懂 CoordinatorLayout 源码,比较容易理解。
demo地址:https://github.com/zjf71165/BehaviorDemo.git