天天看點

Material Design——Coordinator Layout楔子CoordinatorLayout簡介Coordinator常用實作

楔子

最近正在模仿制作知乎日報,知乎日報的詳情頁有這樣的效果。經過查詢之後發現原來可以使用Coordinator Layout完成該效果,是以就好好學了一下這個View。

Material Design——Coordinator Layout楔子CoordinatorLayout簡介Coordinator常用實作

CoordinatorLayout簡介

CoordinatorLayout的作用

首先我們要知道,CoordinatorLayout是繼承于FrameLayout。然後才是其功能,作用是放入CoordinatorLayout中的View都能夠發生相對滑動。也就是說CoordinatorLayout中的子View都可以監聽其他子View的滑動。那麼如何監聽其他子View的行為并采取相應措施的呢?這就需要用到CoordinatorLayout的輔助類Behavior

CoordinatorLayout的簡單使用

主要參數介紹

1、首先需要引入Meterial Design的外部依賴:

2、 CoordinatorLayout繼承了FrameLayout,是以我們使用

android:layout_gravity="left|top|right|bottom"
           

控制View在Layout中的位置。

3、 對子View設定Behavior類監聽

這個就相當于setListener()方法,綁定View的行為。當被觀察者發生改變,就調用Behavior的方法。讓目前View對該事件做出相應的處理。

Behavior類的使用

Behavior的方法可以分為兩組

第一組:某個view監聽另一個view的狀态變化,例如大小、位置、顯示狀态等

方法解析

//作用:判斷目前View監聽CoordinatorLayout中的哪一個View

    /**
    *  CoordinatorLayout parent:這個不用說就是目前的Cooridnator
    *  View child:這個表示我們設定這個Behavior的View,
    *  View dependency:我們關心的那個View。或者說是被監聽的View
    *  (這個View從哪裡來的呢? CoordinatorLayout的子類都是dependency)
    *  傳回值:是否view是否監聽目前的這個dependency。
    */
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }

    //作用:當被監聽對象的大小、位置、顯示狀态等發生改變的時候,就會回調該方法。

    /**
    *參數與layoutDependsOn()的參數一緻就不解釋了。
    */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return super.onDependentViewChanged(parent, child, dependency);
    }
           

實際操作

任務:實作一個關聯的TextView

效果圖:

Material Design——Coordinator Layout楔子CoordinatorLayout簡介Coordinator常用實作

原理:

  1. 首先我們需要制作一個可以跟着滑鼠移動的TextView,叫做MoveTextView
  2. 右邊的TextView通過監聽左邊TextView的滑動,建立自己的Behavior
  3. 通過使用CooridnatorLayout設定xml布局

展示代碼:

//第一步:設定可随手指移動的TextView
public class MoveTextView extends TextView {

    private int lastY = ;

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

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

    public MoveTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //擷取左上角的Y值
        int currentY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                //擷取距離差
                int dy = currentY - lastY;
                //擷取移動後的Y值
                int tvY = (int) this.getY() + dy;
                //設定tv的位置
                this.setY(tvY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        lastY = currentY;
        return true;
    }
}
           
//第二步:将tvRight的邏輯寫在Behavior中
public class MoveBehavior extends    CoordinatorLayout.Behavior<View> {

    //注意:必須重寫構造方法
    public MoveBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {

        return dependency instanceof MoveTextView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        //設定值
        child.setY(dependency.getY());
        return true;
    }
}
           
<!--第三步:設定xml布局-->
    <android.support.design.widget.CoordinatorLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.newbiechen.usecoordinator.MainActivity">

    <com.newbiechen.usecoordinator.widget.MoveTextView
        android:id="@+id/main_tv_left"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:gravity="center"
        android:layout_gravity="left|top"
        android:background="@color/colorPrimary"
        android:text="Hello World!"
        android:textColor="@android:color/white"/>

    <!--在這裡設定app:layout_behavior="xxx"-->

    <TextView
        android:id="@+id/main_tv_right"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:gravity="center"
        android:layout_gravity="right|top"
        android:text="Hello EveryOne"
        android:background="@color/colorAccent"
        android:textColor="@android:color/white"                                
        app:layout_behavior="com.newbiechen.usecoordinator.behavior.MoveBehavior"/>
</android.support.design.widget.CoordinatorLayout>
           

補充:

  1. 在第二步中我們自定了一個Behavior,發現這個類居然有一個泛型參數,在一般情況下我們可以不需要管他,直接設定為View就可以了。
  2. 同樣是在第二步自定義View中,有一個地方我們需要十分注意,那就是必須要重寫Behavior的構造方法!!Behavior的構造方法!!Behavior的構造方法!!因為,CoordinatorLayout是通過反射調用Behavior的構造方法來建立Behavior的。是以必須要重寫Behavior的構造方法,否則會報ClassNotFoundException。

