一步一步深入理解CoordinatorLayout

一步一步深入理解CoordinatorLayout

Google推出Design庫已經一年了,國內也出過一些文章關于CoordinatorLayout,但是都是叫你怎么用,或者簡單的自定義一些Behavior,并沒有一篇文章深入去了解它的原理。

剛好這兩天為了實現一個UI效果,看了CoordinatorLayout(后面簡稱Col(我懶- -))的官方文檔以及源碼,搞懂了它的原理,于是想著拿出來分享,特在此記錄分享如何一步一步深入理解Col,希望可以填補這個空缺。

補充說明:

  1. Col等源碼基于23.2.1版本
  2. 本文側重在于Col與Behavior之間的交互,側重于原理,并選擇要點進行講解,所以可能會有一些點被我忽略(不是不重要),不過看完后我相信你對Col會有更深一層的了解。

初步了解

學習最好的習慣就是看官方文檔,來看看Col的定義以及官網的介紹:

public class CoordinatorLayout
extends ViewGroup implements NestedScrollingParent

CoordinatorLayout is a super-powered FrameLayout.
CoordinatorLayout is intended for two primary use cases:

  1. As a top-level application decor or chrome layout
  2. As a container for a specific interaction with one or more child views

從定義可以看到Col繼承自ViewGroup,并且它被設計成一個top-level的根布局,它本身只是一個ViewGroup,實現了NestedScrollingParent接口,看似非常普通,
但是說CoordinatorLayout是Design庫最為重要的控件也不為過。

這里額外需要注意的是:

  1. 由于Col只實現了NestedScrollingParent,所以當Col嵌套(作為一個子View)的時候會得不到你想要的效果,需要自己寫一個Col去實現NestedScrollingChild接口!
  2. 沒有實現NestedScrollingChild接口的子View如:ListViewScrollView在5.0以下版本跟Col是配合不了的需要使用RecyclerViewNestedScrollView才行

why?super-powered在哪里呢?

Col最為重要的作用是:提供給子View實現各種交互的極大便利
直觀的表現是我們可以使用Col非常方便地實現很多交互效果,具體效果可以看cheesesquare這個項目。

要知道,在沒有Col的日子要實現簡單的交互也不是件容易的事,需要通過各種回調/Event,相互回調,相互通知,甚至相互持有引用,復雜而且難以復用,但是現在有了Col,一切都變得方便了~

How?它是怎么做到的呢?
說到這里,不得不提到Col的靜態內部類--->Behavior
接下去來了解一下它,老司機要開車了,快上車~

攔截一切的Behavior

Behavior是什么,有什么作用?

Interaction behavior plugin for child views of CoordinatorLayout.

A Behavior implements one or more interactions that a user can take on a child view. These interactions may include drags, swipes, flings, or any other gestures.

簡單說,Behavior可以負責所有的交互甚至測量以及布局。

其實官網資料說得挺含蓄的,官方在Medium有一篇文章,叫:Intercepting everything with CoordinatorLayout Behaviors
,私以為用這個標題來形容Behavior,再合適不過,intercepting-everything!(這篇文章講得很好也很全面,極力推薦閱讀

攔截一切!!迫不及待進一步了解!!

深入了解

如何實例化指定Behavior

  1. 通過構造方法實例,并在代碼中設置到LayoutParamas里
  2. Xml里指定,比如app:app:layout_behavior="me.yifeiyuan.demo.HeaderBehavior"
  3. 通過DefaultBehavior注解指定,比如@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)

第一種方式很簡單,不多說,這里針對其他兩種方式講解一下,有一些注意點我們需要知道:

Xml方式

先撇一下Behavior的定義以及其構造方法如下:

//定義 V 為泛型,可指定針對哪種類型的View
public static abstract class Behavior<V extends View>
//默認的構造方法
public Behavior() {}
// xml里使用
public Behavior(Context context, AttributeSet attrs) {}

當我們在Xml里指定的時候,在LayoutParams的構造方法里會去調用parseBehavior這個方法,parseBehavior關鍵代碼如下(不貼代碼不行了,已盡量精簡):

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    //...省略了很多代碼,只留下關鍵的部分
    try {
        //獲取構造方法
        Map<String, Constructor<Behavior>> constructors = sConstructors.get(); 
        //...
        return c.newInstance(context, attrs); // 注意這一行,這里傳遞了attrs,所以我們必須要有第二個構造方法!!!
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);//否則會報錯 crash
    }
}

這里我們需要注意的是: 如果要在xml里使用Behavior 那么第二個構造方法必不可少,所以我們自定義Behavior的時候需要注意;另外你在xml定義的屬性會傳遞到第二個構造方法里去,可以獲取你在xml里配置的屬性,非常方便,可以說考慮還是非常周到的

注解方式

第三種通過注解的方式,又是在什么時候,怎么去實例化的呢?

在Col中的onMeasure中會去調用prepareChildren方法,而prepareChildren方法又調用了一個叫getResolvedLayoutParams的方法如下:

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) {//如果 有DefaultBehavior這個注解
            childClass = childClass.getSuperclass();
        }
        if (defaultBehavior != null) {
            try {
                result.setBehavior(defaultBehavior.value().newInstance());//實例化Behavior并把這個Behavior賦值給result
            } 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;
}

所以注解方式是在onMeasure中通過getResolvedLayoutParams去實例化的。

另外還需要知道的是,Behavior是Col.LayoutParams的成員變量,那么也就是說只有當你的Behavior設置給Col的 直接子View 才會有效果,這點要記住,不然徒勞無功。(Col的子View的子View就不要給它設置Behavior啦,沒效果的)

以上需要牢記,不過僅僅知道這些顯然是不夠的!至少我不會到這里就停~

接下去繼續深入閱讀Behavior的源碼一探究竟(一言不合就看源碼)

在我閱讀了Behavior的源碼后,我覺得非常有必要先搞清楚幾個非常重要的概念。

child與dependency

  1. childthe child view associated with this Behavior
    它是一個View,是該Behavior的關聯對象,也即Behavior所要操作的對象
  2. dependency,也是個View,是 child的依賴對象,同時也是Behavior對child進行操作的根據

弄清楚這些個概念后看源碼會比較簡單了,Behavior除了構造方法外,有23個方法,限于篇幅與精力,我挑選幾個最重要的方法來講解,當然我不會死板的一個一個毫無邏輯地解釋過去。

那些不能不懂的方法

layoutDependsOn

之前提到了child與dependency有著依賴關系,那么問題來了: 這個依賴關系是如何建立的?

在Behavior類中有個方法:

public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)

它會被Behavior的LayoutParamas的dependsOn方法調用:

boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild
            || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}

而LayoutParamas的dependsOn方法會被Col調用,dependsOn方法就是用來確定依賴關系的。

所以,最簡單的確定依賴關系的方法是重寫layoutDependsOn方法,并在一定條件下返回true即可確立依賴關系。

那為什么說一定條件呢?

比如FAB依賴于SnackBar,是因為它在SnackBar出現以及消失的時候需要改變自身的位置,所以FAB的layoutDependsOn方法中對Snackbar.SnackbarLayout返回了true,而沒有依賴其他的控件:

@Override
public boolean layoutDependsOn(CoordinatorLayout parent,FloatingActionButton child, View dependency) {
    // We're dependent on all SnackbarLayouts (if enabled)
    return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
}

另外需要注意的是:當確定依賴關系后,當dependency被布局(或測量)后child會緊接著被布局(或測量),Col會無視子view的順序(原因是Col內有個ComparatormLayoutDependencyComparator會按照依賴關系對所有的子View進行排序),這會影響它們的測量以及布局順序

可以說layoutDependsOn方法是自定義Behavior最為重要的方法

onDependentViewChanged

建立起依賴關系之后呢?

想要做交互,似乎還缺點什么,我想在dependency發生變化的時候改變一下child,我該如何知道這個改變的時機呢?

其實不需要我們去主動獲取去判斷,其實Col跟Behavior已經幫我們做好了這一切,onDependentViewChanged登場。

onDependentViewChanged方法的定義:

/**
 * Respond to a change in a child's dependent view
 * This method is called whenever a dependent view changes in size or position outside
 * of the standard layout flow. A Behavior may use this method to appropriately update
 * the child view in response.
 */
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
    return false;
}

簡單來說就是,當我們的dependency發生改變的時候,這個方法會調用,而我們在onDependentViewChanged方法里做出相應的改變,就能做出我們想要的交互效果了!

可能你也注意到了onDependentViewChanged方法是有返回值的

當我們改變了child的size或者position的時候我們需要返回true,差不多可以理解為 當我們的dependency發生了改變,同樣的,child也需要發生改變,這個時候我們需要返回true

提一下:onDependentViewChanged方法是在Col的dispatchOnDependentViewChanged里調用的

其他

除了以上兩個特別重要的方法外,Nested系列方法也非常重要,如onStartNestedScrollonStopNestedScroll來監聽嵌套滾動的開始和結束,不過限于篇幅,想再另外開篇去寫,這里就不寫了

另外還有onMeasureChildonLayoutChild這個后面會講。

為什么Behavior可以攔截一切?

