1.前言
提起CoordinatorLayout,大家立马能想到绚丽的首页滚动交互。可是真当去实践时,官方Demo提供的那一堆控件及其属性组合,令人头昏眼花、理不清思路。这次,我们从外(父)到内(子)来分析各控件组合及作用。
2.CoordinatorLayout
这是一个布局,继承自ViewGroup,从安置子视图的角度来说,相当于是强大的FrameLayout。对它的使用要明确两点:作为顶层布局使用;给内部的一个或多个子视图提供指定交互。先说说两个高级属性(已被封装好的交互):
- anchor及anchorGravity。这可以将悬浮视图与内容视图关联,使之跟随内容移动。anchor可以指定为CoordinatorLayout内的任意视图Id(除了使用这个属性的视图及其子视图),而anchorGravity负责在内容视图内放置悬浮视图。
- insetEdge和dodgeInsetEdges。这是一对同时使用的属性,目的是不让视图被遮挡。当两个视图有遮挡时,其中一个insetEdge设置为bottom,另一个dodgeInsetEdges也设置为bottom,则第二个向top方向避让。若两属性设置方向不一致时,无效;也可以设置多个方向或者给多个视图设置(部分情况会出问题)。详细参考jscoolstar的文章。
由于这两对交互比较常用且逻辑清晰,所以抽取出来。那么内部是由什么实现的呢?自己如何定义交互呢?这时得使用Behavior类,具体逻辑是这样:
-
CoordinatorLayout作为容器可以监听子视图状态上的变化,但是滚动视图只是内容变化,状态并未变化,监听不到。所以系统给监听者和被监听者分别提供两个接口NestedScrollingParent和NestedScrollingChild。第一个接口需要被ViewGroup的子类实现,表明希望协助完成嵌套子视图的滚动操作;第二个接口需要被View的子类实现,表明希望分发嵌套滚动操作给协作的父布局。有这些类满足:
CoordinatorLayout则将状态变化和滚动变化分发给直接子视图,由它们的Behavior对象接受判断是否符合条件,进行什么操作,有点类似广播机制(其实是遍历子视图)。注意,Behavior在布局文件中由app:layout_behavior声明或者在从动视图类上加 @DefaultBehavior() 声明,初始化是在CoordinatorLayout的LayoutParams中通过反射完成的。详细信息可以参考源码分析。
3.Behavior
在自定义Behavior类之前,先声明两个概念,即主动与从动。由于是视图的交互,必然是其中一个发生变化引起另一个的变化,那么前者就是主动,后者便是从动。Behavior可以接收许多事件,我们主要重写依赖(状态变化)和滚动。
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
// 1.为了演示,声明Behavior的从动对象是TextView
// 使用时,被设置的对象类型是它或子类,否则报错
public class MyBehavior extends CoordinatorLayout.Behavior<TextView> {
// 2.重写构造函数,若在XML布局中使用,得有AttributeSet
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 3.自己判断是否需要执行依赖交互,返回true执行,false不执行
// child从动对象,dependency主动对象
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
return super.layoutDependsOn(parent, child, dependency);
}
// 4.自己实现交互操作,返回true表示需改变child大小和位置,false则不用
// child从动对象,dependency主动对象
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
return super.onDependentViewChanged(parent, child, dependency);
}
// 5.自己判断是否需要执行滚动交互,返回true执行,false不执行
// child从动对象,target主动对象,directTarget为CoordinatorLayout子视图,是或包含主动对象,nestedScrollAxes水平还是竖直滑动
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, TextView child, View directTargetChild, View target, int nestedScrollAxes) {
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
// 6.自己实现交互操作
// child从动对象,target主动对象,dxConsumed/dyConsumed为主动对象在水平和竖直上已滚动距离,另外两个是未滚动距离
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, TextView child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
// 7.这是快速滑动操作
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, TextView child, View target, float velocityX, float velocityY, boolean consumed) {
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
}
Behavior能在从动视图前收到CoordinatorLayout的所有触摸事件,并做出相应处理,与View的事件分发一致。详细参考Jude95的文章。
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
return super.onTouchEvent(parent, child, ev);
}
4.AppBarLayout
AppBarLayout继承自LinearLayout,最大的特色就是实现了滚动手势,并通过给子视图设置scrollFlags来操作它们的滚动行为。若要滚动,scroll必须第一个设置。但是此功能仅当它作为CoordinatorLayout的直接子视图时有效,这不是和Behavior的用法一样吗?看看源码。
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
public AppBarLayout(Context context) {
this(context, null);
}
public AppBarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior {
public ScrollingViewBehavior() {}
public ScrollingViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@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;
}
}
public static class Behavior extends HeaderBehavior<AppBarLayout> {
public Behavior() {}
public Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
// Return true if we're nested scrolling vertically, and we have scrollable children
// and the scrolling view is big enough to scroll
final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& child.hasScrollableChildren()
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
if (started && mOffsetAnimator != null) {
// Cancel any offset animation
mOffsetAnimator.cancel();
}
// A new nested scroll has started so clear out the previous ref
mLastNestedScrollingChildRef = null;
return started;
}
}
果然是通过Behavior实现AppBarLayout与滚动手势交互的。按照前面学的知识可以知道,onStartNestedScroll() 方法中的条件就是实现滚动的条件。那官方的Demo中为什么要给NestedScrollView设置AppBarLayout.ScrollingViewBehavior?通过ScrollingViewBehavior的 layoutDependsOn() 方法可知,是设置的视图依赖AppBarLayout,看来是为了根据AppBarLayout的移动来调整自己在界面中的位置,onDependentViewChanged() 方法证明了这点。
5.总结
知识点太杂了,我来理一理思路。CoordinatorLayout作为容器,接收子视图的变化,再分发对应事件,起到解耦的作用,使事件的交互对象只有它。Behavior则是从视图的事件处理,只在CoordinatorLayout的直接子视图中起作用,当满足条件就可以操作视图。AppBarLayout目的就一个,带着子视图一起响应滚动手势。