在开发过程中,有时需要实现一些比较复杂的联动效果:比如在滚动列表的时候改变某个View的状态,随着滚动程度的变化,View也跟随变化等等。要想实现这些效果,用普通的方法也可以实现,不过需要设计很多的监听来控制,逻辑也比较复杂,而通过CoordinatorLayout可以更优雅的实现同样的效果。
1.1 CoordinatorLayout介绍
CoordinatorLayout 是 Google 在 Design Support 包中提供的一个十分强大的布局视图,我们先来看下官网介绍
CoordinatorLayout
- As a top-level application decor or chrome layout
- As a container for a specific interaction with one or more child views
By specifying 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 annotation.
官网说它本质是一个 FrameLayout
,它可以作为一个容器指定与child 的一些交互规则。通过给View
设置Behaviors
,就可以和 child 进行交互,或者是 child 之间互相进行相关的交互,并且自定义 View 时,可以通过DefaultBehavior
这个注解来指定它关联的 Behavior。
如此看来,我们只需要定制Behavior就可以定制我们的交互了,再来看下Behavior的内容。
1.2 CoordinatorLayout.Behavior介绍
Behavior是CoordinatorLayout中的一个静态内部类。
CoordinatorLayout.Behavior
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
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
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
中的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<View>
/**
* 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。