CoordinatorLayout使用解析
一. CoordinatorLayout介绍
1. CoordinatorLayout是一个“加强版”FrameLayout,
它主要有两个用途:
- 用作应用的顶层布局管理器,也就是作为用户界面中所有UI控件的容器
- 用作相互之间距有特定交互行为的UI控件的容器
通过为CoordinatorLayout的子View指定Behavior,就可以实现它们之间的交互行为。
Behavior可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的UI元素,以及跟随着其他UI控件移动的按钮等。
2.文字不够形象, 直接来欣赏一下facebook的效果
3. CoordinatorLayout的使用
使用CoordinatorLayout需要在Gradle加入Support Design Library:
compile 'com.android.support:appcompat-v7:26.1.0'
二.AppBarLayout, CollapsingToolbarLayout的使用
1.AppBarLayout,CollapsingToolbarLayout是为了配合CoordinatorLayout使用而简单实现相互关系的控件
2.AppBarLayout 官方文档
1). AppBarLayout 介绍:
- 实现了material designs 滑动手势
- AppBarLayout的子View应该通过{setScrollFlags(int)}或者相关的布局xml属性{app:layout_scrollFlags}提供他们想要的滚动行为。
- 最好作为CoordinatorLayou的直接子View使用
- 要拥有一个可滚动的兄弟View并且通过为可滚动的兄弟View设置ScrollingViewBehavior实例来实现绑定。
2).AppBarLayout滑动区域 查看
当设计滚动行为时,App bar包含构成滚动结构的四个主要区域(称为块):
- Status bar
- Tool bar
- Tab bar/search bar
- Flexible space: 用来容纳图像或者扩展app bar的期望宽高比
3) 滑动类型
AppBarLayout里面的View都是通过设置app:layout_scrollFlags属性控制滑动,Google提供了5种滑动表现类型,分别是
- scroll:表示向下滚动的时候,设置了这个属性的View会被滚出屏幕范围,直到消失
- enterAlways:表示向上滚动的时候,设置了这个属性的View会随着滚动手势逐渐出现,直到恢复原来设置的位置(需搭配scroll使用)
- enterAlwaysCollapsed:是enterAlways的附加选项,一般跟enterAlways一起使用,它是指,View在往下“出现”的时候,首先是enterAlways效果,当View的高度达到最小高度时,View就暂时不去往下滚动,直到ScrollView滑动到顶部不再滑动时,View再继续往下滑动,直到滑到View的顶部结束。
- exitUntilCollapsed:值设为exitUntilCollapsed的View,当这个View要往上逐渐“消逝”时,会一直往上滑动,直到剩下的的高度达到它的最小高度后,再响应ScrollView的内部滑动事件(需搭配scroll使用)
- snap 表示在滑动过程中如果停止滑动,则头部会就近折叠(要么恢复原状,要么折叠成一个Toolbar或者不显示)(需搭配scroll使用)
4)重要的监听方法
addOnOffsetChangedListener()
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
// TODO
}
});
3.CollapsingToolbarLayout 官方文档
1.)CollapsingToolbarLayout 介绍:
CollapsingToolbarLayout是实现一个可以折叠的Toolbar,作为AppbarLayout的直接子View使用,CollapsingToolbarLayout控件提供了一下功能:
- 折叠标题:当标题栏较大的时候,在布局完全显示的情况下可以显示标题栏,但是标题栏折叠、变小或者布局滚动出屏幕的时候,可以通过setTitle(CharSequence)设置标题显示,通过设置collapsedTextAppearance和expandedTextAppearance属性可以调整标题栏的外观。
- 内容遮罩:通过 setContentScrim(Drawable)更改当滚动到临界点的时候显示或者隐藏
- 状态栏遮罩:可以通过setStatusBarScrim(Drawable)设置遮罩在滚动到临界点之后是显示还是隐藏,必须要在5.0以后设置了fitSystemWindows属性才可以使用
- 视差滚动子视图:子视图可以在这个视差范围内滚动,See COLLAPSE_MODE_PARALLAX and setParallaxMultiplier(float).
- 寄托子视图:子视图可以选择在全局范围内被固定,当实现一个折叠的时候,允许Toolbar被固定在合适的位置,详细见COLLAPSE_MODE_PIN
2) 折叠塌陷的属性collapseMode
app:layout_collapseMode="pin"属性:这个属性是设置折叠的模式,Android提供有两个值,分别是:
- pin:设置这个值,当CollapsingToolbarLayout完全折叠之后,View还会显示在屏幕上
- parallax:设置这个值,在内容滚动时,CollapsingToolbarLayout中的View(比如我们这里的ImageView)也会同时滚动,实现视差滚动效果,通常和layout_collapseParallaxMultiplier(设置视差因子)搭配使用。
app:layout_collapseParallaxMultiplier="0.7"属性:设置视差滚动因子,值得范围是0~1.
视差滚动因子数值越大,视觉差越大
- 如果这里的值为0,则在头部折叠的过程中,ImageView的顶部在慢慢隐藏,底部不动
- 如果这里的值为1,ImageView的顶部不懂,底部慢慢隐藏,
- 如果这里的取值为0~1之间,则在折叠的过程中,ImageView的顶部和底部都会隐藏,但是头部和底部隐藏的快慢是不一样的,具体速度和视觉乘数有关
简单来说 0~1代表上下方向折叠的快慢 0上部折叠速度快 1下部折叠速度快
三.Behavior
1.Behavior介绍
1.作用于CoordinatorLayout的子View的交互行为插件。
一个Behavior 实现了用户的一个或者多个交互行为,它们可能包括拖拽、滑动、快滑或者其他一些手势。
2. Behavior 是一个顶层抽象类,其他的一些具体行为的Behavior 都是继承自这个类。
public static abstract class Behavior<V extends View> {
public Behavior() {
}
public Behavior(Context context, AttributeSet attrs) {
}
//省略了若干方法
}
其中有一个泛型,它的作用是指定要使用这个Behavior的View的类型,可以是Button、TextView等等。
3.自定义Behavior可以选择重写以下的几个重要方法:
- layoutDependsOn():确定使用Behavior的View要依赖的View的类型
- onDependentViewChanged():当被依赖的View状态改变时调用
- onDependentViewRemoved():当被依赖的View移除时调用
- onStartNestedScroll():嵌套滑动开始(ACTION_DOWN),确定Behavior是否要监听此次事件
- onNestedScrollAccepted() : onStartNestedScroll返回true才会触发这个方法,接受滚动处理后回调,可以在这个方法里做一些准备工作,如一些状态的重置等。
- onStopNestedScroll():嵌套滑动结束(ACTION_UP或ACTION_CANCEL)
- onNestedPreScroll():嵌套滑动进行中,要监听的子 View将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制)
- onNestedScroll():嵌套滑动进行中,要监听的子 View的滑动事件已经被消费
- onNestedFling():要监听的子 View在快速滑动中
- onNestedPreFling():要监听的子View即将快速滑动
- onLayoutChild 确定使用Behavior的View位置
2.绑定Behavior的三种方式
Behavior无法独立完成工作,必须与实际调用的CoordinatorLayout子视图相绑定。具体有三种方式:通过代码绑定、在XML中绑定或者通过注释实现自动绑定。
1. 通过代码绑定Behavior
如果将Behavior当作绑定到CoordinatorLayout中每个视图的附加数据,那么发现Behavior实际上是存储在各个视图的LayoutParams中也就不足为奇了(之前有关于布局的博文)。也是因此,Behavior需要绑定到CoordinatorLayout的直接子项中,因为只有那些子项会包含LayoutParams的特定Behavior子类
TitleBehavior titleBehavior = new TitleBehavior();
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) yourView.getLayoutParams();
params.setBehavior(titleBehavior);
2. app:layout_behavior布局属性
在布局中设置,值为自定义 Behavior类的名字字符串(包含路径)
有两种写法,包含包名的全路径和以”.”开头的省略项目包名的路径:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_behavior="com.yasin.coordinatorlayoutdemo.TitleBehavior"
app:layout_behavior=".TitleBehavior"
3. @CoordinatorLayout.DefaultBehavior类注解
在需要使用 Behavior的控件源码定义中添加该注解,然后通过反射机制获取。系统的 AppBarLayout、 FloatingActionButton都采用了这种方式,所以无需在布局中重复设置。
@CoordinatorLayout.DefaultBehavior(TitleBehavior.class)
public class TitleLayout extends FrameLayout {}
3.Behavior实现View间交互的原理
behavior像是view的一个属性,其实它是view的LayoutParam的一个属性,就像宽高一样。当然不是任何一个view的LayoutParam都有这个属性的,只有LayoutParam为android.support.design.widget.CoordinatorLayout.LayoutParams才有这个属性.
所以,就是只有CoordinatorLayout的子view的LayoutParam可以设置behavior。
- 我们可以在CoordinatorLayout.LayoutParams中找到Behavior属性.
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
Behavior mBehavior;
boolean mBehaviorResolved = false;
...
final Rect mLastChildRect = new Rect();
}
1.Behavior初始化过程
- xml方式:app:layout_behavior=”com.yasin.coordinatorlayoutdemo.TitleBehavior”
infate的时候,会根据xml去构造LayoutParams,所以我们可以在CoordinatorLayout.LayoutParams看到behavior的初始化过程
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_LayoutParams);
...
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}
a.recycle();
}
2.注解方式:@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
在CoordinatorLayout的onMeasure的时候会调用prepareChildren,进而调用getResolvedLayoutParams,在getResolvedLayoutParams里会把注解里的默认Behavior赋值给mBehavior,主要代码如下:
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
//如果xml内写了behavior,此时result.mBehaviorResolved就为true,不会进去
if (!result.mBehaviorResolved) {
Class<?> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
while (childClass != null &&
(defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
childClass = childClass.getSuperclass();
}
if (defaultBehavior != null) {
try {
result.setBehavior(defaultBehavior.value().newInstance());
} catch (Exception e) {
Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
" could not be instantiated. Did you forget a default constructor?", e);
}
}
result.mBehaviorResolved = true;
}
return result;
}
2.Behavior是如何发挥作用的
measure和layout是Android绘制视图的关键组件,因此Behavior只有在onMeasureChild()和onLayoutChild()回调前拦截父视图的measure和layout,才能达到预计的效果。
我们再来看看onMeasure的代码,和Behavior相关的主要看prepareChildren和ensurePreDrawListener两个方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
ensurePreDrawListener();
。。。
先看 preareChildren()
private void prepareChildren() {
//清空mDependencySortedChildren
mDependencySortedChildren.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View child = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(child);
lp.findAnchorView(this, child);
//加入child
mDependencySortedChildren.add(child);
}
// Finally add the sorted graph list to our list
mDependencySortedChildren.addAll(mChildDag.getSortedList());
// 我们还需要反转结果,因为我们希望列表的开头包含没有依赖项的视图,然后在此之后依赖视图。
Collections.reverse(mDependencySortedChildren);
}
prepareChildren内主要是搞出来一个mDependencySortedChildren,根据依赖关系对child进行排序。先把mDependencySortedChildren clear,然后遍历子view,全部加入到mDependencySortedChildren内,最后对mDependencySortedChildren进行排序
排序这块没看懂.看了翻译和别的文档大概意思是:被依赖的view放前面,依赖的view放后面. 比如我们fab依赖于snackbar,那么snackbar必然放在fab的前边。这么排序有什么用?其实是提高一点效率,后文会说的。
次看ensurePreDrawListener()
void ensurePreDrawListener() {
//判断是否存在依赖关系
boolean hasDependencies = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (hasDependencies(child)) {
hasDependencies = true;
break;
}
}
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
//加入PreDrawListener
addPreDrawListener();
} else {
removePreDrawListener();
}
}
}
在prepareChildren确定mDependencySortedChildren之后,会执行ensurePreDrawListener,在这里写判断下CoordinatorLayout的子view是否存在依赖关系,如果存在的话就hasDependencies为true,后边会加入OnPreDrawListener,也就是监听依赖View的布局变化
就是在重绘之前,会调用OnPreDrawListener的onPreDraw方法。再在onPreDraw里面调用了onChildViewsChanged。
再看onChildViewsChanged()
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
........
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
....
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
}
.......
}
显然从这里就开始触发View的交互了
onDependentViewRemoved()和onDependentViewChanged()方法
四.自定义Behavior
通常自定义Behavior分为两种情况:
-
1.通过监听一个View的状态,如位置、大小的变化,来改变其他View的行为,这种只需要重写2个方法就可以了,分别是layoutDependsOn 和onDependentViewChanged, layoutDependsOn方法判断是指定依赖的View时,返回true,然后在onDependentViewChanged 里,被依赖的View做需要的行为动作。
-
2.是重写onStartNestedScroll、onNestedPreScroll、onNestedScroll等一系列方法.可以实现比较复杂的方法
相关链接
Android 详细分析AppBarLayout的五种ScrollFlags链接