1、什么是 Behavior ?

Behavior 是谷歌 Material 设计中重要的一员,用来完成复杂的视觉联动效果。

运用 Behavior 的控件需求被包裹在 CoordinateLayout 内部。Behavior 便是一个接口。Behavior 实际上便是经过将 CoordinateLayout 的布局和接触事情传递给 Behavior 来完成的。

从设计办法上讲,就一个 Behavior 而言,它是一种拜访者办法,相当于将 CoordinateLayout 的布局和接触进程对外提供的拜访器;而多个 Behavior 在 CoordinateLayout 内部的事情分发则是一种职责链机制,呈现出长幼有序的状况。

以 layout 进程为例,

// androidx.coordinatorlayout.widget.CoordinatorLayout#onLayout
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);
        if (child.getVisibility() == GONE) {
            continue;
        }
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();
        // 这儿
        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

可见 Behavior 便是将子控件的布局经过 onLayoutChild() 办法对外回调了出来。控件的 behavior 优先阻拦和处理 layout 事情。

那 Behavior 比较于咱们直接覆写接触事情的办法处理手势有什么长处呢?

其长处在于,咱们能够将页面的布局、接触和滑动等事情封装到 Behavior 接口的完成类中以到达交互逻辑的复用宽和耦的目的。

2、Behavior 接口的重要办法

Behavior 接口界说了许多办法,用于将 CoordinateLayout 的布局、丈量和事情分发事情向外传递。这儿我根据其效果将其概括为以下几组。

2.1 Behavior 生命周期相关的回调办法

首要是 Behavior 和 LayoutParams 相关和接触绑守时回调的办法。它们被回调的世纪别离是,

  • onAttachedToLayoutParams:LayoutParams 的构造函数中回调
  • onDetachedFromLayoutParams:调用 LayoutParams 的 setBehavior,用一个新的 Behavior 覆盖旧的 Behavior 时回调
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {}
public void onDetachedFromLayoutParams() {}

2.2 子控件上色相关的回调办法

然后是跟 scrim color 相关的办法,这些办法会在 CoordinateLayout 的制作进程中被调用。主要是跟制作相关的,即用来对指定的 child 进行上色。

这儿的 child 是指该 Behavior 所相关的控件,parent 便是指包裹这个 child 的最外层的 CoordinatorLayout. 后面的办法都是如此。

public int getScrimColor(@NonNull CoordinatorLayout parent, @NonNull V child)
public float getScrimOpacity(@NonNull CoordinatorLayout parent, @NonNull V child)
public boolean blocksInteractionBelow(@NonNull CoordinatorLayout parent, @NonNull V child)

2.3 丈量和布局相关的回调办法

然后一组办法是用来将 CoordinatorLayout 的丈量和布局进程对外回调。不论是丈量仍是布局的回调办法,优先级都是回调办法优先。也便是回调办法能够经过回来 true 阻拦 CoordinatorLayout 的逻辑。

别的,CoordinatorLayout 里运用 Behavior 的时分只会从直系子控件上读取,所以,子控件的子控件上即使有 Behavior 也不会被阻拦处理。所以,在一般运用 CoordinatorLayout 的时分,假如咱们需求在某个控件上运用 Behavior,都是将其作为 CoordinatorLayout 的直系子控件。

还要留意,一个 CoordinatorLayout 的直系子控件包含多个 Behavior 的时分,这些 Behavior 被回调的先后次序和它们在 CoordinatorLayout 里布局的先后次序共同。也便是说,排序在前的子控件优先阻拦和处理事情。这和中国古代的王位继承制差不多。

public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child, int parentWidthMeasureSpec,
                int widthUsed, int parentHeightMeasureSpec, int heightUsed)
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection)

2.4 描述子控件之间依靠联系的回调

接下来的一组办法用来描述子控件之间的依靠联系。它的效果原理是,当 CoordinatorLayout 产生以下三类事情

  • NestedScroll 翻滚事情,经过 onNestedScroll() 获取(后面会剖析这个事情作业原理)
  • PreDraw 事情,经过 ViewTreeObserver.OnPreDrawListener 获取到该事情
  • 控件被移除事情,经过 OnHierarchyChangeListener 获取到该事情

的时分会运用 layoutDependsOn() 办法,针对 CoordinatorLayout 的每个子控件,判别其他子控件与其是否构成依靠联系。假如构成了依靠联系,就回调其对应的 Behavior 的 onDependentViewChanged() 或许 onDependentViewRemoved() 办法。

public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)

2.5 与窗口改变和状况保存与恢复相关的事情

然后是与窗口改变和状况保存与恢复相关的事情。

public WindowInsetsCompat onApplyWindowInsets(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull WindowInsetsCompat insets)
public boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull Rect rectangle, boolean immediate)
public void onRestoreInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child,
                @NonNull Parcelable state)
public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child)
public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child,
                @NonNull Rect rect)

这些事情一般不会用到。

3、Behavior 的事情分发机制

以上是 Behavior 内界说的一些办法。Behavior 主要的用途仍是用来做接触事情的分发。这儿,咱们来重点重视和接触事情分发相关的办法。

3.1 安卓的接触事情分发机制

首要咱们来回忆传统的事情分发机制。当 window 将接触事情交给 DecorView 之后,接触事情在 ViewGroup 和 View 之间传递遵从如下模型,

// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
    if ACTION_DOWN 事情并且 FLAG_DISALLOW_INTERCEPT 答应阻拦 {
        final boolean intercepted = onInterceptTouchEvent(ev) // 留意 onInterceptTouchEvent 的方位
    }
    boolean handled;
    if !intercepted {
        if child == null {
            handled = super.dispatchTouchEvent(ev)
        } else {
            handled = child.dispatchTouchEvent(ev)
        }
    }
    return handled;
}
// View
public boolean dispatchTouchEvent(MotionEvent event) {
    if mOnTouchListener.onTouch(this, event) {
        return true
    }
    if onTouchEvent(event) { // 留意 onTouchEvent 的方位
        return true
    }
    return false
}

所以,子控件能够经过调用父控件的 requestDisallowInterceptTouchEvent() 办法不让父控件阻拦事情。可是这种阻拦机制完全是基于默认的完成逻辑。假如父控件修改了 requestDisallowInterceptTouchEvent() 办法或许 dispatchTouchEvent() 办法的逻辑,子控件的束缚效果是无效的。

父控件经过 onInterceptTouchEvent() 阻拦事情只能阻拦部分事情。

比较于父控件,子控件的事情分发则简单得多。首要是先将事情交给自界说的 mOnTouchListener 来处理,其没有消费才将其交给默认的 onTouchEvent 来处理。在 onTouchEvent 里则会判别事情的类型,比方点击和长按之类的,并且能够看到系统源码在判别详细的事情类型的时分运用了 post Runnable 的办法。

在父控件中假如子控件没有处理,则父控件将会走 View 的 dispatchTouchEvent() 逻辑,也便是去判别事情的类型来消费了。

3.2 与接触事情分发机制相关的办法

在 Behavior 中界说了两个与接触事情分发相关的办法,

public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev)
public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev)

对照上面的事情分发机制中 onInterceptTouchEvent 和 onTouchEvent 的逻辑,这儿的 Behavior 的阻拦逻辑是:CoordinatorLayout 按照 Behavior 的呈现次序进行遍历,先走 CoordinatorLayout 的 onInterceptTouchEvent,假如一个 Behavior 的 onInterceptTouchEvent 阻拦了该事情,则会记录阻拦该事情的 View 并给其他 Behavior 的 onInterceptTouchEvent 发送给一个 Cancel 类型的接触事情。然后,在 CoordinatorLayout 的 onTouchEvent 办法中会执行该 View 对应的 Behavior 的 onTouchEvent 办法。

3.3 安卓的 NestedScrolling 机制

安卓在 5.0 上引进了 NestedScrolling 机制。之所以引进该事情是因为传统的事情分发机制 MOVE 事情当父控件阻拦了之后就无法再交给子 View. 而 NestedScrolling 机制能够指定在一个滑动事情中,父控件和子控件别离消费多少。比方,在一个向上的滑动事情中,咱们需求 toolbar 先向上滑动 50dp,然后列表再向上滑动。此刻,咱们能够先让 toolbar 消费 50dp 的事情,剩余的再交给列表处理,让其向上滑动 6dp 的距离。

在 NestedScrolling 机制中界说了 NestedScrollingChildNestedScrollingParent 两个接口(为了支持更多功用后续又界说了 NestedScrollingChild2 和 NestedScrollingChild3 等接口)。外部容器一般完成 NestedScrollingParent 接口,而子控件一般完成 NestedScrollingChild 接口。在常规的事情分发机制中,子控件(比方 RecyclerView 或许 NestedScrollView )会在 Move 事情中找到父控件,假如该父控件完成了 NestedScrollingParent 接口,就会告诉该父控件产生了滑动事情。然后,父控件能够对滑动事情进行进一步的分发。以 RecyclerView 为例,

