iOS UIView动画实践(四):过渡与伪3D动画

前言

上三篇关于UIView Animation的文章向你们介绍了基础的UIView动画,包括移动位置、改变大小、旋转、弹簧动画、过渡动画。这些虽然看起来很简单,可是若是咱们仔细分析、分解一个复杂动画时,就会发现这些复杂的动画实际上是由若干基础的动画组合而成的。今天这篇文章是实践篇,我选择了Raywenderlich Top 5 iOS 7 Animations这篇文章中的一个动画效果,带你们一块儿实现。要实现这个动画效果,除了用到咱们上三篇介绍过的知识点之外,还有两个知识点在这篇会介绍给你们,咱们先看看实现的效果:ios

这个动画示例实现的是一个展现航班信息的应用,左右滑动显示不一样的航班信息。咱们能够分析一下都用到了哪些动画:闭包

  • 淡入淡出:起飞地和目的地、起飞地和目的地下面的横线、底部的航班时间都使用了该动画。app

  • 位置移动:起飞地和目的地、小飞机都使用了该动画。iview

  • 旋转:航站楼登机口前面的小箭头、小飞机都使用了该动画。动画

  • 过渡动画: 背景图片使用了淡入淡出效果的图片替换过渡动画。ui

  • 伪3D动画:顶部的时间、航班号、航站楼登机口信息、底部的起飞降落文字都是用了该动画。spa

前三个动画咱们以前已经介绍过了,如今咱们来介绍后两个动画。.net

伪3D动画效果

这个伪3D的效果模拟的是一个立体长方形由一面翻转到另外一面。由于这不是真正的3D效果,因此咱们能够分析一下它是如何模拟的,以上面动画中从下往上翻的效果为例。首先显示的是一个UILabel,当开始进行翻转时,当前显示的UILabel的高度开始慢慢变矮:翻译

咱们看看用代码怎么实现:code

[cpp] view plaincopy

  1. UIView.animateWithDuration(1, animations: {  

  2.     self.devtalkingLabel.transform = CGAffineTransformMakeScale(1.0, 0.5)  

  3. })  

咱们可使用一个转换动画,使用CGAffineTransformMakeScale,它的第一个参数是x坐标的比例,第二个参数是y坐标的比例,这两个值的范围是1.0到0之间。上面的代码用白话文翻译出来就是在1秒内,devtalkingLabel的宽度不变,高度减小一半,减小的过程会自动生成补间动画。

咱们接着来分析,在UILabel高度减小的同时,它的位置也会向上移动,咱们能够用另一个转换的动画:

[cpp] view plaincopy

  1. UIView.animateWithDuration(1, animations: {  

  2.     self.devtalkingLabel.transform = CGAffineTransformMakeScale(1.0, 0.5)  

  3.     self.devtalkingLabel.transform = CGAffineTransformMakeTranslation(1.0, -self.devtalkingLabel.frame.height / 2)  

  4. })  

CGFffineTransformMakeTranslation这个转换动画能够移动UIView的位置,这里须要注意它是以初始位置为基础进行移动的,因此上述代码在字面上的意思是devtalkingLabel在高度变小的同时向上移动它初始宽度一半的距离:

可是当咱们编译运行后发现事与愿违,转换动画不像动画属性动画那样能够在animations闭包中写多个进行组合,而是由另外一个组合转换动画来实现:

[cpp] view plaincopy

  1. UIView.animateWithDuration(1, animations: {  

  2.     self.devtalkingLabel.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.5), CGAffineTransformMakeTranslation(1.0, -self.devtalkingLabel.frame.height / 2))  

  3.     self.devtalkingLabel.alpha = 0  

  4. })  

来看看效果:

此时,3D翻转效果的一个面已经成型了,也就是当前显示的这一面被向上翻转到顶部去了。接下来咱们要实现底部的面翻转到当前显示的这一面。很明显这须要两个面,但咱们只有一个UILabel,因此在执行整个翻转效果前须要先复制一个当前UILabel

[cpp] view plaincopy

  1. let devtalkingLabelCopy = UILabel(frame: self.devtalkingLabel.frame)  

  2. devtalkingLabelCopy.alpha = 0  

  3. devtalkingLabelCopy.text = self.devtalkingLabel.text  

  4. devtalkingLabelCopy.font = self.devtalkingLabel.font  

  5. devtalkingLabelCopy.textAlignment = self.devtalkingLabel.textAlignment  

  6. devtalkingLabelCopy.textColor = self.devtalkingLabel.textColor  

  7. devtalkingLabelCopy.backgroundColor = UIColor.clearColor()  

这样咱们就复制了一个devtalkingLabel,这个复制品将做为底部的那一面,并且在一开始它的透明度是零,由于底面是看不到的。咱们能够想象一下底面向上翻转的效果,其实就是底面的高度从很小慢慢变大,位置从下慢慢向上移动,而后有一个淡入的效果,因此咱们在复制出devtalkingLabelCopy后,要调整它的高度和位置,而后添加到父视图中:

[cpp] view plaincopy

  1. devtalkingLabelCopy.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, self.devtalkingLabel.frame.height / 2))  

  2. self.view.addSubview(devtalkingLabelCopy)  

上述代码将devtalkingLabelCopy的高度减少到本来的十分之一,位置向下移动半个高度的位置,而后在以前的animateWithDuration方法的animations闭包中添加以下两行代码:

[cpp] view plaincopy

  1. devtalkingLabelCopy.alpha = 1  

  2. devtalkingLabelCopy.transform = CGAffineTransformIdentity  

CGAffineTransformIdentity的做用是将UIViewtransform恢复到初始状态,而后将透明度设为1。编译运行代码咱们会看到devtalkingLabel的高度会慢慢变小,位置慢慢上移,最后淡出,devtalkingLabelCopy的高度慢慢变大,位置慢慢上移,最后淡入,整个效果看上去就像一个长方体在向上翻转,达到3D的效果:

替换UIView过渡动画

在要实现的动画示例中,背景图作了淡入淡出的图片替换过渡动画,这个动画很简单,咱们来看看这段伪代码:

[cpp] view plaincopy

  1. UIView.transitionWithView(backgroundImageView, duration: 2, options: .TransitionCrossDissolve, animations: {  

  2.     backgroundImageView.image = UIImage(named: "imageName")  

  3. }, completion: nil)  

这个方法在上一篇文章中已经介绍过,咱们只须要设置动画选项为.TransitionCrossDissolve,在animations闭包中给目标UIImageView设置要过渡的图片便可。

示例动画

至此,示例动画中用到的动画知识点都向你们介绍过了,在这一节我会将示例动画中主要的效果的伪代码贴出来给你们说说。关于左右滑动的手势以及PageControl在这里就不在累赘了。

数据源

为了方便,咱们建立一个Flight.plist文件做为数据源:

咱们定义一个延迟加载的属性flight

[cpp] view plaincopy

  1. lazy var flight: NSArray = {  

  2.     let path = NSBundle.mainBundle().pathForResource("Flight", ofType: "plist")  

  3.     return NSArray(contentsOfFile: path!)!  

  4. }()  

背景图片过渡

[cpp] view plaincopy

  1. UIView.transitionWithView(self.backgroundImageView, duration: 2, options: .TransitionCrossDissolve, animations: {  

  2.     self.backgroundImageView.image = UIImage(named: flightItem["bg"] as! String)  

  3. }, completion: nil)  

上一节刚介绍过,只是这里图片名称是从数据源中获取的。

3D翻转

由于有3D翻转动画效果的UIView比较多,并且有UILabel也有UIImageView,因此咱们能够提炼成一个方法,将目标UIView和数据源做为参数:

[cpp] view plaincopy

  1. func cubeAnimate(targetView: UIView, flightInfo: String) {  

  2.     // 判断UIView的具体实现类  

  3.     if targetView.isKindOfClass(UILabel) {  

  4.         let virtualTargetView = targetView as! UILabel  

  5.         // 复制UIView,做为底面  

  6.         let viewCopy = UILabel(frame: virtualTargetView.frame)  

  7.         viewCopy.alpha = 0  

  8.         viewCopy.text = flightInfo  

  9.         viewCopy.font = virtualTargetView.font  

  10.         viewCopy.textAlignment = virtualTargetView.textAlignment  

  11.         viewCopy.textColor = virtualTargetView.textColor  

  12.         viewCopy.backgroundColor = UIColor.clearColor()  

  13.         // 设置底面UIView的初始位置和高度  

  14.         viewCopy.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, viewCopy.frame.height / 2))  

  15.         self.topView.addSubview(viewCopy)  

  16.         UIView.animateWithDuration(2, animations: {  

  17.             // 执行UIView和UIViewCopy的动画  

  18.             virtualTargetView.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, -virtualTargetView.frame.height / 2))  

  19.             virtualTargetView.alpha = 0  

  20.             viewCopy.alpha = 1  

  21.             viewCopy.transform = CGAffineTransformIdentity  

  22.         }, completion: { _ in  

  23.             // 当动画执行完毕后,将UIViewCopy的信息赋值给UIView,并还原UIView的状态,即与UIViewCopy相同的状态,而后移除UIViewCopy  

  24.             virtualTargetView.alpha = 1  

  25.             virtualTargetView.text = viewCopy.text  

  26.             virtualTargetView.transform = CGAffineTransformIdentity  

  27.             viewCopy.removeFromSuperview()  

  28.         })  

  29.      } else if targetView.isKindOfClass(UIImageView) {  

  30.          let virtualTargetView = targetView as! UIImageView  

  31.          let viewCopy = UIImageView(frame: virtualTargetView.frame)  

  32.          viewCopy.alpha = 0  

  33.          viewCopy.image = UIImage(named: flightInfo)  

  34.          viewCopy.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, viewCopy.frame.height / 2))  

  35.          self.topView.addSubview(viewCopy)  

  36.          UIView.animateWithDuration(2, animations: {  

  37.              virtualTargetView.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, -virtualTargetView.frame.height / 2))  

  38.              virtualTargetView.alpha = 0  

  39.              viewCopy.alpha = 1  

  40.              viewCopy.transform = CGAffineTransformIdentity  

  41.          }, completion: { _ in  

  42.              virtualTargetView.alpha = 1  

  43.              virtualTargetView.image = viewCopy.image  

  44.              virtualTargetView.transform = CGAffineTransformIdentity  

  45.              viewCopy.removeFromSuperview()  

  46.          })  

  47.      }  

  48. }  