我們知道,ViewGroup的測量,布局,事件分發都是需要自己處理的,那么Col究竟給了Behavior什么特權,讓它能夠讓它攔截一切?

讓我們挨個一點一點看下去

onMeasure

直接備注在源碼里了,不多說啦!~

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //之前已經提到過了 解析Behavior,并按依賴順序重排子View順序 
    prepareChildren();
    //用于addPreDrawListener,OnPreDrawListener里會調用 dispatchOnDependentViewChanged(false)
    ensurePreDrawListener();
    //...
    // 計算 padding width height 處理 fitSystemWindow等
    //...
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        int keylineWidthUsed = 0;
        //...處理keyline childWidthMeasureSpec等
        final Behavior b = lp.getBehavior();
        // 跟onMeasure相同,當behavior的onMeasureChild方法返回true的時候,我們就可以攔截Col默認的measure
        if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
        }
        //...
    }
    //...
    final int width = ViewCompat.resolveSizeAndState(widthUsed, widthMeasureSpec,
            childState & ViewCompat.MEASURED_STATE_MASK);
    final int height = ViewCompat.resolveSizeAndState(heightUsed, heightMeasureSpec,
            childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT);
    setMeasuredDimension(width, height);
}

onLayout

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    //mDependencySortedChildren 在 onMeasure里已經排過序了
    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.onLayoutChild()返回true的時候,就可以攔截掉Col的默認Layout操作!    
        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

原理其實跟onMeasure方法一樣的。

onInterceptTouchEvent & onTouchEvent

在處理touch事件中,Col重寫了onInterceptTouchEventonTouchEvent,另外,它們都調用了Col里定義的一個處理攔截的方法,performIntercept(關鍵代碼都在這方法之中),就看一下它們的實現吧:

onInterceptTouchEvent的實現:

@Override
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) {
        //down的時候,跟大部分ViewGroup一樣,需要重置一些狀態以及變量,比如 mBehaviorTouchView
        resetTouchBehaviors();
    }
    //這里看performIntercept TYPE_ON_INTERCEPT標記是 onInterceptTouchEvent
    final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
    if (cancelEvent != null) {
        cancelEvent.recycle();
    }
    //當事件為UP和Cancel的時候去重置(同down)
    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }
    return intercepted;
}

onTouchEvent的實現:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;
    final int action = MotionEventCompat.getActionMasked(ev);
    // mBehaviorTouchView不為null(代表之前有behavior處理了down事件) 或者 performIntercept返回true 那么事件就交給mBehaviorTouchView
    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) {
            // 交給 behavior去處理事件  
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }
    // Keep the super implementation correct
    // 省略調用默認實現 up&cancel的時候重置狀態
    //...
    return handled;
}

可以看到,其實這兩個方法做的事情并不多,其實都交給performIntercept方法去做處理了!

performIntercept的實現如下:

// type 標記是intercept還是touch
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;
    //按Z軸排序 原因很簡單 讓最上面的View先處理事件  
    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();
        //當前事件已經被某個behavior攔截了(or newBlock),并且事件不為down,那么就發送一個 取消事件 給所有在攔截的behavior之后的behavior
        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;
        }
        // 如果還沒有被攔截,那么繼續詢問每個Behavior 是否要處理該事件
        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;
            }
            //如果有behavior處理了當前的事件,那么把它賦值給mBehaviorTouchView,它其實跟ViewGroup源碼中的mFirstTouchTarget作用是一樣的
            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.
        // 是否攔截一切在它之后的交互 好暴力-0-
        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;
}

通過分析源碼,可以知道,Col在關鍵的方法里把處理權優先交給了Behavior,所以才讓Behavior擁有了攔截一切的能力,所以,原來是Col放任了Behavior!!~

結語

Col以及Behavior的重要的幾個環節分析完畢,相信大家看完后能夠對它們有更深層次的了解,而不是僅僅停留在使用上面。

這篇文章斷斷續續寫了快一個月,思路斷斷續續,也有幾次推翻重來,原本也打算想講得更多更細一些,只是限于篇幅與精力,有些內容沒有寫,最終的效果跟我最初的預期有所差距,可能也有些錯誤或者講解不清晰的地方。如有可能,會開下一篇繼續講解。

如果你發現任何錯誤,或者寫得不好的地方,或者不理解的地方,非常歡迎批評指正,也非常歡迎吐槽!!!!

其實我還順帶看了AppBarLayout等的源碼,如有可能,我還想把Design庫下的所有控件都分析一遍。

感謝你的閱讀。

推薦閱讀

Intercepting everything with CoordinatorLayout Behaviors

CoordinatorLayout.Behavior

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/0224/3991.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容