AppBarLayout滑动原理

这篇将重点讲解AppBarLayout的滑动原理以及behavior是如何影响onTouchEvent与onInterceptTouchEvent的。

基本原理

介绍AppBarLayout的mTotalScrollRange,mDownPreScrollRange,mDownScrollRange,滑动的基本概念
mTotalScrollRange内部可以滑动的view的高度(包括上下margin)总和

官方介绍

先来看看google的介绍
AppBarLayout is a vertical LinearLayout which implements many of the features of material designs app bar concept, namely scrolling gestures.

Children should provide their desired scrolling behavior through setScrollFlags(int) and the associated layout xml attribute: app:layout_scrollFlags.

This view depends heavily on being used as a direct child within a CoordinatorLayout. If you use AppBarLayout within a different ViewGroup, most of it’s functionality will not work.

AppBarLayout also requires a separate scrolling sibling in order to know when to scroll. The binding is done through the AppBarLayout.ScrollingViewBehavior behavior class, meaning that you should set your scrolling view’s behavior to be an instance of AppBarLayout.ScrollingViewBehavior. A string resource containing the full class name is available.

简单的整理下,AppBarLayout是一个vertical的LinearLayout,实现了很多material的概念,主要是跟滑动相关的。AppBarLayout的子view需要提供layout_scrollFlags参数。AppBarLayout和CoordinatorLayout强相关,一般作为CoordinatorLayout的子类,配套使用。
按我的理解,AppBarLayout内部有2种view,一种可滑出(屏幕),另一种不可滑出,根据app:layout_scrollFlags区分。一般上边放可滑出的下边放不可滑出的。

举个例子如下,内有个Toolbar、TextView,Toolbar写了app:layout_scrollFlags=”scroll”表示可滑动,Toolbar高200dp,TextView高100dp。Toolbar就是可滑出的,TextView就是不可滑出的。此时框高300(200+100),内容300,可滑动范围200

总高度300,可滑出部分高度200,剩下100不可滑出

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

        <TextView
            android:background="#ff0000"
            android:layout_width="match_parent"
            android:layout_height="100dp"></TextView>

    </android.support.design.widget.AppBarLayout>

效果如下所示


图片.png
图片.png

这个跟ScrollView有所不同,框的大小和内容大小一样,这样上滑的时候,底部必然会空出一部分(200),ScrollView的实现是通过修改scrollY,而AppBarLayout的实现是直接修改top和bottom的,其实就是把整个AppBarLayout内部的东西往上平移。

down事件

来看看上图的事件传递的顺序,先看down。简单来说,这个down事件被传递下来,一直无人处理,然后往上传到CoordinatorLayout被处理。但实际上CoordinatorLayout本身无法处理事件(他只是个壳),内部实际交由AppBarLayout的behavior处理。

总体分析

首先,down事件从CoordinatorLayout传到AppBarLayout再到TextView,没人处理,然后回传回来到AppBarLayout的onTouchEvent,不处理,再回传给CoordinatorLayout的onTouchEvent,这里主要看L10 performIntercept,type为TYPE_ON_TOUCH。

   @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        boolean cancelSuper = false;
        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        //此处会分发事件给behavior
        if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
            // Safe since performIntercept guarantees that
            // mBehaviorTouchView != null if it returns true
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }

        // Keep the super implementation correct
        if (mBehaviorTouchView == null) {
            handled |= super.onTouchEvent(ev);
        } else if (cancelSuper) {
            if (cancelEvent == null) {
                final long now = SystemClock.uptimeMillis();
                cancelEvent = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            }
            super.onTouchEvent(cancelEvent);
        }

        if (!handled && action == MotionEvent.ACTION_DOWN) {

        }

        if (cancelEvent != null) {
            cancelEvent.recycle();
        }

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            resetTouchBehaviors();
        }

        return handled;
    }

再看performIntercept,type为TYPE_ON_TOUCH,首先获取topmostChildList,这是把child按照z轴排序,最上面的排前面,CoordinatorLayout跟FrameLayout类似,越后边的child,在z轴上越靠上。所以,这里topmostChildList就是FloatingActionButton、AppBarLayout。然后在for循环里调用behavior的onTouchEvent。此时AppBarLayout.Behavior的onTouchEvent会返回true(具体后边分析, 这里第一次调用onTouchEvent()),所以intercepted就为true,mBehaviorTouchView就会设置为AppBarLayout,然后performIntercept结束返回true。这个mBehaviorTouchView就相当于一般的ViewGroup里的mFirstTouchTarget的作用。
再回头看上边代码,performIntercept返回true了,那就能进入L13,会调用mBehaviorTouchView.behavior.onTouchEvent(这里第二次调用onTouchEvent()),在这里把CoordinatorLayout的onTouchEvent,传递给了AppBarLayout.Behavior的onTouchEvent
而L16也会返回true,那整个CoordinatorLayout的onTouchEvent就返回true了,按照事件分发的规则,此时这个down事件被CoordinatorLayout消费了。但是实际上down事件的处理者是AppBarLayout.Behavior。他们之间通过mBehaviorTouchView连接。

    private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        final List<View> topmostChildList = mTempList1;
        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();
            。。。

            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;
                }
            }

            ...
        }

        topmostChildList.clear();

        return intercepted;
    }

AppBarLayout.Behavior的onTouchEvent为何返回true

上文说了“此时AppBarLayout.Behavior的onTouchEvent会返回true”,我们来具体分析下。来看AppBarLayout.Behavior的onTouchEvent。AppBarLayout.Behavior的onTouchEvent代码在HeaderBehavior内,看L12只要触摸点在AppBarLayout内,而且canDragView,那就返回true,否则返回false。在AppBarLayout内明显是满足的,那就看canDragView。

   @Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        if (mTouchSlop < 0) {
            mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
        }

        switch (MotionEventCompat.getActionMasked(ev)) {
            case MotionEvent.ACTION_DOWN: {
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();

                if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
                    mLastMotionY = y;
                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                    ensureVelocityTracker();
                } else {
                    return false;
                }
                break;
            }
            。。。        
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(ev);
        }

        return true;
    }

