Android CoordinatorLayout源碼分析

分類:IT技術 時間:2016-10-18

CoordinatorLayout作為協調布局,而真正實現功能的部分在於Behavior,所以我打算將這兩地方都捎帶說說,若有意見請及時提出幫助我改正

Behavior的初始化

Behavior是CoordinatorLayout內部靜態抽象類,它是一種新的view關系描述,即依賴關系。一般我們都是繼承這個類去完成自己的自定義功能

之前我們提及Behavior可以通過註解或者layout_behavior來聲明,如果你是通過xml來初始化,那麽在CoordinatorLayout初始化的時候就完成了

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    LayoutParams(Context context, AttributeSet attrs) {
        mBehaviorResolved = a.hasValue( R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
        if (mBehaviorResolved) {
            mBehavior = parseBehavior(context, attrs, a.getString(
                R.styleable.CoordinatorLayout_Layout_layout_behavior));}
    }
}

如果你是使用註解進行初始化,那麽他在onMeasure的時候通過prepareChildren才進行初始化,註意看setBehavior這裏。所以xml裏初始化優先級高。xml內指定的話,是在inflate的時候對mBehavior賦值;註解裏指定的話,是在onMeasure內賦值,稍有不同。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();
    ....
}

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

前面我們提及反射初始化Behavior的,在這個parseBehavior裏面就能看到

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    try {
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        if (c == null) {
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                    context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

還有一個需要註意的地方,我們看到反射的方法是2個參數的構造方法

static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
        Context.class,
        AttributeSet.class
};

所以我們在自定義Behavior的時候,一定要去重寫

NestedScrolling概念

其實想說一下為什麽叫嵌套滑動,之前我們老是提及這個概念。CoordinatorLayout本身是不能動的,但是一旦其中包含了具備NestedScrolling功能的滾動視圖,那就不一樣了。它在滑動過程中會對Behavior產生影響,進而可以通過動畫或者View之間的關聯關系進行改變。這裏,就是有嵌套這麽一層關系

之前那種TouchEvent形式的滑動方式,一旦子View攔截了事件,除非重新進行一次事件傳遞,不然父View是拿不到事件的。而NestedScrolling很好的解決了這個問題

在閱讀源碼的時候,請著重關註這4個類

  1. NestedScrollingChild :
    如果你有一個可以滑動的 View,需要被用來作為嵌入滑動的子 View,就必須實現本接口
  2. NestedScrollingParent :
    作為一個可以嵌入 NestedScrollingChild 的父 View,需要實現 NestedScrollingParent接口,這個接口方法和 NestedScrollingChild大致有一一對應的關系
  3. NestedScrollingChildHelper
    實現好了 Child 和 Parent 交互的邏輯
  4. NestedScrollingParentHelper
    實現好了 Child 和 Parent 交互的邏輯

NestedScrolling滑動機制流程

完整的事件流程大致是這樣的:

滑動開始的調用startNestedScroll(),Parent收到onStartNestedScroll()回調,決定是否需要配合Child一起進行處理滑動,如果需要配合,還會回調onNestedScrollAccepted()。每次滑動前,Child 先詢問Parent是否需要滑動,即 dispatchNestedPreScroll(),這就回調到Parent的onNestedPreScroll(),Parent可以在這個回調中“劫持”掉Child的滑動,也就是先於Child滑動。Child滑動以後,會調用onNestedScroll(),回調到Parent的onNestedScroll()。最後滑動結束,調用 onStopNestedScroll()表示本次處理結束。

NestedScrollingChild與NestedScrollingChildHelper的交互流程

NestedScrollingChildHelper與ViewParentCompat的交互流程

ViewParentCompat與CoordinatorLayout的交互流程

CoordinatorLayout與Behavior的交互流程

主要回調方法介紹

  • onStartNestedScroll

在NestedScrollView的ACTION_DOWN事件中開始流程

startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);

NestedScrollingChildHelper裏循環查找直到找出CoordinatorLayout,繼續發送

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        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;
}

ViewParentCompat裏面,parent只要實現了onStartNestedScroll就可以繼續流程,這裏也是說添加Behavior的控件必須直接從屬於CoordinatorLayout,否則沒有效果

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    try {
        return parent.onStartNestedScroll(child, target, nestedScrollAxes);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onStartNestedScroll", e);
        return false;
    }
}

CoordinatorLayout循環通知所有第一層子視圖中的Behavior

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    boolean handled = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                    nestedScrollAxes);
            handled |= accepted;
            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }
    return handled;
}

它的返回值,決定了NestedScrollingChildHelper.onStartNestedScroll是不是要繼續遍歷,如果我們的Behavior對這個滑動感興趣,就返回true,它的遍歷就會結束掉。

  • onNestedPreScroll

在ACTION_MOVE中進行觸發傳遞,註意這邊的deltaY是已經計算好的偏移量,deltaY>0就是往上滑動,反之往下滑動

final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
    deltaY -= mScrollConsumed[1];
    vtev.offsetLocation(0, mScrollOffset[1]);
    mNestedYOffset += mScrollOffset[1];
}

其實這邊所有Behavior接收流程都是一樣的,主要看看AppBarLayout對onNestedPreScroll的處理以便於我們後續自定義Behavior的實現。這裏的dy就是剛才說的偏移量,target就是發起者NestedScrollView。consumed數組是由x\y組成,AppBarLayout執行完成之後存儲其本次垂直方向的滾動值。這裏scroll方法會將AppBarLayout的移動範圍固定在0-AppBarLayout高度這2個值範圍內執行滾動操作,如果在範圍外的話,AppBarLayout就不執行滾動操作,consumed[1]的值也為0

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
        View target, int dx, int dy, int[] consumed) {
    if (dy != 0 && !mSkipNestedPreScroll) {
        int min, max;
        if (dy < 0) {
            // We're scrolling down
            min = -child.getTotalScrollRange();
            max = min + child.getDownNestedPreScrollRange();
        } else {
            // We're scrolling up
            min = -child.getUpNestedPreScrollRange();
            max = 0;
        }
        consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
    }
}

只要你記得dy是已經處理好的偏移量並且方向不要搞錯就行了。這個函數一般在scroll前調用。

  • onNestedScroll

這個實際上是NestedScrollingChild自身改變的回調,看看之前dispatchNestedPreScroll觸發的部分有一句這個

deltaY -= mScrollConsumed[1];

剛才也說了AppBarLayout在不超過滾動範圍的時候,consumed[1]為實際Y方向滾動量,反之則為0,那麽也就是在滾夠了的情況下才會調用onNestedScroll

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

再看看源碼,使用overScrollByCompat發生了自身的滾動,所以兩次滾動之間的值就是scrolledDeltaY,作為已消費的值。未消費部分unconsumedY就是手指之間的距離減去滾動值之差。其實這個也好理解,當這個NestedScrollView滾到最底部的時候滾不動了,那麽它的消費值就是0,未消費值就是手指之間的距離

if (mIsBeingDragged) {
    // Scroll to follow the motion event
    mLastMotionY = y - mScrollOffset[1];
    final int oldY = getScrollY();
    final int range = getScrollRange();
    final int overscrollMode = getOverScrollMode();
    boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
            || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
            0, true) && !hasNestedScrollingParent()) {
        mVelocityTracker.clear();
    }
    final int scrolledDeltaY = getScrollY() - oldY;
    final int unconsumedY = deltaY - scrolledDeltaY;
    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset))
    .........
}

其實我不知道什麽情況下unconsumedY是負數,AppBarLayout倒是處理了這個情況。這個函數一般在scroll後調用。

總之滑動過程為AppBarlayout先滑,NestedScrollView再滑

  • onNestedPreFling與 onNestedFling

    這個其實與onNestedPreScroll,onNestedScroll之間的關系差不多,我就不多說了

  • onStopNestedScroll

    一切都結束的時候,執行這個方法

  • onDependentViewChanged與 layoutDependsOnonDependentViewRemoved

layoutDependsOn就是用來告訴NestedScrollingParent我們依賴的是哪個View。除了滾動事件會被處理以外,這個View的大小、位置等變化也一樣可以通過回調方法進行通知,通知是通過onDependentViewChanged回調告訴Behavior的

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    // We depend on any AppBarLayouts
    return dependency instanceof AppBarLayout;
}

看看源碼,在onAttachedToWindow中我們看到了ViewTreeObserver的身影,那麽view的各種狀態變化都會被他抓到

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    resetTouchBehaviors();
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }
    ....
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Override
    public boolean onPreDraw() {
        onChildViewsChanged(EVENT_PRE_DRAW);
        return true;
    }
}

