重要的ui組件——Behavior


v7包下的組件類似CoordinatorLayout推出也有一段時間了,大家使用的時候應該會體會到其中很多的便利,今天這篇文章帶大家來了解一個比較重要的ui組件——Behavior。從字面意思上就可以看出它的作用,就是用來規定某些組件的行為的,那它到底是什么,又該怎么用呢?看完這篇文章希望大家會有自己的收獲~

前言

寫這篇文章的起因是因為我無意中在GitHub上發現了Jake Wharton大神新建了一個Repo,內容是JakeWharton/DrawerBehavior。有興趣的同學可以去看看,其實就是通過Behavior去構造一個類似於DrawerLayout的布局。想了想已經挺長時間沒有搞ui方面的代碼了,所以趁着這個機會復習了一下,順便寫一篇文章鞏固,也給想要了解這方面內容的同學一個平台吧。

Behavior是什么

在文章的開始,我們先要了解什么是Behavior。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Interaction behavior plugin for child views of {@link CoordinatorLayout}.
*
* <p>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.</p>
*
* @param <V> The View type that this Behavior operates on
*/
public static abstract class Behavior<V extends View> {
...........

public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}

public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
return false;
}

...........
}

它是CoordinatorLayout的內部類,從它的注釋和其中的方法可以看出來,它其實就是給CoordinatorLayout的子View提供了一些交互的方法,用來規范它們的交互行為,比如上面出現的onTouchEvent可以用來規范子View的觸摸事件,onLayoutChild可以用來規范子View的布局。

說到這里,大家可能會有一個問題,CoordinatorLayout又是個什么東西?

1
2
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {
}

可以看出,它其實就是一個ViewGroup,實現了NestedScrollingParent用來執行嵌套滑動。至於嵌套滑動的機制大家可以看我博客的第一篇文章,這不是我們這篇文章的重點。

既然CoordinatorLayout僅僅只是一個ViewGroup,它又為什么能展示出它在xml布局中展示的威力呢?其中的秘密就是在Behavior中。我們可以這么說,CoordinatorLayout利用了Behavior作為一個代理,去控制管理其下的子View做到各種布局和動畫效果。那為什么要使用Behavior呢?我想原因大概就是解耦吧,如果把所有的邏輯都寫死在CoordinatorLayout中,一來不利於維護,二來我們就沒有做一些自定義的事情,會顯得非常的笨重。

為什么要用Behavior

這里我們舉一個非常簡單的例子。首先來看看我們的布局文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?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:id="@+id/main_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="48dp">

<TextView
android:background="#ff0000"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="title"
android:textColor="#00ff00"
android:gravity="center"/>
</android.support.design.widget.AppBarLayout>

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

</android.support.v7.widget.RecyclerView>

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

非常簡單有木有,CoordinatorLayout作為根布局,里面一個AppBarLayout一個RecyclerView。讓我們看看界面是怎么樣的。

good

可以看到顯示是正確的。但是如果我把xml里RecyclerView的那行layout_behavior刪掉呢?就像這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?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:id="@+id/main_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="48dp">

<TextView
android:background="#ff0000"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="title"
android:textColor="#00ff00"
android:gravity="center"/>
</android.support.design.widget.AppBarLayout>

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

</android.support.v7.widget.RecyclerView>

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

bad

最終界面的展示就像這樣,RecyclerView把AppBarLayout給覆蓋了。這里其實很好理解,如剛才的代碼所示,CoordinatorLayout其實只是一個ViewGroup,它不像LinearLayout那樣具有特定的布局特點,甚至可以說它內部的邏輯和FrameLayout是沒什么差別的,所以如果你不設置對應的Behavior的話,布局就會有問題。從這里也可以反映出Behavior的作用,就是規范子View的顯示和交互。

原理&系統是怎么用Behavior的

說完了Behavior的作用,那該怎么用它呢?這一小節讓我們來講講Behavior的原理以及系統是如何使用它的。

首先先看原理。我們知道Behavior是用來幫助CoordinatorLayout的,所以我們要從CoordinatorLayout中尋找答案。首先,我們可以看到CoordinatorLayout中有一個LayoutParams,它的子View的LayoutParams都是這個,其中它的構造函數如下。

1
2
3
4
5
6
7
8
9
10
11
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);

.........
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}

a.recycle();
}

可以看到它通過parseBehavior去得到了對應子View的Behavior。大家可以試試用RecyclerView的getLayoutParams方法去獲取LayoutParams並且調用getBehavior方法,可以得到的就是我們在xml文件中設置的那個Behavior。

知道了如何將Behavior設置進去,那它是如何發揮作用的呢?讓我們來看看onLayout函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();

if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}

