Material Design 系列之 CoordinatorLayout 开发详解

前言

CoordinatorLayout 遵循 Material 风格,结合 AppbarLayout,CollapsingToolbarLayout 等可产生各种炫酷的效果,本篇博客就将介绍 CoordinatorLayout 的各种酷炫效果。

image

一、View 介绍

1、CoordinatorLayout

又名协调者布局,它是 support.design 包中的控件。简单来说,CoordinatorLayout 是用来协调其子 view 并以触摸影响布局的形式产生动画效果的一个 super-powered FrameLayout,其典型的子 View 包括:FloatingActionButtonSnackBar。注意:CoordinatorLayout 是一个顶级父 View

2、AppBarLayout

AppBarLayoutLinearLayout 的子类,必须在它的子 view 上设置 app:layout_scrollFlags 属性或者是在代码中调用 setScrollFlags()设置这个属性。

AppBarLayout 的子布局有 5 种滚动标识(上面代码 CollapsingToolbarLayout 中配置的app:layout_scrollFlags 属性):

  • scroll:所有想滚动出屏幕的 view 都需要设置这个 flag, 没有设置这个 flag 的 view 将被固定在屏幕顶部。
  • enterAlways:这个 flag 让任意向下的滚动都会导致该 view 变为可见,启用快速“返回模式”。
  • enterAlwaysCollapsed:假设你定义了一个最小高度(minHeight)同时 enterAlways 也定义了,那么 view 将在到达这个最小高度的时候开始显示,并且从这个时候开始慢慢展开,当滚动到顶部的时候展开完。
  • exitUntilCollapsed:当你定义了一个 minHeight,此布局将在滚动到达这个最小高度的时候折叠。
  • snap:当一个滚动事件结束,如果视图是部分可见的,那么它将被滚动到收缩或展开。例如,如果视图只有底部 25%显示,它将折叠。相反,如果它的底部 75%可见,那么它将完全展开。

3、CollapsingToolbarLayout

CollapsingToolbarLayout 作用是提供了一个可以折叠的 Toolbar,它继承自 FrameLayout,给它设置 layout_scrollFlags,它可以控制包含在 CollapsingToolbarLayout 中的控件(如:ImageView、Toolbar)在响应 layout_behavior 事件时作出相应的 scrollFlags 滚动事件(移除屏幕或固定在屏幕顶端)。

CollapsingToolbarLayout 可以通过 app:contentScrim 设置折叠时工具栏布局的颜色,通过 app:statusBarScrim 设置折叠时状态栏的颜色。默认 contentScrimcolorPrimary 的色值,statusBarScrimcolorPrimaryDark 的色值。

CollapsingToolbarLayout 的子布局有 3 种折叠模式(Toolbar 中设置的 app:layout_collapseMode

  • off:默认属性,布局将正常显示,无折叠行为。
  • pin:CollapsingToolbarLayout 折叠后,此布局将固定在顶部。
  • parallax:CollapsingToolbarLayout 折叠时,此布局也会有视差折叠效果。当 CollapsingToolbarLayout 的子布局设置了 parallax 模式时,我们还可以通过 app:layout_collapseParallaxMultiplier 设置视差滚动因子,值为:0~1。

常用属性:

  • app:contentScrim:当 Toolbar 收缩到一定程度时的所展现的主体颜色。即 Toolbar 的颜色。
  • app:title:当 titleEnable 设置为 true 的时候,在 toolbar 展开的时候,显示大标题,toolbar 收缩时,显示为 toolbar 上面的小标题。
  • app:scrimAnimationDuration:该属性控制 toolbar 收缩时,颜色变化的动画持续时间。即颜色变为 contentScrim 所指定的颜色进行的动画所需要的时间
  • app:collapsedTitleTextAppearance:指定 toolbar 收缩时,标题字体的样式。
  • app:collapsedTitleGravity : 指定 toolbar 收缩时的标题文字对齐方式。
  • app:expandedTitleTextAppearance : 指定 toolbar 展开后的标题文字字体。
  • app:expandedTitleGravity:指定 toolbar 展开时,title 所在的位置。类似的还有 expandedTitleMargin、collapsedTitleGravity 这些属性。
  • app:expandedTitleMargin : 指定展开后的标题四周间距。

4、NestedScrollView

在新版的 support-v4 兼容包里面有一个 NestedScrollView 控件,这个控件其实和普通的 ScrollView 并没有多大的区别,这个控件其实是 Meterial Design 中设计的一个控件,目的是跟 MD 中的其他控件兼容。应该说在 MD 中,RecyclerView 代替了ListView,而 NestedScrollView 代替了 ScrollView,他们两个都可以用来跟ToolBar 交互,实现上拉下滑中 ToolBar 的变化。在 NestedScrollView 的名字中其实就可以看出他的作用了,Nested 是嵌套的意思,而 ToolBar 基本需要嵌套使用。

5、FloatingActionButton

FloatingActionButton 就是一个漂亮的按钮,其本质是一个 ImageVeiw。有一点要注意,Meterial Design 引入了 Z 轴的概念,就是所有的 view 都有了高度,他们一层一层贴在手机屏幕上,而 FloatingActionButton 的 Z 轴高度最高,它贴在所有 view 的最上面,没有 view 能覆盖它。

6、Behavior

Behavior 只有是 CoordinatorLayout 的直接子 View 才有意义。只要将 Behavior 绑定到 CoordinatorLayout 的直接子元素上,就能对触摸事件(touch events)、window insets、measurement、layout 以及嵌套滚动(nested scrolling)等动作进行拦截。Design Library 的大多功能都是借助 Behavior 的大量运用来实现的。当然,Behavior 无法独立完成工作,必须与实际调用的 CoordinatorLayout 子视图相绑定。具体有三种方式:通过代码绑定、在 XML 中绑定或者通过注释实现自动绑定

二、应用实战

1、CoordinatorLayout + FloatingActionButton 使用

市面上众多 APP 首页都会使用 CoordinatorLayout 结合 FloatingActionButton 的效果,当列表垂直方向滑动时,FloatingActionButton 随之显示与隐藏。

  • 布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.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"
      android:background="@color/white">
    
    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize"
    android:clipChildren="false"
    android:clipToPadding="false"/>
    
    <androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/colorPrimary"
    app:titleTextColor="@color/white"
    tools:ignore="MissingConstraints" />
    
    <com.google.android.material.floatingactionbutton.FloatingActionButton
    android:onClick="fabClick"
    app:layout_behavior=".coordinatorLayout.behavior.FabBehavior"
    app:rippleColor="@color/colorPrimaryDark"
    app:backgroundTint="@color/colorPrimary"
    android:id="@+id/fab"
    android:clickable="true"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end|bottom"
    android:layout_margin="20dp"
    app:fabSize="auto"
    android:focusable="true" />
    

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

可以看到布局文件最外层使用 CoordinatorLayout,里面包含了 RecyclerViewToolbarFloatingActionButton三个子 View,使用android:layout_gravity="end|bottom"属性设置 FloatingActionButton 显示在右下角位置。关于 ToolBarFloatingActionButton 使用还不熟悉的朋友请自行补课。

FloatingActionButton 设置app:layout_behavior属性,这里的".coordinatorLayout.behavior.FabBehavior"是自定义 Behavior 实现。

  • 自定义 Behavior

       public class FabBehavior extends FloatingActionButton.Behavior {
    
        private boolean visible = true;
    
        public FabBehavior(Context context, AttributeSet attrs) {
              super(context, attrs);
      }
    
        @Override
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                   @NonNull FloatingActionButton child,
                                   @NonNull View directTargetChild,
                                   @NonNull View target,
                                   int axes, int type) {
    //判断如果是垂直滚动则返回true
    return axes == ViewCompat.SCROLL_AXIS_VERTICAL || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
      }
    
        @Override
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                           @NonNull FloatingActionButton child,
                           @NonNull View target,
                           int dxConsumed, int dyConsumed,
                           int dxUnconsumed, int dyUnconsumed, int type) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed  , type);
    
    if (dyConsumed > 0 && visible) {
        visible = false;
        child.animate().scaleX(0).scaleY(0).setInterpolator(new AccelerateInterpolator(3));
    } else if (dyConsumed < 0 && !visible) {
        visible = true;
        child.animate().scaleX(1).scaleY(1).setInterpolator(new DecelerateInterpolator(3));
    }
      }
    }
    
  • onStartNestedScroll:一定要按照自己的需求返回 true,该方法决定了当前控件是否能接收到其内部 View(非并非是直接子 View)滑动时的参数;假设你只涉及到纵向滑动,这里可以根据 nestedScrollAxes 这个参数,进行纵向判断。

  • onNestedScroll:在内层 view 将剩下的滚动消耗完之后调用,可以在这里处理最后剩下的滚动

image.gif

2、CoordinatorLayout + FloatingActionButton + Snackbar 使用

日常开发过程中,使用 FloatingActionButton + Snackbar 会出现一个 BUG,当 Snackbar 显示时会覆盖 FloatingActionButton 显示,这种展示效果很不友好,如下图所示。

image

解决问题最简单的方式就是最外层布局更换成 CoordinatorLayout,即可轻松完成

  • Activity 类:(提醒:布局文件与上面一致)

    public class SnackBarBehaviorActivity extends BaseActivity {
    
      @BindView(R.id.recyclerView)
      RecyclerView recyclerView;
      RecyclerViewAdapter recyclerViewAdapter;
      private List<String> stringList = new ArrayList<>();
    
      @Override
      protected void initView() {
          setToolbarTitle("SnackBar Behavior");
          setToolBarCallBack();
          for (int i = 1; i <= 100; i++) {
            stringList.add("ITEM " + i);
        }
    recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
    recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
    recyclerViewAdapter = new RecyclerViewAdapter(R.layout.recyclerview_item, stringList);
    recyclerViewAdapter.bindToRecyclerView(recyclerView);
    }
    
    @Override
    protected int getLayoutResID() {
    return R.layout.activity_fabbehavior_layout;
    }
    
      public void fabClick(View view) {
          Snackbar.make(view, "谁让你点击的?", Snackbar.LENGTH_SHORT)
            .setAction("关闭", v -> {
                Toast.makeText(SnackBarBehaviorActivity.this, "Sorry", Toast.LENGTH_SHORT).show();
            }).addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
        @Override
        public void onDismissed(Snackbar transientBottomBar, int event) {
            super.onDismissed(transientBottomBar, event);
        }
    
        @Override
        public void onShown(Snackbar transientBottomBar) {
            super.onShown(transientBottomBar);
        }
    }).show();
    }
    }
    

其实代码很简单,按照正常逻辑处理就完事了,主要在于布局文件的合理搭配使用上。

image

3、CoordinatorLayout + AppBarLayout 使用

CoordinatorLayout + AppBarLayout 应该是现在 APP 主流的布局方式,配合 ToolbarTabLayout 可以实现炫酷的列表滑动折叠效果,提升应用整体档次,增加用户体验。

  • 布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.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"
      android:orientation="vertical">
    
    <com.google.android.material.appbar.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        app:layout_scrollFlags="scroll|enterAlways"
        app:titleTextColor="@color/white"
        tools:ignore="MissingConstraints" />
    
    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    
      </com.google.android.material.appbar.AppBarLayout>
    
    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewPage"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
      </androidx.coordinatorlayout.widget.CoordinatorLayout>` </pre>
    

使用 AppBarLayoutToolbarTabLayout 包裹,底部使用 ViewPager 布局,ViewPagerapp:layout_behavior=”@string/appbar_scrolling_view_behavior”Behavior 是系统默认的,我们也可以根据自己的需求来自定义 Behavior

注意 Toolbar 设置了 app:layout_scrollFlags="scroll|enterAlways"属性,这是一个非常重要的属性,子布局通过此确定是否可滑动。 其中有五种属性,具体含义可阅读上文 AppBarLayout 介绍。

  • Activity 类

      public class CoordinatorCardViewActivity extends BaseActivity {
        private List<Fragment> fragments = new ArrayList<>();
        @BindView(R.id.tabLayout)
        TabLayout tabLayout;
        @BindView(R.id.viewPage)
        ViewPager viewPage;
        private String mTitles[] = {"TAB 0", "TAB 1", "TAB 2"};
    
        @Override
        protected void initView() {
          setToolBarCallBack();
          setToolbarTitle("Behavior");
    // 设置文本字体颜色[未选中颜色、选中颜色]
    tabLayout.setTabTextColors(getResources().getColor(R.color.gray),
            getResources().getColor(R.color.white));
    // 设置下划线跟文本宽度一致
    tabLayout.setTabIndicatorFullWidth(true);
    // 设置TabLayout和ViewPager绑定
    tabLayout.setupWithViewPager(viewPage, false);
    // 添加TAB标签
    for (String mTitle : mTitles) {
        tabLayout.addTab(tabLayout.newTab().setText(mTitle));
    }
    fragments.add(CardImageFragment.newInstance());
    fragments.add(CardTextFragment.newInstance());
    fragments.add(CardBelleFragment.newInstance());
    
    viewPage.setAdapter(new FragmentAdapter(getSupportFragmentManager(), tabLayout.getTabCount()));
    // 设置ViewPager默认显示index
    viewPage.setCurrentItem(0);
      }
    
      @Override
        protected int getLayoutResID() {
          return R.layout.activity_coordinator_cardviewlayout;
      }
    
      class FragmentAdapter extends FragmentPagerAdapter {
    
    public FragmentAdapter(@NonNull FragmentManager fm, int behavior) {
        super(fm, behavior);
    }
    
    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        return mTitles[position];
    }
    
    @NonNull
    @Override
    public Fragment getItem(int position) {
        return fragments.get(position);
    }
    
    @Override
    public int getCount() {
        return fragments.size();
    }
      }
    
    } 
    

Activity 类主要设置了 TabLayoutViewPager 相关属性,如果对 TabLayout + ViewPager 还不熟悉的朋友可以阅读之前文章。实现具体效果如下:

image

4、CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout 使用

  • 布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
    
    <com.google.android.material.appbar.AppBarLayout
    android:fitsSystemWindows="true"
    android:id="@+id/app_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
    
    <com.google.android.material.appbar.CollapsingToolbarLayout
        app:title="Android"
        app:titleEnabled="true"
        android:id="@+id/toolbar_layout"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        app:contentScrim="?attr/colorPrimary"
        app:layout_scrollFlags="scroll|exitUntilCollapsed">
    
        <ImageView
            android:scaleType="centerCrop"
            android:fitsSystemWindows="true"
            app:layout_collapseMode="parallax"
            app:layout_collapseParallaxMultiplier="0.8"
            app:layout_scrollFlags="scroll|snap|enterAlways|enterAlwaysCollapsed"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@mipmap/android" />
    
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_collapseMode="pin"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    
    </com.google.android.material.appbar.CollapsingToolbarLayout>
      </com.google.android.material.appbar.AppBarLayout>
    
      <androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Design for Android"
        android:textColor="@color/white"
        android:textStyle="bold" />
    
      </androidx.core.widget.NestedScrollView>
    
      <com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    android:src="@android:drawable/ic_dialog_email"
    app:layout_anchor="@id/app_bar"
    app:layout_anchorGravity="bottom|end" />
    
    </androidx.coordinatorlayout.widget.CoordinatorLayout>` </pre>
    

FloatingActionButton 这个控件通过 app:layout_anchor 这个设置锚定在了 AppBarLayout 下方。FloatingActionButton 源码中有一个 Behavior 方法,当 AppBarLayout 收缩时,FloatingActionButton 就会跟着做出相应变化。

  • Activity 类

    public class AppBarBehaviorActivity extends BaseActivity {
    
      @Override
      protected void initView() {
          StatusBarUtils.setTransparent(this);
          setToolBarCallBack();
          setSupportActionBar(toolbar);
          getSupportActionBar().setDisplayHomeAsUpEnabled(true);
      }
    
      @Override
        protected int getLayoutResID() {
          return R.layout.activity_coordinator_collapsinglayout;
      }
    } 
    

注意:在背景图片沉浸式的时候,只靠上面的 XML 布局是无法实现的,还需要 2 行代码:

      setSupportActionBar(toolbar);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
image

三、总结

本文主要介绍 CoordinatorLayout 的基本使用,还是非常简单的,CoordinatorLayout作为协调者,肯定是非常重要的。CoordinatorLayout 是一个“加强版”的 FrameLayout,我们只需要知道他两个作用

(1) 用作应用的顶层布局管理器

(2) 通过为子 View 指定 behavior 实现自定义的交互行为。在我们做 Material Design 风格的 app 时通常都使用 CoordinatorLayout 作为布局的根节点,以便实现特定的 UI 交互行为。

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

推荐阅读更多精彩内容