功能
CoordinatorLayout 是一个“增强版”的 FrameLayout,它的主要作用便是作为一系列相互之间有交互行为的子View的容器。CoordinatorLayout像是一个事情转发中心,它感知全部子View的改动,并把这些改动通知给其他子View。
Behavior 就像是CoordinatorLayout与子View之间的通信协议,经过给CoordinatorLayout的子View指定Behavior,就能够完成它们之间的交互行为。Behavior能够用来完成一系列的交互行为和布局改动,比方说侧滑菜单、可滑动删除的UI元素,以及跟从着其他UI控件移动的按钮等。文字表达不行直观,直接看下面的效果图:
依靠
dependencies {
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
}
简略运用
网上讲CoordinatorLayout 时分常将AppBarLayout,CollapsingToolbarLayout放到一起去做Demo,尽管看上去做出来比较酷炫的效果,但是关于初学者而言不太好get到CoordinatorLayout以及Behavior在其中究竟起到什么作用。这儿用如下一个简略的Demo演示下,一个紫色按钮跟从黑块(MoveView)反向移动。
MoveView的代码十分简略,便是随着Touch事情的改动,改动自身的translation ,不是重点。
定义Behavior
由于咱们这儿只关心MoveView的方位改动,只用完成如下两个办法:
- layoutDependsOn 回来true表明child依靠dependency , dependency的measure和layout都会在child之前进行,而且当dependency的巨细方位发生改动时分会回调 onDependentViewChanged
- onDependentViewChanged 当一个依靠的View的巨细或方位发生改动时分会调用
class FollowBehavior : CoordinatorLayout.Behavior<View> {
constructor() : super()
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
return dependency is MoveView
}
private var dependencyX = Float.MAX_VALUE
private var dependencyY = Float.MAX_VALUE
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
if (dependencyX == Float.MAX_VALUE || dependencyY == Float.MAX_VALUE) {
dependencyX = dependency.x
dependencyY = dependency.y
} else {
val dX = dependency.x - dependencyX
val dy = dependency.y - dependencyY
child.translationX -= dX
child.translationY -= dy
dependencyX = dependency.x
dependencyY = dependency.y
}
return true
}
}
绑定Behavior
绑定Behavior有两种方式:
- 经过布局参数去设置,你能够在xml中指定,当然也能够在Java代码中经过CoordinatorLayout.LayoutParams动态指定
<androidx.coordinatorlayout.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=".MainActivity">
<com.threeloe.testdemo.view.MoveView
android:background="@color/black"
android:layout_width="100dp"
android:layout_gravity="center_vertical"
android:layout_height="100dp"/>
<Button
android:id="@+id/btn"
android:layout_gravity="center_vertical"
android:layout_marginStart="200dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跟从黑块移动"
app:layout_behavior="com.threeloe.testdemo.behavior.FollowBehavior"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
- 默认绑定Behavior ,让View完成AttachedBehavior接口,完成getBehavior办法即可。这个优先级比布局参数低,当布局参数中没有指定Behavior时分会运用AttachedBehavior回来的。
class FollowTextView : TextView, CoordinatorLayout.AttachedBehavior{
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
return FollowBehavior()
}
}
长处
- Behavior的复用性十分好。比方FollowBehavior能够给任何其他的子View直接运用
- 当场景复杂的状况下Behavior也能表现出杰出的解耦。在没有CoordinatorLayout的状况下,咱们会给MoveView规划一个监听改动的接口,然后紫色按钮去监听Move的改动,然后自身移动。这在简略的场景下,不显得有什么,一旦场景变得复杂,相互之间有交互的子View较多的状况下,就会注册各种监听,代码之间的耦合会变得比较严重。CoordinatorLayout将各种子View的布局以及交互等行为笼统为Behavior,完成了代码的解耦,同时Behavior本身也具有很好的复用性。
进阶运用(Behavior阻拦全部)
Behavior简直能够阻拦全部View的行为,给子View添加Behavior之后,能够阻拦到父View CoordinatorLayout的measure,layout, 接触事情,嵌套滑动等等。 咱们经过下面滑动的Demo来说明:
对应的xml如下所示,完成十分简略全体上便是一个AppBarLayout + NestedScrollVIew.
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="二月二,龙抬头..." />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Title" />
<TextView
android:background="@color/purple_200"
android:textColor="@color/white"
android:text="惊蛰"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="45dp"/>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
看到这个Demo,关于不太了解的同学会有比较多的疑问,我会经过以下四个问题帮大家更好了解Behavior的作用。
- 咱们开篇就说过,CoordinatorLayout是一个“增强版”的FrameLayout,那为什么上述xml中NestedScrollView没有设置任何的marginTop内容却没有被遮挡?
- NestedScrollView实践丈量的高度应该是多大?
- 为什么手指按在AppBarLayout的区域上也能触发滑动事情?
- 为什么手指在NestedScrollView上滑动能把ToolBar “顶出去” ?
阻拦Measure/Layout
第一个问题中按咱们了解ToolBar应该挡住NestedScrollView最上面一部分才对,但展示出来却刚好在ToolBar的下方,这其实是由于Behavior其实供给了onMeasureChild,onLayoutChild让咱们自己去接管对子VIew的丈量和布局。上述中NestedScrollView运用了ScrollingViewBehavior,它是规划给能在竖直方向上滑动而且支撑嵌套滑动的View运用的,运用这个Behavior能够和AppBarLayout之间发生联动效果。
首先看ScrollingViewBehavior的layoutDependsOn办法,是依靠于AppBarLayout的。
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
咱们知道View的方位是由layout进程决议的,所以咱们直接看ScrollingViewBehavior的
boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection)
办法,终究找到要害的逻辑在父类HeaderScrollingViewBehavior的layoutChild中,要害代码主要就三行:
@Override
protected void layoutChild(
@NonNull final CoordinatorLayout parent,
@NonNull final View child,
final int layoutDirection) {
final List<View> dependencies = parent.getDependencies(child);
//header即是AppBarLayout
final View header = findFirstDependency(dependencies);
if (header != null) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
final Rect available = tempRect1;
available.set(
parent.getPaddingLeft() + lp.leftMargin,
//top的方位是在header的bottom下
header.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);
...
final Rect out = tempRect2;
//RTL处理
GravityCompat.apply(
resolveGravity(lp.gravity),
child.getMeasuredWidth(),
child.getMeasuredHeight(),
available,
out,
layoutDirection);
final int overlap = getOverlapPixelsForOffset(header);
child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
verticalLayoutGap = out.top - header.getBottom();
} else {
// If we don't have a dependency, let super handle it
super.layoutChild(parent, child, layoutDirection);
verticalLayoutGap = 0;
}
}
咱们给NestedScrollView设置高度为match_parent,那它的实践高度真的便是和CoordinatorLayout相同高么?实践并不是,由于它在屏幕上能展示的最大高度只有如下黄色箭头部分的长度,假如高度太大的话或许会导致一部分内容展示不出来。
这部分逻辑咱们能够在onMeasureChild办法中找到:
public boolean onMeasureChild(
@NonNull CoordinatorLayout parent,
@NonNull View child,
int parentWidthMeasureSpec,
int widthUsed,
int parentHeightMeasureSpec,
int heightUsed) {
final int childLpHeight = child.getLayoutParams().height;
//假如是match_parent或许wrap_content
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
|| childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
final List<View> dependencies = parent.getDependencies(child);
//获取到AppBarLayout
final View header = findFirstDependency(dependencies);
if (header != null) {
//父View也便是CoordinatorLayout的高度
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
...
//getScrollRange(header)是AppBarLayout中能够滑动的规模,关于上述Demo中便是ToolBar的高度
int height = availableHeight + getScrollRange(header);
//AppBarLayout的整个高度
int headerHeight = header.getMeasuredHeight();
if (shouldHeaderOverlapScrollingChild()) {
child.setTranslationY(-headerHeight);
} else {
//得到屏幕上黄色箭头的高度
height -= headerHeight;
}
final int heightMeasureSpec =
View.MeasureSpec.makeMeasureSpec(
height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
? View.MeasureSpec.EXACTLY
: View.MeasureSpec.AT_MOST);
// Now measure the scrolling view with the correct height
parent.onMeasureChild(
child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
return true;
}
}
return false;
}
阻拦Touch事情
咱们知道正常状况下,View要呼应Touch时间肯定要覆写View的onTouchEvent办法的,但是AppBarLayout并没有覆写。咱们当然能够持续联想Behavior, 但是上述xml中咱们并没有看到AppBarLayout有经过布局参数指定Behavior,不要忘了还有默认绑定的办法。
@Override
@NonNull
public CoordinatorLayout.Behavior<AppBarLayout> getBehavior() {
return new AppBarLayout.Behavior();
}
Behavior同样供给了onInterceptTouchEvent和onTouchEvent让子View自己去处理Touch事情。
onInterceptTouchEvent如下:
public boolean onInterceptTouchEvent(
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
...
// 假如是move事情而且在拖动中,就核算yDiff并阻拦事情
if (ev.getActionMasked() == MotionEvent.ACTION_MOVE && isBeingDragged) {
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
return false;
}
int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
return false;
}
int y = (int) ev.getY(pointerIndex);
int yDiff = Math.abs(y - lastMotionY);
if (yDiff > touchSlop) {
lastMotionY = y;
return true;
}
}
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
activePointerId = INVALID_POINTER;
int x = (int) ev.getX();
int y = (int) ev.getY();
//假如canDragView而且事情是在子View的规模中就认为进入拖动状态
isBeingDragged = canDragView(child) && parent.isPointInChildBounds(child, x, y);
if (isBeingDragged) {
lastMotionY = y;
activePointerId = ev.getPointerId(0);
ensureVelocityTracker();
// There is an animation in progress. Stop it and catch the view.
if (scroller != null && !scroller.isFinished()) {
scroller.abortAnimation();
return true;
}
}
}
if (velocityTracker != null) {
velocityTracker.addMovement(ev);
}
return false;
}
canDragView的逻辑如下,只有当NestedScrollView的scrollY是0的时分,也便是还没滑动过时分,才能拖动AppBarLayout。
@Override
boolean canDragView(T view) {
...
// Else we'll use the default behaviour of seeing if it can scroll down
if (lastNestedScrollingChildRef != null) {
// If we have a reference to a scrolling view, check it
final View scrollingView = lastNestedScrollingChildRef.get();
return scrollingView != null
&& scrollingView.isShown()
&& !scrollingView.canScrollVertically(-1);
} else {
// Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
return true;
}
}
onTouchEvent办法中核算移动距离dy,然后调用scroll办法翻滚。
@Override
public boolean onTouchEvent(
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
boolean consumeUp = false;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(activePointerId);
if (activePointerIndex == -1) {
return false;
}
final int y = (int) ev.getY(activePointerIndex);
int dy = lastMotionY - y;
lastMotionY = y;
// We're being dragged so scroll the ABL
scroll(parent, child, dy, getMaxDragOffset(child), 0);
break;
...
return isBeingDragged || consumeUp;
}
还有一个问题是在AppBarLayout scroll的进程中,NestedScrollView是怎样移动的呢?这个问题其实便是和咱们“简略运用”部分的那个问题相似,毫无疑问是在ScrollingViewBehavior的onDependentViewChanged中完成的,这儿不再具体分析代码了。
阻拦嵌套滑动
最后一个问题,为什么手指在NestedScrollView上滑动能把ToolBar “顶出去” ?这个假如从传统的事情分发视点看的话好像已经超出了咱们的“认知”,一个滑动事情怎样能从一个View搬运给另一个平级的子View,在了解这个之前咱们需求先了解下NestedScroling机制,本文只做简略介绍,需求详细了解的话能够看这篇NestedScrolling机制详解 。
NestedScrolling机制
NestedScroling机制供给两个接口:
- NestedScrollingParent,嵌套滑动的父View需求完成。已有完成CoordinatorLayout,NestedScroView
- NestedScrollingChild, 嵌套滑动的子View需求完成。已有完成RecyclerView,NestedScroView
由于发现规划的能力有些不足,Google前后又引进NestedScrollingParent2/NestedScrollingChild2以及NestedScrollingParent3/NestedScrollingChild3。
Google在给我供给这两个接口的时分,同时也给咱们供给了完成这两个接口时一些办法的规范完成,
分别是
- NestedScrollingChildHelper
- NestedScrollingParentHelper
咱们在完成上面两个接口的办法时,只需求调用相应Helper中相同签名的办法即可。
基本原理:
对原始的事情分发机制做了一层封装,子View完成NestedScrollingChild接口,父View完成NestedScrollingParent 接口。 在NetstedScroll的世界里,NestedScrollingChild是发动机,它自己和父VIew都能消费滑动事情,但是父VIew具有优先消费权。假定发生一个竖直滑动,简略来说滑动事情会由NestedScrollingChild先接收到发生一个dy,然后问询NestedScrollingParent要耗费多少(dyConsumed),自己再拿dy-dyConsumed来进行滑动。当然NestedScrollingChild有或许自己本身也并不会耗费完,此时会再向父View陈述状况。
在咱们的Demo中CoordinatorLayout便是这个滑动事情的转发中心,它接收到来自NestedScrollView的滑动事情,并将这些事情经过Behavior转发给AppBarLayout。
AppBarLayout.Behavior相关完成
- onStartNestedScroll 决议是否要接受嵌套滑动事情
@Override
public boolean onStartNestedScroll(
@NonNull CoordinatorLayout parent,
@NonNull T child,
@NonNull View directTargetChild,
View target,
int nestedScrollAxes,
int type) {
// 假如是竖直方向的翻滚而且有可翻滚的child
final boolean started =
(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& (child.isLiftOnScroll() || canScrollChildren(parent, child, directTargetChild));
if (started && offsetAnimator != null) {
// Cancel any offset animation
offsetAnimator.cancel();
}
// A new nested scroll has started so clear out the previous ref
lastNestedScrollingChildRef = null;
// Track the last started type so we know if a fling is about to happen once scrolling ends
lastStartedType = type;
return started;
}
private boolean canScrollChildren(
@NonNull CoordinatorLayout parent, @NonNull T child, @NonNull View directTargetChild) {
//总滑动规模大约0 而且 CoordinatorLayout 减去NestedScrollView的高度小于 AppBarLayout的高度
return child.hasScrollableChildren()
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
}
- onNestedPreScroll 在NestedScrollChild滑动之前决议自己是否要耗费
@Override
public void onNestedPreScroll(
CoordinatorLayout coordinatorLayout,
@NonNull T child,
View target,
int dx,
int dy,
int[] consumed,
int type) {
if (dy != 0) {
int min;
int max;
if (dy < 0) {
// 向下滑动
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
// 向上滑 ,确认翻滚规模
min = -child.getUpNestedPreScrollRange();
max = 0;
}
if (min != max) {
// 竖直方向的耗费仿制,传回给NestedScrollView
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}
if (child.isLiftOnScroll()) {
child.setLiftedState(child.shouldLift(target));
}
}
final int scroll(
CoordinatorLayout coordinatorLayout, V header, int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(
coordinatorLayout,
header,
//核算新的offset
getTopBottomOffsetForScrollingSibling() - dy,
minOffset,
maxOffset);
}
int setHeaderTopBottomOffset(
CoordinatorLayout parent, V header, int newOffset, int minOffset, int maxOffset) {
final int curOffset = getTopAndBottomOffset();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
//鸿沟处理
newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
//将整个View的方位再竖直方向上平移
setTopAndBottomOffset(newOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
}
}
return consumed;
}
- 子View滑动完毕之后决议自己是否要耗费滑动事情
@Override
public void onNestedScroll(
CoordinatorLayout coordinatorLayout,
@NonNull T child,
View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
int type,
int[] consumed) {
if (dyUnconsumed < 0) {
//NestedScroll View向下滑,滑动到自己内容的顶部时分,dy并没有耗费完毕,这个时分事情给AppBarLayout持续滑动
consumed[1] =
scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
}
if (dyUnconsumed == 0) {
// The scrolling view may scroll to the top of its content without updating the actions, so
// update here.
updateAccessibilityActions(coordinatorLayout, child);
}
}
- 中止嵌套滑动
@Override
public void onStopNestedScroll(
CoordinatorLayout coordinatorLayout, @NonNull T abl, View target, int type) {
// onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
// isn't necessarily guaranteed yet, but it should be in the future. We use this to our
// advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll
// (ViewCompat.TYPE_TOUCH) ends
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
// If we haven't been flung, or a fling is ending
snapToChildIfNeeded(coordinatorLayout, abl);
if (abl.isLiftOnScroll()) {
abl.setLiftedState(abl.shouldLift(target));
}
}
// Keep a reference to the previous nested scrolling child
lastNestedScrollingChildRef = new WeakReference<>(target);
}