第二組:某個view監聽另一個View的滑動狀态

方法解析

//作用:當View開始滑動的時候回調。

    /**
    *  主要參數說明:
    *  View target:依賴的View
    *  int nestedScrollAxes:表示滑動的方法,(橫向或者縱向)
    *  傳回值:判斷判斷目前View是否接收這種滑動。
    */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    //作用:當正在滑動的時候回調

    /**
    * 主要參數說明:
    * int dx:表示與上次相比x的偏移量
    * int dy:表示與上次相比y的偏移量
    */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    //作用:當ScrollView脫離點選,自動滑動時候的回調

    /**
    * 主要參數說明:
    * float velocityX:表示滑動時候橫向上的速度
    * float velocityY:表示滑動時候縱向上的速度
    */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }
           

實際操作

任務:實作關聯的ScrollView

效果圖:

Material Design——Coordinator Layout楔子CoordinatorLayout簡介Coordinator常用實作

展示代碼:

public class ScrollBehavior extends CoordinatorLayout.Behavior<View>{

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

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        //表示:判斷目前滑動是否為豎直方向上的滑動,如果是則接受該滑動
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != ;
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        //擷取被監聽的ScrollView的内部Y的偏移量
        int scrollY = target.getScrollY();
        //将被監聽View的Y的偏移量指派給目前View
        child.setScrollY(scrollY);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
        //表示child自動滑動的速度與被監聽的View的速度相同
        ((NestedScrollView) child).fling((int) velocityY);
        return true;
    }
}
           
<!--xml布局-->
    <android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.newbiechen.usecoordinator.MainActivity">

    <android.support.v4.widget.NestedScrollView
        android:layout_width="150dp"
        android:layout_height="match_parent"
        android:layout_gravity="left|top">
        <!--滑動的内容,太多了,不展示-->
    </android.support.v4.widget.NestedScrollView>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="150dp"
        android:layout_height="match_parent"
        android:layout_gravity="right|top"
        app:layout_behavior="com.newbiechen.usecoordinator.behavior.ScrollBehavior">
        <!--滑動的内容,太多了,不展示-->
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
           

補充

發現在xml布局中,我并沒有使用ScrollView,而是使用了NestedView。原因是ScrollView在滑動過程中,無法觸發Behavior的onStartNestedScroll()等一系列滑動回調。為什麼會這樣呢?

原因是Coordinator是通過NestedScrollingChild接口,來回調Behavior的滑動方法的。在api23中,隻有RecyclerView和NestedView繼承了NestedScrollingChild接口,是以當滑動的時候,觸發NestedScollingChild進而調用Behavior的滑動方法。是以如果想進行滑動監聽ListView和ScrollView是無法使用的。如果真的想監聽其他View,那麼隻能繼承NestedScrollingChild并根據源碼重寫了。

Coordinator的優點

從上述的例子我們知道了Coordinator是如何使用的,那麼Google為什麼需要建立一個這樣的Layout,在沒有Coordinator之前,我們是通過什麼方式建立相對滑動的呢?Coordinator相對于之前的方式帶給我們的優點是什麼呢?

不利用Coordinator實作第一幅Gif中,兩個TextView的關聯

展示代碼:

/*  繼承TextView,自定義MoveTextView。
 1.  通過OnTouchEvent()方法,監聽點選事件。自定義Listener設定回調
 */
public class MoveTextView extends TextView {
    private OnMoveListener mMoveListener;
    private int lastY = ;

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

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

    public MoveTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //擷取左上角的Y值
        int currentY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                //擷取距離差
                int dy = currentY - lastY;
                //擷取移動後的Y值
                int tvY = (int) this.getY() + dy;
                //設定tv的位置
                this.setY(tvY);
                if (mMoveListener != null){
                    mMoveListener.onMove(tvY);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        lastY = currentY;
        return true;
    }

    //設定TextView移動的監聽器
    public interface OnMoveListener{
        void onMove(int y);
    }

    //添加監聽器的方法
    public void setOnMoveListener(OnMoveListener listener){
        mMoveListener = listener;
    }
}
           
public class MainActivity extends AppCompatActivity {
    private MoveTextView mTvLeft;
    private TextView mTvRight;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTvLeft = (MoveTextView) findViewById(R.id.main_tv_left);
        mTvRight = (TextView) findViewById(R.id.main_tv_right);
        initClick();
    }

    private void initClick(){
        //設定監聽的回調
        mTvLeft.setOnMoveListener(new MoveTextView.OnMoveListener() {
            @Override
            public void onMove(int y) {
            //在Activity中,通過設定回調,實作右側的TextView移動
                mTvRight.setY(y);
            }
        });
    }
}
           

根據目前代碼與使用CoordinatorLayout的代碼進行對比。我們可以很容易的得出下列結論:

