在开发过程中,有时需要实现一些比较复杂的联动效果:比如在滚动列表的时候改变某个View的状态,随着滚动程度的变化,View也跟随变化等等。要想实现这些效果,用普通的方法也可以实现,不过需要设计很多的监听来控制,逻辑也比较复杂,而通过CoordinatorLayout可以更优雅的实现同样的效果。
1.1 CoordinatorLayout介绍
CoordinatorLayout 是 Google 在 Design Support 包中提供的一个十分强大的布局视图,我们先来看下官网介绍
CoordinatorLayout
public class CoordinatorLayout
extends ViewGroup implements NestedScrollingParent2, NestedScrollingParent3
java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ androidx.coordinatorlayout.widget.CoordinatorLayout
CoordinatorLayout is a super-powered FrameLayout.
CoordinatorLayout is intended for two primary use cases:
- As a top-level application decor or chrome layout
- As a container for a specific interaction with one or more child views
By specifying Behaviors for child views of a CoordinatorLayout you can provide many different interactions within a single parent and those views can also interact with one another. View classes can specify a default behavior when used as a child of a CoordinatorLayout using the CoordinatorLayout.DefaultBehavior annotation.
官网说它本质是一个 FrameLayout
,它可以作为一个容器指定与child 的一些交互规则。通过给View
设置Behaviors
,就可以和 child 进行交互,或者是 child 之间互相进行相关的交互,并且自定义 View 时,可以通过DefaultBehavior
这个注解来指定它关联的 Behavior。
如此看来,我们只需要定制Behavior就可以定制我们的交互了,再来看下Behavior的内容。
1.2 CoordinatorLayout.Behavior介绍
Behavior是CoordinatorLayout中的一个静态内部类。
CoordinatorLayout.Behavior
public static abstract class CoordinatorLayout.Behavior extends Object
java.lang.Object
↳androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior<V extends android.view.View>
Interaction behavior plugin for child views of CoordinatorLayout.
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.
Behavior是针对CoordinatorLayout中child的交互插件。Behavior同时也是一个抽象类,它的实现类都是为了能够让用户作用在一个View上进行拖拽、滑动、快速滑动等手势。
下面我们就来看下Behavior中的关键代码
//类型一
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency){
return false;
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
//类型二
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return false;
}
@Override
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View directTargetChild, @NonNull View target, int axes, int type) {
super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@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);
}
@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);
}
@Override
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, int type) {
super.onStopNestedScroll(coordinatorLayout, child, target, type);
}
@Override
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, float velocityX, float velocityY, boolean consumed) {
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
@Override
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, float velocityX, float velocityY) {
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
从方法的功能侧重来看,可以分为两类,一是根据某些依赖的View的变化来实现效果;二是根据某些组件的滑动事件来实现效果;其中第一类对应前三个API,第二类对应后面的API。我们先看第一类情况。
2.3 Behavior设置View之间依赖
View之间的依赖使用的是第一类API,其具体作用介绍如下:
-
确定一个
View(child)
是否依赖于另一个View(dependency)
,需要在layoutDependsOn()
方法中进行判断并返回一个布尔值,returntrue
表示依赖成立,反之不成立。并且只有在layoutDependsOn()
返回为true时,后面的onDependentViewChanged()
和onDependentViewRemoved()
方法才会被调用。 -
当确定依赖的
View(dependency)
发生变化时,onDependentViewChanged()
方法会被调用,我们可以在这个方法中拿到变化后的dependency,并对自己的View进行处理。 -
当
View(dependency)
被移除时,onDependentViewRemoved()
方法会被调用。
#####为避免内容不易理解,我们来举例说明。
首先我们自定义了一个可以跟随手指滑动变化位置的DragView。代码很简单,如下所示:
public class DragView extends AppCompatTextView {
private final int mSlop;
private float mLastX;
private float mLastY;
public DragView(Context context) {
this(context,null);
}
public DragView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setClickable(true);
mSlop = ViewConfiguration.getTouchSlop();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getRawX();
mLastY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int deltax = (int) (event.getRawX() - mLastX);
int deltay = (int) (event.getRawY() - mLastY);
if (Math.abs(deltax) > mSlop || Math.abs(deltay) > mSlop) {
ViewCompat.offsetTopAndBottom(this,deltay);
ViewCompat.offsetLeftAndRight(this,deltax);
mLastX = event.getRawX();
mLastY = event.getRawY();
}
break;
case MotionEvent.ACTION_UP:
mLastX = event.getRawX();
mLastY = event.getRawY();
break;
default:
break;
}
return true;
}
}
同时,在布局文件中引入,作为CoordinatorLayout中的一个child,默认初始位置是CoordinatorLayout的中心位置,布局如下所示:
<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"
tools:context="com.hikvision.update.demo.behaivior.BehaviorTestActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<com.update.demo.behaivior.DragView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/isms_size_10dp"
android:layout_gravity="center"
android:text="DragView"
android:background="@color/colorPrimary"
android:textColor="#fff"
android:textSize="16sp"/>
</android.support.design.widget.CoordinatorLayout>
接下来,我们来自定义一个DependencyBehavior,让使用这个Behavior的View位于DragView的上方:
public class DependencyBehavior extends CoordinatorLayout.Behavior<View> {
public DependencyBehavior() {
super();
}
public DependencyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
//判断依赖是否为DragView
return dependency instanceof DragView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//获取DragView的顶部,让child位于DragView的左上方
int top = dependency.getTop();
int childHeight = child.getHeight();
child.setY(top - childHeight);
child.setX(dependency.getLeft());
return true;
}
}
在CoordinatorLayout布局中添加一个ImageView,并使用这个Behavior:
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:src="@mipmap/ic_launcher_round"
app:layout_behavior="com.demo.behaivior.DependencyBehavior" />
实现效果如下:
到此,View之间的依赖如何使用已经演示明白。我们接着来看对于滑动事件的响应。
2.4 Behavior对滑动事件的响应
首先,我们来看下onStartNestedScroll()
方法:
/**
* Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll.
*
* <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
* to this event and return true to indicate that the CoordinatorLayout should act as
* a nested scrolling parent for this scroll. Only Behaviors that return true from
* this method will receive subsequent nested scroll events.</p>
*
* @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
* associated with
* @param child the child view of the CoordinatorLayout this Behavior is associated with
* @param directTargetChild the child view of the CoordinatorLayout that either is or
* contains the target of the nested scroll operation
* @param target the descendant view of the CoordinatorLayout initiating the nested scroll
* @param axes the axes that this nested scroll applies to. See
* {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
* {@link ViewCompat#SCROLL_AXIS_VERTICAL}
* @param type the type of input which cause this scroll event
* @return true if the Behavior wishes to accept this nested scroll
*
* @see NestedScrollingParent2#onStartNestedScroll(View, View, int, int)
*/
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
target, axes);
}
return false;
}
注释中说,当一个CoordinatorLayout中的子View
企图触发一个Nested scroll
事件时,这个方法会被调用。并且只有在onStartNestedScroll()
方法返回为true
时,后续的Nested Scroll
事件才会响应。
后续的回调是这几个:
@Override
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View directTargetChild, @NonNull View target, int axes, int type) {
super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@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);
}
@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);
}
@Override
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, int type) {
super.onStopNestedScroll(coordinatorLayout, child, target, type);
}
@Override
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, float velocityX, float velocityY, boolean consumed) {
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
@Override
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, float velocityX, float velocityY) {
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
那么Nested Scroll
又是什么呢?哪些控件可以触发Nested Scroll
呢?
通过追踪调用onStartNestedScroll()
方法的源码,最终可以得到结论:如果在5.0的系统版本以上,我们需要对View.setNestedScrollingEnable(true)
,如果在这个版本之下,得保证这个View本身是NestedScrollingChild
的实现类,只有这样,才可以触发Nested Scroll
。
借助于AndroidStudio,我们可以知道NestedScrollingChild的实现类有:RecyclerView
、NavigationMenuView
、SwipeRefreshLayout
、NestedScrollView
接下来,我们用NestedScrollView
举例,来实现一个对Nested Scroll
响应的简单Behavior
,布局如下所示:
<?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"
tools:context="com.demo.behaivior.BehaviorTestActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/a_lot_of_text"
android:textSize="@dimen/isms_text_size_16sp"/>
</android.support.v4.widget.NestedScrollView>
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:src="@mipmap/ic_launcher_round"
app:layout_behavior="com.demo.behaivior.DependencyBehavior" />
</android.support.design.widget.CoordinatorLayout>
我们新增了一个NestedScrollView
,同时我们希望在NestedScrollView
滑动的时候,ImageView
可以跟随着一起滑动。现在我们来改造下之前的DependencyBehavior
。
首先去除View的依赖关系:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
//判断依赖是否为DragView
// return dependency instanceof DragView;
return false;
}
然后在onStartNestedScroll()方法中作如下修改,以保证对竖直方向滑动的接收:
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View directTargetChild, @NonNull View target, int axes, int type) {
//child为ImageView 并且滑动方向为竖直方向才响应
return child instanceof ImageView && ViewCompat.SCROLL_AXIS_VERTICAL == axes;
}
我们继续重写OnNestedPreScroll()
方法,这个方法会在NestedScrollView
准备滑动的时候被调用,用以通知Behavior,NestedScrollView
准备滑动多少距离,dx
和dy
分别是横向和竖向的滑动位移,int[ ] consumed
用以记录Behavior
消耗的dx
和dy
;
@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);
Log.d("DependencyBehavior", "onNestedPreScroll dx:" + dx + " dy:" + dy);
ViewCompat.offsetTopAndBottom(child, dy);
}
在接收到dy
滑动距离后,直接移动childView
。这样就可以实现我们预计的效果了。
//TODO:动图一张待传
如果我们想让child
消费掉所有的dy
偏移量,只需要再加上一行代码 :
@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);
Log.d("DependencyBehavior", "onNestedPreScroll dx:" + dx + " dy:" + dy);
//加上这句,child消费掉所有dy
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, dy);
}
此时的效果就是:不论NestedScrollView
如何滑动,仅能看到ImageView
跟随手势动作。
上面举例说明了下Behavior
响应NestedScroll
的简单方式,如果你还是一头雾水,搞不清楚用法,不用担心,下面我们就来具体说明下这几个方法的调用流程和具体功能:
首先我们来看一张流程图
图中Child
对应我们上面例子中的NestedScrollView
,Parent
是CoordinatorLayout
,而CoordinatorLayout
会将接收到的NestedScroll
向各个child
中的Behavior
进行分发,我们可以简单理解为此处的Parent
就是Behavior
。
(PS:流程图来自这篇文章,有兴趣的也可以看看)
Child
中的DOWN
、MOVE
、UP
均为child
在OnTouchEvent()
中接收到的手势事件;
我们可以看到:
-
在
child
在接收到DOWN
手势时,发起嵌套滚动请求,请求中携带有嵌套滑动的方向(方向为child在初始化时已经被声明过的); -
Parent
接收到嵌套滚动请求,如果滚动方向是自己需要的则同意嵌套滚动,这时一般主动放弃拦截MOVE
事件,Parent
在这个过程中调用了自身的onStartNestedScroll()
和onNestedScrollAccepted()
; -
Child
在接收到MOVE
手势时,在自身准备滚动前,去询问Parent
是否需要滚动(dispatchNestedPreScroll
),参数中声明了本次滚动的横向和竖向距离dx
,dy
,并要求告知Parent
消费掉的距离和窗口偏移大小 -
Parent
在onNestedPreScroll()
方法中接收到滚动准备请求,如果需要可以执行滑动操作,并根据需求,将消耗的距离保存到int[ ] consumed
中,consumed[0]
保存dx
消耗,consumed[1]
保存dy
消耗; -
Child
在接收到Parent
的反馈后,执行自身的滚动,这个滚动是将计划滚动距离减去consumed
数组中消耗的剩余距离,在滚动之后分发剩余的未消费的滚动距离 (dispatchNestedScroll
),参数中声明自己已消费的x
、y
距离和未消费的x
、y
距离,并要求告知窗口偏移 -
Parent
在onNestedScroll()
方法中接收到滚动请求,此时可以根据需求,通过滑动消费掉child
提供的未消费距离; -
Child
在接收到UP
手势时,如果判断当前滚动仍需要继续,那么会在自身滚动前询问Parent
是否需要继续滚动,参数中会声明x
、y
的速度; -
Parent
在onNestedPreFling()
中接收到预遗留滚动请求,根据自身需要选择执行逻辑; -
Child
在自身执行完遗留滚动后,询问Parent
是否需要执行,参数中声明x
、y
的速度已经是否已消费;
10.Parent
在onNestedFling()
接收到child
询问后,可以选择执行未消费的遗留滚动;
-
Child
滚动执行结束,通知Parent
; -
Parent
在onStopNestedScroll()
接收到结束滚动的通知,停止滚动操作,此时可根据Parent
的当前状态,作一些逻辑处理
以上,就是Nested Scroll
的完整的处理流程。
了解了上面对Behavior
的介绍,我们可以明白一个Behavior
的运作机制。下面我们将对Android官方提供的BottomSheetBehavior
进行分析,以加深理解。
2.5 BottomSheetBehavior源码分析
BottomSheetBehavior直接继承自CoordinatorLayout.Behavior
/**
* An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as
* a bottom sheet.
*/
public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V>{
...
}
先看下构造方法
/**
* Default constructor for inflating BottomSheetBehaviors from layout.
*
* @param context The {@link Context}.
* @param attrs The {@link AttributeSet}.
*/
public BottomSheetBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.BottomSheetBehavior_Layout);
TypedValue value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight);
if (value != null && value.data == PEEK_HEIGHT_AUTO) {
setPeekHeight(value.data);
} else {
setPeekHeight(a.getDimensionPixelSize(
R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO));
}
setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false));
setSkipCollapsed(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed,
false));
a.recycle();
ViewConfiguration configuration = ViewConfiguration.get(context);
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
在构造方法中获取了设置的弹出高度,是否支持手势下拉隐藏功能以及弹出时是否支持动画的属性。
继续看onLayoutChild****的源码(我们称使用了BottomSheetBehavior的View为BottomView)
@Override
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) {
ViewCompat.setFitsSystemWindows(child, true);//1
}
int savedTop = child.getTop();
// First let the parent lay it out
parent.onLayoutChild(child, layoutDirection);//2
// Offset the bottom sheet
mParentHeight = parent.getHeight();
int peekHeight;
if (mPeekHeightAuto) {
if (mPeekHeightMin == 0) {
mPeekHeightMin = parent.getResources().getDimensionPixelSize(
R.dimen.design_bottom_sheet_peek_height_min);
}
peekHeight = Math.max(mPeekHeightMin, mParentHeight - parent.getWidth() * 9 / 16);//2
} else {
peekHeight = mPeekHeight;
}
mMinOffset = Math.max(0, mParentHeight - child.getHeight());//3
mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);//3
if (mState == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, mMinOffset);
} else if (mHideable && mState == STATE_HIDDEN) {
ViewCompat.offsetTopAndBottom(child, mParentHeight);
} else if (mState == STATE_COLLAPSED) {
ViewCompat.offsetTopAndBottom(child, mMaxOffset);
} else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
}
if (mViewDragHelper == null) {
mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);//4
}
mViewRef = new WeakReference<>(child);
mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));//5
return true;
}
这个方法中,主要做了几件事:
-
首先设置BottomView适配屏幕;
-
对BottomView进行摆放:先调用父类对BottomView进行布局,根据PeekHeight和State对BottomView位置进行偏移,如果PeekHeight没有设置,一般默认为屏幕高度的9/16的位置;
-
对mMinOffset,mMaxOffset进行计算,用来确定BottomView的偏移范围。即距离CoordinatorLayout原点Y轴 mMinOffset到mMaxOffset之间;
-
初始化ViewDragHelper类,用以处理拖拽和滑动事件;
-
存储BottomView的软引用并递归寻找到BottomView中的第一个NestedScrollingChild组件;
说明一下:由于Android中屏幕的坐标轴是向下为y轴正方向,因此在计算PeekHeight时,会让ParentHeight-mPeekHeight,此时显示的高度才是设置的高度。
对于事件拦截的处理
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
if (!child.isShown()) {
mIgnoreEvents = true;
return false;
}
int action = event.getActionMasked();
// Record the velocity
if (action == MotionEvent.ACTION_DOWN) {
reset();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event); // 2
switch (action) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mTouchingScrollingChild = false;
mActivePointerId = MotionEvent.INVALID_POINTER_ID;
// Reset the ignore flag
if (mIgnoreEvents) { //4
mIgnoreEvents = false;
return false;
}
break;
case MotionEvent.ACTION_DOWN:
int initialX = (int) event.getX();
mInitialY = (int) event.getY();
View scroll = mNestedScrollingChildRef != null
? mNestedScrollingChildRef.get() : null;
if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
mActivePointerId = event.getPointerId(event.getActionIndex());
mTouchingScrollingChild = true;
}
mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
!parent.isPointInChildBounds(child, initialX, mInitialY);
break;
}
// 1
if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) {
return true;
}
// We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
// it is not the top most view of its parent. This is not necessary when the touch event is
// happening over the scrolling content as nested scrolling logic handles that case.
View scroll = mNestedScrollingChildRef.get();
//3
return action == MotionEvent.ACTION_MOVE && scroll != null &&
!mIgnoreEvents && mState != STATE_DRAGGING &&
!parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) &&
Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop();
}
onInterceptTouchEvent()中做了这几件事:
-
判断是否拦截事件,先使用ViewDragHelper进行拦截;
-
使用mVelocityTracker用以记录手指的动作,用于计算Y轴的滚动速率;
-
判断点击是否在NestedScrollView上,将结果保存在mTouchingScrollingChild标记位上,用于在ViewDragHelper的回调处理中判断;
-
在ACTION_UP和ACTION_CANCEL对标记为进行复位,为下一次Touch准备;
对事件的处理
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
if (!child.isShown()) {
return false;
}
int action = event.getActionMasked();
if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
return true;
}
if (mViewDragHelper != null) {
mViewDragHelper.processTouchEvent(event);//2
}
// Record the velocity
if (action == MotionEvent.ACTION_DOWN) {
reset();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);//1
// The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
// to capture the bottom sheet in case it is not captured and the touch slop is passed.
if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents) {
if (Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop()) {
mViewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex()));//3
}
}
return !mIgnoreEvents;
}
OnTouchEvnet中做了如下处理:
-
使用mVelocityTracker用以记录手指的动作,用于计算Y轴的滚动速率;
-
使用ViewDragHelper处理Touch事件,产生拖动效果;
-
ViewDragHelper在滑动的时候对BottomView的再次捕获。再次明确告诉ViewDragHelper我需要移动的是BottomView。在如下场景中需要做这个处理:当你点击在BottomView的区域,但是BottomView的视图层级不是最高的,或者你点击的区域不在BottomView上,ViewDragHelper在处理滑动的时候找不到BottomView,这个时候你需要主动告知ViewDragHelper现在要移动的是BottomView。、
对NestedScroll****的处理
onStartNestedScroll中声明接收Y轴方向的滑动
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
View directTargetChild, View target, int nestedScrollAxes) {
mLastNestedScrollDy = 0;
mNestedScrolled = false;
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
在onNestedPreScroll中判断发起NestedScroll的 View 是否是我们在onLayoutChild 找到的那个控件.不是的话,不做处理。不处理就是不消耗y 轴,把所有的Scroll 交给发起的 View 自己消耗。如果处理,则根据dy判断滑动方向,根据之前计算出的偏移量,使用ViewCompat.offsetTopAndBottom()方法对BottomView进行偏移操作,并将消耗的dy值记录。
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx,
int dy, int[] consumed) {
View scrollingChild = mNestedScrollingChildRef.get();
if (target != scrollingChild) {
return;
}
int currentTop = child.getTop();
int newTop = currentTop - dy;
if (dy > 0) { // Upward
if (newTop < mMinOffset) {
consumed[1] = currentTop - mMinOffset;
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
setStateInternal(STATE_EXPANDED);
} else {
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
setStateInternal(STATE_DRAGGING);
}
} else if (dy < 0) { // Downward
if (!target.canScrollVertically(-1)) {
if (newTop <= mMaxOffset || mHideable) {
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
setStateInternal(STATE_DRAGGING);
} else {
consumed[1] = currentTop - mMaxOffset;
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
setStateInternal(STATE_COLLAPSED);
}
}
}
dispatchOnSlide(child.getTop());
mLastNestedScrollDy = dy;
mNestedScrolled = true;
}
在onStopNestedScroll中,根据当前BottomView所处的状态确定它的最终位置,有必要的话,还会调用ViewDragHelper.smoothSlideViewTo进行滑动。
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
if (child.getTop() == mMinOffset) {
setStateInternal(STATE_EXPANDED);
return;
}
if (mNestedScrollingChildRef == null || target != mNestedScrollingChildRef.get()
|| !mNestedScrolled) {
return;
}
int top;
int targetState;
if (mLastNestedScrollDy > 0) {
top = mMinOffset;
targetState = STATE_EXPANDED;
} else if (mHideable && shouldHide(child, getYVelocity())) {
top = mParentHeight;
targetState = STATE_HIDDEN;
} else if (mLastNestedScrollDy == 0) {
int currentTop = child.getTop();
if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
top = mMinOffset;
targetState = STATE_EXPANDED;
} else {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
} else {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
setStateInternal(STATE_SETTLING);
ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
} else {
setStateInternal(targetState);
}
mNestedScrolled = false;
}
当向下滑动且Hideable为true时,会根据记录的Y轴上的速率进行判断,是否应该切换到Hideable状态
在onNestedPreFling中处理快速滑动触发,判断逻辑是当前触发滑动的控件为onLayoutChild中找到的那个并且当前BottomView的状态不是完全展开的,此时会消耗快速滑动事件,其他情况下不处理,交给child自己处理。
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY) {
return target == mNestedScrollingChildRef.get() &&
(mState != STATE_EXPANDED ||
super.onNestedPreFling(coordinatorLayout, child, target,
velocityX, velocityY));
}
最后我们总结一下:在BottomSheetBehavior中,对事件的拦截和处理通过ViewDragHelper来辅助处理拖拽滑动操作,对于NestedScroll,则是通过对滑动方向的判断结合ViewCompat对BottomView进行处理。
3. 总结
-
CoordinatorLayout是一个
super FrameLayout
,它可以通过Behavior
与child
进行交互; -
我们可以通过自定义Behavior来设计child的交互规则,可以很灵活的实现比较复杂的联动效果;
-
自定义Behavior主要有两个大类:确定一个View和另一个View的依赖关系;指定某一个View响应Nested Scroll;
-
Behavior是一种插件机制,如果没有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 无异。Behavior 的存在,可以决定 CoordinatorLayout 中对应的 childview 的测量尺寸、布局位置、触摸响应。
-
Behavior具有解耦功能,使用Behavior可以抽象出某个模块的View的行为,而不再是依赖于特定的View。