本文是我学习《iOS Animations by Tutorials》 笔记中的一篇。 文中详细代码都放在个人Github上 andyRon/LearniOSAnimations。html
到目前为止,以前的文章只使用了二维动画——这是在平面设备屏幕上动画元素的最天然方式。 毕竟,从iOS 7扁平化后的世界中的按钮,文本字段,开关和图像没有了第三维; 这些元素存在于由X和Y轴定义的平面中:ios
核心动画能够帮助咱们摆脱这个二维世界; 虽然它不是真正的3D框架,但核心动画有不少好的方法能够帮助咱们在3D空间中描绘二维对象。git
换句话说,图层和动画仍然以二维方式进行描绘,但能够在3D空间中旋转和定位每一个元素的2D平面,以下所示:github
上面显示的是在3D空间中旋转的两个2D图像。 透视变形使咱们能够从渲染器的角度了解它们的位置。swift
本文将学习如何在3D空间中定位和旋转图层。CATransform3D
相似于CGAffineTransform
,但除了在x和y方向上缩放,倾斜和平移以外,它还带来了第三维:z。 z轴直接从设备屏幕朝向您的眼睛。数组
请考虑如下几个示例,以更好地了解透视的工做原理。缓存
将相机设置得很是靠近屏幕会相应地扭曲图层的视角:闭包
若是将相机离物体比较远时的视角:框架
最后,若是你在相机和屏幕之间设置了很大的距离:ide
预览:
24-简单的3D动画 —— 尝试新发现的有关相机距离和视角的知识。设置图层的透视图,处理图层的变换以旋转,平移和缩放三维图层。
25-中级3D动画 —— 在前一章的基础上,既然知道了m34和相机距离的秘密,就能够建立具备多个视图的各类3D动画。
本章将尝新发现的有关相机距离和视角的知识。
开始项目 Office Buddy是一个办公室帮助应用程序,供员工访问有关平常公司生活的分类信息。这个应用很简单就是点击左上角的按钮或者左右滑到,而后左边侧栏出现。下面👇将向这个开始项目中添加一些3D元素。
开始项目预览:
打开ContainerViewController.swift
,ContainerViewController
在屏幕上显示菜单视图控制器和内容视图控制器。 它还处理平移手势,以便用户能够打开和关闭菜单。
您的第一个任务是构建一个类方法,该方法为侧面菜单的给定百分比“开放性”建立相应的3D变换。 将如下方法声明添加到ContainerViewController
:
func menuTransform(percent: CGFloat) -> CATransform3D {
}
复制代码
上述方法接受菜单当前进度的单个参数,该参数由handleGesture(_ :)
中的代码计算,并返回CATransform3D
的实例。 您将直接将此方法的结果分配给菜单图层的transform属性。
将如下代码添加到上面方法中:
var identity = CATransform3DIdentity
identity.m34 = -1.0/1000
复制代码
这段代码可能看起来有点使人惊讶; 到目前为止,您只使用函数来建立或修改变换。 可是,这一次,您正在修改其中一个类的属性。
注意:
CATransform3D
和CGAffineTransform
分表表示4*4
和3*3
的数学矩阵,在Swift和OC中都是用结构体表示的。属性
m34
指矩阵的第3行第4列,这个属性比较经常使用,表示透视效果,m34 = -1 / D
,D能够理解为相机距离,D越小,透视效果越明显,必须在有旋转效果的前提下,才会看到透视效果。
对于普通应用程序中的UI元素,相机距离大概能够表示:
0.1 ... 500:很是接近,透视失真。
750 ... 2,000:视角不错,内容清晰可见。
2000+:几乎没有透视失真。
对于Office Buddy应用程序,1000点的距离将为菜单提供一个很是微妙的视角。
将如下代码添加到menuTransform(percent:)
的底部:
let remainingPercent = 1.0 - percent
let angle = remainingPercent * .pi * -0.5
复制代码
将如下代码添加到menuTransform(percent:)
的底部:
let rotationTransform = CATransform3DRotate(identity, angle, 0.0, 1.0, 0.0)
let translationTransform = CATransform3DMakeTranslation(menuWidth * percent, 0, 0)
return CATransform3DConcat(rotationTransform, translationTransform)
复制代码
在这里,使用rotationTransform
将图层绕y轴旋转。 菜单从左侧移动,所以还须要建立平移变换以沿x轴移动它,最终将菜单宽度设置为100%。 最后,链接两个转换并返回结果。
从setMenu(toPercent:)
中删除下面:
menuViewController.view.frame.origin.x = menuWidth * CGFloat(percent) - menuWidth
复制代码
替代为:
menuViewController.view.layer.transform = menuTransform(percent: percent)
复制代码
菜单栏的位置经过转换来控制了。
运行项目, 向右平移查看菜单如何围绕其y轴旋转:
菜单以3D形式旋转,但它围绕其水平中心旋转,菜单与内容视图控制器中间有间隙。
默认状况下,图层的锚点的x坐标为0.5,表示它位于中心。 将锚点的x设置为1.0,就不会出现上面的那种间隙,以下所示:
全部变换都是围绕图层的锚点计算的。
在viewDidLoad()
中找到如下行:
menuViewController.view.frame = CGRect(x: -menuWidth, y: 0, width: menuWidth, height: view.frame.height)
复制代码
如今在该行上方插入如下代码(在设置视图帧以前插入行很是重要,不然设置锚点将偏移视图):
menuViewController.view.layer.anchorPoint.x = 1.0
复制代码
这会使菜单围绕其右边缘旋转。
运行效果:
这看起来好多了!
阴影为3D动画带来了不少真实感。这里不须要使用任何先进的着色技术,只要旋转时更改alpha
。
将如下代码添加到setMenu(toPercent:)
:
menuViewController.view.alpha = CGFloat(max(0.2, percent))
复制代码
0.2让菜单最小还可见,百分比让菜单越小透明度越低。
因为此应用程序的背景为黑色,所以下降菜单视图的alpha值会使菜单中显示黑色并模拟阴影效果。
运行效果:
这是一个让3D效果更加真实的小细节。
若是仔细观察,会发现第一次点击按钮时,菜单不是以3D效果展现,之后才是。这是由于第一次切换菜单以前,设置3D动画参数和图层转换。在viewDidLoad()
中添加:
setMenu(toPercent: 0.0)
复制代码
让动画更加“完美”。若是在来回平移时盯着菜单足够长,会注意到菜单项的边框看起来像素化,以下所示:
核心动画不断重绘菜单视图控制器的全部内容,并在全部元素移动时从新计算全部元素的透视失真,这个过程当中会出现锯齿状边缘。
最好让Core Animation知道咱们不会在动画期间更改菜单内容,以便它能够渲染菜单一次并简单地旋转渲染和缓存的图像。 这听起来很复杂,但很容易实现。
找到handleGesture()
中的.began
代码块,此代码在用户平移操做时执行。
将如下代码添加到.began
代码块的末尾:
menuViewController.view.layer.shouldRasterize = true
menuViewController.view.layer.rasterizationScale = UIScreen.main.scale
复制代码
shouldRasterize
让核心动画将图层内容缓存为图像。 而后设置rasterizationScale
以匹配当前的屏幕比例。
运行,效果:
为避免在使用应用程序时进行任何没必要要的缓存,应该在动画完成后当即关闭光栅化。 在.failed
代码块找到动画完成闭包并添加如下代码:
self.menuViewController.view.layer.shouldRasterize = false
复制代码
如今,只在动画期间激活光栅化。提升了效率!😊
菜单展现时,菜单按钮也进行自身的旋转。具体来讲,您将围绕x轴和y轴建立旋转,以使菜单按钮在其对角线上翻转。
在ContainerViewController
的setMenu(toPercent:)
中添加:
let centerVC = centerViewController.viewControllers.first as? CenterViewController
if let menuButton = centerVC?.menuButton {
menuButton.imageView.layer.transform = buttonTransform(percent: percent)
}
复制代码
buttonTransform
函数为:
func buttonTransform(percent: CGFloat) -> CATransform3D {
var identity = CATransform3DIdentity
identity.m34 = -1.0/1000
let angle = percent * .pi
let rotationTransform = CATransform3DRotate(identity, angle, 1.0, 1.0, 0.0)
return rotationTransform
}
复制代码
效果以下:
在上一章24-简单3D动画中,学习了将透视应用到单个视图制做出简单的3D效果的动画; 事实上,一旦咱们知道m34和相机距离的秘密,就能够建立各类3D动画。
本章之前面的内容为基础,学习如何使用多个视图建立有意思的3D动画。
本章的开始项目 ***ImageGallery***是一个简单的飓风图库。
本章的开始项目是:
只是一个空白屏幕,顶部有两个按钮。
打开ViewController.swift
,会看到一个名为images
的数组,此数组就是一些图片信息。
ImagViewCard
类继承自UIImageView
而且有一个字符串属性title
来保存飓风标题,有一个名为didSelect
的属性,以便您能够轻松地在图像上设置点击处理程序。
第一个任务是将全部图像添加到视图控制器的视图中。 将如下代码添加到viewDidAppeae(_:)
的末尾:
for image in images {
image.layer.anchorPoint.y = 0.0
image.frame = view.bounds
view.addSubview(image)
}
复制代码
在上面的代码中,循环遍历全部图像,在y轴上将每一个图像的锚点设置为0.0,并调整每一个图像的大小,使其占据整个屏幕。 设置锚点可以让图像围绕其上边缘而不是中心的默认值旋转,以下图所示:
运行只会看到最后一张图片Hurricane Irene
,由于图片位置相同,叠加在一块儿来
显示飓风图像的名字,在viewDidAppear(_:)
的末尾添加如下行:
navigationItem.title = images.last?.title
复制代码
注意,目前没有在图像上设置任何透视转换;以后将直接在视图控制器的视图上设置透视图。
在上一章中,在单个视图上调整了transfor
m属性,而后在3D空间中旋转它。可是,因为您当前的项目有更多的我的视图,须要在3D中操做,您能够设置其父视图的透视图,从而节省大量工做。
将如下代码添加到viewDidAppear(_:)
:
var perspective = CATransform3DIdentity
perspective.m34 = -1.0/250.0
view.layer.sublayerTransform = perspective
复制代码
在这里,您可使用图层属性sublayerTransform
来设置视图控制器图层的全部子图层的透视图。 而后将子层转换与每一个单独层的自身变换组合。
这使您能够专一于管理子视图的旋转或平移,而无需担忧透视。 您将在下一节中更详细地了解它的工做原理。
toggleGallery(_:)
链接着右上方的“浏览”按钮,在此处将3D变换应用于四个图像。
将如下变量添加到toggleGallery(_:)
:
var imageYOffset: CGFloat = 50.0
for subview in view.subviews {
guard let image = subview as? ImageViewCard else {
continue
}
}
复制代码
因为您不仅是将全部图像旋转到原位而只是移动它们以产生”扇形“动画,所以您可使用imageYOffset
来设置每一个图像的偏移。 接下来,您须要遍历全部图像并运行其各自的动画。
在这里,您循环浏览视图控制器视图的全部子视图,并仅对做为ImageViewCard
实例的子视图执行操做。 在上面添加的guard
块以后添加如下代码,以替换此处的更多代码注释:
var imageTransform = CATransform3DIdentity
// 1
imageTransform = CATransform3DTranslate(imageTransform, 0.0, imageYOffset, 0.0)
// 2
imageTransform = CATransform3DScale(imageTransform, 0.95, 0.6, 1.0)
// 3
imageTransform = CATransform3DRotate(imageTransform, .pi/8, -1.0, 0.0, 0.0)
复制代码
首先将标识转换分配给imageTransform,而后对其添加一系列调整。 这是每一个单独的调整对图像的做用:
// 1
使用CATransform3DTranslate
在y轴上移动图像; 这会使图像偏离其默认的0.0 y坐标,以下所示:
以后,将要分别计算每一个图像的imageYOffset
,不然图片仍是叠加在一块儿。
// 2
经过使用CATransform3DScale
调整转换的比例份量来缩放图像。 能够在x轴上稍微缩小图像,可是在y轴上将其缩小到60%以丰富旋转3D效果:
// 3
最后,使用CATransform3DRotate
将图像旋转22.5度,使其具备一些透视变形,以下所示:
请记住,以前已经设置了锚点,所以图像围绕其顶部边缘旋转。
如今你看到经过view.layer.sublayerTransform设置上面的m34值的值; 您的旋转变换只需从新使用子层变换中的m34值,而无需在此处应用它。 那很方便!
如今剩下的就是将转换应用于每一个图像。 添加如下行(仍在for代码块中):
image.layer.transform = imageTransform
复制代码
将如下行添加到for块的末尾,修改每一个图像的位置:
imageYOffset += view.frame.height / CGFloat(images.count)
复制代码
这会调整每一个图像的y偏移量,具体取决于它在堆栈中的位置。 将屏幕高度除以图像数量,以便它们在屏幕上均匀分布。 运行后效果:
下面让它动起来!
在上面的image.layer.transform = imageTransform
的前面添加:
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D: image.layer.transform)
animation.toValue = NSValue(caTransform3D: imageTransform)
animation.duration = 0.33
image.layer.add(animation, forKey: nil)
复制代码
这段代码很是熟悉:在transform属性上建立一个图层动画,并将其从当前值设置为以前设计的imageTransform
。 运行后, 点击“浏览”按钮,效果:
你如今已经完成了画廊; 当您在用户点击“浏览”按钮时添加关闭风扇的功能时,您将在“挑战”部分从新访问它。
为图像库添加一点交互性:点击图像,变成全屏,而且位置移到最前面,以便用户能够更好地查看它。
ImageViewCard
已经具备名为didSelect
的闭包表达式属性,当用户点击图像,就将点击的图像视图做为输入参数给这个闭包。
首先将如下代码添加viewDidAppear()
的for循环体内:
image.didSelect = selectImage
复制代码
在ViewController
中添加方法:
func selectImage(selectedImage: ImageViewCard) {
for subview in view.subviews {
guard let image = subview as? ImageViewCard else {
continue
}
if image === selectedImage {
} else {
}
}
}
复制代码
如今您还须要两个动画:一个用于为所选图像设置动画,另外一个用于为图库中的全部其余图像设置动画。 你将反过来解决这个问题并首先淡出未选择的图像。
上面的方法还缺乏两个动画,当image === selectedImage
,就是所选图像的动画;或者,未选择的全部其余图像的动画,前者代码为:
UIView.animate(withDuration: 0.33, delay: 0.0, options: .curveEaseIn, animations: {
image.alpha = 0.0
}, completion: { (_) in
image.alpha = 1.0
image.layer.transform = CATransform3DIdentity
})
复制代码
后者代码为:
UIView.animate(withDuration: 0.33, delay: 0.0, options: .curveEaseIn, animations: {
image.layer.transform = CATransform3DIdentity
}, completion: {_ in
self.view.bringSubview(toFront: image)
})
复制代码
在这里,没有对动画进行3D变换,而后确保图像位于视图堆栈的顶部,以便它可见。
最后,将如下代码添加到selectImage(selectedImage:)
的末尾,更新标题:
self.navigationItem.title = selectedImage.title
复制代码
这小结工做是将使“浏览”按钮能够关闭图库视图。
向ViewController
添加一个isGalleryOpen
的新属性,并将其初始值设置为false
。
须要在代码中的两个位置更新此属性的值:
toggleGallery(_:)
结束时将其设置为true
selectImage(selectedImage:)
结束时将其设置为false
在toggleGallery()
的顶部,添加一个检查以查看图库是否已打开。 若是打开,则遍历全部图像并将其转换设置为原始值。 不要忘记重置isGalleryOpen
并返回,所以其他的方法代码也不会执行。
if isGalleryOpen {
for subview in view.subviews {
guard let image = subview as? ImageViewCard else {
continue
}
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D: image.layer.transform)
animation.toValue = NSValue(caTransform3D: CATransform3DIdentity)
animation.duration = 0.33
image.layer.add(animation, forKey: nil)
image.layer.transform = CATransform3DIdentity
}
isGalleryOpen = false
return
}
复制代码
本章的最后效果:
本文在个人我的博客中地址:系统学习iOS动画之六:3D动画