1CoordinatorLayout与behavior入门

前言

闲来无事,掏出AS熟练的new一个project,发现默认出来的activity点击之后有如下动画

fab 和snackbar

感觉很神奇,于是分析以下,这是怎么做到的?
这个activity的style是@style/AppTheme.NoActionBar,布局如下所示,这是目前android比较推崇的做法,基本理念就是脱离actionbar,在自己的布局里写toolbar,这样会提高更大的自由度,toolbar就变成了一个普通的view,我想放哪里放哪里,想怎么放怎么放(其实默认actionbar也是toolbar实现的,view层次结构可以参考AppCompatActivity的View树)。

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.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"
    android:fitsSystemWindows="true"
    tools:context="com.fish.a2.MainActivity">

    <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>

    <include layout="@layout/content_main" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>

这一个xml中,CoordinatorLayout、FloatingActionButton、AppBarLayout都是陌生的。FloatingActionButton就是右下角的带阴影圆形按钮,可以简单看做一个ImageButton,如果对此控件有兴趣,可以阅读http://blog.csdn.net/lmj623565791/article/details/46678867,此文内鸿洋大神还自己实现了阴影效果。
我们再来看自动生成的部分代码,FloatingActionButton(后边简称fab)的点击就是弹出一个Snackbar,又出来一个新玩意,Snackbar可以看做是Toast的升级版本,没什么高深的,下边代码的意思就是点击fab弹出一个Snackbar,Snackbar像toast一样几秒后就消失。

      FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_SHORT)
                        .setAction("Action", null).show();
            }
        });

这些看起来都很简单,但是Snackbar弹出的时候,为何fab为何为往上跑呢?然后过了几秒钟,Snackbar消失了,fab又自动往下滑了,这2个动画是怎么做到的?从代码里看不出任何端倪.

CoordinatorLayout

先简单介绍下CoordinatorLayout,看官方文档
CoordinatorLayout is a super-powered FrameLayout.

CoordinatorLayout is intended for two primary use cases:

1.As a top-level application decor or chrome layout
2.As a container for a specific interaction with one or more child views

By specifying Behaviors 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 DefaultBehavior annotation.

Behaviors may be used to implement a variety of interactions and additional layout modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons that stick to other elements as they move and animate.

CoordinatorLayout是个超级强大的FrameLayout(其实他并不继承FrameLayout,只是里面的view布局和FrameLayout一样,一层层叠上去)

CoordinatorLayout主要有2种用法
1.作为app的rootview(contentview的child)
2.作为一个容器,包含几个view,这些view有特殊行为,互相作用

给CoordinatorLayout的子view指定behavior可以让他们互相作用互相依赖

behavior

概述

Behavior是一种新的view关系描述,CoordinatorLayout的一个子view可以依赖于另一个子view做出响应,比如有子view A和B,A依赖于B,一旦B发生变化(比如大小位置可见性改变),A就可以知道并且响应,这其实就是观察者模式。一个子view可以依赖于多个子view,比如A可以依赖于B,C这个依赖关系会在一个类内写明,这个类是CoordinatorLayout.Behavior的子类,可以自定义。

例子解释

还是有点难以理解,看上文的实际例子吧。
fab会因为Snackbar的弹出而上移,随着Snackbar的消失而下移,是因为fab依赖于Snackbar。 这种依赖关系由FloatingActionButton内部的Behavior类决定的。
Behavior内有如下代码,看layoutDependsOn代码,dependency是SnackbarLayout就返回true(SNACKBAR_BEHAVIOR_ENABLED前提下),这就说明了fab依赖于SnackbarLayout,一旦SnackbarLayout发生变化,fab就会立刻知道并作出反应。

    @Override
        public boolean layoutDependsOn(CoordinatorLayout parent,
                FloatingActionButton child, View dependency) {
            // We're dependent on all SnackbarLayouts (if enabled)
            return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
        }

做出什么反应呢?来看onDependentViewChanged的代码,主要调用updateFabTranslationForSnackbar,注意updateFabTranslationForSnackbar 的L34、L35,这里触发了平移动画。这就是为什么snackbar出现或消失的时候,fab会有平移动画。


       @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
                View dependency) {
            if (dependency instanceof Snackbar.SnackbarLayout) {
                updateFabTranslationForSnackbar(parent, child, dependency);
            } 
            return false;
        }
        
       private void updateFabTranslationForSnackbar(CoordinatorLayout parent,
                final FloatingActionButton fab, View snackbar) {
            final float targetTransY = getFabTranslationYForSnackbar(parent, fab);
            if (mFabTranslationY == targetTransY) {
                // We're already at (or currently animating to) the target value, return...
                return;
            }

            final float currentTransY = ViewCompat.getTranslationY(fab);

            // Make sure that any current animation is cancelled
            if (mFabTranslationYAnimator != null && mFabTranslationYAnimator.isRunning()) {
                mFabTranslationYAnimator.cancel();
            }

            if (fab.isShown()
                    && Math.abs(currentTransY - targetTransY) > (fab.getHeight() * 0.667f)) {
                // If the FAB will be travelling by more than 2/3 of it's height, let's animate
                // it instead
                if (mFabTranslationYAnimator == null) {
                    mFabTranslationYAnimator = ViewUtils.createAnimator();
                    mFabTranslationYAnimator.setInterpolator(
                            AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
                    mFabTranslationYAnimator.setUpdateListener(
                            new ValueAnimatorCompat.AnimatorUpdateListener() {
                                @Override
                                public void onAnimationUpdate(ValueAnimatorCompat animator) {
                                    ViewCompat.setTranslationY(fab,
                                            animator.getAnimatedFloatValue());
                                }
                            });
                }
                //这里制造动画来完成上移或者下移
                mFabTranslationYAnimator.setFloatValues(currentTransY, targetTransY);
                mFabTranslationYAnimator.start();
            } else {
                // Now update the translation Y
                ViewCompat.setTranslationY(fab, targetTransY);
            }

            mFabTranslationY = targetTransY;
        }

我们再具体分析下,为何snackbar出现的时候,fab上移,而snackbar消失的时候fab下移呢?
在snackbar出现的时候,会调用onDependentViewChanged()然后调用updateFabTranslationForSnackbar(),这里重点是L34代码,意思就是从currentTransY平移到targetTransY,关注下这2个变量。currentTransY怎么来的,看L9就是fab本身的TranslationY,那targetTransY呢?看L3,由getFabTranslationYForSnackbar得到。

所以看一下getFabTranslationYForSnackbar的代码,L5获取CoordinatorLayout内部被依赖的子view,在这个CoordinatorLayout内只有fab依赖于Snackbar.SnackbarLayout,所以Snackbar.SnackbarLayout是被依赖的子view,也只有这一个。看L10,此时view就是Snackbar.SnackbarLayout ,minOffset为0,ViewCompat.getTranslationY(view) 为0,所以最终得到的minOffset就是- view.getHeight(),其实就是snackbar的高度的相反数。我们假设-200好了。再回到updateFabTranslationForSnackbar内,此时targetTransY为-200,而currentTransY为0,那会发生什么?从0上移到-200,所以snackbar出现的时候,fab做了一个上移动画。

       private float getFabTranslationYForSnackbar(CoordinatorLayout parent,
                FloatingActionButton fab) {
            float minOffset = 0;
            //获取被依赖的子view
            final List<View> dependencies = parent.getDependencies(fab);
            for (int i = 0, z = dependencies.size(); i < z; i++) {
                final View view = dependencies.get(i);
                if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) {
                //重点是这句代码
                    minOffset = Math.min(minOffset,
                            ViewCompat.getTranslationY(view) - view.getHeight());
                }
            }

            return minOffset;
        }

下面我们再可以分析snackbar消失的时候的场景,注意这里稍有不同,snackbar 出现调用了onDependentViewChanged,但是snackbar消失调用的是onDependentViewRemoved, 但最终调的还是updateFabTranslationForSnackbar ,殊途同归啊,此时getFabTranslationYForSnackbar内dependencies为空(因为snackbar 不见了),所以最终返回0,所以targetTransY为0,而currentTransY位-200,所以就从-200下移到了0,下移就是这么产生的。

    @Override
        public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionButton child,
                View dependency) {
            if (dependency instanceof Snackbar.SnackbarLayout) {
                updateFabTranslationForSnackbar(parent, child, dependency);
            }
        }

snackbar出现回调到onDependentViewChanged,然后调用updateFabTranslationForSnackbar完成上移。
snackbar消失回调到onDependentViewRemoved,然后调用updateFabTranslationForSnackbar完成上移。

总结

简单的说CoordinatorLayout是容器,behavior是核心。behavior封装了CoordinatorLayout的子view随着其他子view的变化而变化的规则,主要是通过layoutDependsOn和onDependentViewChanged这2个方法。再看看CoordinatorLayout的名字,就是协调布局的意思,协调子view之间的关系,被协调的2个view必须是CoordinatorLayout的直接子view

dependency概念

fab依赖于Snackbar.SnackbarLayout,那我们就可以说fab的dependency是Snackbar.SnackbarLayout。

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

推荐阅读更多精彩内容