在MD系列的前幾篇文章中,通過基礎知識和實戰案例配合講解的形式介紹了CoordinatorLayout
與AppBarLayout
、Toolbar
、CollapsingToolbarLayout
的使用,并實現了幾種MD風格下比較炫酷的交互效果。學會怎么用之后,我們再想想,為什么它們之間能夠產生這樣的交互行為呢?其實就是因為CoordinatorLayout.Behavior
的存在,這也是本文所要講述的內容。至此,Android Material Design系列的學習已進行到第八篇,大家可以點擊以下鏈接查看之前的文章:
- Android TabLayout 分分鐘打造一個滑動標簽頁
- Android 一文告訴你到底是用Dialog,Snackbar,還是Toast
- Android FloatingActionButton 重要的操作不要太多,一個就好
- Android 初識AppBarLayout 和 CoordinatorLayout
- Android CoordinatorLayout實戰案例學習《一》
- Android CoordinatorLayout 實戰案例學習《二》
- Android 詳細分析AppBarLayout的五種ScrollFlags
關于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
有兩種方式:
app:layout_behavior
布局屬性
在布局中設置,值為自定義Behavior
類的名字字符串(包含路徑),類似在AndroidManifest.xml
中定義四大組件的名字一樣,有兩種寫法,包含包名的全路徑和以"."開頭的省略項目包名的路徑。@CoordinatorLayout.DefaultBehavior
類注解
在需要使用Behavior
的控件源碼定義中添加該注解,然后通過反射機制獲取。這個方式就解決了我們前面產生的疑惑,系統的AppBarLayout
、FloatingActionButton
都采用了這種方式,所以無需在布局中重復設置。
看到這里,也告訴我們一點,在自定義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
機制最常見的案例就是FloatingActionButton
和SnackBar
的交互行為,效果如下:
系統的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
控件產生如上圖所示的協調交互效果。
比如我們再看一下這樣一個效果:
列表上下滑動式,底部評論區域隨著頂部Toolbar
的移動而移動,這里我們就可以自定義一個Dependent
機制的Behavior
,設置給底部視圖,讓其依賴于包裹Toolbar
的AppBarLayout
控件:
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
,實現如下與列表協調交互的效果:
為了能夠使用系統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
將覆蓋在列表上面,出現重疊部分,如圖
添加之后,RecyclerView
將位于Toolbar
下面,類似在RelativeLayout
中設置了below
屬性,如圖:
示例源碼
我在GitHub上建立了一個Repository,用來存放整個Android Material Design系列控件的學習案例,會伴隨著文章逐漸更新完善,歡迎大家補充交流,Star地址: