Behavior是Android Support Design库里面新增的布局概念,主要的作用是用来协调CoordinatorLayout里面直接Child Views之间交互行为的。
特别要注意的点是Behavior只能作用于CoordinatorLayout的直接Child View.
既然Behavior是用来协调CoordinatorLayout直接Child View的交互行为的。那Behavior是怎么工作的呢,这个也是我们本文的重点。我们准备从以下四条线路来做简单的分析。
Behavior的测量和布局。(Behavior里面onMeasureChild、onLayoutChild函数)
Behavior的普通触摸事件。(Behavior里面的onInterceptTouchEvent,onTouchEvent函数)
Behavior的嵌套NestedScrolling触摸事件。(Behavior里面的onStartNestedScroll、onNestedScrollAccepted、onStopNestedScroll、onNestedScroll、onNestedPreScroll、onNestedFling、onNestedPreFling函数)
Behavior的依赖关系。(Behavior里面的layoutDependsOn、onDependentViewChanged、onDependentViewRemoved函数)
CoordinatorLayout直接Child View的LayoutParam里面的Behavior是怎么实例化得到.有三种方式:第一种,注解设置,类似@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)的形似;第二种,java代码设置;第三种,app:layout_behavior来设置.关于Behavior的实例化这里我们就不展开来讲,有兴趣的可以参考CoordinatorLayout里Behavior简单分析里面Behavior对象是怎么被实例化的.
第一种注解方式的使用来设置默认Behavior的.
一、Behavior的测量和布局
Behavior可以引导CoordinatorLayout的直接Child View 进行测量和布局。CoordinatorLayout需要进行measure、layout的时候,都会通过Behavior询问该Behavior对应的View是否需要进行相应的测量和布局操作,如果不需要,就进行默认的行为。如果需要则按照Behavior里面编写的规则来测量和布局。这里我们只需要关注Behavior类的onMeasureChild()、onLayoutChild()两个函数。
我们以一个具体的例子来简单的解释下Behavior怎么引导CoordinatorLayout的直接Child View 进行测量和布局的.在上一篇文章Android Design Support Library 控件的使用中有一个CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 实现AppBarLayout里面Toolbar的收缩和展开效果图的例子.如下图所示
并且他的布局文件如下
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:textColor="@android:color/white"
android:gravity="center"
android:text="自定义标题"
android:textSize="18sp" />
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/AppTheme.TabStyle"
app:tabMode="scrollable"
app:tabGravity="fill" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/page_collapsing"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
最外层一个CoordinatorLayout布局,并且CoordinatorLayout里面有两个直接的子View:AppBarLayout和ViewPager.其中AppBarLayout有一个默认的AppBarLayout.Behavior,同时ViewPager我们通过app:layout_behavior="@string/appbar_scrolling_view_behavior"给设置了AppBarLayout.ScrollingViewBehavior.这样CoordinatorLayout两个直接子View都有对应的Behavior了.从界面结果出咱也能看到刚进入界面的时候ViewPager是在AppBarLayout的下面的.咱们就分析分析他是怎么做到的.肯定和测量和布局相关,那出发点肯定是CoordinatorLayout类的onMeasure()和onLayout().
CoordinatorLayout类onMeasure()函数
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
for (int i = 0; i < childCount; i++) {
final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
......
final CoordinatorLayout.Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
......
}
......
}
分析可以发现如果对应的子View有对应的Behavior的时候,会先去调用Behavior里面的onMeasureChild()看Behavior有没有制定自己的测量方式.这下咱就的进入ViewPager对应的Behavior AppBarLayout.ScrollingViewBehavior里面的onMeasureChild()方法里面去瞧一瞧了,这里我们就不进去了.里面也就是一些正常的测量方法.测量完成接下来就是layout了.CoordinatorLayout类的onLayout()方法.
CoordinatorLayout类onLayout()方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
......
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
......
final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
final CoordinatorLayout.Behavior behavior = lp.getBehavior();
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
同样分析可以得到有对应的Behavior就先进入到Behavior的onLayoutChild()方法了.ViewPager设置的AppBarLayout.ScrollingViewBehavior的onLayoutChild()方法里面获取得到AppBarLayout的区域,之后把ViewPager布局layout到AppBarLayout的下面.
这样咱们以一个简单的例子对Behavior的测量和布局做了一个非常简单的分析.里面很多地方也没有去深究.如果大家有什么疑问的话,可以留言.在能力范围之内的都会尽力为大家解答的.
二、Behavior的普通触摸事件
Behavior的普通触摸事件主要和Behavior里面的onInterceptTouchEvent()和onTouchEvent()两个函数相关.最终的目的也就是想把对应的触摸时间传递到Behavior对应的View里面去,让View做一些相应的处理.
父布局CoordinatorLayout产生的onInterceptTouchEvent,onTouchEvent事件都会先送到Behavior的onInterceptTouchEvent()和onTouchEvent()里面,让去问问Behavior对应的View要不要处理.你要处理就先给你处理.你不处理才轮到CoordinatorLayout来处理.关于这部分的内容之前有写过一个文章.我们就不展开讨论了.有兴趣的可以参考下CoordinatorLayout里Behavior简单分析里面Behavior的onInterceptTouchEvent + onTouchEvent一部分的分析.
三、Behavior的嵌套NestedScrolling触摸事件
关于Behavior嵌套滑动主要涉及Behavior里面的onStartNestedScroll(), onNestedScrollAccepted(), onStopNestedScroll(), onNestedScroll(), onNestedPreScroll(), onNestedFling(), onNestedPreFling() 函数.
这里我们多次提到了嵌套滑动,有兴趣的可以参考我之前写的Android 嵌套滑动分析一文的简单分析.
Behavior的嵌套NestedScrolling事件,大部分情况下是这样的.CoordinatorLayout里面另一个子View产生了嵌套滑动事件,这个事件先传递到CoordinatorLayout,然后CoordinatorLayout在把这个嵌套事件过渡到Behavior里面去.之后在让Beahaior对应的View按照实际情况做不同的处理.同样关于这部分内容的具体分析,有兴趣的可以参考下之前写的CoordinatorLayout里Behavior简单分析里面Behavior的onStartNestedScroll + onNestedScrollAccepted + onStopNestedScroll + onNestedScroll + onNestedPreScroll + onNestedFling + onNestedPreFling。嵌套滑动引起的变化部分的简单分析.
同样为了加深理解,这里还是以上文CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 实现AppBarLayout里面Toolbar的收缩和展开效果图的例子来做一个简单的说明.这也是ViewPager里面为什么一定要放置实现了NestedScrollingChild2接口的View.这里ViewPager里面放了RecyclerView(RecyclerView实现了NestedScrollingChild接口).当RecyclerView有对应的NestedScrollingChild滑动的时候,都会先传递到CoordinatorLayout里面对应函数里面去,然后CoordinatorLayout又会原封不动的传递到Behavior对应的onStartNestedScroll(), onNestedScrollAccepted(),onStopNestedScroll(),onNestedScroll(), onNestedPreScroll(),onNestedFling(),onNestedPreFling()的函数里面去.换句话说就是传递到了AppBarLayout对应的AppBarLayout.Behavior里面去.在里面让AppBarLayout对某个View的上移和下移的处理.
四、Behavior的依赖关系
关于Behavior依赖关系对应Behavior里面的layoutDependsOn(), onDependentViewChanged(),onDependentViewRemoved()这三个函数.
Behavior的依赖指的是当前Behavior对应的View依赖于哪个View.当依赖的View有变化的时候.会调用Behavior里面对应的函数.然我们对Behavior对应的View做相应的处理.同样关于这一部分的具体分析可以参考之前写的CoordinatorLayout里Behavior简单分析里面Behavior的layoutDependsOn + onDependentViewChanged + onDependentViewRemoved。View引起的变化部分.这里我们就不重新拿出来讲了,而且里面有一个简单的例子.
为了加深理解,咱们还是以上文提到的CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 实现AppBarLayout里面Toolbar的收缩和展开效果图的例子来做一个简单的说明哈,其实在这个里面ViewPager会依赖AppBarLayout的变化.为什么这么说呢.看ViewPager对应的AppBarLayout.ScrollingViewBehavior里面
@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;
}
看到了吧,如果是AppBarLayout就依赖他.并且在onDependentViewChanged函数中ViewPager也会跟着AppBarLayout的移动而移动.
五、Behavior的具体使用
5.1 BottomSheetBehavior的使用
BottomSheetBehavior:实现底部弹出框的一个Behavior,注意BottomSheetBehavior一定要配合CoordinatorLayout一起使用才有效果。
BottomSheetBehavior对应的View的状态:
状态 | 解释 |
---|---|
STATE_EXPANDED | bottom sheet 处于完全展开的状态:当bottom sheet的高度低于CoordinatorLayout容器时,整个bottom sheet都可见;或者CoordinatorLayout容器已经被bottom sheet填满 |
STATE_COLLAPSED | 折叠状态(默认), bottom sheets只在底部显示一部分布局。显示高度可以通过 app:behavior_peekHeight 设置 |
STATE_DRAGGING | 过渡状态,此时用户正在向上或者向下拖动bottom sheet |
STATE_SETTLING | 视图从脱离手指自由滑动到最终停下的这一小段时间 |
STATE_HIDDEN | 默认无此状态(需要通过app:behavior_hideable 启用此状态),启用后用户将能通过向下滑动完全隐藏 bottom sheet |
BottomSheetBehavior属性设置
属性 | 解释 |
---|---|
app:behavior_hideable | bottom sheet是否可以完全隐藏,默认为false |
app:behavior_peekHeight | bottom sheet为STATE_COLLAPSED(折叠)状态的时残留的高度 |
app:behavior_skipCollapsed | 是否跳过STATE_COLLAPSED状态 |
BottomSheetBehavior有两种实现方式,一个之直接嵌套在布局里面,一个是通过dialog的方式弹出.两种使用方式都不难.所以我们也就以一个具体的实例来说明.效果图如下:
5.2 自定义Behavior
关于自定义Behavior,我们也实现了两个简单的效果.
5.2.1 上滑下滑的时候FloatingActionButton底部弹入或者弹出
效果图
Behavior
public class FabBottomInOutBehavior extends FloatingActionButton.Behavior {
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
private boolean mAnimatingOut = false;
public FabBottomInOutBehavior() {
}
public FabBottomInOutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull FloatingActionButton child,
@NonNull View directTargetChild,
@NonNull View target,
int axes,
int type) {
//需要垂直的滑动
return axes == ViewCompat.SCROLL_AXIS_VERTICAL ||
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull FloatingActionButton child,
@NonNull View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
if (dyConsumed > 0 && !mAnimatingOut) {
//向上滑动
animateOut(child);
} else if (dyConsumed < 0) {
//向下滑动
animateIn(child);
}
}
private void animateOut(final FloatingActionButton button) {
ViewCompat.animate(button)
.translationY(button.getHeight() + getMarginBottom(button))
.setInterpolator(INTERPOLATOR)
.withLayer()
.setListener(new ViewPropertyAnimatorListener() {
public void onAnimationStart(View view) {
mAnimatingOut = true;
}
public void onAnimationCancel(View view) {
mAnimatingOut = false;
}
public void onAnimationEnd(View view) {
mAnimatingOut = false;
}
})
.start();
}
private void animateIn(FloatingActionButton button) {
ViewCompat.animate(button).translationY(0).setInterpolator(INTERPOLATOR).withLayer().setListener(null).start();
}
private int getMarginBottom(View v) {
final ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
return ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
}
return 0;
}
}
5.2.2 上滑的时候以覆盖的方式盖住头部
效果图
Behavior
public class HeaderCoverBehavior extends CoordinatorLayout.Behavior<View> {
public HeaderCoverBehavior() {
}
public HeaderCoverBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return super.layoutDependsOn(parent, child, dependency);
}
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
if (params != null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT) {
child.layout(0, 0, parent.getWidth(), parent.getHeight());
child.setTranslationY(getFirstChildHeight(parent));
return true;
}
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,
@NonNull View directTargetChild,
@NonNull View target,
int axes,
int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,
@NonNull View target,
int dx,
int dy,
@NonNull int[] consumed,
int type) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
// 在这个方法里面只处理向上滑动
if (dy < 0) {
return;
}
float transY = child.getTranslationY() - dy;
if (transY > 0) {
child.setTranslationY(transY);
consumed[1] = dy;
}
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,
@NonNull View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
// 在这个方法里只处理向下滑动
if (dyUnconsumed > 0) {
return;
}
float transY = child.getTranslationY() - dyUnconsumed;
if (transY > 0 && transY < getFirstChildHeight(coordinatorLayout)) {
child.setTranslationY(transY);
}
}
/**
* 这里有优化的空间,这里纯粹的去取了第一个view的measure height 有点限制的太死了
*/
private int getFirstChildHeight(CoordinatorLayout coordinatorLayout) {
return coordinatorLayout.getChildAt(0).getMeasuredHeight();
}
}
关于Behavior所要想分享的东西就这些了,如果后面自定义Behavior实现的特别有意思的效果也会第一时间分享给大家.最后上文涉及的所有实例的下载地址 https://github.com/tuacy/DesignWidget