下边是AppBarLayout的canDragView,此时mLastNestedScrollingChildRef为null,所以走的是L16,返回true,那回头看上边的onTouchEvent也返回true。

    @Override
        boolean canDragView(AppBarLayout view) {
            if (mOnDragCallback != null) {
                // If there is a drag callback set, it's in control
                return mOnDragCallback.canDrag(view);
            }

            // Else we'll use the default behaviour of seeing if it can scroll down
            if (mLastNestedScrollingChildRef != null) {
                // If we have a reference to a scrolling view, check it
                final View scrollingView = mLastNestedScrollingChildRef.get();
                return scrollingView != null && scrollingView.isShown()
                        && !ViewCompat.canScrollVertically(scrollingView, -1);
            } else {
                // Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
                return true;
            }
        }

ps

可以看出在CoordinatorLayout的onTouchEvent处理down事件的过程中,调用了2次AppBarLayout.Behavior的onTouchEvent

MOVE事件

由上文可知down事件被CoordinatorLayout消费,所以move事件不会走到CoordinatorLayout的onInterceptTouchEvent,而直接进入onTouchEvent。此时mBehaviorTouchView就是AppBarLayout。看L10,直接进入,然后把move事件发给了AppBarLayout.Behavior。

   @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        boolean cancelSuper = false;
        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        //此处会分发事件给behavior
        if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
            // Safe since performIntercept guarantees that
            // mBehaviorTouchView != null if it returns true
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }
            。。。

        return handled;
    }

AppBarLayout.Behavior处理move事件的代码比较简单,判断超过mTouchSlop就调用scroll,而scroll等于调用setHeaderTopBottomOffset。这里主要关注scroll的后2个参数,minOffset和maxOffset,minOffset传的是getMaxDragOffset(child)即AppBarlayout的-mDownScrollRange。这里就是AppBarlayout的可滑动范围,即toolbar的高度(包括margin)的负值。minOffset和maxOffset代表的是滑动上下限制,这个很好理解,因为移动的时候改的是top和bottom,比如top范围就是[initTop-滑动范围,initTop],所以这里的minOffset是-mDownScrollRange,maxOffset是0.

//HeaderBehavior
 @Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        if (mTouchSlop < 0) {
            mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
        }

        switch (MotionEventCompat.getActionMasked(ev)) {
            case MotionEvent.ACTION_MOVE: {
                final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
                        mActivePointerId);
                if (activePointerIndex == -1) {
                    return false;
                }

                final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
                int dy = mLastMotionY - y;

                if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
                    mIsBeingDragged = true;
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                }

                if (mIsBeingDragged) {
                    mLastMotionY = y;
                    // We're being dragged so scroll the ABL
                    scroll(parent, child, dy, getMaxDragOffset(child), 0);
                }
                break;
            }

        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(ev);
        }

        return true;
    }

      final int scroll(CoordinatorLayout coordinatorLayout, V header,
            int dy, int minOffset, int maxOffset) {
        return setHeaderTopBottomOffset(coordinatorLayout, header,
                getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
    }

再看scroll里面,简单调用setHeaderTopBottomOffset,重点看第三个参数getTopBottomOffsetForScrollingSibling() - dy,这个算出来的就是经过这次move即将到达的offset(不是top哦,top=offset+mLayoutTop)。getTopBottomOffsetForScrollingSibling就是获取当前的偏移量,这个命名我不太理解。setHeaderTopBottomOffset就是给header设置一个新的offset,这个offset用一个min一个max来制约,很简单。setHeaderTopBottomOffset可以认为就是view的offsetTopAndBottom,调整top和bottom达到平移的效果

发现AppBarlayout对getTopBottomOffsetForScrollingSibling复写了,加了个mOffsetDelta,但是mOffsetDelta一直是0.

  @Override
        int getTopBottomOffsetForScrollingSibling() {
            return getTopAndBottomOffset() + mOffsetDelta;
        }

measure过程

http://blog.csdn.net/litefish/article/details/52327502曾经分析过简单情况下CoordinatorLayout的布局过程。这里稍有变化,主要在于第三次measure RelativeLayout的时候getScrollRange不再是0
final int height = availableHeight - header.getMeasuredHeight()

  • getScrollRange(header);
    就是availableHeight-AppBar.measuredheight+toolbar高度,结果就是availableHeight。
    所以此时RelativeLayout的最终measure高度是1731,这个高度是有意义的,他比不可滚动的appbar多了一个toolbar的高度,这么高的一个RelativeLayout在当前屏幕是放不下的,所以RelativeLayout往往会用一个可滚动的view来替换,比如Recyclerview或者NestedScrollView。

上滑可以滑到状态栏

上滑用的是setTopAndBottomOffset,并不会重新measure,layout,而fitSystemWindow是在measure,layout的时候发挥作用的

总结

1、ScrollView滑动的实现是通过修改scrollY,而AppBarLayout的实现是通过直接修改top和bottom的,其实就是把整个AppBarLayout内部的东西往上平移。
2、CoordinatorLayout里的mBehaviorTouchView就相当于一般的ViewGroup里的mFirstTouchTarget的作用
3、和嵌套滑动一样始终只有一个view可以fling,不可能A fling完 B fling

参考文章

http://dk-exp.com/2016/03/30/CoordinatorLayout/
http://www.jianshu.com/p/99adaad8d55c
https://code.google.com/p/android/issues/detail?id=177729

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容