Swift中的动画

本文翻译自O(∩_∩)O
Prototyping iOS Animations in Swift中介绍了UIKit中基于block方法的一些小的动画效果并且教你如何利用随机数的变化来创建一个复杂的场景.
这些知识能够创建一些有意思的动画,但仅是苹果所提供创建动画效果工具中的冰山一角.
这篇文章将教你一些进阶的动画制作,一旦你掌握这些知识,将会打开动画的潘多拉墨盒:)

  • 容器的页面转换
    苹果提供了许多默认的动画效果,使你能很轻松地完成页面之间的动画过渡.
    为了很好地利用这些方法,你需要一个父容器.这个父容器一般为一个不可见的大尺寸的UIView.做这些动画效果之前需要做一些准备工作.
    我们要创建两种不同颜色的UIView之间的转换动画需要另外创建一个UIView.
    首先,需要在viewDidLoad()方法中创建三个views...
let container = UIView()
let redSquare = UIView()
let blueSquare = UIView()
override func viewDidLoad() {
    super.viewDidLoad()
    // set container frame and add to the screen
    self.container.frame = CGRect(x: 60, y: 60, width: 200, height: 200)
    self.view.addSubview(container)
    // set red square frame up
    // we want the blue square to have the same position as redSquare 
    // so lets just reuse blueSquare.frame
    self.redSquare.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
    self.blueSquare.frame = redSquare.frame
    // set background colors
    self.redSquare.backgroundColor = UIColor.redColor()
    self.blueSquare.backgroundColor = UIColor.blueColor()
    // for now just add the redSquare
    // we'll add blueSquare as part of the transition animation 
    self.container.addSubview(self.redSquare)   
}

运行程序,你将看到一个红色色块:

static-red-square.png

现在我们在storyboard中添加一个按钮来控制动画方法.
UIView.transitionWithView有许多参数:

  • view: 添加转场动画的页面
  • duration: 动画所持续的秒数
  • options: 可选参数(例如动画的类型)
  • animations: 定义要变换部分的block
  • completion: 动画结束时的block
    动画block中要完成移走一个view的同时在容器中新增一个view.
@IBAction func animateButtonTapped(sender: AnyObject) {
  
    // create a 'tuple' (a pair or more of objects assigned to a single variable)
    let views = (frontView: self.redSquare, backView: self.blueSquare)

    // set a transition style
    let transitionOptions = UIViewAnimationOptions.TransitionCurlUp

    UIView.transitionWithView(self.container, duration: 1.0, options: transitionOptions, animations: {
        // remove the front object...
        views.frontView.removeFromSuperview()
   
        // ... and add the other object
        self.container.addSubview(views.backView)
   
    }, completion: { finished in
        // any code entered here will be applied
        // .once the animation has completed
    })
}

container-1.gif

红色方块转换到蓝色方块时,一切正常,但之后就一直是蓝色方块.
这是因为我们的@IBAction方法设置的是红色可见方块.而这只有在首次点击动画按钮时才为true.
要解决这个问题需要判断出哪个色块是可见的,从而实现从从红色转变为蓝色,或者蓝色转变为红色.
有许多方法可以做到,但我们现在使用Swift的一个特性'tuple'来实现.

// create a 'tuple' (a pair or more of objects assigned to a single variable)
var views : (frontView: UIView, backView: UIView)

// if redSquare has a superView (e.g it's in the container)
// set redSquare as front, and blueSquare as back
// otherwise flip the order
if(self.redSquare.superview){
    views = (frontView: self.redSquare, backView: self.blueSquare)
}
else {
    views = (frontView: self.blueSquare, backView: self.redSquare)
}

现在我们能够在红蓝色块中自由转换了!

container-2.gif

从一个View转变到另一个View是常见的动画效果,苹果为我们提供了一种简洁直接的用法来自动实现removeSuperview()和addSubView()的效果:

@IBAction func animateButtonTapped(sender: AnyObject) {
    
    // create a 'tuple' (a pair or more of objects assigned to a single variable)
    var views : (frontView: UIView, backView: UIView)

    if(self.redSquare.superview){
        views = (frontView: self.redSquare, backView: self.blueSquare)
    }
    else {
        views = (frontView: self.blueSquare, backView: self.redSquare)
    }
    
    // set a transition style
    let transitionOptions = UIViewAnimationOptions.TransitionCurlUp

    // with no animation block, and a completion block set to 'nil' this makes a single line of code  
    UIView.transitionFromView(views.frontView, toView: views.backView, duration: 1.0, options: transitionOptions, completion: nil)
    
}

我们来尝试下其他的转换效果:

let transitionOptions = UIViewAnimationOptions.TransitionCurlDown
container-3.gif
let transitionOptions = UIViewAnimationOptions.TransitionFlipFromLeft
container-4.gif
  • Keyframe block animations
    另一个iOS7新增加的特性,用于替代创建动画时插入开始和结束值,它是将一个整体分为你想要的多个部分.
    例如我们想要将一张图片旋转360度.
    用我们熟知的动画方法通过改变transform属性来实现:
// create and add blue-fish.png image to screen
let fish = UIImageView()
fish.image = UIImage(named: "blue-fish.png")
fish.frame = CGRect(x: 50, y: 50, width: 50, height: 50)
self.view.addSubview(fish)
// angles in iOS are measured as radians PI is 180 degrees so PI × 2 is 360 degrees
let fullRotation = CGFloat(M_PI * 2)
UIView.animateWithDuration(1.0, animations: {
    // animating `transform` allows us to change 2D geometry of the object 
    // like `scale`, `rotation` or `translate`
    self.fish.transform = CGAffineTransformMakeRotation(fullRotation)
})

因为初始值和结束值相同,iOS不能在中间添加新值.
如果要实现如此,我们可以利用animateKeyFramesWithDuration来将翻转分割成许多小组合,然后将他们组成一个完整的动画效果:
我们将整个翻转分割成3部分,每一部分翻转1/3:

let duration = 2.0
let delay = 0.0
let options = UIViewKeyframeAnimationOptions.CalculationModeLinear
UIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {
    // each keyframe needs to be added here
    // within each keyframe the relativeStartTime and relativeDuration need to be values between 0.0 and 1.0
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1/3, animations: {
        // start at 0.00s (5s × 0)
        // duration 1.67s (5s × 1/3)
        // end at   1.67s (0.00s + 1.67s)
        self.fish.transform = CGAffineTransformMakeRotation(1/3 * fullRotation)
    })
    UIView.addKeyframeWithRelativeStartTime(1/3, relativeDuration: 1/3, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(2/3 * fullRotation)
    })
    UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(3/3 * fullRotation)
    })
    }, completion: {finished in
        // any code entered here will be applied
        // once the animation has completed
    })
}

现在我们可以很轻松的实现翻转360的动画效果.

rotate-1.gif

如果你手动输入开始和持续时间很容易出错,但如果要实现keyframes动画之间的连贯顺滑,我们可以通过CalculationModePaced参数来忽略你输入的开始和持续时间,自动计算出动画相对应的时间:

let duration = 2.0
let delay = 0.0
let options = UIViewKeyframeAnimationOptions.CalculationModePaced

UIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {
    
    // note that we've set relativeStartTime and relativeDuration to zero. 
    // Because we're using `CalculationModePaced` these values are ignored 
    // and iOS figures out values that are needed to create a smooth constant transition
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(1/3 * fullRotation)
    })
    
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(2/3 * fullRotation)
    })
    
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(3/3 * fullRotation)
    })
    
}, completion: nil)
  • 使一个物体按照贝塞尔曲线运动
    一个有意思的动画效果是物体按照曲线运动.
    我们很容易实现从A到B移动的效果,而实现多个坐标点ABCDE的移动我们需要再次利用keyframebased动画.
    我们可以利用keyframe block来实现移动效果,但如果想要顺滑我们必须定义多个keyframe,但却使代码变得复杂难懂.
    好消息是我们可以用贝塞尔曲线自动完成keyframe所做的工作.
    这需要我们使用更强大的iOS动画特性,了解它后并不会很难.
// first set up an object to animate
// we'll use a familiar red square
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)
square.backgroundColor = UIColor.redColor()
// add the square to the screen
self.view.addSubview(square)
// now create a bezier path that defines our curve
// the animation function needs the curve defined as a CGPath
// but these are more difficult to work with, so instead
// we'll create a UIBezierPath, and then create a 
// CGPath from the bezier when we need it
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 16,y: 239))
path.addCurveToPoint(CGPoint(x: 301, y: 239), controlPoint1: CGPoint(x: 136, y: 373), controlPoint2: CGPoint(x: 178, y: 110))
// create a new CAKeyframeAnimation that animates the objects position 
let anim = CAKeyframeAnimation(keyPath: "position")
// set the animations path to our bezier curve
anim.path = path.CGPath
// set some more parameters for the animation
// this rotation mode means that our object will rotate so that it's parallel to whatever point it is currently on the curve 
anim.rotationMode = kCAAnimationRotateAuto
anim.repeatCount = Float.infinity
anim.duration = 5.0
// we add the animation to the squares 'layer' property
square.layer.addAnimation(anim, forKey: "animate position along path")
path-1.gif

现在有了一个单一的动画,我们需要把它成倍增加来创建一个复杂的场景:

// loop from 0 to 5
for i in 0...5 {
    
    // create a square 
    let square = UIView()
    square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)
    square.backgroundColor = UIColor.redColor()
    self.view.addSubview(square)
    
    // randomly create a value between 0.0 and 150.0
    let randomYOffset = CGFloat( arc4random_uniform(150))
    
    // for every y-value on the bezier curve
    // add our random y offset so that each individual animation
    // will appear at a different y-position
    let path = UIBezierPath()
    path.moveToPoint(CGPoint(x: 16,y: 239 + randomYOffset))
    path.addCurveToPoint(CGPoint(x: 301, y: 239 + randomYOffset), controlPoint1: CGPoint(x: 136, y: 373 + randomYOffset), controlPoint2: CGPoint(x: 178, y: 110 + randomYOffset))
    
    // create the animation 
    let anim = CAKeyframeAnimation(keyPath: "position")
    anim.path = path.CGPath
    anim.rotationMode = kCAAnimationRotateAuto
    anim.repeatCount = Float.infinity
    anim.duration = 5.0
    
    // add the animation 
    square.layer.addAnimation(anim, forKey: "animate position along path")
}

现在我们有多个方块,但他们开始时间相同,看起来不太自然:

path-2.gif

我们可以设定不同方块的动画时间为随机数(这样方块就有不同的移动速度),还有方块开始的位置(以使它们交错出现).

// each square will take between 4.0 and 8.0 seconds
// to complete one animation loop
anim.duration = Double(arc4random_uniform(40)+30) / 10

// stagger each animation by a random value
// `290` was chosen simply by experimentation
anim.timeOffset = Double(arc4random_uniform(290))

由于设置的动画不同的属性值,现在我们的方块随贝塞尔曲线移动看起来自然多了.

path-3.gif

现在我们很容易就能将红色色块换成图片并且增加页面的背景,从而创建出鱼儿自由游动的场景.

fishes.gif
  • 贝塞尔曲线动画
    另一个有用的知识是画出曲线的动画.
    当我们创建一个曲线的动画时,贝塞尔曲线的路径并不能展示在屏幕上,但可以用keyframe动画替代.
    在本示例中我们要实现在屏幕中从0到100%画出曲线的动画.
    需要在点击方法@IBAction中增加一些代码.
// set up some values to use in the curve
let ovalStartAngle = CGFloat(90.01 * M_PI/180)
let ovalEndAngle = CGFloat(90 * M_PI/180)
let ovalRect = CGRectMake(97.5, 58.5, 125, 125)
// create the bezier path
let ovalPath = UIBezierPath()
ovalPath.addArcWithCenter(CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)),
    radius: CGRectGetWidth(ovalRect) / 2,
    startAngle: ovalStartAngle,
    endAngle: ovalEndAngle, clockwise: true)
// create an object that represents how the curve 
// should be presented on the screen
let progressLine = CAShapeLayer()
progressLine.path = ovalPath.CGPath
progressLine.strokeColor = UIColor.blueColor().CGColor
progressLine.fillColor = UIColor.clearColor().CGColor
progressLine.lineWidth = 10.0
progressLine.lineCap = kCALineCapRound
// add the curve to the screen
self.view.layer.addSublayer(progressLine)
// create a basic animation that animates the value 'strokeEnd'
// from 0.0 to 1.0 over 3.0 seconds
let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
animateStrokeEnd.duration = 3.0
animateStrokeEnd.fromValue = 0.0
animateStrokeEnd.toValue = 1.0
// add the animation
progressLine.addAnimation(animateStrokeEnd, forKey: "animate stroke end animation")

这个动画简单地画出了一个圆形,但可以推广于各种图形.有人还将此用于画出文字的路径,或者你可以显示你画任意一条曲线时的路径.

progress-1.gif
  • 系统默认动画
    iOS7另一个新特性是UIView.performSystemAnimation,但其中仅有UISystemAnimation.Delete的参数,希望苹果能不断丰富此功能以使之能够简单的调用.
// create and add blue-fish.png image to screen
let fish = UIImageView()
fish.image = UIImage(named: "blue-fish.png")
fish.frame = CGRect(x: 50, y: 50, width: 50, height: 50)
self.view.addSubview(fish)
// create an array of views to animate (in this case just one)
let viewsToAnimate = [fish]
// perform the system animation
// as of iOS 8 UISystemAnimation.Delete is the only valid option
UIView.performSystemAnimation(UISystemAnimation.Delete, onViews: viewsToAnimate, options: nil, animations: {
    // any changes defined here will occur
    // in parallel with the system animation 
}, completion: { finished in 
    // any code entered here will be applied
    // once the animation has completed
})
delete.gif

Girl学iOS100天 第5天

推荐阅读更多精彩内容