CoordinatorLayout解析

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:滑动停止时的回调,在这里可以清理数据状态、做滚动到指定位置等等。注意在一个滚动事件的生命周期内被回调次数

注意点:

  1. onStartNestedScroll:拦截事件点需要认真考虑清楚,例如:RecycleView跟随headerView连续滑动的场景下,当headerview已经滑到底时,若RecycleView继续向上滑动就不需要拦截事件,使用RecycleView自身的处理逻辑更合理。
  2. 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值