本文首发CSDN,如需转载请与CSDN联系。

前言

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

这个动画示例实现的是一个展示航班信息的应用,左右滑动显示不同的航班信息。我们可以分析一下都用到了哪些动画:

  • 淡入淡出:起飞地和目的地、起飞地和目的地下面的横线、底部的航班时间都使用了该动画。
  • 位置移动:起飞地和目的地、小飞机都使用了该动画。
  • 旋转:航站楼登机口前面的小箭头、小飞机都使用了该动画。
  • 过渡动画: 背景图片使用了淡入淡出效果的图片替换过渡动画。
  • 伪3D动画:顶部的时间、航班号、航站楼登机口信息、底部的起飞降落文字都是用了该动画。

前三个动画我们之前已经介绍过了,现在我们来介绍后两个动画。

伪3D动画效果

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

Practice-1

我们看看用代码怎么实现:


UIView.animateWithDuration(1, animations: {
self.devtalkingLabel.transform = CGAffineTransformMakeScale(1.0, 0.5)
})

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

我们接着来分析,在UILabel高度减少的同时,它的位置也会向上移动,我们可以用另外一个转换的动画:


UIView.animateWithDuration(1, animations: {
self.devtalkingLabel.transform = CGAffineTransformMakeScale(1.0, 0.5)
self.devtalkingLabel.transform = CGAffineTransformMakeTranslation(1.0, -self.devtalkingLabel.frame.height / 2)
})

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

Practice-2

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


UIView.animateWithDuration(1, animations: {
self.devtalkingLabel.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.5), CGAffineTransformMakeTranslation(1.0, -self.devtalkingLabel.frame.height / 2))
self.devtalkingLabel.alpha = 0
})

来看看效果:

Practice-4

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


let devtalkingLabelCopy = UILabel(frame: self.devtalkingLabel.frame)
devtalkingLabelCopy.alpha = 0
devtalkingLabelCopy.text = self.devtalkingLabel.text
devtalkingLabelCopy.font = self.devtalkingLabel.font
devtalkingLabelCopy.textAlignment = self.devtalkingLabel.textAlignment
devtalkingLabelCopy.textColor = self.devtalkingLabel.textColor
devtalkingLabelCopy.backgroundColor = UIColor.clearColor()

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


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

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


devtalkingLabelCopy.alpha = 1
devtalkingLabelCopy.transform = CGAffineTransformIdentity

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

Practice-5

替换UIView过渡动画

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


UIView.transitionWithView(backgroundImageView, duration: 2, options: .TransitionCrossDissolve, animations: {
backgroundImageView.image = UIImage(named: "imageName")
}, completion: nil)

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

示例动画

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

数据源

为了方便,我们创建一个Flight.plist文件作为数据源:

Practice-6

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


lazy var flight: NSArray = {
let path = NSBundle.mainBundle().pathForResource("Flight", ofType: "plist")
return NSArray(contentsOfFile: path!)!
}()

背景图片过渡


UIView.transitionWithView(self.backgroundImageView, duration: 2, options: .TransitionCrossDissolve, animations: {
self.backgroundImageView.image = UIImage(named: flightItem["bg"] as! String)
}, completion: nil)

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

3D翻转

因为有3D翻转动画效果的UIView比较多,而且有UILabel也有UIImageView,所以我们可以提炼成一个方法,将目标UIView和数据源作为参数:


func cubeAnimate(targetView: UIView, flightInfo: String) {
// 判断UIView的具体实现类
if targetView.isKindOfClass(UILabel) {
let virtualTargetView = targetView as! UILabel
// 复制UIView,作为底面
let viewCopy = UILabel(frame: virtualTargetView.frame)
viewCopy.alpha = 0
viewCopy.text = flightInfo
viewCopy.font = virtualTargetView.font
viewCopy.textAlignment = virtualTargetView.textAlignment
viewCopy.textColor = virtualTargetView.textColor
viewCopy.backgroundColor = UIColor.clearColor()
// 设置底面UIView的初始位置和高度
viewCopy.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, viewCopy.frame.height / 2))
self.topView.addSubview(viewCopy)
UIView.animateWithDuration(2, animations: {
// 执行UIView和UIViewCopy的动画
virtualTargetView.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, -virtualTargetView.frame.height / 2))
virtualTargetView.alpha = 0
viewCopy.alpha = 1
viewCopy.transform = CGAffineTransformIdentity
}, completion: { _ in
// 当动画执行完毕后,将UIViewCopy的信息赋值给UIView,并还原UIView的状态,即与UIViewCopy相同的状态,然后移除UIViewCopy
virtualTargetView.alpha = 1
virtualTargetView.text = viewCopy.text
virtualTargetView.transform = CGAffineTransformIdentity
viewCopy.removeFromSuperview()
})
} else if targetView.isKindOfClass(UIImageView) {
let virtualTargetView = targetView as! UIImageView
let viewCopy = UIImageView(frame: virtualTargetView.frame)
viewCopy.alpha = 0
viewCopy.image = UIImage(named: flightInfo)
viewCopy.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, viewCopy.frame.height / 2))
self.topView.addSubview(viewCopy)
UIView.animateWithDuration(2, animations: {
virtualTargetView.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1.0, 0.1), CGAffineTransformMakeTranslation(1.0, -virtualTargetView.frame.height / 2))
virtualTargetView.alpha = 0
viewCopy.alpha = 1
viewCopy.transform = CGAffineTransformIdentity
}, completion: { _ in
virtualTargetView.alpha = 1
virtualTargetView.image = viewCopy.image
virtualTargetView.transform = CGAffineTransformIdentity
viewCopy.removeFromSuperview()
})
}
}

具体有这么几个步骤:

  • 判断UIView的具体实现类,判断是UILabel还是UIImageView
  • 复制一份UIView,作为底面。
  • 设置UIViewCopy的初始位置和高度。
  • 执行UIViewUIViewCopy`的动画。
  • 当动画执行完毕后,将UIViewCopy的信息赋值给UIView,并还原UIView的状态,即与UIViewCopy相同的状态,然后移除UIViewCopy

小箭头旋转动画

因为航班信息有已降落和即将起飞两种状态,所以小箭头旋转涉及到一个方向问题,我们可以先定义一个枚举类型:


enum RotateDirection: Int {
case Positive = 1
case Negative = -1
}

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


func rotateAnimate(direction: Int) {
UIView.animateWithDuration(2, animations: {
// 判断向上还是向下旋转
if RotateDirection.Positive.rawValue == direction {
// 在这个示例中小箭头的初始状态是飞机已降落状态,所以想要箭头从起飞状态旋转到降落状态,只要恢复初始状态即可
self.landedOrDepatureSmallArrowImageView.transform = CGAffineTransformIdentity
} else {
// 向上旋转
let rotation = CGAffineTransformMakeRotation(CGFloat(RotateDirection.Negative.rawValue) * CGFloat(M_PI_2))
self.landedOrDepatureSmallArrowImageView.transform = rotation
}
}, completion: nil)
}

给大家解释一下上述方法的几个步骤:

  • 首先判断旋转的方向,通过传入的direction参数。
  • 如果判断出是降落状态的箭头,也就是向下旋转的箭头,那么我们只需要将landedOrDepatureSmallArrowImageViewtransform属性恢复初始值即可,因为在这个示例中小箭头的初始状态就是飞机降落状态。
  • 向上旋转时创建一个CGAffineTransformMakeRotation,然后设置正确地方向和角度即可。

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

地点和飞机动画

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


func placeAndAirplaneAnimate(flightItem: NSDictionary) {
UIView.animateWithDuration(1, delay: 0, options: [], animations: {
// 将起飞地向上移动,同时淡出
self.targetPlace.center.y += 100
self.targetPlace.alpha = 0
// 将目的地向下移动,同时淡出
self.sourcePlace.center.y -= 100
self.sourcePlace.alpha = 0
// 将飞机向右移出屏幕
self.airplaneImageView.center.x += self.view.bounds.width / 1.5
}, completion: { _ in
// 根据传入的数据源更改起飞地和目的地
self.targetPlace.text = flightItem["targetPlace"] as? String
self.sourcePlace.text = flightItem["sourcePlace"] as? String
// 将飞机移到屏幕左侧外,这里没有补间动画
self.airplaneImageView.center.x -= self.view.bounds.width * 1.5
// 将飞机向上旋转一个角度,这里没有补间动画
self.airplaneImageView.transform = CGAffineTransformMakeRotation(-3.14/10)
UIView.animateWithDuration(1.0, animations: {
// 将起飞地向下移动,也就是恢复到初始位值,同时淡入
self.targetPlace.center.y -= 100
self.targetPlace.alpha = 1
// 将目的地向上移动,也就是恢复到初始位值,同时淡入
self.sourcePlace.center.y += 100
self.sourcePlace.alpha = 1
// 将飞机移动到初始位置
self.airplaneImageView.center = self.airplaneImageViewOriginalCenter
})
UIView.animateWithDuration(0.7, delay: 0.5, options: [], animations: {
// 将飞机的角度恢复到初始状态
self.airplaneImageView.transform = CGAffineTransformIdentity
}, completion: nil)
})
}

刨析一下这个方法:

  • 首先是起飞地向上移动同时淡出、目的地向下移动同时淡出、将飞机向右移出屏幕,这些动画属性的改变会产生补间动画。
  • 然后当上面这些动画结束后,根据数据源参数更改起飞地和目的地的值,同时将飞机移动屏幕左侧外并向上旋转一个角度,这些属性的改变是不会产生补间动画的,应为它们在completion闭包中。
  • 最后再使用两个动画方法将起飞地向下移动,也就是恢复到初始位值同时淡入,将目的地向上移动,也就是恢复到初始位值同时淡入,将飞机移动到初始位置,将飞机的角度恢复到初始状态。这里为什么不把恢复飞机角度和恢复位置放在一个动画方法里呢?因为恢复飞机角度需要一个延迟时间,也就是当飞机飞入屏幕一会后再恢复角度,表示一个降落的效果,使动画看起来更加逼真。

还有底部的时间还有地点下地横线都是淡入淡出的动画比较简单,这里就不在累赘了。

结束语

再简单地动画效果只要组合的恰当,值设置的考究都可以做出出色的动画效果。这些简单地动画效果也是复杂动画效果的基础。上述动画示例的代码可能写的不够精细,还可以提炼的有层次,不过大家了解了知识点后可以自己实现更考究的代码结构,实现更精致的动画。

Demo地址

本文首发CSDN,如需转载请与CSDN联系。

前言

在上两篇文章中向大家介绍了如何创建基于动画属性的视图动画,比如位置、透明度等。但是大家有没有想过添加或删除一个视图时怎样添加相应地动画呢?

当然我们可以用第一篇文章中对用户名、密码输入框的处理办法,但是还有更好的办法处理这种状况。那就是在这篇文章中将向大家介绍的过渡转变(Transition)动画。

过渡转变动画是Apple预定义的动画集,它没有更改视图某属性起始值和终止值的概念,而只需要你设定不同的动画选项即可。

添加新视图

在进行示例之前,大家需要注意一点过渡转变动画与动画属性动画的不同之处。我们在创建动画属性动画时只需要在animations闭包中添加对视图动画属性修改的代码即可,它没有作用域或作用视图的概念。而在过渡转变动画中有作用视图的概念,也就是说我们调用过渡转变动画方法时需要指定一个作用视图。

明确这点不同之后,我们对作用视图再作进一步的说明。过渡转变动画中的作用视图并不是我们的目标视图,而是目标视图的容器视图,那么大家不难想象,如果该容器视图中有多个子视图,那么这些子视图都会有过渡转变动画效果。下面用示例像大家说明。

先看看一个简单的视图结构:

视图结构

很明显,我们添加了一个视图作为容器视图,并且尺寸等于屏幕尺寸。在ViewController.swift中有该容器视图的Outlet以及一个图片视图:


@IBOutlet weak var containerView: UIView!
let ipadView = UIImageView(frame: CGRectMake(100, 100, 200, 151.5))

viewDidLoad()方法中给ipadView指定图片:


ipadView.image = UIImage(named: "ipad")

然后在viewDidAppear()方法中添加如下代码:


UIView.transitionWithView(self.containerView, duration: 1.5, options: .TransitionFlipFromBottom, animations: {
self.containerView.addSubview(self.ipadView)
}, completion: nil)

上述代码就是我们今天的主角,过渡转变方法之一,它同样是UIView的类方法,共有五个参数:

  • view:第一个参数,也就是作用视图,一般都是容器视图。
  • duration:动画持续时间。
  • options:过渡转变动画选项,由它来确定过渡转变的具体展现形式。
  • animations:动画闭包。
  • completion:动画结束后执行该闭包中的代码。

除了第一个参数,其他四个参数大家应该都不会陌生。这段代码用文字解释出来就是将容器视图(containerView)添加子视图(ipadView)的过程使用.TransitionFlipFromBottom类型的过渡转变动画展示出来,持续时间为1.5秒。编译运行看看效果:

我们再来添加一个子视图(该子视图的初始化代码不再累赘):


UIView.transitionWithView(self.containerView, duration: 1.5, options: .TransitionFlipFromBottom, animations: {
self.containerView.addSubview(self.ipadView)
self.containerView.addSubview(self.iphoneView)
}, completion: nil)

编译运行看看效果:

大家可以看到这两个子视图都依托与它们的容器视图进行了过渡转变动画。下面的列表是所有过渡转变动画的动画选项,大家可以在你们自己的项目中逐个实验:

  • .TransitionFlipFromLeft
  • .TransitionFlipFromRight
  • .TransitionCurlUp
  • .TransitionCurlDown
  • .TransitionCrossDissolve
  • .TransitionFlipFromTop
  • .TransitionFlipFromBottom

如果我们有多个目标视图,想进行不同的过渡转变动画怎么办?那我们就创建多个目标视图的容器视图,尺寸与目标视图一致,放置在合适的位置:

视图结构

从上图中可以看出,我们在屏幕上放置了四个容器视图,显而易见,我们要分别对这四个容器视图添加过渡转变动画。当然容器视图里要添加什么样的视图随个人喜好。

viewDidLoad()方法中添加如下代码:


UIView.transitionWithView(self.ipadContainerView, duration: 1.5, options: [.CurveEaseOut, .TransitionFlipFromBottom], animations: {
self.ipadContainerView.addSubview(self.ipadView)
}, completion: nil)

UIView.transitionWithView(self.iphoneContainerView, duration: 1.5, options: [.CurveEaseOut, .TransitionFlipFromLeft], animations: {
self.iphoneContainerView.addSubview(self.iphoneView)
}, completion: nil)

UIView.transitionWithView(self.webContainerView, duration: 1.5, options: [.CurveEaseOut, .TransitionFlipFromRight], animations: {
self.webContainerView.addSubview(self.webView)
}, completion: nil)

UIView.transitionWithView(textContainerView, duration: 2, options: [.CurveEaseOut, .TransitionCrossDissolve], animations: {
self.textContainerView.addSubview(self.textView)
}, completion: nil)

通过上述代码可以看出,我们对四个容器视图分别添加了过渡转变动画,并且options参数使用了.CurveEaseOut和不同的过渡转变动画选项。编译运行看看效果:

是不是有点儿意思!不过细心的朋友应该发现了,过渡转变动画的方法没有delay这个很有用的属性,这就导致过渡转变动画都是同时发生,不能设置延迟时间。不过我们可以曲线救国,自己写一个delay方法:


func delay(seconds: Double, completion:()->()) {
let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64( Double(NSEC_PER_SEC) * seconds ))
dispatch_after(popTime, dispatch_get_main_queue()) {
completion()
}
}

然后我们修改viewDidLoad()方法中的代码如下:


delay(0, completion: {
UIView.transitionWithView(self.ipadContainerView, duration: 1.5, options: [.CurveEaseOut, .TransitionFlipFromBottom], animations: {
self.ipadContainerView.addSubview(self.ipadView)
}, completion: nil)
})

delay(1, completion: {
UIView.transitionWithView(self.iphoneContainerView, duration: 1.5, options: [.CurveEaseOut, .TransitionFlipFromLeft], animations: {
self.iphoneContainerView.addSubview(self.iphoneView)
}, completion: nil)
})

delay(2, completion: {
UIView.transitionWithView(self.webContainerView, duration: 1.5, options: [.CurveEaseOut, .TransitionFlipFromRight], animations: {
self.webContainerView.addSubview(self.webView)
}, completion: nil)
})

delay(3, completion: {
UIView.transitionWithView(self.textContainerView, duration: 2, options: [.CurveEaseOut, .TransitionCrossDissolve], animations: {
self.textContainerView.addSubview(self.textView)
}, completion: nil)
})

我们将每个过渡转变动画延迟1秒进行,编译运行看看效果:

现在的效果是不是更好了呢! : ]

移除视图

过渡转变动画同样可以用来移除视图。我们在屏幕底部添加一个UIButton,当点击这个按钮的时候,通过过渡转变动画移除按钮上方的那两排字,并且改变屏幕背景色。go()方法是按钮连接在代码中的Touch Up Inside方法,在该方法中添加如下代码:


UIView.animateWithDuration(0.5, animations: {
self.view.backgroundColor = UIColor(red: 252.0/255.0, green: 155.0/255.0, blue: 65.0/255.0, alpha: 1)
})

编译运行看看效果:

替换视图

在这一节我们将要学习过渡转变动画的另一个方法,替换视图方法。我设计的场景是当点击Go按钮后,除了上一节中的动画效果以外,iPad、iPhone、Web视图也会移位并且替换为别的视图,我们继续在go()方法中的添加如下代码:

 
UIView.animateWithDuration(1, delay: 0, options: [], animations: {
self.iphoneView.frame = CGRectMake(0, 0, 334, 72)
self.iphoneContainerView.frame = CGRectMake(26, 130, 334, 72)
}, completion: {
(flag: Bool) in
if flag {
UIView.transitionFromView(self.iphoneContainerView, toView: self.supportIphone, duration: 0.33, options: .TransitionCrossDissolve, completion: nil)
}
})

UIView.animateWithDuration(1, delay: 1, options: [], animations: {
self.ipadView.frame = CGRectMake(0, 0, 334, 72)
self.ipadContainerView.frame = CGRectMake(26, 242, 334, 72)
}, completion: {
(flag: Bool) in
if flag {
UIView.transitionFromView(self.ipadContainerView, toView: self.supportIpad, duration: 0.33, options: .TransitionCrossDissolve, completion: nil)
}
})

UIView.animateWithDuration(1, delay: 2, options: [], animations: {
self.webView.frame = CGRectMake(0, 0, 334, 72)
self.webContainerView.frame = CGRectMake(26, 354, 334, 72)
}, completion: {
(flag: Bool) in
if flag {
UIView.transitionFromView(self.webContainerView, toView: self.supportWeb, duration: 0.33, options: .TransitionCrossDissolve, completion: nil)
}
})

我们来解释一下上述的代码,拿iPhone视图为例,首先通过动画属性动画改变它的尺寸大小和位置。然后在completion闭包中添加替换视图方法,该方法有五个参数:

  • fromView:被替换的视图。
  • toView:替换之后的视图。
  • duration:动画持续时间。
  • options:动画选项。
  • completion:动画执行结束后执行该闭包中的代码。

要注意的是该方法的作用视图可以是容器视图,也可以是目标视图。编译运行看看效果:

显示/隐藏视图

过渡转变动画也可以用于显示或隐藏视图,这里给出伪代码供参考:


UIView.transitionWithView(self.someContainerView, duration: 1.5, options: [.CurveEaseOut, .TransitionFlipFromBottom], animations: {
self.someView.hidden = true
// self.someView.hidden = false
}, completion: nil)

结束语

过渡转变动画有很多动画选项,大家可以自行试试,找出自己喜欢的或最合适的过渡转变动画选项,并且可以尝试过渡转变动画和属性动画的组合,可以使你们的App更加有趣。好了今天就到这里。

参考文献来自:iOS Animations by Tutorials

本文首发CSDN,如需转载请与CSDN联系。

前言

通过上一篇文章,大家应该学会了如何使用UIKit创建最基本的视图动画,包括如何指定视图某属性的起始值和结束值,以及动画持续时间、动画延迟时间、动画选项等概念。

但是目前大家实现的动画不论是位置移动还是大小改变都是以一个单一的方向在运动,比如位置从A点到B点,大小半径从1到2:

图1

在这篇文章中,大家会学到更复杂一点的动画,让视图在一次动画中的运动轨迹像弹簧一样,有多次不同方向的运动,最后停止在终点:

图2

如果给位置移动的动画添加弹簧效果,那么视图的运动轨迹应该像下图中展现的一样:

图3

这会使你的动画看起来更逼真、更真实、更贴近现实。在某些情况下带给用户更好的用户体验。那么让我们开始学习吧。

Spring动画

我们还是以上一篇文章中的登录页面为例,大家应该发现了那个呆板的登录按钮吧,我们今天就让它Q弹起来。

打开ViewController.swift,在viewWillAppear()方法的底部添加如下代码:


self.loginButton.center.y += 30
self.loginButton.alpha = 0

让登录按钮在屏幕呈现之前位置下移30,并且让它透明。然后在viewDidAppear()方法的底部添加如下代码:


UIView.animateWithDuration(1, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: .AllowUserInteraction, animations: {
self.loginButton.center.y -= 30
self.loginButton.alpha = 1
}, completion: nil)

上述的代码大家是不是似曾相识,没错,依然是UIView的类方法,不过又多了两个参数:

  • usingSpringWithDamping:弹簧动画的阻尼值,也就是相当于摩擦力的大小,该属性的值从0.0到1.0之间,越靠近0,阻尼越小,弹动的幅度越大,反之阻尼越大,弹动的幅度越小,如果大道一定程度,会出现弹不动的情况。
  • initialSpringVelocity:弹簧动画的速率,或者说是动力。值越小弹簧的动力越小,弹簧拉伸的幅度越小,反之动力越大,弹簧拉伸的幅度越大。这里需要注意的是,如果设置为0,表示忽略该属性,由动画持续时间和阻尼计算动画的效果。

下面我们先来看看不同动力的效果:

  • 持续时间为3秒,阻尼为0.5,动力为1:
  • 持续时间为3秒,阻尼为0.5,动力为20:

initialSpringVelocity的值为1时,登录按钮上下弹力并不是很强劲,当设置为20时,登录按钮直接冲过了密码输入框,这就是动力的效果。

然后我们再看看不同阻尼的效果:

  • 持续时间为3秒,阻尼为0.1,动力为0:
  • 持续时间为3秒,阻尼为1,动力为0:

usingSpringWithDamping属性值为0.1时,表示阻尼很小,虽然没有动力因素的影响,但登录按钮弹动的幅度依然比较大,相当于在冰面滑行一样。当该属性为1时,表示阻尼非常大,可以看到登录按钮几乎是没有什么弹动的幅度。这就是阻尼的效果。

大家需要注意的一点是,弹簧动画并不只作用于位置的变化,它可以作用于所有动画属性的变化,比如我们在animations的闭包中除了位置的变化外,还有透明度的变化,它也同样有弹簧动画的效果,只不过它没有位置变化那么明显和贴近真实,它会表现出一闪一闪的效果:

大家可以尝试这几个属性不同值的组合,选出一个自己觉得满意的弹簧效果即可。

将动画运用到人机交互

上一节讲的弹簧动画的确可以让我们的UI变得鲜活起来,但这仅仅是看在眼里的,对于用户的操作却并没有什么响应和反馈。在这节会教大家如何让视图在用户进行点击操作时以动画的形式给予响应和反馈。

ViewController.swift中有一个名为login()的方法,关联了登录按钮的Touch Up Inside事件,在该方法中添加如下代码:


UIView.animateWithDuration(0.5, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options: .AllowUserInteraction, animations: {
self.loginButton.bounds.size.width += 25
}, completion: nil)

此时每当我们点击登录按钮一次,它就会变胖一些。编译运行看一下效果:

我们可以再组合一个效果,使点击登录按钮时不仅让它变胖,还有轻微的向下弹跳效果。在login()方法底部添加如下代码:


UIView.animateWithDuration(0.3, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: .AllowUserInteraction, animations: {
self.loginButton.center.y += 10
}, completion: nil)

编译运行看一下效果:

以上只是两个简单的将动画运用到人机交互的例子,大家可以在自己的项目中将一些用户的操作加上视图的动画反馈,让你的应用鲜活起来。

结束语

这一篇文章向大家讲解了UIView的Spring动画,以及将动画运用到人机交互中,使提升应用的用户体验,大家可以多多尝试Spring动画的各个属性,以及结合上一篇文章的知识,组合出更美妙的动画效果。下一篇文章会给大家介绍UIView的Transition动画,敬请期待。

参考文献来自:iOS Animations by Tutorials

本文首发CSDN,如需转载请与CSDN联系。

前言

在一个看脸的社会中,不论什么事物,长的好看总是能多吸引一些目光。App同样不例外,一款面相不错的App就算功能已经被轮子千百遍,依然会有人买账,理由就是看的顺眼,于是平面设计人员越来越被重视。白驹过隙,斗转星移,人们已然不满足于静态的美感,于是动态的用户体验应运而生,平面设计人员捉襟见肘,是我们程序员出马的时候了。

这篇文章是UIView Animation的第一篇,从极简的概念开始,为大家揭开Animation的神秘面纱。我们以一个登录界面为例。美丽的太阳,婀娜的云,还有几个小山包,中间静躺着用户名、密码输入框和登录按钮。搁以前,这个界面许是会亮瞎眼现如今尼玛狗都嫌。所以我们的目标是赋予这个界面生命力。

登录页

注意:本文章基于Swift 2.0和Xcode 7 Beta2编写。登录界面中的所有元素都已经连接到了代码中(outlet),在这个示例中我们先不使用Auto Layout和SizeClasses。

会动的输入框

我们的第一个场景应该是这样。用户打开App,启动画面过后显示登录界面,此时屏幕上还没有用户名和密码的输入框,下一秒他们从屏幕左侧飘然而至。

首先

我们需要在登录界面还没有展现给用户的时候把用户名和密码的输入框移至屏幕外面。打开ViewController.swift,在viewWillAppear()方法中添加以下代码:


self.username.center.x -= self.view.bounds.width
self.password.center.x -= self.view.bounds.width

这两行代码使用户名、密码输入框移出屏幕外,这里可以使用简单暴力的方式,直接让centerx减去屏幕宽度。

输入框移出屏幕

然后

我们在viewDidAppear()方法中添加以下代码:


UIView.animateWithDuration(0.5, animations: {
self.username.center.x += self.view.bounds.width
self.password.center.x += self.view.bounds.width
})

animationWithDuration(_:animations:)UIView的类方法,从方法名就可以看出,该方法可使UIView动起来。它有两个参数:

  • duration:动画的持续时间。
  • animation:动画闭包,在这个闭包中你可以改变UIView的各种动画属性。

因为该方法是一个类方法,所以在闭包中你可以同时改变多个views的动画属性。所以在上述代码中,同时改变了用户名和密码输入框的位置。编译运行,我们可以看到如下效果:

FirstAnimation-1

但是由于两个输入框是同时从屏幕外滑入,略显呆板,所以我们用另一个方法再来润色一下。

最后

我们更新viewDidAppear()中的代码:


UIView.animateWithDuration(0.5, animations: {
self.username.center.x += self.view.bounds.width
// self.password.center.x += self.view.bounds.width
})

UIView.animateWithDuration(0.5, delay: 0.3, options: .AllowUserInteraction, animations: {
self.password.center.x += self.view.bounds.width
}, completion: nil)

animationWithDuration(_:delay:options:animations:completion:)方法同样是UIView的类方法,但是多了3个参数:

  • delay:顾名思义,动画迟延执行的时间。
  • options:自定义动画的一些效果,比如重复动画、前后运动等。这个参数在后面的文章中会说明。
  • completion:也是一个闭包,当动画执行完之后会执行该闭包中的逻辑,可以用来连接动画,或者是在动画结束后你需要做一些清理工作等。

现在编译运行,可以看到如下效果:

FirstAnimation-2

用户名输入框先滑入屏幕,在0.3秒的延迟后,密码输入框紧随其后。

动画属性

在上一节,我们体验了视图的简单动画效果,不难发现,其实真正导致视图动起来的是animations闭包中的代码,也就是对视图属性的改变,然后UIView的类方法生成了视图某属性的起始值和终止值之间的补间动画。这引出了另一个概念,那就是视图的动画属性,诚然不是所有的视图属性都是动画属性,下面给大家介绍一下视图的动画属性。

位置和大小

  • bounds:改变视图内容的位置和尺寸大小的属性。
  • frame:改变视图的位置和尺寸大小的属性。
  • center:改变视图位置的属性。

外观

  • backgroundColor:改变背景色时,UIKit会线性的从原始颜色转变为目标颜色。
  • alpha:改变透明度,UIKit会创建淡入淡出的效果。

转换

transform属性的类型为CGAffineTransform,它是一个结构体,CoreGraphics中有若干方法可生成不同的CGAffineTransform结构,使视图旋转、按比例缩放、翻转等,我们来看看它如何使用。在viewDidAppear()方法中添加如下代码:


let rotation = CGAffineTransformMakeRotation(CGFloat(M_PI))
UIView.animateWithDuration(1, animations: {
self.sun.transform = rotation
})

首先创建了一个旋转的结构,参数是一个CGFloat类型的角度,这里我们使用预定义好的常量比如M_PI代表3.14…,也就是旋转一周、M_PI_2代表1.57…,也就是旋转半周等。

然后在animations闭包中将创建的旋转结构赋值给屏幕上太阳视图的transform属性。编译运行可以看到如下效果:

我们再来看看缩放,在viewDidAppear()方法中添加如下代码:


let scale = CGAffineTransformMakeScale(0.5, 0.5)
UIView.animateWithDuration(1, animations: {
self.cloudBig.transform = scale
})

首先创建了一个缩放的结构,第一个参数是x轴的缩放比例,第二个参数是y轴的缩放比例。同样在animations闭包中将创建的缩放结构赋值给屏幕上云朵视图的transform属性。编译运行可以看到如下效果:

动画选项

大家应该还记得我们之前使用过animationWithDuration(_:delay:options:animations:completion:)方法,其中的options当时没有详细的讲述,这节会向大家说明该属性。options选项可以使你自定义让UIKit如何创建你的动画。该属性需要一个或多个UIAnimationOptions枚举类型,让我们来看看都有哪些动画选项吧。

重复类

  • .Repeat:该属性可以使你的动画永远重复的运行。
  • .Autoreverse:该属性可以使你的动画当运行结束后按照相反的行为继续运行回去。该属性只能和.Repeat属性组合使用。

我们来看看怎么使用这两个属性,我们修改一个密码输入框的动画:


UIView.animateWithDuration(0.5, delay: 0.3, options: .Repeat, animations: {
self.password.center.x += self.view.bounds.width
}, completion: nil)

编译运行看看效果:

可以看到密码输入框不停的从左向右滑入。大家可以自己试试.Autoreverse的效果或者[.Repeat, .Autoreverse]组合效果。

动画缓冲

在现实生活中,几乎没有什么东西可以突然开始运动,然后突然停止一动不动。可以运动的物体基本都是以较慢的速度启动,逐渐加速,达到一个稳定的速度,然后当要停止时,会逐渐减速,最后停止。所以要使动画更加逼真,也可以采用这种方式,那就是ease-inease-out

  • .CurveLinear :该属性既不会使动画加速也不会使动画减速,只是做以线性运动。
  • .CurveEaseIn:该属性使动画在开始时加速运行。
  • .CurveEaseOut:该属性使动画在结束时减速运行。
  • .CurveEaseInOut:该属性结合了上述两种情况,使动画在开始时加速,在结束时减速。

下面依然以密码输入框作为示例,修改密码输入框的动画代码:


UIView.animateWithDuration(0.5, delay: 0.3, options: [.Repeat, .Autoreverse, .CurveEaseOut], animations: {
self.password.center.x += self.view.bounds.width
}, completion: nil)

上面的代码中组合了三种动画选项,首先让动画重复执行,然后让动画在一次执行完毕后接着反方向再次执行,最后让动画在结束时减速。编译运行,这次我们减慢动画的运行速度来看看:

从上面的效果中可以看到当密码输入框滑入屏幕的后半段时速度有明显的减慢。大家也可以在自己的项目中试试其他动画选项的组合。

结束语

看完这篇文章后,相信大家对iOS的动画有了大致的了解,也学会了如何实现简单的视图动画,当然这些只是iOS Animation的冰山一角,我会陆续向大家介绍iOS Animation的其他知识,今天就先到这吧。

参考文献来自:iOS Animations by Tutorials

转眼间,Swift已经一岁多了,这门新鲜、语法时尚、类型安全、执行速度更快的语言已经渐渐的深入广大开发者的心。我同样也是非常喜爱这门新的编程语言。

今年6月,一年一度的WWDC大会如期而至,在大会上Apple发布了Swift 2.0,引入了很多新的特性,以帮助开发者能更快,更简单的构建应用。我在这里也说道说道Swift 2.0中值得大家注意的新特性。

guard语句

guard语句和if语句有点类似,都是根据其关键字之后的表达式的布尔值决定下一步执行什么。但与if语句不同的是,guard语句只会有一个代码块,不像if语句可以if else多个代码块。

那么guard语句的作用到底是什么呢?顾名思义,就是守护。guard语句判断其后的表达式布尔值为false时,才会执行之后代码块里的代码,如果为true,则跳过整个guard语句,我们举例来看看。

我们以今年高考为例,在进入考场时一般都会检查身份证和准考证,我们写这样一个方法:


func checkup(person: [String: String!]) {

// 检查身份证,如果身份证没带,则不能进入考场
guard let id = person["id"] else {
print("没有身份证,不能进入考场!")
return
}

// 检查准考证,如果准考证没带,则不能进入考场
guard let examNumber = person["examNumber"] else {
print("没有准考证,不能进入考场!")
return
}

// 身份证和准考证齐全,方可进入考场
print("您的身份证号为:\(id),准考证号为:\(examNumber)。请进入考场!")

}

checkup(["id": "123456"]) // 没有准考证,不能进入考场!
checkup(["examNumber": "654321"]) // 没有身份证,不能进入考场!
checkup(["id": "123456", "examNumber": "654321"]) // 您的身份证号为:123456,准考证号为:654321。请进入考场!

上述代码中的第一个guard语句用于检查身份证,如果检查到身份证没带,也就是表达式为false时,执行大括号里的代码,并返回。第二个guard语句则检查准考证。

如果两证齐全,则执行最后一个打印语句,上面的两个guard语句大括号内的代码都不会执行,因为他们表达式的布尔值都是true

这里值得注意的是,idexamNumber可以在guard语句之外使用,也就是说当guard对其表达式进行验证后,idexamNumber可在整个方法的作用域中使用,并且是解包后的。

我们再用if else语句写一个类似的方法:


func checkupUseIf(person: [String: String!]) {

if let id = person["id"], let examNumber = person["examNumber"] {
print("您的身份证号为:\(id),准考证号为:\(examNumber)。请进入考场!")
} else {
print("证件不齐全,不能进入考场!")
}

print("您的身份证号为:\(id),准考证号为:\(examNumber)") // 报异常

}

checkupUseIf(["id": "123456"]) // 证件不齐全,不能进入考场!
checkupUseIf(["examNumber": "654321"]) // 证件不齐全,不能进入考场!
checkupUseIf(["id": "123456", "examNumber": "654321"]) // 您的身份证号为:123456,准考证号为:654321。请进入考场!

我们可以看到用if else实现的方法显然不如guard实现的那么精准。而且idexamNumber的作用域只限在if的第一个大括号内,超出这个作用域编译就会报错。

通过上述两个小例子不难看出,guard语句正如一个称职的守卫,层层把关,严防一切不允许发生的事,并且让代码具有更高的可读性,非常棒。

异常处理

在Swift 1.0时代是没有异常处理和抛出机制的,如果要处理异常,要么使用if else语句或switch语句判断处理,要么使用闭包形式的回调函数处理,再要么就使用NSError处理。以上这些方法都不能像Java中的try catch异常控制语句那样行如流水、从容不迫的处理异常,而且也会降低代码的可读性。当Swift 2.0到来后,一切都不一样了。

在Swift 2.0中Apple提供了使用throwsthrowtrydocatch这五个关键字组成的异常控制处理机制。下面我们来举例看看如何使用,我用使用手机刷朋友圈为例。

首先我们需要定义异常枚举,在Swift 2.0中Apple提供了ErrorType协议需要我们自定义的异常枚举遵循:


enum WechatError: ErrorType {
case NoBattery // 手机没电
case NoNetwork // 手机没网
case NoDataStream // 手机没有流量
}

我们定义了导致不能刷微信的错误枚举’wechatError。然后定义一个检查是否可以刷微信的方法checkIsWechatOk()


func checkIsWechatOk(isPhoneHasBattery: Bool, isPhoneHasNetwork: Bool, dataStream: Int) throws {

guard isPhoneHasBattery else {
throw WechatError.NoBattery
}

guard isPhoneHasNetwork else {
throw WechatError.NoNetwork
}

guard dataStream > 50 else {
throw WechatError.NoDataStream
}

}

这里注意,在方法名后有throws关键字,意思为该方法产生的异常向上层抛出。在方法体内使用guard语句对各种状态进行判断,然后使用throw关键字抛出对应的异常。然后我们定义刷微信的方法:


func playWechat(isPhoneHasBattery: Bool, isPhoneHasNetwork: Bool, dataStream: Int) {

do {
try checkIsWechatOk(isPhoneHasBattery, isPhoneHasNetwork: isPhoneHasNetwork, dataStream: dataStream)
print("放心刷,刷到天昏地暗!")
} catch WechatError.NoBattery {
print("手机都没电,刷个鬼啊!")
} catch WechatError.NoNetwork {
print("没有网络哎,洗洗玩单机吧!")
} catch WechatError.NoDataStream {
print("没有流量了,去蹭Wifi吧!")
} catch {
print("见鬼了!")
}

}

playWechat(true, isPhoneHasNetwork: true, dataStream: 60) // 放心刷,刷到天昏地暗!
playWechat(true, isPhoneHasNetwork: false, dataStream: 60) // 没有网络哎,洗洗玩单机吧!
playWechat(false, isPhoneHasNetwork: true, dataStream: 60) // 手机都没电,刷个鬼啊!
playWechat(true, isPhoneHasNetwork: true, dataStream: 30) // 没有流量了,去蹭Wifi吧!

上述的代码示例中,首先检查是否可以刷微信的方法前使用try关键字,表示允许该方法抛出异常,然后使用了do catch控制语句捕获抛出的异常,进而做相关的逻辑处理。

这套异常处理机制使Swift更加的全面和安全,并且提高了代码的可读性,非常棒。

协议扩展

在Swift 1.0 时代,协议(Protocol)基本上类似一个接口,定义若干属性和方法,供类、结构体、枚举遵循和实现。在Swift 2.0中,可以对协议进行属性或者方法的扩展,和扩展类与结构体类似。这让我们开启了面向协议编程的篇章。

Swift中,大多数基础对象都遵循了CustomStringConvertible协议,比如ArrayDictionary(Swift 1.0中的Printable协议),该协议定义了description方法,用于print方法打印对象。现在我们对该协议扩展一个方法,让其打印出大写的内容:


var arr = ["hello", "world"]
print(arr.description) // "[hello, world]"

extension CustomStringConvertible {
var upperDescription: String {
return "\(self.description.uppercaseString)"
}
}

print(arr.upperDescription) // "[HELLO, WORLD]"

如果在Swfit 1.0时代,要想达到上述示例的效果,那么我们需要分别对ArrayDictionary进行扩展,所以协议的扩展极大的提高了我们的编程效率,也同样使代码更简洁和易读。

打印语句的改变

在Swift1中,有’println()’和’print()’两个在控制台打印语句的方法,前者是换行打印,后者是连行打印。在Swift2中,’println()’已成为过去,取而代之的是他俩的结合体。如果你想做换行打印,现在需要这样写:


print("我要换行!", appendNewline: true)

available检查

作为iOS开发者,谁都希望使用最新版本iOS的Api进行开发,省事省力。但常常事与愿违,因为我们经常需要适配老版本的iOS,这就会面临一个问题,一些新特性特性或一些类无法在老版本的iOS中使用,所以在编码过程中经常会对iOS的版本做以判断,就像这样:


if NSClassFromString("NSURLQueryItem") != nil {
    // iOS 8或更高版本
} else{
    // iOS8之前的版本
}

以上这只是一种方式,在Swift 2.0之前也没有一个标准的模式或机制帮助开发者判断iOS版本,而且容易出现疏漏。在Swift 2.0到来后,我们有了标准的方式来做这个工作:


if #available(iOS 8, *) {
    // iOS 8或更高版本
    let queryItem = NSURLQueryItem()
    
} else {
    // iOS8之前的版本
    
}

这个特性让我们太幸福。

do-while语句重命名

经典的do-while语句改名了,改为了repeat-while


var i = 0
repeat {
    i++
    print(i)
} while i < 10

个人感觉更加直观了。

defer关键字

在一些语言中,有try/finally这样的控制语句,比如Java。这种语句可以让我们在finally代码块中执行必须要执行的代码,不管之前怎样的兴风作浪。在Swift 2.0中,Apple提供了defer关键字,让我们可以实现同样的效果。


func checkSomething() {

print("CheckPoint 1")
doSomething()
print("CheckPoint 4")

}

func doSomething() {

print("CheckPoint 2")
defer {
print("Clean up here")
}
print("CheckPoint 3")

}

checkSomething() // CheckPoint 1, CheckPoint 2, CheckPoint 3, Clean up here, CheckPoint 4

上述示例可以看到,在打印出“CheckPoint 2”之后并没有打印出“Clean up here”,而是“CheckPoint 3”,这就是defer的作用,它对进行了print("Clean up here")延迟。我们再来看一个I/O的示例:


// 伪代码
func writeSomething() {

let file = OpenFile()

let ioStatus = fetchIOStatus()
guard ioStatus != "error" else {
return
}
file.write()

closeFile(file)

}

上述示例是一个I/O操作的伪代码,如果获取到的ioStatus正常,那么该方法没有问题,如果ioStatus取到的是error,那么会被guard语句抓到执行return操作,这样的话closeFile(file)就永远都不会执行了,一个严重的Bug就这样产生了。下面我们看看如何用defer来解决这个问题:


// 伪代码
func writeSomething() {

let file = OpenFile()
defer {
closeFile(file)
}

let ioStatus = fetchIOStatus()
guard ioStatus != "error" else {
return
}
file.write()

}

我们将closeFile(file)放在defer代码块里,这样即使ioStatuserror,在执行return前会先执行defer里的代码,这样就保证了不管发生什么,最后都会将文件关闭。

defer又一个保证我们代码健壮性的特性,我非常喜欢。

Swift 2.0中的新特性当然不止以上这些,但窥一斑可见全豹,Swift 2.0努力将更快、更安全做到极致,这是开发人员的福音,让我们尽情享受这门美妙的语言吧。