Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 4AppBarLayout滑動原理

4AppBarLayout滑動原理

編輯:關於Android編程

4AppBarLayout滑動原理

在CoordinatorLayout的measure和layout裡,其實介紹過一點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不可滑出

    

        

        

    

效果如下所示

\

這個跟ScrollView有所不同,框的大小和內容大小一樣,這樣上滑的時候,底部必然會空出一部分(200),ScrollView的實現是通過修改scrollY,而AppBarLayout的實現是直接修改top和bottom的,其實就是把整個AppBarLayout內部的東西往上平移。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMSBpZD0="down事件">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(具體後邊分析),所以intercepted就為true,mBehaviorTouchView就會設置為AppBarLayout,然後performIntercept結束返回true。這個mBehaviorTouchView就相當於一般的ViewGroup裡的mFirstTouchTarget的作用。再回頭看上邊代碼,performIntercept返回true了,那就能進入L13,會調用mBehaviorTouchView.behavior.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 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的時候發揮作用的

AppBarLayout的range

mTotalScrollRange 525
mDownPreScrollRange -1
mDownScrollRange 525

總結

1、ScrollView滑動的實現是通過修改scrollY,而AppBarLayout的實現是通過直接修改top和bottom的,其實就是把整個AppBarLayout內部的東西往上平移。
2、CoordinatorLayout裡的mBehaviorTouchView就相當於一般的ViewGroup裡的mFirstTouchTarget的作用
3、和嵌套滑動一樣始終只有一個view可以fling,不可能A fling完 B fling

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved