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 {}
第一种情况
现在我们就来根据第一种情况尝试自定义一个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);
}
});
第二种情况
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,它的遍历就会结束掉。