Behavior是Android新出的Design库里新增的布局概念。Behavior只有是CoordinatorLayout的直接子View才有意义。可以为任何View添加一个Behavior。
Behavior是一系列回调。让你有机会以非侵入的为View添加动态的依赖布局,和处理父布局(CoordinatorLayout)滑动手势的机会。如果我们想实现控件之间任意的交互效果,完全可以通过自定义 Behavior 的方式达到。

Behavior是CoordinatorLayout的一个抽象内部类

public abstract static class Behavior<V extends View> {
      public Behavior() {
      }

      public Behavior(Context context, AttributeSet attrs) {
      }
      ...
}

1,官方BottomSheetBehavior使用

app:behavior_hideable="true"
        app:behavior_peekHeight="0dp"
        app:layout_behavior="@string/bottom_sheet_behavior">

public void onCl(View view) {
        BottomSheetBehavior behavior = BottomSheetBehavior.from(findViewById(R.id.scroll));
        if(behavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
            behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
        }else {
            behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        }
    }
   // BottomSheetDialog 
   /* BottomSheetDialog sheetDialog = new BottomSheetDialog();
      sheetDialog.setContentView(view);
      sheetDialog.show();*/
   //setBottomSheetCallback
 /*   behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {

        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {

        }
    });*/

2,自定义Behavior
我们可以按照两种目的来实现自己的Behavior,当然也可以两种都实现啦

某个view监听另一个view的状态变化,例如大小、位置、显示状态等

某个view监听CoordinatorLayout内的NestedScrollingChild的接口实现类的滑动状态

第一种情况需要重写layoutDependsOn和onDependentViewChanged方法

第二种情况需要重写onStartNestedScroll和onNestedPreScroll系列方法

如果你的自定义View默认使用一个Behavior。
在你的自定义View类上添加@DefaultBehavior(你的Behavior.class)这句注解。
你的View就默认使用这个Behavior。就像AppBarLayout一样。

@DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {}

第一种情况

android reveriver 自定义action android 自定义behavior_xml


现在我们就来根据第一种情况尝试自定义一个Behavior,这里我们实现一个简单的效果,让一个View根据另一个View上下移动。

public class DependencyBehavior extends CoordinatorLayout.Behavior<View> {

    public DependencyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof Button;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        int offset = dependency.getTop();
        ViewCompat.offsetTopAndBottom(child, -offset);
        return true;
    }
}

我们覆写了两个方法layoutDependsOn和onDependentViewChanged,这两个方法的参数都是一样的,解释一下,第一个不用说,就是当前的CoordinatorLayout,第二个参数是我们设置这个Behavior的View,第三个是我们关心的那个View。如何知道关心的哪个呢?layoutDependsOn的返回值决定了一切!

return dependency instanceof Button; 关心所有Button的变化。
return dependency.getId()==R.id.Btn; 关心id为R.id.Btn的变化。

现在设置好了关心谁,接下来就是在这个View状态发生变化的时候,我们现在的View该做些什么了,这里肯定是在onDependentViewChanged做工作了。我们的任务就是获取dependency距离顶部的距离,并且设置给child。

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    int offset = dependency.getTop();
    ViewCompat.offsetTopAndBottom(child, -offset);
    return true;
}

在XML中使用 app:layout_behavior=”com.lava.toolbar.behave.DependencyBehavior”

<?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"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:fitsSystemWindows="true"
    tools:context="com.lava.toolbar.behave.MainActivity">

    <Button
        android:id="@+id/depentent"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:textColor="@android:color/white"
        android:layout_gravity="top|left"
        android:text="depentent"/>

    <Button
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:textColor="@android:color/white"
        android:layout_gravity="bottom|right"
        app:layout_behavior="com.lava.toolbar.behave.DependencyBehavior"
        android:text="auto"/>



</android.support.design.widget.CoordinatorLayout>
final Button depentent = (Button) findViewById(R.id.depentent);
        depentent.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ViewCompat.offsetTopAndBottom(v, 5);
            }
        });

第二种情况

android reveriver 自定义action android 自定义behavior_android_02

public class ScrollBehavior extends CoordinatorLayout.Behavior<View> {

    public ScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

需要重写上述方法

首先来看看onStartNestedScroll,这里的返回值表明这次滑动我们要不要关心,我们要关心什么样的滑动?当然是y轴方向上的。

@Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

demo代码

public class ScrollBehavior extends CoordinatorLayout.Behavior<View> {

    public ScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        int leftScrolled = target.getScrollY();
        child.setScrollY(leftScrolled);
    }

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        ((NestedScrollView) child).fling((int)velocityY);
        return true;
    }
}
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:fitsSystemWindows="true"
    tools:context="com.lava.toolbar.behave.MainActivity">

    <android.support.v4.widget.NestedScrollView
        android:layout_gravity="left"
        android:layout_width="wrap_content"
        android:background="@color/colorAccent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            ...
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingTop="50dp"
                android:paddingBottom="50dp"
                android:textColor="@android:color/white"
                android:text="Left"/>
                ...
        </LinearLayout>

    </android.support.v4.widget.NestedScrollView>

    <android.support.v4.widget.NestedScrollView
        android:layout_gravity="right"
        android:layout_width="wrap_content"
        android:background="@color/colorPrimary"
        android:layout_height="match_parent"
        app:layout_behavior="com.lava.toolbar.behave.ScrollBehavior">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">
             ....
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingTop="50dp"
                android:paddingBottom="50dp"
                android:textColor="@android:color/white"
                android:text="Right"/>
                ....
        </LinearLayout>

    </android.support.v4.widget.NestedScrollView>

CoordinatorLayout的源码之Behavior

简单来看看Behavior是如何从xml中解析的,通过检测xxx:behavior属性,通过全限定名或者相对路径的形式指定路径,最后通过反射来新建实例,默认的构造器是Behavior(Context context, AttributeSet attrs),如果你需要配置额外的参数,可以在外部构造好Behavior并通过setter的方式注入到LayoutParams或者获取到解析好的Behavior进行额外的参数设定
实例化Behavior

LayoutParams(Context context, AttributeSet attrs) {
    super(context, attrs);
    final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout_LayoutParams);
    //....
    mBehaviorResolved = a.hasValue(R.styleable.CoordinatorLayout_LayoutParams_layout_behavior); //通过apps:behavior属性·
    if (mBehaviorResolved) {
        mBehavior = parseBehavior(context, attrs, a.getString(R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
    }
    a.recycle();
}

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 {

        // 我们在自定义Behavior的时候,必须要有这种类型的构造方法

        if (c == null) {
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true, context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            //...
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

layoutDependsOn和onDependentViewChanged调用过程
在CoordinatorLayout中注册了一个ViewTreeObserver,我们可以从这里入手,因为它可以监听到view的各种状态变化,

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    resetTouchBehaviors();
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            // 实例化了OnPreDrawListener
            // 并在下面注册到了ViewTreeObserver中
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }
    if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
        // We're set to fitSystemWindows but we haven't had any insets yet...
        // We should request a new dispatch of window insets
        ViewCompat.requestApplyInsets(this);
    }
    mIsAttachedToWindow = true;
}
在onAttachedToWindow向ViewTreeObserver注册了一个监听draw变化的Observer,那在这里Observer中到底干了嘛呢?

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Override
    public boolean onPreDraw() {
        dispatchOnDependentViewChanged(false);
        return true;
    }
}

就两行代码,调用了dispatchOnDependentViewChanged方法

void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    // 遍历所有的子view
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        ...

        // Did it change? if not continue
        // 检查是否变化了,没有变化直接下一次循环
        final Rect oldRect = mTempRect1;
        final Rect newRect = mTempRect2;
        getLastChildRect(child, oldRect);
        getChildRect(child, true, newRect);
        if (oldRect.equals(newRect)) {
          continue;
        }

        // Update any behavior-dependent views for the change
        // 这里从下一个子view开始
        //mDependencySortedChildren有一个排序规则
        // selectionSort
        // 感兴趣的可以看一下mDependencySortedChildren部分。
        for (int j = i + 1; j < childCount; j++) {
            final View checkChild = mDependencySortedChildren.get(j);
            final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
            // 获取到Behavior
            final Behavior b = checkLp.getBehavior();
            // 这里调用Behavior的layoutDependsOn来判断我们的带有behavior的view是不是依赖这个view
            if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
                    // If this is not from a nested scroll and we have already been changed
                    // from a nested scroll, skip the dispatch and reset the flag
                    checkLp.resetChangedAfterNestedScroll();
                    continue;
                }

                // 这里调用了Behavior的onDependentViewChanged
                final boolean handled = b.onDependentViewChanged(this, checkChild, child);
                ...
            }
        }
    }
}

dispatchOnDependentViewChanged方法有一个布尔类型的参数,上面我们传递的是false, 这里主要是区分是view引起的状态变化还是布局引起的,在一些的scroll中也会调用dispatchOnDependentViewChanged这个方法。

onStartNestedScroll和onNestedPreScroll
大体的流程就是:

NestedScrollView.onInterceptTouchEvent->NestedScrollingChildHelper.onStartNestedScroll->CoordinatorLayout.onStartNestedScroll

CoordinatorLayout中:

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    boolean handled = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            // 调用遍历出来的这个子view的onStartNestedScroll方法
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                    nestedScrollAxes);
            handled |= accepted;

            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }
    return handled;

这里还是去遍历了所有子view,然后去调用它的onStartNestedScroll方法,它的返回值,决定了NestedScrollingChildHelper.onStartNestedScroll是不是要继续遍历,如果我们的子view对这个view的滑动感兴趣,就返回true,它的遍历就会结束掉。