這裏有一個mNeedsPreDrawListener,它是什麽情況變成true的?原來是ensurePreDrawListener這個方法裏面判斷了只要它有依賴關系,就可以添加監聽。ensurePreDrawListener在剛才所說的prepareChildren之後調用,符合邏輯。

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

回頭看看prepareChildren方法,存儲了全部被依賴的子View

private void prepareChildren() {
    mDependencySortedChildren.clear();
    mChildDag.clear();
    for (int i = 0, count = getChildCount(); i < count; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = getResolvedLayoutParams(view);
        lp.findAnchorView(this, view);
        mChildDag.addNode(view);
        // Now iterate again over the other children, adding any dependencies to the graph
        for (int j = 0; j < count; j++) {
            if (j == i) {
                continue;
            }
            final View other = getChildAt(j);
            final LayoutParams otherLp = getResolvedLayoutParams(other);
            if (otherLp.dependsOn(this, other, view)) {
                if (!mChildDag.contains(other)) {
                    // Make sure that the other node is added
                    mChildDag.addNode(other);
                }
                // Now add the dependency to the graph
                mChildDag.addEdge(view, other);
            }
        }
    }
    // Finally add the sorted graph list to our list
    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    // We also need to reverse the result since we want the start of the list to contain
    // Views which have no dependencies, then dependent views after that
    Collections.reverse(mDependencySortedChildren);}

再來看看onChildViewsChanged方法,循環遍歷所有Child, 將每個子View都使用layoutDependsOn來比較一下, 確保所有互相依賴的子View都可以聯動起來,如果是依賴關系,再調用onDependentViewChanged。這裏checkChild是待檢查的View,也就是我們添加Behavior的那個View,child就是被checkChild所依賴的View

....
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 (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
            checkLp.resetChangedAfterNestedScroll();
            continue;
        }
        final boolean handled;
        switch (type) {
            case EVENT_VIEW_REMOVED:
                // EVENT_VIEW_REMOVED means that we need to dispatch
                // onDependentViewRemoved() instead
                b.onDependentViewRemoved(this, checkChild, child);
                handled = true;
                break;
            default:
                // Otherwise we dispatch onDependentViewChanged()
                handled = b.onDependentViewChanged(this, checkChild, child);
                break;
        }
        if (type == EVENT_NESTED_SCROLL) {
            // If this is from a nested scroll, set the flag so that we may skip
            // any resulting onPreDraw dispatch (if needed)
            checkLp.setChangedAfterNestedScroll(handled);
        }
    }
}
....

最後我們就來解決上一篇文章中那個思考題,為什麽NestedScrollView下面會有一截在屏幕外,這是因為他依賴於AppBarLayout,否則他們的頂點應該在一個位置

private void layoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect parent = mTempRect1;
    parent.set(getPaddingLeft() + lp.leftMargin,
            getPaddingTop() + lp.topMargin,
            getWidth() - getPaddingRight() - lp.rightMargin,
            getHeight() - getPaddingBottom() - lp.bottomMargin);
    if (mLastInsets != null && ViewCompat.getFitssystemWindows(this)
            && !ViewCompat.getFitsSystemWindows(child)) {
        // If we're set to handle insets but this child isn't, then it has been measured as
        // if there are no insets. We need to lay it out to match.
        parent.left += mLastInsets.getSystemWindowInsetLeft();
        parent.top += mLastInsets.getSystemWindowInsetTop();
        parent.right -= mLastInsets.getSystemWindowInsetRight();
        parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
    }
    final Rect out = mTempRect2;
    GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
            child.getMeasuredHeight(), parent, out, layoutDirection);
    child.layout(out.left, out.top, out.right, out.bottom);
}

關於onLayout方面的問題,可以通過onLayoutChild這個方法來細細研究

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);
    } else if (lp.keyline >= 0) {
        layoutChildWithKeyline(child, lp.keyline, layoutDirection);
    } else {
        layoutChild(child, layoutDirection);
    }
}

onDependentViewRemoved就是移除View後進行調用,想象一下Snackbar與FloatingActionButton的使用場景就可以理解

 

 

來自:http://www.jianshu.com/p/2245af12b241

 


Tags:

文章來源:


ads
ads

相關文章
ads

相關文章

ad