一文让你搞懂design设计的CoordinatorLayout和AppbarLayout联动,让Design设计更简单~

转载

https://blog.csdn.net/gyyak46/article/details/53103796

一、写在前面

其实博主在之前已经对design包的各个控件都做了博文说明,无奈个人觉得理解不够深入,所以有了这篇更加深入的介绍,希望各位看官拍砖~

二、从是什么开始

1、首先我们得知道CoordinatorLayout是什么玩意儿,到底有什么用,我们不妨看看官方文档的描述:

  CoordinatorLayout是一个“加强版”FrameLayout,它主要有两个用途:

1、用作应用的顶层布局管理器,也就是作为用户界面中所有UI控件的容器

2、用作相互之间具有特定交互行为的UI控件的容器

通过为CoordinatorLayout的子View指定Behavior,就可以实现它们之间的交互行为。 Behavior可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的UI元素,以及跟随着其他UI控件移动的按钮等。

2、交互行为

先看个简单的效果图

图片.png

可能大家看到这,就自然能想到观察者模式,或者我昨日写的Rx模式:知识必备】RxJava+Retrofit最佳结合体验,打造懒人封装框架~

我们的Button就是一个被观察者,TextView作为一个观察者,当Button移动的时候通知TextView,TextView就跟着移动。看看其布局:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_coordinator"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.nanchen.coordinatorlayoutdemo.CoordinatorActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="观察者"
        app:layout_behavior=".FollowBehavior"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="被观察者"
        android:layout_gravity="center"
        android:id="@+id/btn"/>

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

很简单,一个TextView,一个Button,外层用CoordinatorLayout,然后给我们的TextView加一个自定义的Behavior文件,内容如下:

package com.nanchen.coordinatorlayoutdemo;

import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

/**
 *
 * 自定义CoordinatorLayout的Behavior,泛型为观察者View(要跟着别人动的那个)
 *
 * 必须重写两个方法,layoutDependOn和onDependentViewChanged
 *
 * @author nanchen
 * @fileName CoordinatorLayoutDemo
 * @packageName com.nanchen.coordinatorlayoutdemo
 * @date 2016/12/13  10:13
 */

public class FollowBehavior extends CoordinatorLayout.Behavior<TextView>{

    /**
     * 构造方法
     */
    public FollowBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 判断child的布局是否依赖dependency
     *
     * 根据逻辑来判断返回值,返回false表示不依赖,返回true表示依赖
     *
     * 在一个交互行为中,dependent view的变化决定了另一个相关View的行为。
     * 在这个例子中,Button就是dependent view,因为TextView跟随着它。
     * 实际上dependent view就相当于我们前面介绍的被观察者
     *
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
        return dependency instanceof Button;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
        child.setX(dependency.getX());
        child.setY(dependency.getY() + 100);
        return true;
    }
}

重点看看其中重写的两个方法layoutDependsOn()和onDependentViewChanged()。在介绍这两个方法的作用前,我们先来介绍一下dependent view。在一个交互行为中,dependent view的变化决定了另一个相关View的行为。在这个例子中,Button就是dependent view,因为TextView跟随着它。实际上dependent view就相当于我们前面介绍的被观察者。知道了这个概念,让我们看看重写的两个方法的作用:

  • layoutDependsOn():这个方法在对界面进行布局时至少会调用一次,用来确定本次交互行为中的dependent view,在上面的代码中,当dependency是Button类的实例时返回true,就可以让系统知道布局文件中的Button就是本次交互行为中的dependent view。

  • onDependentViewChanged():当dependent view发生变化时,这个方法会被调用,参数中的child相当于本次交互行为中的观察者,观察者可以在这个方法中对被观察者的变化做出响应,从而完成一次交互行为。

所以我们现在可以开始写Activity中的代码:

findViewById(R.id.btn).setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if (motionEvent.getAction() == MotionEvent.ACTION_MOVE){
                    view.setX(motionEvent.getRawX()-view.getWidth()/2);
                    view.setY(motionEvent.getRawY()-view.getHeight()/2);
                }
                return true;
            }
        });

这样一来,我们就完成了为TextView和Button设置跟随移动这个交互行为。很简单有木有,其实为CoordinatorLayout的子View设置交互行为只需三步:

  • 自定义一个继承自Behavior类的交互行为类;
  • 把观察者的layout_behavior属性设置为自定义行为类的类名;
  • 重写Behavior类的相关方法来实现我们想要的交互行为.

值得注意的是,有些时候,并不需要我们自己来定义一个Behavior类,因为系统为我们预定义了不少Behavior类。在接下来的篇章中,我们会做出进一步的介绍。

3、更进一步

现在我们已经知道了怎么通过给CoordinatorLayout的子View设置Behavior来实现交互行为。现在,让我们更进一步地挖掘下CoordinatorLayout,深入了解一下隐藏在表象背后的神秘细节。

实际上,CoordinatorLayout本身并没有做过多工作,实现交互行为的主要幕后推手是CoordinatorLayout的内部类——Behavior。通过为CoordinatorLayout的直接子View绑定一个Behavior,这个Behavior就会拦截发生在这个View上的Touch事件、嵌套滚动等。不仅如此,Behavior还能拦截对与它绑定的View的测量及布局。关于嵌套滚动,我们会在后续文章中进行详细介绍。下面我们来深入了解一下Behavior是如何做到这一切的。

4、深入理解Behavior

a) 拦截Touch事件

当我们为一个CoordinatorLayout的直接子View设置了Behavior时,这个Behavior就能拦截发生在这个View上的Touch事件,那么它是如何做到的呢?实际上,CoordinatorLayout重写了onInterceptTouchEvent()方法,并在其中给Behavior开了个后门,让它能够先于View本身处理Touch事件。具体来说,CoordinatorLayout的onInterceptTouchEvent()方法中会遍历所有直接子View,对于绑定了Behavior的直接子View调用Behavior的onInterceptTouchEvent()方法,若这个方法返回true,那么后续本该由相应子View处理的Touch事件都会交由Behavior处理,而View本身表示懵逼,完全不知道发生了什么。

b)拦截测量及布局

了解了Behavior是怎养拦截Touch事件的,想必大家已经猜出来了它拦截测量及布局事件的方式——CoordinatorLayout重写了测量及布局相关的方法并为Behavior开了个后门。没错,真相就是如此。

CoordinatorLayout在onMeasure()方法中,会遍历所有直接子View,若该子View绑定了一个Behavior,就会调用相应Behavior的onMeasureChild()方法,若此方法返回true,那么CoordinatorLayout对该子View的测量就不会进行。这样一来,Behavior就成功接管了对View的测量。

同样,CoordinatorLayout在onLayout()方法中也做了与onMeasure()方法中相似的事,让Behavior能够接管对相关子View的布局。

c) view的依赖关系的确定

现在,我们在探究一下交互行为中的两个View之间的依赖关系是怎么确定的。我们称child为交互行为中根据另一个View的变化做出响应的那个个体,而dependent view为child所依赖的View。实际上,确立child和dependent view的依赖关系有两种方式:

  • 显式依赖:为child绑定一个Behavior,并在Behavior类的layoutDependsOn()方法中做手脚。即当传入的dependency为dependent view时返回true,这样就建立了child和dependent view之间的依赖关系。

  • 隐式依赖:通过我们最开始提到的锚(anchor)来确立。具体做法可以这样:在XML布局文件中,把child的layout_anchor属性设为dependent view的id,然后child的layout_anchorGravity属性用来描述为它想对dependent view的变化做出什么样的响应。关于这个我们会在后续篇章给出具体示例。

    无论是隐式依赖还是显式依赖,在dependent view发生变化时,相应Behavior类的onDependentViewChanged()方法都会被调用,在这个方法中,我们可以让child做出改变以响应dependent view的变化。

三、玩转AppBarLayout

实际上我们在应用中有CoordinatorLayout的地方通常都会有AppBarLayout的联用,作为同样的出自Design包的库,我们看看官方文档怎么说:
AppBarLayout是一个垂直的LinearLayout,实现了Material Design中app bar的scrolling gestures特性。AppBarLayout的子View应该声明想要具有的“滚动行为”,这可以通过layout_scrollFlags属性或是setScrollFlags()方法来指定。
  AppBarLayout只有作为CoordinatorLayout的直接子View时才能正常工作,
  为了让AppBarLayout能够知道何时滚动其子View,我们还应该在CoordinatorLayout布局中提供一个可滚动View,我们称之为scrolling view。scrolling view和AppBarLayout之间的关联,通过将scrolling view的Behavior设为AppBarLayout.ScrollingViewBehavior来建立.

1、一般怎么用?

AppBar是Design的一个概念,其实我们也可以把它看做一种5.0出的ToolBar,先感受一下AppBarLayout+CoordinatorLayout的魅力。


图片.png

  实际效果就是这样,当向上滑动View的时候,ToolBar会小时,向下滑动的时候,ToolBar又会出现,但别忘了,这是AppBarLayout的功能,ToolBar可办不到。由于要滑动,那么我们的AppBarLayout一定是和可以滑动的View一起使用的,比如RecyclerView,ScollView等。

我们看看上面的到底怎么实现的:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_coor_app_bar"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.nanchen.coordinatorlayoutdemo.CoorAppBarActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways">

        </android.support.v7.widget.Toolbar>
    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recycler"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

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

我们可以看到,上面出现了一个app:layouy_scrollFrags的自定义属性设置,这个属性可以定义我们不同的滚动行为。

2、layout_scrollFlags

根据官方文档,layout_scrollFlags的取值可以为以下几种。
a) scroll
设成这个值的效果就好比本View和scrolling view是“一体”的。具体示例我们在上面已经给出。有一点特别需要我们的注意,为了其他的滚动行为生效,必须同时指定scroll和相应的标记,比如我们想要exitUntilCollapsed所表现的滚动行为,必须将layout_scrollFlags指定为“scroll|exitUntilCollapsed”。
b) exitUntilCollapsed
当scrolling view向下滚动时,本View会一起跟着向下滚动。实际上就好比我们同时对scrolling view和本View进行向下滚动。
c)enterAlways
当scrolling view向下滚动时,本View会一起跟着向下滚动。实际上就好比我们同时对scrolling view和本View进行向下滚动。
d)enterAlwaysCollapsed
从名字上就可以看出,这是在enterAlways的基础上,加上了“折叠”的效果。当我们开始向下滚动scrolling view时,本View会一起跟着滚动直到达到其“折叠高度”(即最小高度)。然后当scrolling view滚动至顶部内容完全显示后,再向下滚动scrolling view,本View会继续滚动到完全显示出来。 
e)snap
在一次滚动结束时,本View很可能只处于“部分显示”的状态,加上这个标记能够达到“要么完全隐藏,要么完全显示”的效果。

四、CollapsingToolBarLayout

这个东西,我相信很多博客和技术文章都会把CollapsingToolBarLayout和CoordinatorLayout放一起讲,这个东西的确很牛。我们同样先看看官方文档介绍:
  CollapsingToolbarLayout通常用来在布局中包裹一个Toolbar,以实现具有“折叠效果“”的顶部栏。它需要是AppBarLayout的直接子View,这样才能发挥出效果。CollapsingToolbarLayout包含以下特性:

1、Collasping title(可折叠标题):当布局完全可见时,这个标题比较大;当折叠起来时,标题也会变小。标题的外观可以通过expandedTextAppearance和collapsedTextAppearance属性来调整。

2、Content scrim(内容纱布):根据CollapsingToolbarLayout是否滚动到一个临界点,内容纱布会显示或隐藏。可以通过setContentScrim(Drawable)来设置内容纱布。

3、Status bar scrim(状态栏纱布):也是根据是否滚动到临界点,来决定是否显示。可以通过setStatusBarScrim(Drawable)方法来设置。这个特性只有在Android5.0及其以上版本,我们设置fitSystemWindows为ture时才能生效。

4、Parallax scrolling children(视差滚动子View):子View可以选择以“视差”的方式来进行滚动。(视觉效果上就是子View滚动的比其他View稍微慢些)

5、Pinned position children:子View可以选择固定在某一位置上。

上面的描述有些抽象,实际上对于Content scrim、Status bar scrim我们可以暂时予以忽略,只要留个大概印象待以后需要时再查阅相关资料即可。下面我们通过一个常见的例子介绍下CollapsingToolbarLayout的基本使用姿势。

我们来看看一个常用的效果:


图片.png
<?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:id="@+id/activity_coor_tool_bar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.nanchen.coordinatorlayoutdemo.CoorToolBarActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:fitsSystemWindows="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="200dp"
            app:contentScrim="@color/colorPrimary"
            app:expandedTitleMarginStart="100dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
            app:statusBarScrim="@android:color/transparent"
            app:titleEnabled="false">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                android:scaleType="centerCrop"
                android:src="@mipmap/logo"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.6"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay"
                app:title=""/>

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

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

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recycler"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    <TextView
        android:id="@+id/toolbar_title"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="-100dp"
        android:alpha="0"
        android:elevation="10dp"
        android:gravity="center_vertical"
        android:text="爱吖校推-你关注的,我们才推"
        android:textColor="@android:color/white"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_behavior=".SimpleViewBehavior"
        app:svb_dependOn="@id/appbar"
        app:svb_dependType="y"
        app:svb_targetAlpha="1"
        app:svb_targetY="0dp"/>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@mipmap/ic_start"
        app:layout_anchor="@id/appbar"
        app:layout_anchorGravity="bottom|right"/>

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

我们在xml文件中为CollapsingToolBarLayout的layout_scrollFlags指定为“scroll|exitUntilCollapsed|snap”,这样便实现了向上滚动的折叠效果。

CollapsingToolbarLayout本质上同样是一个FrameLayout,我们在布局文件中指定了一个ImageView和一个Toolbar。ImageView的layout_collapseMode属性设为了parallax,也就是我们前面介绍的视差滚动;而Toolbar的layout_collaspeMode设为了pin,也就是Toolbar会始终固定在顶部。

五、写在最后

本次的design包下的CoordinatorLayout和AppBarLayout就讲述到这里,后续还将持续更新,欢迎拍砖~

查看源码请移步Github

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

推荐阅读更多精彩内容