完全搞懂CoordinatorLayout Behavior之源碼解析

完全搞懂CoordinatorLayout Behavior 你能做些什麼
完全搞懂CoordinatorLayout Behavior 系列之API講解
完全搞懂CoordinatorLayout Behavior之源碼學習
完全搞懂CoordinatorLayout Behavior之實戰一

前面我們已經簡單介紹了CoordinatorLayout 的工作機制以及Behavior核心API,原則上是已經可以上手寫demo。但是這一節,我想從源碼的角度在講一次CoordinatorLayout 與 Behavior的工作原理。
在看源碼之前先要明白一個前提: 1、CoordinatorLayout 之所以能夠協調子視圖的相關動作,是因爲它實現了NestedScrollingParent2接口;2、NestedScrollView之所以能夠作爲嵌套滑動的子視圖,讓其他View跟隨它的滑動變化而變化,是因爲它實現了NestedScrollingChild2接口。

一、佈局加載過程

瞭解這個過程幫助我們知道 父View 是怎麼得到 子佈局的app:layout_behavior 屬性然後並創建出behavior對象的。

系統得到佈局layoutId 通過LayoutInflator類去加載佈局View,佈局加載過程中先創建出父View然後加載子佈局。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

        try {
            // Look for the root node.
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }

            final String name = parser.getName();

         
//merge佈局
            if (TAG_MERGE.equals(name)) {
      
            } else {
            	//得到一個佈局節點以後,先創建出佈局View
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

                // Inflate all children under temp against its context.			
                //加載所有的子View
                rInflateChildren(parser, temp, attrs, true);
                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
} finally {
        }

        return result;
    }
}	

由上面的註釋可以看到,當在加載根佈局CoordinatorLayout時然後會依次遍歷加載所有的子佈局xml文件,然後createViewFromTag創建出來,同時會調用父佈局的viewGroup.generateLayoutParams(attrs); 得到一個LayoutParams。最後viewGroup.addView(view, params); 這個過程跟我們動態創建View過程是一樣的,只不過系統加載xml佈局使用的是反射創建,我們則使用的是new 創建。

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;
//循環創建
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
           
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
     
        } else {
        	//如果不滿足上面的情況就會走這個邏輯,先得到子佈局的name,反射得到View對象,然後把父佈局的LayoutParams 創建出來的最後addView。
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}

通過上面的代碼邏輯我們證明了在子View創建以後,他會得到父View 然後調用一個generateLayoutParams(AttributeSet attrs)方法,在add到父View中。

二、Behavior創建過程

爲什麼要看generateLayoutParams 方法,因爲CoordinatorLayout的這個方法被重寫了它創建了一個自己定義的靜態內部類。

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
 return new LayoutParams(getContext(), attrs);
}

//LayoutParams 的構造方法 在這個構造方法中 attrs 就是xml中 子View 也就是app:layout_behavior 屬性所在位置
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
     super(context, attrs);

     final TypedArray a = context.obtainStyledAttributes(attrs,
             R.styleable.CoordinatorLayout_Layout);
    
     mBehaviorResolved = a.hasValue(
             R.styleable.CoordinatorLayout_Layout_layout_behavior);
     if (mBehaviorResolved) {
     	//解析得到Behavior對象
         mBehavior = parseBehavior(context, attrs, a.getString(
                 R.styleable.CoordinatorLayout_Layout_layout_behavior));
     }
     a.recycle();

     if (mBehavior != null) {
         // If we have a Behavior, dispatch that it has been attached
         mBehavior.onAttachedToLayoutParams(this);
     }
 }

可以看到創建LayoutParams時 就會創建出Behavior,至於怎麼創建 也是使用的反射創建出來的。

小結一下:在加載佈局文件時系統會創建出CoordinatorLayout所有子View,同時將它自己定義的一個LayoutParams設置給子視圖,這個LayoutParams就可能攜帶有一個Behavior實例對象。

三、測量 和 佈局

CoordinatorLayout是一個ViewGroup對象,裏面有很多子View, 他們都可以添加Behavior, 同時也可以相互觀察。所以需要在測量的時候將他們的觀察調用循序使用一個數據結構保存起來,便於後面的嵌套滑動方法的順序調用。


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();
    ensurePreDrawListener();
    //後面省略。。
}

