Android 一步一步分析CoordinatorLayout.Behavior

在MD系列的前幾篇文章中,通過基礎知識和實戰案例配合講解的形式介紹了CoordinatorLayoutAppBarLayout、Toolbar、CollapsingToolbarLayout的使用,并實現了幾種MD風格下比較炫酷的交互效果。學會怎么用之后,我們再想想,為什么它們之間能夠產生這樣的交互行為呢?其實就是因為CoordinatorLayout.Behavior的存在,這也是本文所要講述的內容。至此,Android Material Design系列的學習已進行到第八篇,大家可以點擊以下鏈接查看之前的文章:

關于Behavior


官網對于CoordinatorLayout.Behavior的介紹已經將它的作用說明得很清楚了,就是用來協調CoordinatorLayout的Child Views之間的交互行為:

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.

之前學習CoordinatorLayout的使用案例時,用的都是系統的特定控件,比如design包中的FloatingActionButton、AppBarLayout等,而不是普通的控件,如ImageButton之類的,就是因為design包中的這些特定控件已經被系統默認定義了繼承自CoordinatorLayout.Behavior的各種Behavior,比如FloatingActionButton.Behavior
AppBarLayout.Behavior。而像系統的ToolBar控件就沒有自己的Behavior,所以只能將其擱置到AppBarLayout容器里才能產生相應的交互效果。

看到這里就能清楚一點了,如果我們想實現控件之間任意的交互效果,完全可以通過自定義Behavior的方式達到??吹竭@里大家可能會有一個疑惑,就是CoordinatorLayout如何獲取Child Views的Behavior的呢,為什么在布局中,有些滑動型控件定義了app:layout_behavior屬性而系統類似FloatingActionButton的控件則不需要明確定義該屬性呢?看完CoordinatorLayout.Behavior的構造函數就明白了。

        /**
         * Default constructor for instantiating Behaviors.
         */
        public Behavior() {
        }

        /**
         * Default constructor for inflating Behaviors from layout. The Behavior will have
         * the opportunity to parse specially defined layout parameters. These parameters will
         * appear on the child view tag.
         *
         * @param context
         * @param attrs
         */
        public Behavior(Context context, AttributeSet attrs) {
        }

CoordinatorLayout.Behavior有兩個構造函數,注意看第二個帶參數的構造函數的注釋,里面提到,在這個構造函數中,Behavior會解析控件的特殊布局屬性,也就是通過parseBehavior方法獲取對應的Behavior,從而協調Child Views之間的交互行為,可以在CoordinatorLayout類中查看,具體源碼如下:

    static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : 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);
        }
    }

parseBehavior方法告訴我們,給Child Views設置Behavior有兩種方式:

  1. app:layout_behavior布局屬性
    在布局中設置,值為自定義Behavior類的名字字符串(包含路徑),類似在AndroidManifest.xml中定義四大組件的名字一樣,有兩種寫法,包含包名的全路徑和以"."開頭的省略項目包名的路徑。

  2. @CoordinatorLayout.DefaultBehavior類注解
    在需要使用Behavior的控件源碼定義中添加該注解,然后通過反射機制獲取。這個方式就解決了我們前面產生的疑惑,系統的AppBarLayoutFloatingActionButton都采用了這種方式,所以無需在布局中重復設置。

看到這里,也告訴我們一點,在自定義Behavior時,一定要重寫第二個帶參數的構造函數,否則這個Behavior是不會起作用的。

根據CoordinatorLayout.Behavior提供的方法,這里將自定義Behavior分為兩類來講解,一種是dependent機制,一種是nested機制,對應著不同的使用場景。

dependent機制


這種機制描述的是兩個Child Views之間的綁定依賴關系,設置Behavior屬性的Child View跟隨依賴對象Dependency View的大小位置改變而發生變化,對應需要實現的方法常見有兩個:

        /**
         * Determine whether the supplied child view has another specific sibling view as a
         * layout dependency.
         *
         * <p>This method will be called at least once in response to a layout request. If it
         * returns true for a given child and dependency view pair, the parent CoordinatorLayout
         * will:</p>
         * <ol>
         *     <li>Always lay out this child after the dependent child is laid out, regardless
         *     of child order.</li>
         *     <li>Call {@link #onDependentViewChanged} when the dependency view's layout or
         *     position changes.</li>
         * </ol>
         *
         * @param parent the parent view of the given child
         * @param child the child view to test
         * @param dependency the proposed dependency of child
         * @return true if child's layout depends on the proposed dependency's layout,
         *         false otherwise
         *
         * @see #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)
         */
        public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

        /**
         * Respond to a change in a child's dependent view
         *
         * <p>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.</p>
         *
         * <p>A view's dependency is determined by
         * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
         * if {@code child} has set another view as it's anchor.</p>
         *
         * <p>Note that if a Behavior changes the layout of a child via this method, it should
         * also be able to reconstruct the correct position in
         * {@link #onLayoutChild(CoordinatorLayout, android.view.View, int) onLayoutChild}.
         * <code>onDependentViewChanged</code> will not be called during normal layout since
         * the layout of each child view will always happen in dependency order.</p>
         *
         * <p>If the Behavior changes the child view's size or position, it should return true.
         * The default implementation returns false.</p>
         *
         * @param parent the parent view of the given child
         * @param child the child view to manipulate
         * @param dependency the dependent view that changed
         * @return true if the Behavior changed the child view's size or position, false otherwise
         */
        public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

具體含義在注釋中已經很清楚了,layoutDependsOn()方法用于決定是否產生依賴行為,onDependentViewChanged()方法在依賴的控件發生大小或者位置變化時產生回調。dependent機制最常見的案例就是FloatingActionButtonSnackBar的交互行為,效果如下:

Behavior-01

系統的FloatingActionButton已經默認定義了一個Behavior來協調交互,如果不用系統的FAB控件,比如改用GitHub上的一個庫futuresimple/android-floating-action-button,再通過自定義一個Behavior,也能很簡單的實現與SnackBar的協調效果:

package com.yifeng.mdstudysamples;

import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by yifeng on 16/9/20.
 *
 */
public class DependentFABBehavior extends CoordinatorLayout.Behavior {

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

    /**
     * 判斷依賴對象
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof Snackbar.SnackbarLayout;
    }

    /**
     * 當依賴對象發生變化時,產生回調,自定義改變child view
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
        child.setTranslationY(translationY);
        return true;
    }
}

很簡單的一個自定義Behavior處理,然后再為對應的Child View設置該屬性即可。由于這里我們用的是第三方庫,采用遠程依賴的形式引入的,無法修改源碼,所以不方便使用注解的方式為其設置Behavior,所以在布局中為其設置,并且使用了省略包名的方式:

<?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.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <include
            layout="@layout/include_toolbar"/>

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

    <com.getbase.floatingactionbutton.FloatingActionButton
        xmlns:fab="http://schemas.android.com/apk/res-auto"
        android:id="@+id/fab_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/dp_16"
        android:layout_gravity="bottom|right"
        android:onClick="onClickFab"
        fab:fab_icon="@mipmap/ic_toolbar_add"
        fab:fab_colorNormal="?attr/colorPrimary"
        fab:fab_colorPressed="?attr/colorPrimaryDark"
        app:layout_behavior=".DependentFABBehavior"/>

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

這樣,采用dependent機制自定義Behavior,與使用系統FAB按鈕一樣,即可與SnackBar控件產生如上圖所示的協調交互效果。

比如我們再看一下這樣一個效果:

Behavior-03

列表上下滑動式,底部評論區域隨著頂部Toolbar的移動而移動,這里我們就可以自定義一個Dependent機制的Behavior,設置給底部視圖,讓其依賴于包裹ToolbarAppBarLayout控件:

package com.yifeng.mdstudysamples;

import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by yifeng on 16/9/23.
 *
 */

public class CustomExpandBehavior extends CoordinatorLayout.Behavior {

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

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof AppBarLayout;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        int delta = dependency.getTop();
        child.setTranslationY(-delta);
        return true;
    }
}

