专栏首页Godot游戏开发引擎介绍和入门【翻译】MotionLayout实现折叠工具栏(Part 1)

【翻译】MotionLayout实现折叠工具栏(Part 1)

【翻译】MotionLayout实现折叠工具栏(Part 1)

2018-08-13 by Liuqingwen | Tags: Android 翻译 | Hits

一、说明

没有严格按照中英对照进行的翻译,但是我尽量把意思翻译到位,能看原文的朋友可以直接欣赏原文啦。

本文特点:没有 Kotlin/Java 代码,讲解部分全为 XML 代码,阅读时间短,获取技能: MotionLayout 的入门和使用!发布时间: 8 月 10 号 ,作者: Mark Allison ,原文链接: https://blog.stylingandroid.com/motionlayout-collapsing-toolbar-part-1/

二、正文

谷歌 IO 2018 发布了 ConstraintLayout 2.0 版本,其中最重要的部分就是 MotionLayout 了,这玩意就是一个全新的、超牛的布局动画工具! Nicolas Roard 哥们早已发布了一个关于 MotionLayout 的完美详情介绍,我强烈推荐大家去阅读一下,从中理解 MotionLayout 组件的基础架构。本系列教程中,我会讲解如何使用 MotionLayout 来创建一个我们已经非常熟悉的动画行为:一个折叠工具栏动画( a Collapsing Toolbar )。

在我们开始之前,有必要在这里澄清一下:在 CoordinatorLayout 中使用 CollapsingToolbarLayout 来实现折叠工具栏是没任何问题的。当然了,如果你已经在自己的 App 中使用了,那么你在学会了这里的知识后也没什么必要做更改。也就是说, CoordinatorLayout 这个布局已经提供了一些非常有用的行为动画,如果你尝试去修改它,或者创建一些基于它的自定义动画,那都是相当困难的。相反, MotionLayout 提供了更多的灵活性,以我个人早期的经验来看,这是一个非常简单又易学的效果神器。而且, MotionLayout 让那些 CoordinatorLayout 望而却步的动画变得简单直接。学习来吧,骚年!

MotionLayout 和安卓上许多其他的动画框架的一个主要不同点在于:视图动画和属性动画运行的时长是给定的,比如指定动画的时长,取消某个动画都是可行的,但是不能做到用户控制一个正在进行中的动画。举个例子,一个折叠工具栏应该根据用户的滚动进行展开和折叠,所以实际动画的运行应该时刻跟随用户的拖拽进行。这也是那些框架办不到的地方。

废话不多说,让我们看下我们所要尝试模拟做到的行为动作。这里的代码展示了一个折叠工具栏,应用了 Material Components Library 库里的 CollapsingToolbarLayoutCoordinatorLayout 布局。

<?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">
 
    <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/recyclerview"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintTop_toBottomOf="@id/appbar"
      app:layout_behavior="@string/appbar_scrolling_view_behavior" />
 
    <com.google.android.material.appbar.AppBarLayout
      android:id="@+id/appbar"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:fitsSystemWindows="true"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
 
      <com.google.android.material.appbar.CollapsingToolbarLayout
        android:id="@+id/collapsing_toolbar"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:fitsSystemWindows="true"
        app:contentScrim="?attr/colorPrimary"
        app:expandedTitleGravity="bottom"
        app:expandedTitleMarginEnd="@dimen/activity_horizontal_margin"
        app:expandedTitleMarginStart="@dimen/activity_horizontal_margin"
        app:layout_scrollFlags="scroll|exitUntilCollapsed"
        app:title="@string/app_name">
 
        <ImageView
          android:id="@+id/toolbar_image"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:adjustViewBounds="true"
          android:contentDescription="@null"
          android:fitsSystemWindows="true"
          android:scaleType="centerCrop"
          android:src="@drawable/beach_huts" />
 
        <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" />
      </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
  
</androidx.coordinatorlayout.widget.CoordinatorLayout>

运行这段代码所得到的动画行为是这样的:

