CoordinatorLayout解析

CoordinatorLayout使用解析

一. CoordinatorLayout介绍

1. CoordinatorLayout是一个“加强版”FrameLayout,

它主要有两个用途:

  1. 用作应用的顶层布局管理器,也就是作为用户界面中所有UI控件的容器
  2. 用作相互之间距有特定交互行为的UI控件的容器
    通过为CoordinatorLayout的子View指定Behavior,就可以实现它们之间的交互行为。
    Behavior可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的UI元素,以及跟随着其他UI控件移动的按钮等。

2.文字不够形象, 直接来欣赏一下facebook的效果

image

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初始化过程
  1. 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链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容