布局內容如下:

<?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.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_56"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <include
            layout="@layout/include_toolbar"/>

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

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_56"
        android:layout_gravity="bottom"
        app:layout_behavior=".CustomExpandBehavior"
        android:padding="8dp"
        android:background="@color/blue">

        <Button
            android:id="@+id/btn_send"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="Send"
            android:layout_alignParentRight="true"
            android:background="@color/white"/>

        <EditText
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_toLeftOf="@id/btn_send"
            android:layout_marginRight="4dp"
            android:padding="4dp"
            android:hint="Please input the comment"
            android:background="@color/white"/>

    </RelativeLayout>

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

注意,這里將自定義的Behavior設置給了底部內容的外層容器RelativeLayout,即可實現上述效果。

Nested機制


Nested機制要求CoordinatorLayout包含了一個實現了NestedScrollingChild接口的滾動視圖控件,比如v7包中的RecyclerView,設置Behavior屬性的Child View會隨著這個控件的滾動而發生變化,涉及到的方法有:

onStartNestedScroll(View child, View target, int nestedScrollAxes)

onNestedPreScroll(View target, int dx, int dy, int[] consumed)

onNestedPreFling(View target, float velocityX, float velocityY)

onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)

onNestedFling(View target, float velocityX, float velocityY, boolean consumed)

onStopNestedScroll(View target)

其中,onStartNestedScroll方法返回一個boolean類型的值,只有返回true時才能讓自定義的Behavior接受滑動事件。同樣的,舉例說明一下。

通過查看系統FAB控件的源碼可以知道,系統FAB定義的Behavior能夠處理兩個交互,一個是與SnackBar的位置交互,效果如上面的圖示一樣,另一個就是與AppBarLayout的展示交互,都是使用的Dependent機制,效果在之前的文章 -- Android CoordinatorLayout 實戰案例學習《二》 中可以查看,也就是AppBarLayout 滾動到一定程度時,FAB控件的動畫隱藏與展示。下面我們使用Nested機制自定義一個Behavior,實現如下與列表協調交互的效果:

Behavior-02

為了能夠使用系統FAB控件提供的隱藏與顯示的動畫效果,這里直接繼承了系統FAB控件的Behavior

package com.yifeng.mdstudysamples;

import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by yifeng on 16/8/23.
 *
 */
public class NestedFABBehavior extends FloatingActionButton.Behavior {

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

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
                super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
                        nestedScrollAxes);
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
            //系統FAB控件提供的隱藏動畫
            child.hide();
        } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
            //系統FAB控件提供的顯示動畫
            child.show();
        }
    }
}

然后在布局中添加RecyclerView,并為系統FAB控件設置自定義的Behavior,內容如下:

<?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.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <include
            layout="@layout/include_toolbar"/>

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

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/dp_16"
        android:src="@mipmap/ic_toolbar_add"
        app:layout_anchor="@id/rv_content"
        app:layout_anchorGravity="bottom|right"
        app:backgroundTint="@color/fab_ripple"
        app:layout_behavior="com.yifeng.mdstudysamples.NestedFABBehavior"/>

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

這樣,即可實現系統FAB控件與列表滑動控件的交互效果。

@string/appbar_scrolling_view_behavior


這是一個系統字符串,值為:

android.support.design.widget.AppBarLayout$ScrollingViewBehavior

CoordinatorLayout容器中,通常用在AppBarLayout視圖下面(不是里面)的內容控件中,比如上面的RecyclerView,如果我們不給它添加這個Behavior,Toolbar將覆蓋在列表上面,出現重疊部分,如圖

behavior-removed

添加之后,RecyclerView將位于Toolbar下面,類似在RelativeLayout中設置了below屬性,如圖:

beharior-added

示例源碼


我在GitHub上建立了一個Repository,用來存放整個Android Material Design系列控件的學習案例,會伴隨著文章逐漸更新完善,歡迎大家補充交流,Star地址:

https://github.com/Mike-bel/MDStudySamples

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

推薦閱讀更多精彩內容