private void prepareChildren() {
    mDependencySortedChildren.clear();
    mChildDag.clear();
//雙重for循環 排序所有的子View 理清所有子View之間的依賴關係
    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);
            //判斷是否有依賴關係
            if (lp.dependsOn(this, view, other)) {
                if (!mChildDag.contains(other)) {
                    // Make sure that the other node is added
                    mChildDag.addNode(other);
                }
                // Now add the dependency to the graph
                mChildDag.addEdge(other, view);
            }
        }
    }

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

上面的代碼利用一個雙重for循環加上圖的形式把所有的View之間的關係進行排序,因爲依賴關係可能會比較複雜,使用一種特定的數據結構來表示。這裏不做深究,只是做一個猜測。

@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);
        if (child.getVisibility() == GONE) {
            // If the child is GONE, skip...
            continue;
        }

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //每一次得到behavior對象都是從我們第一步和第二步分析的LayoutParams中獲取的
        final Behavior behavior = lp.getBehavior();
		// 調用我們的初始化擺放佈局的方法
        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

注意從此處開始我們的layoutDependsOnonLayoutChild至少被調用一次。而onLayoutChild方法就可以設置我們需要顯示的子View的初始位置(至少我是這麼做的)。如果我們 return true 就不會調用系統的onLayoutChild方法,而是有我們自己控制依賴View的位置。

那麼onDependentViewChanged什麼時候調用呢?前面一節我們說到當被監聽View的大小和位置發生改變時,這個方法就會被調用。 看測量方法調用的第二行執行了什麼樣的邏輯。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();
    ensurePreDrawListener();
    //後面省略。。
}

添加和移除一個繪製監聽

/**
 * Add or remove the pre-draw listener as necessary.
 */
void ensurePreDrawListener() {
    boolean hasDependencies = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        //排序以後,遍歷所有子view 查看是否設置有依賴監聽
        if (hasDependencies(child)) {
            hasDependencies = true;
            break;
        }
    }
// 如果有添加繪製監聽
    if (hasDependencies != mNeedsPreDrawListener) {
        if (hasDependencies) {
            addPreDrawListener();
        } else {
            removePreDrawListener();
        }
    }
}

遍歷了所有子View然後只有有一個滿足監聽關係就會返回true 然後會添加一個繪製View的監聽。

/**
 * Add the pre-draw listener if we're attached to a window and mark that we currently
 * need it when attached.
 */
void addPreDrawListener() {
    if (mIsAttachedToWindow) {
        // Add the listener
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        //View樹觀察者
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }

    // Record that we need the listener regardless of whether or not we're attached.
    // We'll add the real listener when we become attached.
    mNeedsPreDrawListener = true;
}
//監聽的實現是執行child的改變方法
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Override
    public boolean onPreDraw() {
        onChildViewsChanged(EVENT_PRE_DRAW);
        return true;
    }
}

從上面的代碼可以看到它會創建一個View樹觀察者對象然後把OnPreDrawListener添加進去 當View樹發生變化的時候可能就會觸發onChildViewsChanged方法。


/**
 * Register a callback to be invoked when the view tree is about to be drawn
 *
 * @param listener The callback to add
 *
 * @throws IllegalStateException If {@link #isAlive()} returns false
 */
public void addOnPreDrawListener(OnPreDrawListener listener) 

證實我的猜想invoked when the view tree is about to be drawn


final void onChildViewsChanged(@DispatchChangeEvent final int type) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    final Rect inset = acquireTempRect();
    final Rect drawRect = acquireTempRect();
    final Rect lastDrawRect = acquireTempRect();

    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
            // Do not try to update GONE child views in pre draw updates.
            continue;
        }


         //獲取當前view的Rect
        // Get the current draw rect of the view
        getChildRect(child, true, drawRect);

        // Accumulate inset sizes
      

        // Dodge inset edges if necessary
      

        if (type != EVENT_VIEW_REMOVED) {
        	//獲取到上一個View的位置Rect 和這一次的Rect比較 如果相等就不會繼續向下執行,繼續遍歷下一個View看看他的位置是否發生改變。
            // Did it change? if not continue
            getLastChildRect(child, lastDrawRect);
            if (lastDrawRect.equals(drawRect)) {
                continue;
            }
            recordLastChildRect(child, drawRect);
        }

        // 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 (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                    // If this is from a pre-draw and we have already been changed
                    // from a nested scroll, skip the dispatch and reset the flag
                    checkLp.resetChangedAfterNestedScroll();
                    continue;
                }
	            //由於我們傳遞的是 EVENT_PRE_DRAW所以當代碼走到這個位置時,必定會走onDependentViewChanged方法
                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);
                }
            }
        }
    }

    releaseTempRect(inset);
    releaseTempRect(drawRect);
    releaseTempRect(lastDrawRect);
}

從上面的代碼分析可以看到 CoordinatorLayout的測量方法中會添加一個監聽繪製的方法, 這個繪製方法通過一個叫做ViewTreeObserver對象實現,每次View樹發生繪製的時候都會去檢查 這個中的View是否有 有layoutDependsOn依賴關係的View的位置發生了改變,如果改變就執行一次onDependentViewChanged方法。

小結一下: 在測量和佈局兩個方法中CoordinatorLayout完成了三件事:1、理清所有監聽和被監聽View的排序順序;2、設置繪製監聽,當View樹發生繪製的時候,遍歷檢查所有子View跟上一次位置是否發生改變,如果改變就調用onDependentViewChanged; 3、在onLayout 的時候,調用了behavior的onLayoutChild方法給子View進行初始化位置的擺放。

上面講完了佈局初始化後,到界面顯示這個過程CoordinatorLayout是怎麼顯示以及設置監聽,調用behavior相關的API 。剩下的api都是nested相關的,也就是說跟嵌套滑動相關的功能,需要配合NestedScrollView講解。

四、分析NestedScrollView 事件處理方法,找到NestedScrollView 與 CoordinatorLayout 回調的邏輯

首先我們的滑動肯定是因爲NestedScrollView的滑動所以Behavior才產生調用,所以我們閱讀NestedScrollView滑動事件處理相關的方法就可以找到如何調用的。
複習一下事件傳遞相關知識,ViewGroup的事件先從dispatchTouchEvent方法然後到onInterceptTouchEvent 接着到onTouchEvent 。如果需要深入瞭解可以閱讀這篇文章Android View事件分發機制 (一)

1、startNestedScroll 與 onNestedScrollAccepted

閱讀發現NestedScrollView並沒有dispatchTouchEvent方法 ,所以我們從onInterceptTouchEvent開始,首先從Down事件開始。

case MotionEvent.ACTION_DOWN: {
    final int y = (int) ev.getY();
    if (!inChild((int) ev.getX(), y)) {
        mIsBeingDragged = false;
        recycleVelocityTracker();
        break;
    }

    /*
     * Remember location of down touch.
     * ACTION_DOWN always refers to pointer index 0.
     */
    mLastMotionY = y;
    mActivePointerId = ev.getPointerId(0);

    initOrResetVelocityTracker();
    mVelocityTracker.addMovement(ev);
    /*
     * If being flinged and user touches the screen, initiate drag;
     * otherwise don't. mScroller.isFinished should be false when
     * being flinged. We need to call computeScrollOffset() first so that
     * isFinished() is correct.
    */
    mScroller.computeScrollOffset();
    mIsBeingDragged = !mScroller.isFinished();
    //開始startNestedScroll 方法的調用
    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
    break;
}

很明顯我們看到調用了startNestedScroll 方法,但是並不是在這個方法內部完成的調用。需要使用一個叫做NestedScrollingChildHelper代理完成。

    @Override
    public boolean startNestedScroll(int axes, int type) {
        return mChildHelper.startNestedScroll(axes, type);
    }

NestedScrollingChildHelper的startNestedScroll方法。

    /**
     * Start a new nested scroll for this view.
     *
     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
     * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same
     * signature to implement the standard policy.</p>
     *
     * @param axes Supported nested scroll axes.
     *             See {@link androidx.core.view.NestedScrollingChild2#startNestedScroll(int,
     *             int)}.
     * @return true if a cooperating parent view was found and nested scrolling started successfully
     */
    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
            	//很明顯在這裏調用了onStartNestedScroll 同時也調用了onNestedScrollAccepted 
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

從一個API 找到了兩個Behavior名稱類似的方法,之前我們也有說到如果Behavior的onStartNestedScroll 返回false , 那麼後面相關的滑動嵌套方法都得不到調用。看看ViewParentCompat 類完成了怎樣的操作。

    @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
            //這裏的parent 就是CoordinatorLayout 
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    return parent.onStartNestedScroll(child, target, nestedScrollAxes);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onStartNestedScroll", e);
                }
            } else if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
        }
        return false;
    }

可以看到無論是NestedScrollingParent 還是 NestedScrollingParent2 都會調用onStartNestedScroll方法。我們返回回去看看onNestedScrollAccepted是不是也是這樣的。

    public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            ((NestedScrollingParent2) parent).onNestedScrollAccepted(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onNestedScrollAccepted", e);
                }
            } else if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
                        nestedScrollAxes);
            }
        }
    }

幾乎一模一樣的邏輯, 都是通過ViewParentCompat然後在調用parentCoordinatorLayout)相同的名稱的api方法。
接下來我們回到CoordinatorLayout的源碼中 是做了些什麼操作。

 @Override
 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
     return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
 }

 @Override
 public boolean onStartNestedScroll(View child, View target, int axes, int type) {
     boolean handled = false;

     final int childCount = getChildCount();
     //遍歷所有子View
     for (int i = 0; i < childCount; i++) {
         final View view = getChildAt(i);
         if (view.getVisibility() == View.GONE) {
             // If it's GONE, don't dispatch
             continue;
         }
         final LayoutParams lp = (LayoutParams) view.getLayoutParams();
         final Behavior viewBehavior = lp.getBehavior();
         //拿到所有具有Behavior對象的子View 然後調用他們的onStartNestedScroll
         if (viewBehavior != null) {
             final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                     target, axes, type);
             handled |= accepted;
             //同時把accepted 設置到LayoutParams 身上
             lp.setNestedScrollAccepted(type, accepted);
         } else {
             lp.setNestedScrollAccepted(type, false);
         }
     }
     return handled;
 }

看到這個方法它坐了兩件事 調用了viewBehavior.onStartNestedScroll 這個是我們最後真正的onStartNestedScroll方法。同時設置了一個屬性給LayoutParams。
接下來看一下onNestedScrollAccepted 做了那些處理。它調用的前提是onStartNestedScroll 返回true。


    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
    	// 
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
        mNestedScrollingTarget = target;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            //直接取出上一步操作的accepted 如果是false 直接返回後面不會調用,說明onStartNestedScroll 決定了所有嵌套方法是否執行。
            if (!lp.isNestedScrollAccepted(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
            	//最終調用了onNestedScrollAccepted
                viewBehavior.onNestedScrollAccepted(this, view, child, target,
                        nestedScrollAxes, type);
            }
        }
    }

如我們所料真的是調用了viewBehavior.onNestedScrollAccepted 方法,但是在這之前有兩個操作 上一步的accepted 決定代碼是否會執行後面的onNestedScrollAccepted。還有調用了一個mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type)記錄mNestedScrollAxes 值。
至此,一個事件的處理回調週期全部完成,我們可以完整的看到 CoordinatorLayoutNestedScrollView 是怎麼樣通過 一些中介回調,然後回調到Behavior身上的。後面的情況其實跟這個邏輯完全一樣,大概就是NestedScrollView 滑動了多少距離然後通過NestedScrollingChildHelper 以及 ViewParentCompat 回調到 父類身上,前提是父類必須是一個 NestedScrollingParent2 或者 NestedScrollingParent方法。

2、onNestedPreScroll 、 onNestedScroll

這兩個都是滑動的方法,分別在OnTouchEvent的MOVE事件中被調用的。 有什麼區別的呢?

case MotionEvent.ACTION_MOVE:
    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
    if (activePointerIndex == -1) {
        Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
        break;
    }

    final int y = (int) ev.getY(activePointerIndex);
    int deltaY = mLastMotionY - y;
    //註釋1 當得到滑動距離以後,首先不是執行的 NestedScrollView的滑動處理而是調用了onNestedPreScroll 
    if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
            ViewCompat.TYPE_TOUCH)) {
        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];
		//註釋 2 計算 滑動 和未消耗距離
        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);

        // Calling overScrollByCompat will call onOverScrolled, which
        // calls onScrollChanged if applicable.
        //執行NestedScrollView 的滑動處理
        if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
            // Break our velocity if we hit a scroll barrier.
            mVelocityTracker.clear();
        }

        final int scrolledDeltaY = getScrollY() - oldY;
        final int unconsumedY = deltaY - scrolledDeltaY;
        // 執行 onNestedScroll 
        if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                ViewCompat.TYPE_TOUCH)) {
            mLastMotionY -= mScrollOffset[1];
            vtev.offsetLocation(0, mScrollOffset[1]);
            mNestedYOffset += mScrollOffset[1];
        } else if (canOverscroll) {
            ensureGlows();
            final int pulledToY = oldY + deltaY;
            if (pulledToY < 0) {
                EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
                        ev.getX(activePointerIndex) / getWidth());
                if (!mEdgeGlowBottom.isFinished()) {
                    mEdgeGlowBottom.onRelease();
                }
            } else if (pulledToY > range) {
                EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
                        1.f - ev.getX(activePointerIndex)
                                / getWidth());
                if (!mEdgeGlowTop.isFinished()) {
                    mEdgeGlowTop.onRelease();
                }
            }
            if (mEdgeGlowTop != null
                    && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    }

可以看到區別了嗎,一個是在滑動處理之前調用 ,一個是在滑動處理之後調用。接下來分析一下這幾個參數。
在註釋1 處有這樣的一段代碼

    if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
            ViewCompat.TYPE_TOUCH)) {
            //計算的滑動距離最後減除了一個mScrollConsumed[1] 
        deltaY -= mScrollConsumed[1];
        vtev.offsetLocation(0, mScrollOffset[1]);
        mNestedYOffset += mScrollOffset[1];
    }

在講解Behavior API講解是我們說到了一個操作方法,他可以去改變滑動距離,原文我放在下面。Behavior API詳解

consumed:這是個重要的參數consumed,可以修改這個數組表示你消費了多少距離。假設用戶滑動了100px,child 做了90px 的位移,你需要把 consumed[1]的值改成90,這樣coordinatorLayout就能知道只處理剩下的10px的滾動。

看到沒有加入我們在自定義的Behavior的onNestedPreScroll 調用時更改consumed 的值,就可以操作NestedScrollView的值。 比如明明它滑動了100px像素,但是這個時候我給consumed[1]設置成 50px, 經過deltaY -= mScrollConsumed[1] 那麼NestedScrollView最後就只能通過計算以後的deltaY去執行滑動了。

接着看看註釋2 位置如何計算 onNestedScroll相關參數的值

//先記錄上一次滑動的距離
 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);

 // Calling overScrollByCompat will call onOverScrolled, which
 // calls onScrollChanged if applicable.
 //執行滑動
 if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
         0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
     // Break our velocity if we hit a scroll barrier.
     mVelocityTracker.clear();
 }
//在用滑動後的距離減去 沒有滑動之前的距離 得到的是 這一步過程 滑動的實際距離
 final int scrolledDeltaY = getScrollY() - oldY;
 //然後用預計計算的滑動距離 減去實際滑動的距離 得到的就是未被消耗的距離
 final int unconsumedY = deltaY - scrolledDeltaY;
 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
         ViewCompat.TYPE_TOUCH)) {
     mLastMotionY -= mScrollOffset[1];
     vtev.offsetLocation(0, mScrollOffset[1]);
     mNestedYOffset += mScrollOffset[1];
 }

上面的註釋已經寫的很清楚了。scrolledDeltaY 的計算是毋庸置疑的,但是unconsumedY 距離是爲什麼呢?難道他不應該是0 嗎。 可能原因就是,滑動了100px但是NestedScrollView 實際只能滑動90px就已經到底了,剩下的距離消費不了所以纔會出現未消費的距離。
分析了這麼多可能會有人懷疑,到底是不是調用onNestedPreScrollonNestedScroll 可能有人不會死心,接下來我一次打印他們調用的方法順序,方便大家查看源碼。
dispatchNestedPreScroll 最後會調用mChildHelper.dispatchNestedPreScroll方法

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
            int type) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }

NestedScrollingChildHelper 的dispatchNestedPreScroll 最後會調用這個


    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        		//  。。。省略
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

			//  。。。。。省略
        return false;
    }

跟之前的方法調用一樣的邏輯,判斷NestedScrollingParentNestedScrollingParent2 接口然後調用parent 方法也就是CoordinatorLayout相關的方法。

    public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
            int[] consumed, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {
                try {
                    parent.onNestedPreScroll(target, dx, dy, consumed);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onNestedPreScroll", e);
                }
            } else if (parent instanceof NestedScrollingParent) {
            	// 
                ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
            }
        }
    }

最後parent 也就是CoordinatorLayout 它就能夠獲取到所有的子View 然後一次調用對應的Behavior身上的方法。

參考文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章