// androidx.recyclerview.widget.RecyclerView#onTouchEvent
public boolean onTouchEvent(MotionEvent e) {
    // ...
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            // ...
            if (dispatchNestedPreScroll(
                canScrollHorizontally ? dx : 0,
                canScrollVertically ? dy : 0,
                mReusableIntPair, mScrollOffset, TYPE_TOUCH
            )) {
                // ...
            }
        }
    }
}

这儿 dispatchNestedPreScroll() 便是滑动事情的分发逻辑,它最终会走到 ViewParentCompat 的 onNestedPreScroll() 办法,并在该办法中向上交给父控件进行分发。代码如下,

public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
        int[] consumed, int type) {
    if (parent instanceof NestedScrollingParent2) {
        ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
    } else if (type == ViewCompat.TYPE_TOUCH) {
        if (Build.VERSION.SDK_INT >= 21) {
            parent.onNestedPreScroll(target, dx, dy, consumed);
        } else if (parent instanceof NestedScrollingParent) {
            ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
        }
    }
}

3.4 与 NestedScrolling 相关的办法

在 CoordinatorLayout 中,与 NestedScrolling 机制相关的办法主要分红 scroll 和 fling 两类。

public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type)
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type)
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, @NestedScrollType int type)
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                int dyUnconsumed, @NestedScrollType int type, @NonNull int[] consumed)
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type)
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY,
                boolean consumed)
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY)

以 scroll 类型的事情为例,其作业的原理:

CoordinatorLayout 中会对子控件进行遍历,然后将对应的事情传递给子控件的 Behavior (若有)的对应办法。关于滑动类型的事情,在滑动事情传递的时分先传递 onStartNestedScroll 事情,用来判别某个 View 是否阻拦滑动事情。而在 CoordinatorLayout 中,会交给 Beahvior 判别是否处理该事情。然后 CoordinatorLayout 会讲该 Behavior 是否阻拦该事情的状况记录到对应的 View 的 LayoutParam. 然后,当 CoordinatorLayout 的 onNestedPreScroll 被调用的时分,会读取 LayoutParame 上的状况以决定是否调用该 Behavior 的 onNestedPreScroll 办法。别的,只有当一个 CoordinatorLayout 包含的所有的 Behavior 都不处理该滑动事情的时分,才判定 CoordinatorLayout 不处理该滑动事情。

伪代码如下,

// CoordinatorLayout
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
    boolean handled = false;
    for 遍历子 view {
        Behavior viewBehavior = view.getLayoutParams().getBehavior()
        final boolean accepted = viewBehavior.onStartNestedScroll();
        handled |= accepted;
        // 根据 accepted 给 view 的 layoutparams 置位
        view.getLayoutParams().setNestedScrollAccepted(accepted) 
    }
    return handled;
}
// CoordinatorLayout
public void onStopNestedScroll(View target, int type) {
    for 遍历子 view {
        // 读取 view 的 layoutparams 的标记位
        if view.getLayoutParams().isNestedScrollAccepted(type) {
            Behavior viewBehavior = view.getLayoutParams().getBehavior()
            // 将事情交给 behavior
            viewBehavior.onStopNestedScroll(this, view, target, type)
        }
    }
}

在消费事情的时分是经过覆写 onNestedPreScroll() 等办法,以该办法为例,

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int  type) {}

这儿的 dx 和 dy 是翻滚在水平和方向上的总的值,咱们消费的值经过 consumed 指定。比方 dy 表明向上总共翻滚了 50dp,而咱们的 toolbar 需求先向上翻滚 44dp,那么咱们就将 44dp 的数值赋值给 consumed 数组(办法签名中的数组是按引证传递的)。这样父控件就能够将剩余的 6dp 交给列表,所以列表最终会向上翻滚 6dp.

3.5 接触事情分发机制小结

按照上述 Behavior 的完成办法,一个 Behavior 是能够阻拦到 CoordinatorLayout 内所有的 View 的 NestedScrolling 事情的。因此,咱们能够在一个 Behavior 内部对 CoordinatorLayout 内的所有的 NestedScrolling 事情进行统筹阻拦和调度。用一个图来表明整体分发逻辑,如下,

彻底搞懂 Behavior

这儿需求留意,按照咱们上面的剖析,CoordinatorLayout 收集到的事情 NestedScrolling 事情,假如一个控件并没有完成 NestedScrollingChild 接口,或许更严谨得说,没有将翻滚事情传递给 CoordinatorLayout,那么 Behavior 就无法接受到翻滚事情。可是关于一般的接触事情 Behavior 是能够阻拦到的。

4、总结

这篇文章主要用来剖析 Behavior 的整个作业原理。因为篇幅现已比较长,这儿就不再拿详细的事例进行剖析了。关于 Behavior,只需摸透了它是怎么作业的,详细的事例剖析起来也不会太难。