可以看到的是其中會先調用behavior.onLayoutChild(this, child, layoutDirection)。也就是說,Behavior的邏輯要優先於CoordinatorLayout自己的邏輯。其實不止是onLayout,我們還可以看看onTouchEvent這個函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;

final int action = MotionEventCompat.getActionMasked(ev);

if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// Safe since performIntercept guarantees that
// mBehaviorTouchView != null if it returns true
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}

.........

return handled;
}

可以看到也是調用了Behavior的onTouchEvent,我們可以下判斷說Behavior中的那些方法在CoordinatorLayout中都會在合適的時機去調用。這也證明了我們剛才的那句話:[Behavior就是CoordinatorLayout的代理,幫助它去管理子View]。

我們做一個總結,Behavior可以代理哪些行為呢?

1.Measure和Layout的布局行為。

2.onTouchEvent和onInterceptTouchEvent的觸摸行為。比如design包中的SwipeDismissBehavior就是通過這樣的方式完成的。

3.嵌套滑動行為(NestedScrollingParent和NestedScrollingChild中的邏輯)。

4.子View間的依賴行為。

對於第四點我們這里可以細說一下,什么叫子View的依賴行為呢?這里我們舉個例子,我們都知道如果在CoordinatorLayout中使用了FAB並且點擊展示SnackbarLayout的話,FAB會在Snackbar顯示的時候對應的上移,這是因為FAB依賴了SnackbarLayout。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {

........

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

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(parent, child, dependency);
} else if (dependency instanceof AppBarLayout) {
// If we're depending on an AppBarLayout we will show/hide it automatically
// if the FAB is anchored to the AppBarLayout
updateFabVisibility(parent, (AppBarLayout) dependency, child);
}
return false;
}

........
}

這是FAB中的Behavior,可以看到它重寫了layoutDependsOn和onDependentViewChanged,里面的邏輯很簡單的就可以看明白。這里我們[將代碼翻譯成語言]就是說FAB要依賴的組件是SnackbarLayout,所以在之后的操作里當DependentView(SnackbarLayout)發生了改變,自己(FAB)也會相應的做出改變。

值得一提的是,onDependentViewChanged這個函數的調用時機並不是在onLayout之前,而是在onPreDraw中,具體代碼如下:

1
2
3
4
5
6
7
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
dispatchOnDependentViewChanged(false);
return true;
}
}

如此簡單的處理View間的依賴,可見Behavior配合CoordinatorLayout是有多強大。下面我們可以再舉一個例子來講講Behavior的作用。還記得我們上面說的嗎?RecyclerView設置了一個Behavior它就可以和AppBarLayout很好的展示出來。這個Behavior的名字是:

1
2
3
app:layout_behavior="@string/appbar_scrolling_view_behavior"

<string name="appbar_scrolling_view_behavior" translatable="false">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string>

可以看到它是AppBarLayout里的一個內部類,讓我們看看它做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
offsetChildAsNeeded(parent, child, dependency);
return false;
}

private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
final CoordinatorLayout.Behavior behavior =
((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof Behavior) {
// Offset the child, pinning it to the bottom the header-dependency, maintaining
// any vertical gap, and overlap
final Behavior ablBehavior = (Behavior) behavior;
final int offset = ablBehavior.getTopBottomOffsetForScrollingSibling();
child.offsetTopAndBottom((dependency.getBottom() - child.getTop())
+ ablBehavior.mOffsetDelta
+ getVerticalLayoutGap()
- getOverlapPixelsForOffset(dependency));
}
}

我們知道,如果不設置這個Behavior的話,RecyclerView會覆蓋AppBarLayout。而上面這段代碼里的邏輯就可以很好的解釋這個原因了。值得一提的是,在offsetChildAsNeeded方法中有這么一段:

1
2
3
4
5
final CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof Behavior) {
// Offset the child, pinning it to the bottom the header-dependency, maintaining
// any vertical gap, and overlap
final Behavior ablBehavior = (Behavior) behavior;

這里dependency就是AppBarLayout,所以我們可以知道,AppBarLayout中有兩個Behavior,一個是我們前面提到的ScrollingViewBehavior,用來處理它和其他滑動View的關系,另外一個就是Behavior,用來處理自己的邏輯,比如Layout。通過這種巧妙的方式,我們就可以做到非常簡便的控制View本身和View之間的邏輯。

如何自定義Behavior

本來想寫個demo給大家看一看的,不過感覺還是不要重復造輪子了,還是沒用的輪子。推薦大家看SwipeDismissBehavior用法及實現原理這篇文章和一開始提到的Jake大神的新作DrawerBehavior。如果你把這兩個東西搞懂,那么Behavior你可以說已經完全沒問題了~

后記

最近一段時間都在搞hotPatch和插件化相關的東西,看了很多Framework層的源碼,要做的東西也做的七七八八,希望快點解決最后的幾個bug並且之后能開源和大家見面吧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM