程序员人生 网站导航

4AppBarLayout滑动原理

栏目:综合技术时间:2016-10-08 16:06:12

4AppBarLayout滑动原理

在CoordinatorLayout的measure和layout里,其实介绍过1点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是1个vertical的LinearLayout,实现了很多material的概念,主要是跟滑动相干的。AppBarLayout的子view需要提供layout_scrollFlags参数。AppBarLayout和CoordinatorLayout强相干,1般作为CoordinatorLayout的子类,配套使用。
按我的理解,AppBarLayout内部有2种view,1种可滑出(屏幕),另外一种不可滑出,根据app:layout_scrollFlags辨别。1般上边放可滑出的下边放不可滑出的。

举个例子以下,内有个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>

效果以下所示

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

down事件

来看看上图的事件传递的顺序,先看down。简单来讲,这个down事件被传递下来,1直无人处理,然后往上传到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就相当于1般的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<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,重点看第3个参数getTopBottomOffsetForScrollingSibling() - dy,这个算出来的就是经过这次move行将到达的offset(不是top哦,top=offset+mLayoutTop)。getTopBottomOffsetForScrollingSibling就是获得当前的偏移量,这个命名我不太理解。setHeaderTopBottomOffset就是给header设置1个新的offset,这个offset用1个min1个max来制约,很简单。setHeaderTopBottomOffset可以认为就是view的offsetTopAndBottom,调剂top和bottom到达平移的效果

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

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

measure进程

在http://blog.csdn.net/litefish/article/details/52327502曾分析过简单情况下CoordinatorLayout的布局进程。这里稍有变化,主要在于第3次measure RelativeLayout的时候getScrollRange不再是0
final int height = availableHeight - header.getMeasuredHeight()
+ getScrollRange(header);
就是availableHeight-AppBar.measuredheight+toolbar高度,结果就是availableHeight。
所以此时RelativeLayout的终究measure高度是1731,这个高度是成心义的,他比不可转动的appbar多了1个toolbar的高度,这么高的1个RelativeLayout在当前屏幕是放不下的,所以RelativeLayout常常会用1个可转动的view来替换,比如Recyclerview或NestedScrollView。

上滑可以滑到状态栏

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

AppBarLayout的range

mTotalScrollRange 525
mDownPreScrollRange ⑴
mDownScrollRange 525

总结

1、ScrollView滑动的实现是通过修改scrollY,而AppBarLayout的实现是通过直接修改top和bottom的,其实就是把全部AppBarLayout内部的东西往上平移。
2、CoordinatorLayout里的mBehaviorTouchView就相当于1般的ViewGroup里的mFirstTouchTarget的作用
3、和嵌套滑动1样始终只有1个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

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