vlambda博客
学习文章列表

SwiftUI 动画进阶 - part2 几何效果

译自 https://swiftui-lab.com/swiftui-animations-part2/
建议横屏阅读代码
在系列文章的第一篇,我介绍了 Animatable 协议,以及如何用它来动画化路径。接下来我们将用同一个协议来动画化矩阵变换,并且用到一个新工具: GeometryEffect 。如果你还没有读过第一部分,不知道 Animatable 协议是什么,我建议你先阅读这部分。不过,如果你关注的是 GeometryEffect 而不是动画,那也可以跳过第一部分,继续阅读。

几何效果

GeometryEffect 是一个遵循 Animatable ViewModifier 的协议,为了遵循 GeometryEffect ,你需要实现下面的方法:
 
   
   
 
我们不妨称你的效果叫 SkewEffect ,应用在视图上的方式如下:
 
   
   
 
Text("Hello") 将会通过 SkewEfect.effectValue() 方法输出的值做矩阵变换。就这么简单,注意,改变会影响到这个视图,但并不影响它的父视图或者子视图。
由于 GeometryEffect 也遵循 Animatable ,你也可以添加 animatableData 属性,换言之,你可以获得动画效果。
你可能没有意识到,但你其实可能已经一直在用 GeometryEffect 了。如果你使用过 .offset() ,实际上就是在用 GeometryEffect 。让我向你展示它是如何实现的:
 
   
   
 

动画关键帧

大部分动画框架都有关键帧的概念,它是一种将动画分块的方式。尽管 SwiftUI 并没有这些特性,我们可以模拟出来。在后面的例子中,我们要创建一个水平移动视图的效果,伴随着从开始时倾斜到结束时还原的效果:

倾斜的效果会在动画的前 20% 增加,在动画的后 20% 减少,在中间保持稳定。好了,看看如何我们应对这个挑战。
我们将从创建倾斜和平移视图的效果开始,先忽略 20% 的需求。假如你对矩阵变换不太了解,那也无妨。你只需要知道: CGAffineTransform 的参数 c 影响倾斜,而参数 tx 影响 x 轴的偏移量。

SwiftUI 动画进阶 - part2 几何效果

 
   
   
 

仿造关键帧

接下来是有趣的部分,为了模拟关键帧,我们将定义一个从 0 变化到 1 的可动画化参数。当参数到达 0.2 时,表示我们到达动画的 20%,当参数到达 0.8 或者更大时,表示我们处于动画的最后 20%。我们的代码则相应地改变效果。基于以上这些,我们还要告诉效果我们是要把视图往右移动还是往左移,以便它往某一侧倾斜:
 
   
   
 
这里我们为了好玩,可以把效果应用到多个视图上,并且用 .delay() 动画 modifier 让动画效果错开。

SwiftUI 动画进阶 - part2 几何效果

动画反馈

接下来这个例子,我要向你展示一项简单的技术,让视图能够对效果动画的过程做出响应。
我们要创建一个执行 3d 旋转的效果。尽管 SwiftUI 已经有一个实现这个效果的 modifier 了,即 .rotation3DEffect() ,我们这个会有点特别。每当视图旋转到足以看到另外一面时,一个布尔绑定会相应更新。
通过对这个绑定的变化做出响应,我们在动画进行的过程中替换正在旋转的视图,这样能达到一种视图拥有两面的效果,下面是例子:

实现效果

让我们来实现效果。你将注意到,3d 旋转变换和你在 Core Animation 中用到的版本可能稍有不同。在 SwiftUI 中,默认锚点是位于视图的左上角,而 Core Animation 则是在中心。虽然 .rotationg3DEffect() modifier 允许你指定锚点,我们这里是在构建自定义效果,所以要自行处理锚点。由于我们无法修改锚点,我们得额外添加一些平移操作:
 
   
   
 
让我们看一看几何效果的代码,其中有一个有趣的事实,即 flipped @Binding 属性。我们用它来给视图反馈现在视图的哪一面正朝向用户。
在视图代码中,我们将用 flipped 值来条件化显示两个视图中的其中一个。在这个特定的例子中,我们还加了一个小把戏。你注意看视频,卡牌一直在变化,背面总是相同的,但前面每一轮都变化。所以它不是简单地给一面显示一个视图,另一面显示另一个视图。每一轮,我们用不同的卡牌。
为了遍历图像,我们要创建一个自定义的绑定,具体代码如下:
 
   
   
 
如之前提到,我们也可以不替换图像名称,而是使用两个完全不同的视图,这样也是可行的,代码如下:
 
   
   
 

让视图跟随路径

接下来,我们要构造一种完全不同的几何效果。在这个例子里,我们的效果会沿着任意的路径移动视图。这个问题有两大挑战:
  1. 如何获取路径中某一个特定的点的坐标
  2. 在沿着路径移动时,如何确定视图的朝向。在这个特定的例子中,我们要如何知道应该将飞机的机头朝向哪里(友情提示:需要用到一些三角学的知识)

SwiftUI 动画进阶 - part2 几何效果

这个效果的可动画化参数是百分比,代表飞机在路径上的位置。如果希望动画呈现飞机完全一整圈,我们要让参数从 0 变化到 1,对于 0.25,则表示飞机已经飞行了 1/4 路径的航程。

找出路径上的 x,y 位置

为了获取给定百分比值的飞机的位置,我们要利用 Path 结构体的 .trimmedPath() modifier。给定起始百分比和结束百分比,这个方法会返回一个 CGRect ,它包含了路径片段的边界。对于我们的需求,我们只需要以很近的起始和结束点来调用它,这样就能得到一个很小的矩形,然后用它的中心点作为我们的 x,y 位置。
 
   
   
 

找出方向

为了获得飞机的旋转角度,我们需要用到一些三角学。利用上面描述过的技术,我们要获取两个点:当前点和当前点稍微往前一些的点。借助一条辅助的连线,我们能算出一个角度,而这就是飞机的方向。
 
   
   
 

组合在一起

现在你已经拿到我们要实现目标的所有工具,可以实现效果了:
 
   
   
 

IgnoredByLayout()

我们的 GeometryEffect 旅程的最后一站是 .ignoredByLayout() 方法。让我们先瞧一眼文档怎么说:
返回一个跟 “自己” 一样的几何效果,但这个效果只会在渲染视图时应用,不会在布局计算时应用。这个方法通常用于禁用过渡时的布局变化。
我很快会介绍到过渡,与此同时,让我用 .ignoredByLayout() 来展示一些比较明显的效果。从这些效果中我们可以看出,根据添加方式的不同, GeometryReader 是如何报告不同的位置的(即,有无 .ignoredByLayout() 的区别)。

 
   
   
 

下一步

我们今天碰到的三个例子,几乎没有什么共同点,除了它们都是利用相同的协议来达成目标。 GeometryEffect 是简单的,它只有一个方法需要实现,而它的可能性却是无限的。我们要做的只是发挥一点想象力。
接下来,我们要去了解系列的最后一个协议: AnimatableModifier 。假如你觉得 GeometryEffect 算是强大的,那你也可以对 AnimatableModifier 期待满满了。
本文封面,来自 unsplash