作者:Nick Butcher, Android 设计师 + 开发工程师, Google
在为 Google I/O 2018 Android 团队工作期间,我们的主要作品之一就是这个官方的应用,允许与会者和远程人员了解会议细节,建立个性化的时间表,并在会场预订座位。我们在应用中构建了许多有趣的动效,而且我们也开源了这个应用的代码,下面我想强调一些这些设计实例,以及一些有趣的实现细节,希望能给大家日常的动效设计带来灵感。
通常我们会在应用中使用 3 种类型的动效:
主动效:用于强化品牌视觉,并带来令人惊喜的视觉焦点
屏幕切换
状态变化
接下来,我想详细介绍一下这些动效的设计细节。
倒计时动画
可以这么说,Google I/O 官方应用的其中一部分作用就是为会议带来兴奋感和期待感。因此,我们在首屏幕和信息页面都显示了大型的倒计时动画。这也是将大会的活动品牌视觉进行呈现的绝佳机会,为应用增添了很多特色。
△ 主屏幕抓眼球的倒计时动画
这个动效是由一位动效设计师设计的,并以一系列的 Lottie json 文件交到我们手中:每秒显示一个数字,让它以动效的形式 “in” 然后 “out”。Lottie 格式的文件可以轻松地放入 assets,甚至提供了便利的操作方法,如 setMinAndMaxProgress,它允许我们只播放动效的前半部分或后半部分 (从而只显示动效的进场或出场)。
真正好玩的地方是将多个单独的动画文件布局成整体的倒计时画面。为此,我们创建了 CountdownView,一个相当复杂的自定义 ConstraintLayout,其中包含了多个 LottieAnimationViews。在这里,我们创建了一个 Kotlin 委托来封装启动适当的动效。这样我们就可以简单地为每一个应该显示的数字的委托分配一个用来显示的 Int,随后委托就会完成设置并启动动效。我们扩展了 ObservableProperty,确保我们只在数字更改时才运行动效。我们的动效循环每秒只发布一个可运行状态 (在和视图关联起来的时候),这个状态计算每个视图应该显示哪个数字并对委托进行更新。
预约会议
应用的关键操作之一是让与会者预定座位。因此,我们在会议详情屏幕上的 FAB 中突出显示此操作。我们认为应该仅当预定操作在后端成功完成后再进行提示 (不像那些不太重要的操作,例如 “关注” 一场演讲,我们就几乎是同步在界面上显示关注成功)。等待来自后端的响应可能需要一些时间,为了这个预定的过程感觉更快捷,我们使用动效图标,表明我们正在对预定进行处理,然后再平滑过渡到成功的状态中。
△ 预定的动画,有展示后端操作的动画过程
这个图标需要反映的状态很复杂:会议可能是可预订的,可能已经预留了座位,如果座位已满,则可能产生等候名单,他们也可能出现在等待列表上,或是在会议即将开始时预约功能被禁用了。这会导致应用需要显示多种动效以代表各种不同的状态。为了简化这些转场效果,我们决定始终显示 “后端操作” 状态,也就是上面动画中的沙漏。因此,每次动效切换实际上都是成对的:状态 1 → 后端操作 & 后端操作 → 状态 2。这样就简化了很多事情 (不然您可以想象可能出现的排列组合会有多少)。接下来我们使用 shapeshifter 构建了所有这些动效。
为了显示这些动效,我们使用了自定义视图和 AnimatedStateListDrawable (ASLD)。如果你还没有用过 ASLD,我们简单介绍一下:正如其名,它是动效版的 StateListDrawable,让你不仅能够为每个状态提供不同的 Drawable,还能够提供过渡状态之间的转场 (以 AnimatedVectorDrawable 或 AnimationDrawable 的形式)。
我们创建了一个自定义视图来支持这些自定义的预定状态。视图提供一些标准状态,如已按下或已选中。同样,您也可以定义自己的状态,并根据状态显示不同的 Drawable。我们自行定义了 state_reservable,state_reserved 等,随后我们会再为这些不同的状态创建一个枚举,封装视图状态以及任何相关的属性,如相关的内容描述。然后,我们的业务逻辑可以简单地从视图上的这个枚举中设置适当的值 (通过数据绑定),这将更新 Drawable 的状态,然后通过 ASLD 启动动效。自定义状态和 AnimatedStateListDrawable 是实现这一点的一种巧妙方法,将多个状态保留在声明层中,从而产生最少的视图代码。
演讲者动效
许多屏幕间的转场动画都可以直接用标准窗口动效。我们另辟蹊径的地方是,前往演讲者介绍信息屏幕的转场效果。如下图,演讲者头像在演讲信息和演讲者信息两个屏幕之间共享,是典型的共享元素转场动画,这样也有助于理解前后两个屏幕间的内容关联。
△ 演讲者头像在两个屏幕间共享
这里是一个非常标准的共享元素转换,在 ImageView 上使用了 ChangeBounds 和 ArcMotion 类。
更有意思的是,启动这个转场效果本身也会纳入我们的导航设计/开发模式中来。大体上讲,这种模式将输入事件 (如点击一位演讲者) 与导航事件分离,让 ViewModel 来负责如何响应输入。在这种情况下,这种解耦意味着,ViewModel 将会暴露出 Events 的 LiveData,它只知道需要导航到哪个 ID 的演讲者。启动共享元素转场效果需要共享 View,而在这一瞬间这个 View 还没有。我们解决这个问题的方式是,在绑定时将演讲者的 ID 作为标签存储在视图上,以便稍后当我们需要导航到特定的演讲者细节屏幕时检索该视图。
过滤器
会议应用的核心功能之一是帮助用户从许多会议中过滤出感兴趣的那些。每个话题都有一个与之相关的颜色,以便识别。我们收到的设计方案基于 “Chip” 动画,便于用户与各个话题进行操作。
△ Chip 列表很适合用来挑选感兴趣的话题
Material Components 里提供了默认的 Chip 控件,但我们决定使用自定义视图,以便更好地控制各个状态的显示内容和动效。我们使用 canvas 进行绘图并使用 StaticLayout 来显示文本。该视图具有单一的 progress 属性,即 0 - 未选中 / 1 - 已选中。在状态切换的时候,我们会同时修改视图中的显示元素,并使用线性插值来决定每一帧的形状和颜色。
最初在实现这个控件时,我让视图实现了 Checkable 接口,并在 setChecked 方法被调用来给状态赋新值的时候启动动效。不过当我们在 RecyclerView 中显示多个过滤器时,会出现用户的操作触发的动效和数据绑定触发的动效冲突的情况。为了解决这个问题,我们单独提供了一个触发动效的方法来避免这种冲突。
此外,当我们刚刚给应用里引入这个动画效果时,我们发现它有丢帧。是我的动效代码有问题吗?这些控件位于一个 BottomSheet 中,且被放置在会议日程安排画面的前面。当过滤器里的值被修改时,我们会运行过滤会议形成的业务逻辑,并且更新关注列表里的关注内容,这些看来都没什么问题。后来经过排查,我们确定问题在于,当过滤器里的内容被修改的时候,负责显示日程的 RecyclerViews 的 ViewPager 尽职尽责地启动了,并根据新提供的数据进行了更新。这导致许多视图出现了因为内容增加而“膨胀”的现象,更因为这个视图内容的增加而触发了自适应布局的逻辑,来重新渲染其中的内容。所有的这些逻辑都是自动的,也没问题……除了会撑爆我们的刷新率。但关注列表其实在过滤器的 “后面” (被遮住了),于是我们决定延迟执行关注列表里的内容更新 (直到过滤器里的值都被确定,且动效开始运行时才更新),我们牺牲了一些实现的复杂性,换取了更加顺畅的用户体验。最初我使用 postDelayed 实现它,但这导致了 UI 测试时出现问题。于是,我们修改了启动动效的方法,允许它接受一个 lambda 表达式。这样一来,我们就能在保持用户操作的动效和高效的测试之间找到一个平衡。
一起动起来吧!
总的来说,我觉得动效确实有助于改善应用的体验,以及提升品牌表现力。希望这篇文章能让您明白我们在各个环节使用动效的动机,以及具体实现它们的做法。更重要的是,我们希望您的应用中也能出现丰富多彩的动效,在抓人眼球的同时也能很好地表达品牌信息和交互意图。
点击屏末 | 阅读原文 | 获取 “Google I/O 官方应用” 开源代码。
推荐阅读: