Android Design Support Library系列之七:CoordinatorLayout

CoordinatorLayout------協(xié)調(diào)者布局.
首先來看一下CoordinatorLayout的繼承結(jié)構(gòu):


CoordinatorLayout其本質(zhì)是一個超級 FrameLayout,其功能主要有2個:
1)作為頂層布局
2)協(xié)調(diào)其直接子View進行交互

官方文檔
使用前添加依賴:

compile 'com.android.support:design:25.3.1'

一、Behavior

CoordinatorLayout的神奇之處就在于Behavior對象,CoordinatorLayout通過Behavior對象來處理其子View的下列事件:
1)布局事件
2)觸摸事件
3)View狀態(tài)變化事件
4)嵌套滑動事件
Behavior就是CoordinatorLayout處理事件的媒介,在Behavior中定義了 CoordinatorLayout 中直接子 View 的行為規(guī)范,決定了當(dāng)收到不同事件時,應(yīng)該做怎樣的處理。

Behavior繼承結(jié)構(gòu)

Behavior是一個定義在CoordinatorLayout中的抽象內(nèi)部類.

public static abstract class Behavior<V extends View> {

     public Behavior() {
     }

     public Behavior(Context context, AttributeSet attrs) {
     }
     ......
}
下面我們通過自定義Behavior來簡單了解:

3)View狀態(tài)變化事件
某個View監(jiān)聽另一個View的狀態(tài)變化,例如大小、位置、顯示狀態(tài)等
此時,我們需要關(guān)心Behavior的下面兩個方法:

layoutDependsOn()
onDependentViewChanged()

4)嵌套滑動事件
某個View監(jiān)聽另一個View(實現(xiàn)NestedScrollingChild接口)的滑動狀態(tài)
此時,我們需要關(guān)心Behavior中與NestedScrolling相關(guān)的方法.

二、自定義Behavior之一

某個View監(jiān)聽另一個View的狀態(tài)變化

我們首先來看一下效果圖:
1)水平拖動dependency時,child朝著與dependency相反方向移動
2)豎直拖動dependency時,child在相同方向上同步移動


這里我們要理解兩個概念:childdependency.
1) child是CoordinatorLayout的直接子View,也就是要執(zhí)行動作的View
2) dependency是指child依賴的View,也就是child要監(jiān)聽的View

在上面的效果圖中:child的動作依賴于dependency,當(dāng)dependency這個View發(fā)生了變化,那么child這個View就發(fā)生相應(yīng)變化。
child具體變化的動作就定義在Behavior中:
我們定義一個類,繼承CoordinatorLayout.Behavior<T>,其中,泛型參數(shù)T是我們要執(zhí)行動作的View類,也就是Child,然后實現(xiàn)Behavior的兩個方法:

layoutDependsOn()
onDependentViewChanged()
public class DependencyBehavior extends CoordinatorLayout.Behavior<Button> {

    private int width;

    /**
     * 這個構(gòu)造方法必須重載,因為在CoordinatorLayout里利用反射去獲取Behavior的時候就是拿的這個構(gòu)造
     */
    public DependencyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        width  = context.getResources().getDisplayMetrics().widthPixels;
    }

    /**
     *  確定依賴關(guān)系
     * @param child     要執(zhí)行動作的View
     * @param dependency    child要依賴的View,也就是child要監(jiān)聽的View
     * @return  根據(jù)邏輯確定依賴關(guān)系
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
        return dependency instanceof com.my.DragTextView;
    }

    /**
     * dependency狀態(tài)(大小、位置、顯示與否等)發(fā)生變化時該方法執(zhí)行
     *        在這里我們定義child要執(zhí)行的具體動作
     * @return  child是否要執(zhí)行相應(yīng)動作
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {

        int top = dependency.getTop();
        int left = dependency.getLeft();

        int x = width - left - child.getWidth();
        int y = top;

        setPosition(child, x, y);
        return true;
    }

    private void setPosition(View child, int x, int y) {
        CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) child.getLayoutParams();
        layoutParams.leftMargin = x;
        layoutParams.topMargin = y;
        child.setLayoutParams(layoutParams);
    }
}

這里的DragTextView是一個自定義的TextView

/**
 * 隨著手指移動的TextView
 */
public class DragTextView extends TextView {
    private int lastX;
    private int lastY;

    public DragTextView(Context context) {
        super(context);
    }

    public DragTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) getLayoutParams();
                int left = layoutParams.leftMargin + x - lastX;
                int top = layoutParams.topMargin + y - lastY;

                layoutParams.leftMargin = left;
                layoutParams.topMargin = top;
                setLayoutParams(layoutParams);
                requestLayout();
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
        }
        lastX = x;
        lastY = y;
        return true;
    }
}

Behavior的使用方式:

  1. 在布局文件中通過app:layout_behavior=" "引用
  2. 使用注解添加,系統(tǒng)的控件一般使用這種方式,例AppBarLayout:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
  ......
  public static class Behavior extends HeaderBehavior<AppBarLayout> {
    ......
  }
  ......
}
  1. 在代碼中添加
DependencyBehavior mBehavior = new DependencyBehavior(this,null);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) childView.getLayoutParams();
params.setBehavior(mBehavior);

這里我們使用第一種方式:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.my.DragTextView
        android:layout_width="88dp"
        android:layout_height="88dp"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:text="dependency"
        android:textColor="@android:color/white" />

    <Button
        app:layout_behavior="com.my.DependencyBehavior"
        
        android:layout_width="88dp"
        android:layout_height="88dp"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:text="child"
        android:textAllCaps="false"
        android:textColor="@android:color/white"/>
         
</android.support.design.widget.CoordinatorLayout>

這里需要注意的是:
1)一個child可以同時依賴多個dependency
2)dependency也有可能依賴一個或者多個另外的dependency
3)如果你添加了一個依賴,不管child的順序如何,你的child將總是在所依賴的View放置之后才會被放置
ok,簡單的自定義Behavior已經(jīng)完成了,你對于CoordinatorLayout使用Behavior協(xié)調(diào)子View之間的交互是否有所了解了?

三、自定義Behavior之二

某個View監(jiān)聽另一個View(實現(xiàn)NestedScrollingChild接口)的滑動狀態(tài)

我們首先來看一下效果圖:


1)向上滑動時,右下角FloatingActionButton隱藏
2)向下滑動時,右下角FloatingActionButton顯示
3)點擊FloatingActionButton時,彈出Snackbar,同時FloatingActionButton自動上移.

其中效果(3)是FloatingActionButton中自帶的Behavior的效果,相信看了上面你也大概了解這個Behavior中FloatingActionButton一定是依賴Snackbar的了吧。

所以這里我們是直接繼承FloatingActionButton.Behavior:

public class ScrollBehavior extends FloatingActionButton.Behavior {

    public ScrollBehavior(Context context, AttributeSet attrs) {
        super(context,attrs);
    }

    /**
     * 嵌套滑動事件開始
     *
     * @return  根據(jù)返回值確定我們關(guān)心哪個方向的滑動(x軸/y軸),這里我們關(guān)心的是y軸方向
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }


    /**
     *  嵌套滑動正在進行中
     *  參數(shù)有點多,由于這里我們只關(guān)心y軸方向的滑動,所以簡單測試了dyConsumed、dyUnconsumed
     *      dyConsumed > 0 && dyUnconsumed == 0 上滑中
     *      dyConsumed == 0 && dyUnconsumed > 0 到邊界了還在上滑
     *
     *      dyConsumed < 0 && dyUnconsumed == 0 下滑中
     *      dyConsumed == 0 && dyUnconsumed < 0 到邊界了還在下滑
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        if (((dyConsumed > 0 && dyUnconsumed == 0) 
                || (dyConsumed == 0 && dyUnconsumed > 0))
                && child.getVisibility() != View.INVISIBLE) {// 上滑隱藏
            child.setVisibility(View.INVISIBLE);
        } else if (((dyConsumed < 0 && dyUnconsumed == 0) 
                || (dyConsumed == 0 && dyUnconsumed < 0)) 
                && child.getVisibility() != View.VISIBLE ) {//下滑顯示
            child.setVisibility(View.VISIBLE);
        }
    }
}

這里我們采用一種高大上的方式來使用Behavior ,也是我們使用系統(tǒng)定義好Behavior 的常用方式:
res/values/strings.xml中:

<resources>
    <string name="app_name">CoordinatorLayout</string>
    
    <string name="my_scroll_behavior">com.my.ScrollBehavior</string>
    <!--看這里-->

    <string name="text">\n
                    從前現(xiàn)在過去了再不來\n\n
                    紅紅落葉長埋塵土內(nèi)\n\n
                    開始終結(jié)總是沒變改\n\n
                    天邊的你飄泊白云外\n\n
                    苦海翻起愛恨\n\n
                    在世間難逃避命運\n\n
                    相親竟不可接近\n\n
                    或我應(yīng)該相信是緣份\n\n
                    情人別后永遠(yuǎn)再不來(消散的情緣)\n\n
                    無言獨坐放眼塵世外(愿來日再續(xù))\n\n
                    鮮花雖會凋謝(只愿)\n\n
                    但會再開(為你)\n\n
                    一生所愛隱約(守候)\n\n
                    在白云外(期待)\n\n
                    苦海翻起愛恨\n\n
                    在世間難逃避命運\n\n
                    相親竟不可接近\n\n
                    或我應(yīng)該相信是緣份\n\n
                    苦海翻起愛恨\n\n
                    在世間難逃避命運\n\n
                    相親竟不可接近\n\n
                    或我應(yīng)該相信是緣份\n
                    ----------------\n
                    從前現(xiàn)在過去了再不來\n\n
                    紅紅落葉長埋塵土內(nèi)\n\n
                    開始終結(jié)總是沒變改\n\n
                    天邊的你飄泊白云外\n\n
                    苦海翻起愛恨\n\n
                    在世間難逃避命運\n\n
                    相親竟不可接近\n\n
                    或我應(yīng)該相信是緣份\n\n
                    情人別后永遠(yuǎn)再不來(消散的情緣)\n\n
                    無言獨坐放眼塵世外(愿來日再續(xù))\n\n
                    鮮花雖會凋謝(只愿)\n\n
                    但會再開(為你)\n\n
                    一生所愛隱約(守候)\n\n
                    在白云外(期待)\n\n
                    苦海翻起愛恨\n\n
                    在世間難逃避命運\n\n
                    相親竟不可接近\n\n
                    或我應(yīng)該相信是緣份\n\n
                    苦海翻起愛恨\n\n
                    在世間難逃避命運\n\n
                    相親竟不可接近\n\n
                    或我應(yīng)該相信是緣份\n
    </string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="start|top">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/text" />

    </android.support.v4.widget.NestedScrollView>


    <android.support.design.widget.FloatingActionButton
        app:layout_behavior="@string/my_scroll_behavior"
        
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_margin="16dp"
        android:src="@mipmap/add"
        app:backgroundTint="@color/colorPrimary"
        app:borderWidth="0dp"
        app:elevation="8dp"
        app:fabSize="normal"
        app:pressedTranslationZ="16dp" />


</android.support.design.widget.CoordinatorLayout>

Activiy中:

public class MainActivity extends AppCompatActivity {

    private FloatingActionButton mFAB;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scroll);

        mFAB = (FloatingActionButton) findViewById(R.id.fab);
        mFAB.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Snackbar.make(mFAB,"Snackbar",Snackbar.LENGTH_SHORT).show();
            }
        });
    }
}

這里需要注意的是:
1)你不需要在嵌套滑動的Behavior中定義依賴,CoordinatorLayout的每個child都有機會接收到嵌套滑動事件,這里繼承的FloatingActionButton.Behavior中存在依賴是因為要和Snackbar實現(xiàn)聯(lián)動.
2)雖然我叫它嵌套滑動,但其實它包含滾動(scrolling)和滑動(flinging)兩種
3)監(jiān)聽的滑動View必須實現(xiàn)NestedScrollingChild的接口,這是因為CoordinatorLayout中一個View想向外界傳遞滑動事件,即通知 NestedScrollingParent(CoordinatorLayout實現(xiàn)了此接口),就必須實現(xiàn)此接口.而 Child 與 Parent 的具體交互邏輯, NestedScrollingChildHelper 輔助類基本已經(jīng)幫我們封裝好了,所以我們只需要調(diào)用對應(yīng)的方法即可。
這就可以解釋為什么不能用ScrollView、ListView而用NestScrollView來滑動了,當(dāng)然,如果你要自己自定義一個View實現(xiàn)NestedScrollingChild接口也是可以的,不過那樣太麻煩了。像NestScrollView、RecyclerView、SwipeRefreshLayout中都實現(xiàn)了NestedScrollingChild接口.

如果你想了解關(guān)于嵌套滑動機制更多的詳情,你可以去看一看下面幾個類:
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper

本文參考:
CoordinatorLayout的使用如此簡單
Intercepting everything with CoordinatorLayout Behaviors

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內(nèi)容