具体有这么几个步骤:

  • 判断UIView的具体实现类,判断是UILabel仍是UIImageView

  • 复制一份UIView,做为底面。

  • 设置UIViewCopy的初始位置和高度。

  • 执行UIViewUIViewCopy`的动画。

  • 当动画执行完毕后,将UIViewCopy的信息赋值给UIView,并还原UIView的状态,即与UIViewCopy相同的状态,而后移除UIViewCopy

小箭头旋转动画

由于航班信息有已降落和即将起飞两种状态,因此小箭头旋转涉及到一个方向问题,咱们能够先定义一个枚举类型:

[cpp] view plaincopy

  1. enum RotateDirection: Int {  

  2.     case Positive = 1  

  3.     case Negative = -1  

  4. }  

而后写一个箭头旋转的方法:

[cpp] view plaincopy

  1. func rotateAnimate(direction: Int) {  

  2.    UIView.animateWithDuration(2, animations: {  

  3.        // 判断向上仍是向下旋转  

  4.        if RotateDirection.Positive.rawValue == direction {  

  5.            // 在这个示例中小箭头的初始状态是飞机已降落状态,因此想要箭头从起飞状态旋转到降落状态,只要恢复初始状态便可  

  6.            self.landedOrDepatureSmallArrowImageView.transform = CGAffineTransformIdentity  

  7.        } else {  

  8.            // 向上旋转  

  9.            let rotation = CGAffineTransformMakeRotation(CGFloat(RotateDirection.Negative.rawValue) * CGFloat(M_PI_2))  

  10.            self.landedOrDepatureSmallArrowImageView.transform = rotation  

  11.        }  

  12.     }, completion: nil)  

  13. }  

给你们解释一下上述方法的几个步骤:

  • 首先判断旋转的方向,经过传入的direction参数。

  • 若是判断出是降落状态的箭头,也就是向下旋转的箭头,那么咱们只须要将landedOrDepatureSmallArrowImageViewtransform属性恢复初始值便可,由于在这个示例中小箭头的初始状态就是飞机降落状态。

  • 向上旋转时建立一个CGAffineTransformMakeRotation,而后设置正确地方向和角度便可。

注:CGAffineTransformMakeRotation转换每次都是以初始位置为准,CGAffineTransformRotation转换是以每次的旋转位置为准。

地点和飞机动画

起飞地、目的地、飞机的动画是一个组合动画,由于这里面存在飞机出现和消失,以及旋转的时机问题,咱们来看看这个方法:

[cpp] view plaincopy

  1. func placeAndAirplaneA

刨析一下这个方法:

  • 首先是起飞地向上移动同时淡出、目的地向下移动同时淡出、将飞机向右移出屏幕,这些动画属性的改变会产生补间动画。

  • 而后,当上面这些动画结束后,根据数据源参数更改起飞地和目的地的值,同时将飞机移动屏幕左侧外并向上旋转一个角度,这些属性的改变是不会产生补间动画的,应为它们在completion闭包中。

  • 最后再使用两个动画方法将起飞地向下移动,也就是恢复到初始位值同时淡入,将目的地向上移动,也就是恢复到初始位值同时淡入,将飞机移动到初始位置,将飞机的角度恢复到初始状态。这里为何不把恢复飞机角度和恢复位置放在一个动画方法里呢?由于恢复飞机角度须要一个延迟时间,也就是当飞机飞入屏幕一会后再恢复角度,表示一个降落的效果,使动画看起来更加逼真。

还有底部的时间、地点下地横线都是简单的淡入淡出动画,这里就再也不赘述了。

结束语

再简单的动画效果只要组合恰当,值设置得考究均可以作出出色的动画效果,而简单的动画效果也是复杂动画效果的基础。上述动画的示例代码可能写得不够精细,还能够提炼得更有层次,不过你们了解了知识点后能够本身实现更考究的代码结构,以及更精致的动画。

相关文章
相关标签/搜索