使用 MotionLayout 做到接近上述动画效果非常简单。首先从我们的布局文件开始:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout 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"
  tools:context=".MainActivity"
  app:layoutDescription="@xml/collapsing_toolbar"
  tools:showPaths="true">
 
  <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerview"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/toolbar_image" />
 
  <ImageView
    android:id="@+id/toolbar_image"
    android:layout_width="0dp"
    android:layout_height="200dp"
    android:adjustViewBounds="true"
    android:contentDescription="@null"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:fitsSystemWindows="true"
    android:scaleType="center"
    android:src="@drawable/beach_huts"
    android:background="@color/colorPrimary" />
 
  <ImageView
    android:id="@android:id/home"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:paddingTop="16dp"
    android:paddingBottom="16dp"
    android:src="@drawable/abc_ic_ab_back_material"
    android:tint="?android:attr/textColorPrimaryInverse"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
 
  <TextView
    android:id="@+id/title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginBottom="24dp"
    android:text="@string/app_name"
    android:textColor="?android:attr/textColorPrimaryInverse"
    android:textSize="32sp"
    android:textStyle="bold"
    app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
    app:layout_constraintStart_toStartOf="parent" />
 
</androidx.constraintlayout.motion.widget.MotionLayout>

这基本上是使用标准的 ConstraintLayout 创建出来的一个布局,唯一区别在于父布局实际为一个 MotionLayout 布局( MotionLayout 继承于 ConstraintLayout ,所以我们能够把它当做一个普通的 ConstraintLayout 来使用)。这个 MotionLayout 布局有一个属性名为: app:layoutDescription ,它也是奇迹所发生的地方。在这里我特意使用了最基本的 View 控件类型,用来说明视图本身并没有产生任何其他的行为动作。当然在实际 App 开发过程中我应该会使用 AppBarLayout 布局配合 Toolbar 控件吧。

如果在设计视图中查看这个布局,我们能看到布局所展示的工具栏处于展开的状态:

我刚刚提到奇迹发生的地方在于布局文件中的 app:layoutDescription 属性,那么现在我们来仔细瞧瞧它吧:

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
 
  <Transition
    app:constraintSetEnd="@id/collapsed"
    app:constraintSetStart="@id/expanded">
 
    <OnSwipe
      app:dragDirection="dragUp"
      app:touchAnchorId="@id/recyclerview"
      app:touchAnchorSide="top" />
 
  </Transition>
 
  <ConstraintSet android:id="@+id/expanded">
    <Constraint
      android:id="@id/toolbar_image"
      android:layout_height="200dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">
      <CustomAttribute
        app:attributeName="imageAlpha"
        app:customIntegerValue="255" />
    </Constraint>
    <Constraint
      android:id="@id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="8dp"
      android:layout_marginBottom="24dp"
      android:scaleX="1.0"
      android:scaleY="1.0"
      app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
      app:layout_constraintStart_toStartOf="parent">
    </Constraint>
  </ConstraintSet>
 
  <ConstraintSet android:id="@+id/collapsed">
    <Constraint
      android:id="@id/toolbar_image"
      android:layout_height="?attr/actionBarSize"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">
      <CustomAttribute
        app:attributeName="imageAlpha"
        app:customIntegerValue="0" />
    </Constraint>
    <Constraint
      android:id="@id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="20dp"
      android:layout_marginBottom="0dp"
      android:scaleX="0.625"
      android:scaleY="0.625"
      app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="@id/toolbar_image">
    </Constraint>
 
  </ConstraintSet>
 
</MotionScene>

这就是一个崭新的 MotionLayout ,也许看起来会有点恐惧,让我们来把它分解成一块块很好理解的小件然后再进行细细剖析。这里父布局首先是一个 MotionScene ,它持有所有我们定义的过渡动画所需要的组件。它包含两个 ConstraintSet ,每个 ConstraintSet 又定义了一套相关约束,这套约束体现为布局的一个固定的状态,这个我们会在后面深入探讨,目前我们只需要知道:有一个 ConstraintSet 表示工具栏的完全展开状态,而另一个表示工具栏处于完全闭合状态就足以。

这里的 Transition 元素定义了过渡动画的开始和结束状态,以及过渡效果如何和用户进行交互:

<Transition
    app:constraintSetEnd="@id/collapsed"
    app:constraintSetStart="@id/expanded">
 
    <OnSwipe
      app:dragDirection="dragUp"
      app:touchAnchorId="@id/recyclerview"
      app:touchAnchorSide="top" />
 
  </Transition>

两个属性: app:constraintSetStartapp:constraintSetEnd 分别指 ConstrainSet 所定义的两种状态:展开状态和折叠状态。元素 OnSwipe 把过渡动画和用户在 RecyclerView 上的拖拽操作绑定到了一起,也就是之前我们查看到的主布局中的列表。在展开和折叠状态下, RecyclerView 列表的上边缘是处于不同位置的,因为它被约束到了 ID 为 toolbar_imageImageView 图片下边缘,而这个过渡动画的实现正是由于控制着这个位置变量的值,这个值又源于用户拖拽 RecyclerView 来产生。别小看这里短短的 10 行 XML 代码,它背后可为我们做了大量的工作哦。这其中内部原理非常复杂,它由 RecyclerView 的滚动行为所驱动。

为了理解这两个 ConstrainSet 的定义,让我们先假设这里只有两件事情需要进行控制。第一件事情就是作为背景的 ImageView 图片( ID 为 toolbar_image )高度值的改变,以及图片透明度值的改变。通过改变图片的高度,这会导致 RecyclerView 的上边缘的移动,因为后者正是约束在图片的下边缘位置。第二个控件则是包含了标题( ID 为 title )的文本 TextView ,它需要移动的同时改变自身大小尺寸。

让我们首先看看这两个状态下图片 ImageView 的高度差。在展开状态下是这样的:

<ConstraintSet android:id="@+id/expanded">
  <Constraint
    android:id="@id/toolbar_image"
    android:layout_height="200dp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">
    <CustomAttribute
      app:attributeName="imageAlpha"
      app:customIntegerValue="255" />
  </Constraint>

对于折叠状态下则为:

<ConstraintSet android:id="@+id/collapsed">
  <Constraint
    android:id="@id/toolbar_image"
    android:layout_height="?attr/actionBarSize"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">
    <CustomAttribute
      app:attributeName="imageAlpha"
      app:customIntegerValue="0" />
  </Constraint>

这里只有两个小小的区别。第一个就是高度 layout_height ,第二个则为名为 imageAlphaCustomAttribute 。以 CustomAttribute 为名暗示着我们正在使用一个自定义视图 View ,但实际上并不是这样。我们使用的是一个标准的 ImageView 控件,当其位于 ConstraintSet 下的 Constraint 元素中时,其主要的属性变成可以是 ConstraintLayout.LayoutParams 中的任何一个属性,也可以是 View 中的任何一个属性,但即使像 ImageView 这类作为 View 的子类控件,我们仍然需要使用一个 CustomAttribute 符号,这里实际上和 ObjectAnimator 的原理非常类似。在这里,我们需要调整 ImageViewimageAlpha 值。当然,你也可以使用自定义视图上的自定义属性来实现,就如同 ObjectAnimator 一样。

另外 TextView 实际上也非常类似。展开状态是:

<Constraint
  android:id="@id/title"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="8dp"
  android:layout_marginBottom="24dp"
  android:scaleX="1.0"
  android:scaleY="1.0"
  app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
  app:layout_constraintStart_toStartOf="parent" />

还有,折叠状态则为:

<Constraint
  android:id="@id/title"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="20dp"
  android:layout_marginBottom="0dp"
  android:scaleX="0.625"
  android:scaleY="0.625"
  app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
  app:layout_constraintStart_toStartOf="parent" 
  app:layout_constraintTop_toTopOf="@id/toolbar_image"/>

这里,我们通过使用视图的缩放来改变 TextView 的大小。如果你对为什么这里选择缩放而非直接通过一个 CustomAttribute 改变 textSize 来实现表示怀疑的话,那么你要知道,在这里的理由就是因为相比简单直接地在文本上应用一个形变,通过改变文本大小和重新渲染会非常耗计算资源,所以我们为了在过渡动画结束时尽量减少锯齿的产生需要使用这个技巧。

我们所做的另一件事情则是改变边距大小( margins ),以及如何让 TextView 文本的位置相对于 ImageView 图片的位置而固定。在折叠状态下它会垂直居中,而在展开状态下它会对齐在底部,因此 TextView 会更多的相对于 ImageView 的大小尺寸来进行相关设定。

如果我们使用该布局来代替一开始我们就使用的 CoordinatorLayout 布局来实现,那么我们将会得到这样的行为:

这事实上效果已经非常接近,但是仔细看你会发现这里与刚开始我们使用的 CoordinatorLayout 方式有一个细微的区别:在 CoordinatorLayout 布局下图片的褪色渐变动画和 MotionLayout 版本中的行为有点不一致。这里卖个关子,在本系列文章的最后,我们将会介绍关于 MotionLayout 布局中更细粒度的一些控制。

三、总结

本篇的源代码请移步这里

© 2018 , Mark Allison 。保留所有版权。

Android 翻译


Comments:

Please enable JavaScript to view the comments powered by Disqus.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深扒EOSDice被攻击事件始末, TA是如何把游戏体验搞臭的?

    我们用十五期内容结束了对以太坊智能合约常规漏洞、高危漏洞的分析和总结。纵观整个以太坊安全生态发展历史,有太多的教训和痛楚值得我们铭记。

    区块链大本营
  • 优秀Java程序员必须了解的GC工作原理

    一个优秀的Java程序员必须了解GC的工作原理、如何优化GC的性能、如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统、实时系统等,只有...

    哲洛不闹
  • YY和小米直播牵手!直播行业跨平台合作成新趋势?

    11月13日,YY母公司欢聚时代宣布与小米直播达成战略合作,YY直播成为小米直播娱乐秀场内容的独家战略合作伙伴,双方将在内容、产品技术、运营和商业变现上展开全方...

    罗超频道
  • 精灵之息——不一样的游戏

    之前是打算做个纯网游,是以r/place为原型的一个游戏,然后里面塞各种各样的『技术』(比如tensorflow.js)。

    沙因Sign
  • 《不会被机器替代的人》:智能时代的生存策略

    一开始人们以为,高级的脑力劳动不会被替代,比如医生、律师,可是现在医生、律师的活都可以干,而且比人的效率高很多。

    杨熹
  • 腾讯公布Q3财报,云服务连续三季度同比增长超100%

    腾讯在昨天发布了第三季度的财报,财报显示三季度营收806亿元,同比增长24%,净利润(按通用会计准则)为233.3亿元,同比增长30%,仅仅从增长来看,腾讯第三...

    镁客网
  • 你的游戏开发第0课

    电子游戏是许多人喜爱甚至沉迷的事情。尤其对于程序员来说,开发游戏是不少人最初学习编程的动力。在之前,我发过一些游戏开发的教程和案例:

    Crossin先生
  • 打造低延迟互动音频: Oboe

    如果您有玩音乐游戏,或者音乐软件 (如 DJ 或者合成器) 的话,绝对会对音频的延迟深恶痛绝——延迟不但会让您对自己的操作不再自信,更会摧毁一段被打磨了很久的旋...

    Android 开发者
  • 【腾讯圣诞晚会TEG节目】这里的黎明静悄悄

    2018腾讯圣诞晚会·全新出发 ? 梦里好成功 如果你什么都没有,至少得有点想象力。 我们今年的男主角郝成功,就是一直生活在想象的美好中。他每天乘坐价值上亿的...

    腾讯技术工程官方号
  • 资本寒冬却获赌王家族加持,创梦天地凭什么?

    游戏行业因为版号问题、增长瓶颈以及资本寒冬,在2018年迎来至暗时刻。日前,今年5月启动IPO的游戏公司创梦天地有了新消息,让充满寒意的游戏行业多了一丝暖意。

    罗超频道

扫码关注云+社区

领取腾讯云代金券