來自:Intercepting everything with CoordinatorLayout Behaviors
使用過Android Design Support Library
的小伙伴應該對CoordinatorLayout
比較熟悉,它可以讓它的子View產生一系列聯動效應,如下效果圖:
但這些究竟是怎么做到的?其實CoordinatorLayout本身并沒有做太多的事情,它的布局方式和FrameLayout基本相同,那么上圖中我們看到的神奇的動態效果是怎么實現的呢?答案就是
CoordinatorLayout.Behaviors
:
通過把一個Behavior附加到CoordinatorLayout的一個直接子類上面,那么這個子類就擁有了攔截CoordinatorLayout的
touch events
,window insets
,measurement
,layout
和nested scrolling
這一系列事件的能力。
創建一個Behavior
創建一個Behavior很簡單,只要繼承Behavior類即可
public class FancyBehavior<V extends View>
extends CoordinatorLayout.Behavior<V> {
/**
*當FancyBehavior在代碼中被添加到子類時調用的構造函數
*/
public FancyBehavior() {
}
/**
* 當FancyBehavior是從布局文件中被添加到子類時調用的構造函數
*
* @param context The {@link Context}.
* @param attrs The {@link AttributeSet}.
*/
public FancyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
注意,在上面這段代碼中使用了范型,此時FancyBehavior可以被添加到任何View上,如果希望FancyBehavior只被添加到特定的View子類上,可以采用如下寫法:
public class FancyFrameLayoutBehavior
extends CoordinatorLayout.Behavior<FancyFrameLayout>
另外在Behavior的使用中,可以使用Behavior.setTag()/Behavior.getTag()
來保存臨時性的變量,另外也可以使用onSaveInstanceState()/onRestoreInstanceState()
進行數據保存。善用這些方法可以讓Behavior更具狀態性。
添加Behavior
Behavior本身并不做任何事情,它們需要被添加到CoordinatorLayout的直接子類才能被使用。主要有三種方法來將Behavior添加進子類,分別是程序中動態添加
,Xml布局文件添加
和使用注解添加
。
程序中動態添加
FancyBehavior fancyBehavior = new FancyBehavior();
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) yourView.getLayoutParams();
params.setBehavior(fancyBehavior);
在上面的這個例子中,我們在代碼中向yourView添加了fancyBehavior,同時也是使用了FancyBehavior()這個構造函數,如果對FancyBehavior的構造過程需要額外的參數,可以自行重載構造函數。
Xml布局文件中添加
<FrameLayout
android:layout_height=”wrap_content”
android:layout_width=”match_parent”
app:layout_behavior=”.FancyBehavior” />
如果覺得在代碼中添加會讓代碼變得混亂的話可以使用上面這種方式,它需要使用到CoordinatorLayout的一個自定義屬性layout_behavior
,屬性內容是你的類名。
需要注意的是使用這種方法,FancyBehavior(Context context, AttributeSet attrs)這個構造函數會被默認調用,這樣我們就可以為其自定義一些xml屬性,以保證其他開發者可以通過xml來修改FancyBehavior的行為。
Note:
給布局文件添加自定義屬性后,在代碼中一般使用layout_的形式獲取自定義屬性,與此類似,使用behavior_的形式獲取Behavior的自定義屬性。
使用注解添加
如果你構建了一個自己的視圖,并且這個視圖需要一個自定義的Behavior,那么你就可以使用下面這種方式:
@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class)
public class FancyFrameLayout extends FrameLayout {
}
使用這種方式添加的Behavior被設置成了DefaultBehavior,如果這時在Xml中使用layout_behavior的話,這個DefaultBehavior會被覆蓋。
攔截觸摸事件
一但你的Behavior設置到位,那么你就可以真正做一些有意義的事情了。第一個要說的就是攔截觸摸事件
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return super.onTouchEvent(parent, child, ev);
}
需要攔截觸摸事件的話需要重寫上述兩個方法,當onInterceptTouchEvent返回true的時候,那么我們的Behavior就會活的所有后續的onTouchEvent。
另外還有一個簡單粗暴的方法來攔截觸摸事件:
@Override
public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
return true;
}
故名思意,當返回true的時候,我們這個視圖下的其他視圖將獲取不到任何Touch事件。
攔截WindowInsets
如果你的視圖的fitsSystemWindows屬性是true,那么你的Behavior的onApplyWindowInsets()就會被調用,可以在這里優先處理WindowInsets相關問題。
Note:
如果你的視圖沒有消費掉WindowsInsets,那么需要調用ViewCompat.dispatchApplyWindowInsets()將其傳遞給子視圖。
攔截Measurement和Layout
在Behavior中,可以通過重寫onMeasureChild()和onLayoutChild()來攔截父視圖的相關Measurement和Layout操作,比如下面的代碼就是通過重寫onMeasureChild()來攔截父視圖的onMeasureChild(),以達到設置視圖最大寬度的目的。
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.behaviors;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.ViewGroup;
import static android.view.View.MeasureSpec;
/**
* Behavior that imposes a maximum width on any ViewGroup.
*
* <p />Requires an attrs.xml of something like
*
* <pre>
* <declare-styleable name="MaxWidthBehavior_Params">
* <attr name="behavior_maxWidth" format="dimension"/>
* </declare-styleable>
* </pre>
*/
public class MaxWidthBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V> {
private int mMaxWidth;
public MaxWidthBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.MaxWidthBehavior_Params);
mMaxWidth = a.getDimensionPixelSize(
R.styleable.MaxWidthBehavior_Params_behavior_maxWidth, 0);
a.recycle();
}
@Override
public boolean onMeasureChild(CoordinatorLayout parent, V child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
if (mMaxWidth <= 0) {
// No max width means this Behavior is a no-op
return false;
}
int widthMode = MeasureSpec.getMode(parentWidthMeasureSpec);
int width = MeasureSpec.getSize(parentWidthMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED || width > mMaxWidth) {
// Sorry to impose here, but max width is kind of a big deal
width = mMaxWidth;
widthMode = MeasureSpec.AT_MOST;
parent.onMeasureChild(child,
MeasureSpec.makeMeasureSpec(width, widthMode), widthUsed,
parentHeightMeasureSpec, heightUsed);
// We've measured the View, so CoordinatorLayout doesn't have to
return true;
}
// Looks like the default measurement will work great
return false;
}
}
寫一個通用的Behavior固然有用,但我們需要知道的是有時候如果你想讓你的app簡單一點的話完全可以把Behavior的相關功能寫在自定義View的內部,沒必要為了使用Behavior而是用它。
視圖間的依賴
上面說到的這些功能之一來一個視圖,但Behavior的強大之處不在于此,而是它可以支持視圖間的相互依賴,就像一開始的那張動圖一樣。當一個視圖發生變化后,依附在上面的Behavior就會收到一個回調,以此來實現更加豐富有用的功能。
有兩種方法可以實現Behavior對視圖的依賴,一種是Behavior對應的視圖固定
在另一個視圖上,另一種是在layoutDependsOn()
中返回true。
使用固定
的方法也很簡單,只要在Xml中添加CoordinatorLayout的自定義屬性layout_anchor
和layout_anchorGravity
,前者用來確定目標視圖,后者用來確定固定到目標視圖的位置。
例如將FloatingActionButton
固定到AppBarLayout
后,FloatingActionButton的Behavior就會默認使用依賴關系來隱藏自己當AppBarLayout從屏幕滑出后。
通常建立了依賴關系后Behavior的以下兩個方法會被激活,onDependentViewRemoved()
,onDependentViewChanged()
。
Note:
建立依賴關系的兩個視圖中,若被依賴的視圖被從布局中移除,那么相應的那個布局也會被移除。
Nested Scrolling
關于Nested Scrolling,我們需要知道一下幾點:
1.我們沒有必要為了獲得Nested Scrolling相關回調而去寫依賴關系,CoordinatorLayout的每一個子View都能有機會捕獲到Nested Scrolling事件。
2.Nested Scrolling事件不僅可以被CoordinatorLayout的直接子類捕獲,也可以被它的間接子類們捕獲。
3.雖然稱之為nested(折疊) Scrolling
,但它產生的滑動事件是1:1的。
所以,如果你需要捕獲nested scrolling事件,可以在適當的時候在onStartNestedScroll()
里返回true。接著你就能使用以下兩個方法回調了:
1.onNestedPreScroll()會在scrolling View獲得滾動事件前調用,它允許你消費部分或者全部的事件信息。
2.onNestedScroll()會在scrolling View做完滾動后調用,通過回調可以知道scrolling view滾動了多少?和它沒有消耗的滾動事件。
當nested scrolling結束后,你會得到一個onStopNestedScroll()回調,說明這次的滾動事件已經結束。等待下一次的onStartNestedScroll()。
一切才只是個開端
當把上面的所有都結合起來使用的時候,就是見證奇跡的時候了。如果想了解更多相關資料,鼓勵你去查看其源碼,相信你能收獲更多知識。