android5.0協調佈局CoordinatorLayout(第一篇CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout之間的關係詳解)原理
阿新 • • 發佈:2019-02-06
首先從協調佈局最簡單的例子為入口開始分析,由淺到深,看效果圖:
此效果如果不用5.0以下的自定義的效果的話,相對麻煩很多,而用5.0的協調佈局的話只需要簡單的寫一個佈局檔案就搞定了,看佈局檔案程式碼
從這個xml檔案得知,此佈局大約可以分成三部分<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app1="http://schemas.android.com/apk/res/com.ricky.materialdesign.fab.animation" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.ricky.materialdesign.fab.animation.MainActivity" > <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <android.support.v7.widget.CardView android:layout_width="300dp" android:layout_height="200dp" android:layout_margin="0dp" app:cardCornerRadius="20dp" app:cardElevation="10dp" app:contentPadding="5dp" > <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_width="300dp" android:layout_height="200dp" android:layout_margin="0dp" app:cardCornerRadius="20dp" app:cardElevation="10dp" app:contentPadding="5dp" > <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_width="300dp" android:layout_height="200dp" android:layout_margin="0dp" app:cardCornerRadius="20dp" app:cardElevation="10dp" app:contentPadding="5dp" > <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_width="300dp" android:layout_height="200dp" android:layout_margin="0dp" app:cardCornerRadius="20dp" app:cardElevation="10dp" app:contentPadding="5dp" > <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_width="300dp" android:layout_height="200dp" android:layout_margin="0dp" app:cardCornerRadius="20dp" app:cardElevation="10dp" app:contentPadding="5dp" > <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_width="300dp" android:layout_height="200dp" android:layout_margin="0dp" app:cardCornerRadius="20dp" app:cardElevation="10dp" app:contentPadding="5dp" > <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_width="300dp" android:layout_height="200dp" android:layout_margin="0dp" app:cardCornerRadius="20dp" app:cardElevation="10dp" app:contentPadding="5dp" > <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> </android.support.v7.widget.CardView> </LinearLayout> </android.support.v4.widget.NestedScrollView> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="260dp" > <android.support.design.widget.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_scrollFlags="scroll|exitUntilCollapsed" android:minHeight="200dp" app1:collapsedTitleGravity="center_horizontal" app1:contentScrim="@color/mytextcolor" app1:expandedTitleGravity="center" app1:expandedTitleMargin="5dp" app1:statusBarScrim="@color/colorPrimary_pink" app1:title="6666" > <!-- 視差值越小滾動越明顯 --> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.7" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" android:background="?attr/colorPrimary" app:navigationIcon="@drawable/abc_ic_ab_back_mtrl_am_alpha" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="58dp" android:layout_height="58dp" android:layout_gravity="bottom|end" android:layout_margin="16dp" app:layout_behavior="com.ricky.materialdesign.fab.animation.FabBehavior" android:onClick="rotate" android:src="@drawable/ic_favorite_outline_white_24dp" /> </android.support.design.widget.CoordinatorLayout>
也就是說CoordinatorLayout 有三個直接子孩子,我們知道,以前我們常用的佈局有線性佈局、幀佈局、相對佈局、等分比佈局等,其中的控制元件擺在那個位置,根據屬性一看就會知道擺放的位置,那麼CoordinatorLayout 到底是怎麼擺放的呢?以及為什麼內容佈局會在標題佈局的下面呢?帶著問題點進原始碼
public class CoordinatorLayout extends ViewGroup implements
NestedScrollingParent
CoordinatorLayout繼承於ViewGroup,它沒有繼承我們常用的佈局方式,這就有點坑了,那麼既然直接繼承了ViewGroup的話,
public void onLayoutChild(View child, int layoutDirection) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.checkAnchorChanged()) { throw new IllegalStateException( "An anchor may not be changed after CoordinatorLayout" + " measurement begins before layout is complete."); } if (lp.mAnchorView != null) { // 有參照物情況下的佈局 layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection); } // 假如keyline不為空則以它keyline為依據佈局子View else if (lp.keyline >= 0) { layoutChildWithKeyline(child, lp.keyline, layoutDirection); } // 沒有設定參照物或參照線的情況下的普通佈局 else { layoutChild(child, layoutDirection); } }
額,這個方法又分三種形式,如果子View設定了app:layout_anchor="@id/toolbar",app:layout_anchorGravity="bottom|right"屬性的話,就會走layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection)佈局,這個兩個屬性的作用就是如果當前控制元件設定了此屬性的話那麼它將排列在指定id控制元件的範圍內排版,也就是依賴另一個view排版,做個試驗看看效果,現在將懸浮按鈕FloatingActionButton依賴於標題進行排版,看效果
可以看到懸浮按鈕跑到標題的右下方,但是可以看懸浮按鈕的高度好像居中,why?帶著疑問進入原始碼依賴佈局的原始碼看一下
private void layoutChildWithAnchor(View child, View anchor,
int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect anchorRect = mTempRect1;
final Rect childRect = mTempRect2;
// 得到參照物view的位置,child將放在anchor上
getDescendantRect(anchor, anchorRect);
getDesiredAnchoredChildRect(child, layoutDirection, anchorRect,
childRect);
child.layout(childRect.left, childRect.top, childRect.right,
childRect.bottom);
}
在真正佈局子view之前,還需要排序一下直接子View,也就是說,真正佈局之前不是根據xml的樹形結構來排版的,而是根據Behavior行為依賴來排版的,Behavior依賴可以讓我們的當前view根據另一個view的移動而移動,也可以自己擁有自己的Touch事件。好進入getDesiredAnchoredChildRect方法,計算當前子view的位置座標void getDesiredAnchoredChildRect(View child, int layoutDirection,
Rect anchorRect, Rect out) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//absGravity預設為center
final int absGravity = GravityCompat.getAbsoluteGravity(
resolveAnchoredChildGravity(lp.gravity), layoutDirection);
//設定了依賴view的排版屬性
final int absAnchorGravity = GravityCompat.getAbsoluteGravity(
resolveGravity(lp.anchorGravity), layoutDirection);
final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int anchorHgrav = absAnchorGravity
& Gravity.HORIZONTAL_GRAVITY_MASK;
final int anchorVgrav = absAnchorGravity
& Gravity.VERTICAL_GRAVITY_MASK;
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
int left;
int top;
// Align to the anchor. This puts us in an assumed right/bottom child
// view gravity.
// If this is not the case we will subtract out the appropriate portion
// of
// the child size below.
switch (anchorHgrav) {
default:
case Gravity.LEFT:
left = anchorRect.left;
break;
case Gravity.RIGHT:
left = anchorRect.right;
break;
case Gravity.CENTER_HORIZONTAL:
left = anchorRect.left + anchorRect.width() / 2;
break;
}
switch (anchorVgrav) {
default:
case Gravity.TOP:
top = anchorRect.top;
break;
case Gravity.BOTTOM:
top = anchorRect.bottom;
break;
case Gravity.CENTER_VERTICAL:
top = anchorRect.top + anchorRect.height() / 2;
break;
}
// Offset by the child view's gravity itself. The above assumed
// right/bottom gravity.
switch (hgrav) {
default:
case Gravity.LEFT:
left -= childWidth;
break;
case Gravity.RIGHT:
// Do nothing, we're already in position.
break;
case Gravity.CENTER_HORIZONTAL:
left -= childWidth / 2;
break;
}
switch (vgrav) {
default:
case Gravity.TOP:
top -= childHeight;
break;
case Gravity.BOTTOM:
// Do nothing, we're already in position.
break;
case Gravity.CENTER_VERTICAL:
top -= childHeight / 2;
break;
}
final int width = getWidth();
final int height = getHeight();
// Obey margins and padding
// 為子view留足夠的空間
left = Math.max(
getPaddingLeft() + lp.leftMargin,
Math.min(left, width - getPaddingRight() - childWidth
- lp.rightMargin));
top = Math.max(
getPaddingTop() + lp.topMargin,
Math.min(top, height - getPaddingBottom() - childHeight
- lp.bottomMargin));
out.set(left, top, left + childWidth, top + childHeight);
}
xml中,我們設定了左右座標為right,那麼此時懸浮按鈕的座標left=ToolBar的right,同理top為ToolBar的bottom,xml檔案裡沒有設定gravity屬性,預設就為center,這個我怎麼知道的,俗話說沒有程式碼就沒有真相看程式碼/**
* 預設放到參照物中間
*
* @param gravity
* @return
*/
private static int resolveAnchoredChildGravity(int gravity) {
return gravity == Gravity.NO_GRAVITY ? Gravity.CENTER : gravity;
}
this.gravity = a
.getInteger(
R.styleable.CoordinatorLayout_LayoutParams_android_layout_gravity,
Gravity.NO_GRAVITY);
如果沒有設定這個屬性,那麼就會預設NO_GRAVITY,那麼最終懸浮按鈕的gravity就是center,那麼繼續回到前面計算,都預設center了left
-= childWidth / 2,top -= childHeight / 2,這也就是為什麼我們的懸浮按鈕的頂部上在ToolBar的中間了,最後再計算是否為懸浮按鈕留在螢幕上有足夠的顯示空間,當然,你都不在螢幕上了,我還計算它幹嘛。上面提到根據依賴,對view樹的先排列進行重新排序,在哪實現的呢?
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//對view樹進行重新排序,進行layout的時,進行新view樹的layout
prepareChildren();
計運算元View大小的時候在此方法呼叫了prepareChildren()進行重新排序,look原始碼
/**
* 初始化child,為他們新增依賴排序
*/
private void prepareChildren() {
mDependencySortedChildren.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View child = getChildAt(i);
// 為子類準備註解的behavier
final LayoutParams lp = getResolvedLayoutParams(child);
lp.findAnchorView(this, child);
// 加入到依賴集合中
mDependencySortedChildren.add(child);
}
// We need to use a selection sort here to make sure that every item is
// compared
// against each other
selectionSort(mDependencySortedChildren, mLayoutDependencyComparator);
}
看註解,為子類準備註解的Behavier,什麼鬼?LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
Class<?> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
while (childClass != null
&& (defaultBehavior = childClass
.getAnnotation(DefaultBehavior.class)) == null) {
childClass = childClass.getSuperclass();
}
if (defaultBehavior != null) {
try {
result.setBehavior(defaultBehavior.value().newInstance());
} catch (Exception e) {
Log.e(TAG,
"Default behavior class "
+ defaultBehavior.value().getName()
+ " could not be instantiated. Did you forget a default constructor?",
e);
}
}
result.mBehaviorResolved = true;
}
return result;
}
這裡最重要的一句話getAnnotation(DefaultBehavior.class),通過反射獲取屬性,從而為LayoutParams設定Behavior,看一下標題包裹控制元件
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout
AppBarLayout實現了這個註解,關係一已經找到,CoordinatorLayout通過AppBarLayout的Behavior控制AppBarLayout的移動,從而產生關係,接著看排序
private static void selectionSort(final List<View> list,
final Comparator<View> comparator) {
if (list == null || list.size() < 2) {
return;
}
final View[] array = new View[list.size()];
list.toArray(array);
final int count = array.length;
for (int i = 0; i < count; i++) {
int min = i;
for (int j = i + 1; j < count; j++) {
// 第一個view與後面的view依次做比較
if (comparator.compare(array[j], array[min]) < 0) {
min = j;
}
}
if (i != min) {
// We have a different min so swap the items
// 調換位置,小的放前面
final View minItem = array[min];
array[min] = array[i];
array[i] = minItem;
}
}
// Finally add the array back into the collection
list.clear();
for (int i = 0; i < count; i++) {
list.add(array[i]);
}
}
}
這裡用到了自定義Comparator,熟悉java的小夥伴應該對這個類不會陌生,那麼看一下它怎麼排序的
final Comparator<View> mLayoutDependencyComparator = new Comparator<View>() {
@Override
public int compare(View lhs, View rhs) {
// 物件完全一樣返回相等
if (lhs == rhs) {
return 0;
}
// 假如當前的view依賴後面的view,那麼後面的view放在集合的前面
else if (((LayoutParams) lhs.getLayoutParams()).dependsOn(
CoordinatorLayout.this, lhs, rhs)) {
return 1;
// 被依賴的view放前面
} else if (((LayoutParams) rhs.getLayoutParams()).dependsOn(
CoordinatorLayout.this, rhs, lhs)) {
return -1;
} else {
return 0;
}
}
};
是不是很晴朗了,如果當前的子view依賴於另一個子View,那麼就將它排列到集合的前面,最終先對前面的進行佈局,通過分析佈局大體上也清楚了CoordinatorLayout和幀佈局有點像,不過加了其他強大的規則,yes,那麼暫且叫他加強版的幀佈局。
既然是幀佈局的話,為啥內容佈局android.support.v4.widget.NestedScrollView為啥沒把標題佈局給覆蓋掉,或者說剛開始的時候內容佈局為啥排在了標題佈局下面?
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" >
xml屬性中,咱們可以看到NestedScrollView也聲明瞭一個behavior,這個Behavior是android.support.design.widget.AppBarLayout$ScrollingViewBehavior,AppBarLayout的一個內部類,猜測一下如果NestedScrollView要放在AppBarLayout緊貼下方的話,8成是ScrollingViewBehavior中做了什麼手腳,好帶著這個推測,進入原始碼瞧一瞧public boolean onLayoutChild(CoordinatorLayout parent, View child,
int layoutDirection) {
// First lay out the child as normal
super.onLayoutChild(parent, child, layoutDirection);
// Now offset us correctly to be in the correct position. This is
// important for things
// like activity transitions which rely on accurate positioning
// after the first layout.
/**
* 如果用了ScrollingViewBehavior的話,此控制元件將緊貼著appbarlayout
*/
final List<View> dependencies = parent.getDependencies(child);
for (int i = 0, z = dependencies.size(); i < z; i++) {
if (updateOffset(parent, child, dependencies.get(i))) {
// If we updated the offset, break out of the loop now
break;
}
}
return true;
}
ScrollingViewBehavior中的onLayoutChild決定了此控制元件是否決定自己的佈局位置規則,這個方法的核心思想就是獲得所有依賴的View,並通過他們的高度與偏移量讓子View偏移多少量,這裡NestScrollView依賴於AppBarLayout,那麼就會根據AppBarLayout的高度和已經偏移了多少量來讓NestScrollView最初顯示時偏移到AppBarLayout下方,看偏移的方法
private boolean updateOffset(CoordinatorLayout parent, View child,
View dependency) {
final CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) dependency
.getLayoutParams()).getBehavior();
if (behavior instanceof Behavior) {
// Offset the child so that it is below the app-bar (with any
// overlap)
final int offset = ((Behavior) behavior)
.getTopBottomOffsetForScrollingSibling();
//設定子控制元件的偏移位置
setTopAndBottomOffset(dependency.getHeight() + offset
- getOverlapForOffset(dependency, offset));
return true;
}
return false;
}
這裡又有一個疑問了,onLayoutChild方法什麼時候被呼叫的呢?是不是CoordinatorLayout中的Onlayout方法呢?@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
/**
* 註冊behavior的自己實現佈局,behavior.onLayoutChild如果返回為true的話
*/
if (behavior == null
|| !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
果然如所料,如果Behavior的onLayoutChild方法返回true則子view自己確定自己的位置,如果為false則依靠CoordinatorLayout顯示它的位置,分析到這裡可以看出,如果我們想讓一個佈局檔案一直顯示在AppBarLayout下面的話就讓它實現Behavior為ScrollingViewBehavior,看名字它還會滾動,怎麼滾動的呢?我們將手指放在NestedScrollView範圍內移動的話,按道理觸控事件應該被NestedScrollView獲得,除非父類攔截,好看看CoordinatorLayout有沒有攔截,推測不會攔截,攔截話滑動就不連貫了,帶著推測進入
onInterceptTouchEvent的方法,對分發事件不是很瞭解的小夥伴請自行腦補
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors();
}
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}
return intercepted;
}
看這個方法得知只要intercepted為true的話,那麼NestedScrollView的事件將不會被執行,那麼接下來進入performIntercept瞧一瞧什麼時候會攔截,什麼時候不會攔截分發事件
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 || 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;
}
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的Behavier去決定是否攔截,那麼看一下AppBarLayout的Behavier攔截事件
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
if (mTouchSlop < 0) {
mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
final int action = ev.getAction();
// Shortcut since we're being dragged
if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
return true;
}
switch (MotionEventCompat.getActionMasked(ev)) {
case MotionEvent.ACTION_DOWN: {
mIsBeingDragged = false;
final int x = (int) ev.getX();
final int y = (int) ev.getY();
//最關鍵的部分判斷觸控事件是否在AppBarLayout控制元件範圍內,並且
if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {
mLastMotionY = y;
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
ensureVelocityTracker();
}
break;
}
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
if (pointerIndex == -1) {
break;
}
final int y = (int) MotionEventCompat.getY(ev, pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY = y;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
}
return mIsBeingDragged;
}
這個方法最核心的判斷就是觸控事件是否在AppBarLayout內,建立在這個前提下,又滑動了手機認為的最小距離,那麼就會攔截,也就是說,如果手指的觸控在AppBarLayout
內觸控的,那麼事件和NestedScrollView半毛錢關係都沒有了,直接都不會傳到它哪了了,那麼再看一下NestedScrollView的Behavier的攔截事件
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child,
MotionEvent ev) {
return false;
}
壓根就沒複寫父類方法,始終返回false,666666既然大NestedScrollView已經實現了那麼牛逼的事件處理了,自己攔截自己不是脫褲子放屁找麻煩嗎,所以如果沒有其他子控制元件的Behavier攔截的話,手指觸控在NestedScrollView中,事件自動就會被NestedScrollView處理,那麼問題又來了,既然事件在標題欄中處理的話,怎麼將事件傳給NestedScrollView的呢?,好既然你攔截了,我就看看你的Ontouch事件的具體處理方法
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
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;
}
CoordinatorLayout還是什麼事件都不做,還是交給子View的Behavier去處理,相當於它告訴子View你願怎麼滑動就怎麼滑動,我不管,這種設計有利於開發者自己實現自己的事件,提高了可擴充套件性,好繼續看AppBarLayout的Ontouch事件
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;
}
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;
}
case MotionEvent.ACTION_UP:
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
float yvel = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
mActivePointerId);
//滿足快滑條件,開始快滑
fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
}
// $FALLTHROUGH
case MotionEvent.ACTION_CANCEL: {
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
}
return true;
}
程式碼比較多,這裡只看分叉口,當滿足滾動時,滾動scroll(parent, child, dy, getMaxDragOffset(child), 0),進入此方法
final int scroll(CoordinatorLayout coordinatorLayout, V header,
int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(coordinatorLayout, header,
getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
}
看起來很熟悉,最終改變View的top或bottom
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset,
int minOffset, int maxOffset) {
final int curOffset = getTopAndBottomOffset();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
// If we have some scrolling range, and we're currently within the min and max
// offsets, calculate a new offset
newOffset = MathUtils.constrain(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
setTopAndBottomOffset(newOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
}
}
return consumed;
}
經過一系列運算(具體怎麼算的這裡不詳細闡述),來最終確定需要移動的值,看到這裡就更奇怪了,只是appBarLayout移動了,NestedScrollView還在原來的位置,搞毛啊,演示效果明明會跟著一塊移動。那麼繼續找,觀看文件可知Behavier有這麼一個方法
public boolean onDependentViewChanged(CoordinatorLayout parent,
View child, View dependency) {
updateOffset(parent, child, dependency);
return false;
}
這個方法就是在此View所依賴的view發生改變的時候回撥此方法,什麼改變,當然是位置,顯示隱藏等就會回撥此方法,那麼巧了,此時NestedScrollView就是觀察者,appBarLayout是被觀察者,appBarLayout移動一點就會通知NestedScrollView,然後NestedScrollView也改變top或bottom,問題又來了onDependentViewChanged具體回撥是發生在什麼地方?帶著疑問接著深入public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors();
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets
// yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
在這個方法裡,發現端倪,既然子View的位置改變了,那麼肯定會引起view樹的重畫,那麼重畫之前,就會回撥OnPreDrawListener方法,看看它都幹了什麼!
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
Log.i("huoying", "OnPreDrawListener");
dispatchOnDependentViewChanged(false);
return true;
}
}
哎要不錯奧,通知某個View改變了,將訊息發給依賴它改變的某個View 的Behavier,從而實現聯動!void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Check child views before for anchor
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
// Did it change? if not continue
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
recordLastChildRect(child, newRect);
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild
.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (!fromNestedScroll
&& checkLp.getChangedAfterNestedScroll()) {
// If this is not from a nested scroll and we have
// already been changed
// from a nested scroll, skip the dispatch and reset the
// flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
//通知依賴它的View
final boolean handled = b.onDependentViewChanged(this,
checkChild, child);
if (fromNestedScroll) {
// If this is from a nested scroll, set the flag so that
// we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
}
程式碼較多,但是還是能一眼發現重點,b.onDependentViewChanged(this,checkChild, child),哎要不錯奧,看到這已經知道AppbarLayout在移動時怎麼將移動通知給依賴它的View了,很明顯,根據觸控手勢不斷的改變AppbarLayout的距離,然後會引起ViewTree樹的重畫,然後通過重畫之前的回撥事件通知依賴它的子View,從而實現級聯移動的效果。AppBarLayout怎麼影響NestedScrollView介紹完了,接下來介紹NestedScrollView怎麼將事件朝上傳遞並影響AppBarLayout的。
public class CoordinatorLayout extends ViewGroup implements
NestedScrollingParent
看的出來CoordinatorLayout 實現了NestedScrollingParent介面,是不是很熟悉,不熟悉的童鞋可以查一下NestedScrollingChild和NestedScrollingParent用法,既然事件先是在NestedScrollView傳遞的,那麼進入這個類看看它的Ontouch事件
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = MotionEventCompat.getActionMasked(ev);
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
//呼叫滑動開始
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = getScrollY();
final int range = getScrollRange();
final int overscrollMode = ViewCompat.getOverScrollMode(this);
boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
(overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
range > 0);
// Calling overScrollByCompat will call onOverScrolled, which
// calls onScrollChanged if applicable.
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
//把偏移量交給父View處理部分,然後處理餘下的部分
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
ensureGlows();
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
MotionEventCompat.getX(ev, activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - MotionEventCompat.getX(ev, activePointerIndex)
/ getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
//交給fuView飛一會
flingWithNestedDispatch(-initialVelocity);
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
mLastMotionY = (int) MotionEventCompat.getY(ev, index);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) MotionEventCompat.getY(ev,
MotionEventCompat.findPointerIndex(ev, mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
程式碼比較多,大家只看關鍵註釋的位置,快速滑動和滑動差不多,這裡只看滑動部分dispatchNestedScroll方法
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
接著跟進
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
//這裡有個mNestedScrollingParent很重要
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
這個方法有個mNestedScrollingParent,事件傳給父View全靠它
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
它在這個方法裡獲得,NestedScrollView獲取實現了NestedScrollingParent介面的父View,也就是我們的協調佈局,最後通過介面將滑動的事件傳給CoordinatorLayout的,然後CoordinatorLayout處理滑動的部分距離或者全部或者不處理再把剩下的距離交給NestedScrollView進行最後的滑動,從而實現了事件從NestedScrollView>CoordinatorLayout的事件傳輸,那麼是不是CoordinatorLayout又通過Behavier將事件傳給子View最終實現聯動呢?答案是肯定的,不信?那麼再次進入CoordinatorLayout求證這個推論
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//看這裡又將事件傳給ziView的Behavier消耗了
viewBehavior.onNestedScroll(this, view, target, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
accepted = true;
}
}
if (accepted) {
//通知依賴他的view我改變了
dispatchOnDependentViewChanged(true);
}
}
是不是和我們的推論一樣,首先滑動NestedScrollView的時候,通過NestedScrollingParent介面將事件傳給協調佈局,然後協調佈局再通過在View的Behavier,交給AppBarLayout來處理,比如向上滑動時,AppBarLayout根據屬性還沒有滑動到邊界的話,那麼AppBarLayout完全消耗掉滑動事件,然後告訴NestedScrollView的Behavier我改變了,你也改變吧,從而實現兩個view緊緊的聯絡在一塊,這就是CoordinatorLayout和AppBarLayout和實現了NestedScrollingChild介面的滑動View之間的關係了,最後還剩摺疊佈局CollapsingToolbarLayout是怎麼獲取變化通知,形成視差移動,或者摺疊效果的,通過xml可以CollapsingToolbarLayout是AppBarLayout的子類public class AppBarLayout extends LinearLayout {
public AppBarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(VERTICAL);
從上邊兩行原始碼可知AppBarLayout
就是豎向的線性佈局,既然它都是通過Behavier運動的,那麼必然Behavier裡有通知CollapsingToolbarLayout的方式,好找起來
int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
AppBarLayout header, int newOffset, int minOffset, int maxOffset) {
此處省略幾十行............
dispatchOffsetUpdates(appBarLayout);
}
}
return consumed;
}
又是這個方法,也就是說每次AppBarLayout改變位置時會呼叫dispatchOffsetUpdates,看看它都幹了啥?
/**
* 通知註冊了AppBarLayout介面
*
* @param layout
*/
private void dispatchOffsetUpdates(AppBarLayout layout) {
final List<OnOffsetChangedListener> listeners = layout.mListeners;
// Iterate backwards through the list so that most recently added
// listeners
// get the first chance to decide
for (int i = 0, z = listeners.size(); i < z; i++) {
final OnOffsetChangedListener listener = listeners.get(i);
if (listener != null) {
listener.onOffsetChanged(layout, getTopAndBottomOffset());
}
}
}
吼吼吼,回撥有木有,那麼既然有回撥那麼在CollapsingToolbarLayout註冊這個介面不就能收到AppBarLayout改變了嗎?66666,找一下在哪註冊的
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// Add an OnOffsetChangedListener if possible
final ViewParent parent = getParent();
if (parent instanceof AppBarLayout) {
if (mOnOffsetChangedListener == null) {
mOnOffsetChangedListener = new OffsetUpdateListener();
}
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
}
}
摺疊佈局中果然有註冊
private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
@Override
public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
mCurrentOffset = verticalOffset;
final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
final int scrollRange = layout.getTotalScrollRange();
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);