重要的ui组件——Behavior

Posted mb61b711907d5f4

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重要的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_
android:layout_>

<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_
android:layout_>

<TextView
android:background="#ff0000"
android:layout_
android:layout_
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_
android:layout_
app:layout_behavior="@string/appbar_scrolling_view_behavior">

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

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


非常简单有木有,CoordinatorLayout作为根布局,里面一个AppBarLayout一个RecyclerView。让我们看看界面是怎么样的。


可以看到显示是正确的。但是如果我把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_
android:layout_>

<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_
android:layout_>

<TextView
android:background="#ff0000"
android:layout_
android:layout_
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_
android:layout_>

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

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



最终界面的展示就像这样,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)
// Were 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 were 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


以上是关于重要的ui组件——Behavior的主要内容,如果未能解决你的问题,请参考以下文章

yii2 源码分析Behavior类分析

Android开发——UI_片段

我们可以在活动 xml 中编写 UI 以及在片段 xm 中编写 UI 吗?

Mint-UI

Reactreact概述组件事件

组件&Props