1. 沒有CoordinatorLayout的話,需要重寫View并自定義一個監聽器,通過監聽器建立相對滑動。如果使用CoordinatorLayout就不需要自定義監聽器。(友善)

2. CoordinatorLayout的行為是寫在Behavior類中的,未使用Coordinator的行為是寫在Activity中的,這樣無疑增加了Activity類的複雜度,并且如果不同的View有相同的行為的話,這種做法的耦合度太高,不利于代碼的複用。(對行為進行解耦 )

3. Behavior能夠監聽多種View的行為,并作出相應的行為。(監聽多個View)

Coordinator常用實作

模仿知乎的Toolbar(楔子中的Gif圖)

首先需要知道AppBarLayout

作用:是Toolbar的擴充,能夠裝填Toolbar以外的View合并成一個ActionBar。并且内部的View的滑動由AppBarLayout控制。并且設定沉浸式導航欄的背景為透明,Toolbar的标題從大到小的動畫。

主要屬性:

  1. AppBarLayout繼承了LinearLayout,預設是豎直排布
  2. app:layout_scrollFlags=”” 用來控制内部View的滑動。

可用參數:

  • scroll: 所有想滾動出螢幕的view都需要設定這個flag- 沒有設定這個flag的view将被固定在螢幕頂部。
  • enterAlways: 這個flag讓任意向下的滾動都會導緻該view變為可見,啟用快速“傳回模式”。
  • enterAlwaysCollapsed: 當你的視圖已經設定minHeight屬性又使用此标志時,你的視圖隻能已最小高度進入,隻有當滾動視圖到達頂部時才擴大到完整高度。
  • exitUntilCollapsed: 滾動退出螢幕,最後折疊在頂端。

其次需要知道CollaspingToolbarLayout

作用:折疊内部的View,必須在AppbarLayout中使用

主要參數:

  1. app:layout_collapseMode=”pin | parallen”
    pin:固定模式,在折疊的時候最後固定在頂端
    
    parallen:視差模式,在折疊的時候會有個視差折疊的效果
               
  2. app:layout_collapseParallenMultiplier=”0~1” 滑動的時候視覺差的程度,參數由0~1

最後代碼

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinatorLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/content_app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/theme_brief_height"
        android:fitsSystemWindows="true"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/content_collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimaryDark"
            app:expandedTitleMarginStart="5dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/content_iv_title"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7" />

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?android:attr/actionBarSize"
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <WebView
            android:id="@+id/content_webview"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </WebView>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
           

補充

我們在xml中發現,有這麼一行app:layout_behavior=”@string/appbar_scrolling_view_behavior”

這樣使用是因為,android為我們提供了預設的behavior對一些特殊的View。目前的behavior就是針對AppbarLayout的。

FloatingActionButton的使用

作用:顧名思義,是一個浮動的Button,當和SnackBar一起使用的時候,會産生根據SnackBar出現與消失的移動效果。

(本篇主要是講FloatingActionButton與CoordinatorLayout的關系,并不講解FloatingActionButton的具體使用)

效果圖:

Material Design——Coordinator Layout楔子CoordinatorLayout簡介Coordinator常用實作

展示代碼:

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right|bottom"
        android:layout_marginRight="20dp"
        android:layout_marginBottom="20dp"
        android:src="@mipmap/ic_launcher"/>
</android.support.design.widget.CoordinatorLayout>
           
public void onCreate(Bundle saveBundle){
    setContent(R.layout.activity_main);
    //擷取FloatButton,設定監聽器
    findViewById(R.id.fab)
                .setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        //設定SnackBar
                        Snackbar.make(v,"Hello EveryOne",Snackbar.LENGTH_LONG)
                                .setAction("cancel", new View.OnClickListener() {
                                    @Override
                                    public void onClick(View v) {
                                        //關閉SnackBar時候的監聽
                                    }
                                }).show();
                    }
                });
}
           

為什麼FloatingActionButton能夠實作上移下沉的效果,原來是FloatButton預設使用了FloatingActionButton.Behavior。

根據源碼:

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

我們可以得出,FloatActionButton依賴于SnackBar的行為。是以當SnackBar改變的時候,會調用Behavior的回調。

在知乎的首頁就有利用FloatActionButton的執行個體

(黃色按鈕的顯示與隐藏)

效果圖:

Material Design——Coordinator Layout楔子CoordinatorLayout簡介Coordinator常用實作

我們隻需要自定義一下Behavior就能達到這樣的顯示和隐藏效果了,由于效果比較簡單,代碼就不貼了。原理就是,當View向下滑動的時候,顯示Button,怎麼顯示呢通過利用ObjectAnimation,或者在xml中定義d的Animation都是可以的。

結尾

參考文章:Behavior的使用