Swift UIView动画基础教程


原文地址:http://www.raywenderlich.com/76200/basic-uiview-animation-swift-tutorial
泰然翻译组:aquaporcus。校对:glory。

更新提示:本教程已由Bjørn Ruud更新至iOS8、Swift。原始教程作者是malek Trablesi.

Align the top and bottom parts of the basket

iOS应用最酷的一点就是这些应用的动画。可以是view飞过屏幕,淡入、淡出,旋转,等等等等。

这不仅是看起来很酷,动画也可以让用户注意到某些正在进行的事情,例如有更多的可用信息。

iOS里关于动画最棒的部分就是通过程序来实现动画简单到不可思议!简单的几行代码就可以实现动画了。

在本教程中,你有机会通过UIView动画创建一个简单的野餐程序。野餐篮子以一个整洁的动画方式打开,然后你去看看里面有什么 - 并采取果断的行动!

在这个过程里,你将学习如何使用基本的UIView动画APIs,以及如何串联这些动画。

那么,提起篮子,让我们去野餐吧!

注意:在发布本教程的时候,我们的理解是,不能发布测试产品的截图。因此,本教程中没有Xcode6和iOS8的截图,直到我们确认发布截图是合法的。

开始

你会体会到UIView的动画是多么好用、多么简单,因为如果iOS没有内置动画支持,你需要执行一些步骤来完成视图的移动:

  • 调度一个每帧都要调用的方法
  • 每帧都根据最终的目的地、总的动画时间以及当前已执行的时间来计算视图的位置(x和y)
  • 通过执行要求的动画,更新视图的位置

虽然工作量不大,但是足以使你想自己实现一个动画系统的时候三思。而且,当要跟踪更多的动画时,就变得复杂的多。

但是不必担心-使用UIKit,动画就相当简单!一个视图有若干属性,例如视图的frame(大小和位置),alpha(透明度)以及transform(缩放/旋转等等),当在动画块内修改这些属性时,有内置的动画支持。不必手动执行上面描述的动画步骤,你只需要:

  • 设置一个动画块,指明动画时间以及一些其他的可选参数。
  • 在这个动画块内,设置视图的动画属性,例如视图的frame。

就这样-UKit会接管并为你处理这些计算和更新。

言归正传,让我们看看当你启动应用时,创建野餐篮子打开动画的代码是什么样的。

正在打开的野餐篮子

打开Xcode,选择File\New Project,选择iOS\Application\Single View Applicaction然后点击Next。将工程命名为Picnic,选择Swift语言和iPhone设备。点击Next,为项目指定存储位置,然后点Create

紧接着,下载一份由Vicki制作的图片和声音,在项目中会用到。在Xcode中,选择Images.xcassets,解压缩下载的压缩包并将Images目录下的所有文件拖到其中。

在项目导航中,点击Main.storyboard,在编辑器中打开。你会注意到视图显示在一个矩形中,而不是适应设备的尺寸。Xcode6的storyboards默认使用size classes,这代表同一个storyboard可以不考虑屏幕尺寸直接应用到所有的目标设备上。这是个非常棒的特性,但是在本例中用不到,所以就简单点。

选中storyboard后,打开右边面板例的File inspector。在Interface Builder Document部分,有一个Use Size Classess的选择框,取消选中,确保选iPhone作为信的目标,然后点击Disable Size Classes

你还需要取消选中Use Auto Layout选择框来关闭自动布局。对于简单的布局,使用自动布局会复杂很多,但是并不必要,而且这也超出了本教程的范围。参考Beginning Auto Layout Tutorial in iOS7了解自动布局的更多信息。

现在你需要设置用户界面。从篮子的门开始,它将打开,显示篮子里的东西。首先拖拽一个View到主视图中,改变它的大小为完全填充主视图。这个视图将会作为篮子门的容器。到Size inspector中,取消选中所有的弹簧(springs)和支柱(struts),这样视图在父视图的中心。

拖拽两个UIImageViews到容器视图中,一个在上面,另一个在下面,各占一半的空间。选择上面的图片,打开它的属性面板。设置图片为door_topView ModeBottom。设置下面的图片为door_bottomView ModeTop。调整视图大小直到他们没有问题,像下图那样:

Align the top and bottom parts of the basket

对齐篮子的上下两部分

将图片视图放在容器中,而容器至于视图的中心,篮子门就会使用在屏幕的中心,无论屏幕是什么尺寸。

写点代码吧。你需要声明两个图片视图的属性,这样迟点可以动画他们。打开助手编辑器,确保编辑的是ViewController.swift。按住Ctrl,将图片视图拖到编辑器中,放在类声明下面,将插座(outlet)命名为basketTop。同样操作下面的图片视图,命名为basketBottom。结果应该像下面的代码:

class ViewController: UIViewController {
  @IBOutlet var basketTop: UIImageView!
  @IBOutlet var basketBottom: UIImageView!
  //...
}

现在你有引用在界面Builder中创建的图像视图,当视图首次出现时,可以创建动画来打开篮子。回到ViewController.swift,在viewDidLoad()中添加如下方法:

override func viewDidAppear(animated: Bool) {
  UIView.animateWithDuration(0.7, delay: 1.0, options: .CurveEaseOut, animations: {
    var basketTopFrame = self.basketTop.frame
    basketTopFrame.origin.y -= basketTopFrame.size.height

    var basketBottomFrame = self.basketBottom.frame
    basketBottomFrame.origin.y += basketBottomFrame.size.height

    self.basketTop.frame = basketTopFrame
    self.basketBottom.frame = basketBottomFrame
  }, completion: { finished in
    println("Basket doors opened!")
  })
}

调用animateWithDuration(delay:, options:, animations:, completion:)定义了一个动画,其持续时间是半秒,延迟一秒后开始。动画是“ease out”,意思就是速度越来越慢。

animations块定义了动画做哪些事情。这里,两个图片视图的frame设置为最终值:上面的篮子图片上移,下面的下移,从而打开篮子。因为你设置了持续时间,UIKit从那里开始接管,然后执行篮子打开的动画。

completion块在动画完成或者中断后执行-finished参数是布尔类型的,代表动画完成与否。

自己试着创建并运行这些代码-达到这样的整洁效果相当简单,对不?

乐趣多多的第二层

去野餐的时候通常不会直接把食物放到篮子里-相反的,你会在上面放一个餐巾来防止那些厌烦的不请自来者(如苍蝇)。所以为何不用动画来增加点乐趣?增加一个餐巾层,并且也打开它。

回到Main.storyboard,选择之前的两个图片视图。选中后,到Edit菜单中选择Duplicate。将这些复制的视图叠加到原始视图上。

Attributes inspector中,设置新的上面的视图图片为fabir_top,新的底部视图图片为fabric_bottom

你希望布的视图位于野餐篮子的底部。通过Editor\Show Docuemnt Outline打开文档大纲。视图以从底到顶的顺序列出,你希望餐巾在篮子的底下。拖拽大纲里的视图,使得其顺序如下所示:

  • fabric_top
  • fabric_bottom
  • door_top
  • door_bottom

现在,场景中有了新的图片视图,你能自己根据所学的所有知识找到该如何动画吗?目标是使得餐巾从屏幕上移出,但是在篮子开始移动后马上开始移动。试试-如果遇到问题你可以再回到这里。

答案

这里有答案,以防万一你碰到了麻烦。

首先往ViewController.swift中增加两个插座属性,就像之前你做的那样,Ctrl加拖拽,从storyboard到助手编辑器。

@IBOutlet var fabricTop: UIImageView!
@IBOutlet var fabricBottom: UIImageView!

viewDidAppear()的底部增加如下代码:

UIView.animateWithDuration(1.0, delay:1.2, options: .CurveEaseOut, animations: {
  var fabricTopFrame = self.fabricTop.frame
  fabricTopFrame.origin.y -= fabricTopFrame.size.height

  var fabricBottomFrame = self.fabricBottom.frame
  fabricBottomFrame.origin.y += fabricBottomFrame.size.height

  self.fabricTop.frame = fabricTopFrame
  self.fabricBottom.frame = fabricBottomFrame
 }, completion: { finished in
   println("Napkins opened!")
 })

和之前的篮子门的动画相比没有什么大的变化。注意,duration和delay参数都更大一些,这样它就可以在篮子的动画之后开始、结束。

编译并运行代码,篮子的打开方式更酷了。

如何串联动画

目前位置,你只是通过UIView的单个属性来动画-frame。同样的,你也只是完成了单个动画。

但是正如前面提到的,有多个参数可以动画,还可以在一个动画完成后再触发更多的动画。让我们通过另外两个有趣的属性(center和transform)来实践一下,并且使用动画链。

在此之前,让我们先添加野餐篮子的内部!打开Main.storyboard,把另一个图片视图拖到视图容器中。改变它的尺寸和位置使得它充满整个视图,并保证它在列表的最上面,这样它就显示在其他所有视图的下面。将图片设置为plate_cheese

还有一件事要做:尽管有预防措施,但是虫子还是悄无声息地放到了“篮子”中!增加一个UIImageView作为容器试图的子视图。将它放在Plate视图的正下方,设置图片为bug。在Size inspector中设置frame为:x=160,y=185,width=129,height=135。

此时,文档大纲里的View Controller Scene应该像下面这样:

  • plate_cheese
  • bug
  • fabric_top
  • fabric_bottom
  • door_top
  • door_bottom

接着在ViewController.swift中为新的虫子视图增加一个插座:按住Ctrl,从storyboard拖到助手编辑器中。将插座命名为bug

最后,你得压扁虫子,防止它糟蹋野餐。切换到ViewController.swift,在类声明中增加如下属性:

var isBugDead = false

后面会用它来判断虫子是否死了。

接着增加如下方法:

func moveBugLeft() {
  if isBugDead { return }

  UIView.animateWithDuration(1.0,
    delay: 2.0,
    options: .CurveEaseInOut | .AllowUserInteraction,
    animations: {
      self.bug.center = CGPoint(x: 75, y: 200)
    },
    completion: { finished in
      println("Bug moved left!")
      self.faceBugRight()
    })
}

func faceBugRight() {
  if isBugDead { return }

  UIView.animateWithDuration(1.0,
    delay: 0.0,
    options: .CurveEaseInOut | .AllowUserInteraction,
    animations: {
      self.bug.transform = CGAffineTransformMakeRotation(CGFloat(M_PI))
    },
    completion: { finished in
      println("Bug faced right!")
      self.moveBugRight()
    })
}

func moveBugRight() {
  if isBugDead { return }

  UIView.animateWithDuration(1.0,
    delay: 2.0,
    options: .CurveEaseInOut | .AllowUserInteraction,
    animations: {
      self.bug.center = CGPoint(x: 230, y: 250)
    },
    completion: { finished in
      println("Bug moved right!")
      self.faceBugLeft()
    })
}

func faceBugLeft() {
  if isBugDead { return }

  UIView.animateWithDuration(1.0,
    delay: 0.0,
    options: .CurveEaseInOut | .AllowUserInteraction,
    animations: {
      self.bug.transform = CGAffineTransformMakeRotation(0.0)
    },
    completion: { finished in
      println("Bug faced left!")
      self.moveBugLeft()
    })
}

可以看到串联动画的方法:在completion块中开始新的动画。动画链左移虫子,然后向右旋转,右移,向左旋转,如此重复。

注意可选参数中的.AllowUserInteraction选项。你要让虫子视图在移动的时候能够响应触摸事件(来压扁虫子),这个选项让你能够在视图正在动画的时候与它进行交互。

上面的代码修改center属性而不是frame属性来移动虫子。这设置了虫子的中心问题,有时这样做比设置frame要简单些。

旋转则是通过设置仿射变形来完成。利用CGAffineTransformMakeRotation,我们可以创建一个旋转变形,角度参数单位是弧度,所以参数设置为M_PI能够旋转180度。

现在你只需要来启动动画链。在viewDidAppear()最后添加下面一行代码:

moveBugLeft()

建造并运行,你应该可以看到虫子爬来爬去。

bug

虫子!

压扁虫子!

现在是你一直等待的时候了-压扁那个虫子!

添加如下方法来处理挤压虫子的事件:

func handleTap(gesture: UITapGestureRecognizer ) {
  let tapLocation = getsture.locationInView(bug.superview)
  if bug.layer.presentationLayer().frame.contains(tapLocation) {
    println("Bug tapped")
    //在这里添加压扁虫子的代码
  } else {
    println("Bug not tapped")
  }
}

响应触摸的时候,你需要检测是否触摸到虫子了。一般,你会检查tapLocation是否在虫子视图的frame中,但是在这,注意到使用的是视图展现的层的frame。这是一个很重要的区别:UIView动画更新视图“展现”的层,而不是视图本身的frame,这个层代表什么会显示在屏幕上。技术上,虫子在它被放在的位置上,在动画中间,只有展现的层在变化。

查看Introduction to CALayers来获取关于层的更多信息。

接下来,在viewDidAppear()的最后增加如下代码:

let tap = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
view.addGestureRecognizer(tap)

代码初始化了一个新的点击手势识别,然后将它添加到视图中。

建造并运行,然后点击屏幕。在Xcode控制台中,根据是否点中虫子,你可以看到"Bug tapped"或者"Bug not tapped"。

最令人满意的是下一部分。找到handleTap()然后在if块中添加如下代码:

if isBugDead { return }
isBugDead = true
UIView.animateWithDuration(0.7, delay: 0.0, options: .CurseEaseOut, animations: {
  self.bug.transform = CGAffineTransformMakeScale(1.25, 0.75)
}, completion: { finished in
  UIView.animateWithDuration(2.0, delay:2.0, options: nil, animations: {
    self.bug.alpha = 0.0
  }, completions: { finished in
    self.bug.removeFromeSuperView()
  })
})

一旦点击虫子,首先将isBugDead置为true,这样动画链就停止了。然后开始一个新的动画链:虫子先被压扁,通过一个缩放的变形实现,然后一段延迟后,通过设置alpha为0让虫子消失,最后从父视图中移除。

squashed

压扁了

免费音效

这一部分完全没有必要,但是非常有趣-你将添加一个免费的虫子被压扁的音效!

首先将Sounds目录从资源档案中拖到项目里,并确保选中复制文件并创建组。

回到ViewController.swift,在文件顶部增加下面的代码:

import AVFoundation

AVFoundation包括了音频播放,所以你需要导入它来播放挤压虫子的音效。

然后在类声明里增加属性:

let squishPlayer: AVAudioPlayer

这将持有播放声音文件的音频播放器实例。

接着增加如下初始化方法:

required init(coder aDecoder: NSCoder!) {
  let squishPath = NSBundle.mainBundle().pathForResource("squish", ofType: "caf")
  let squishURL = NSURL(fileURLWithPath: squishPath)
  squishPlayer = AVAudioPlayer(contensOfURL: squishURL, error: nil)
  squishPlayer.prepareToPlay()

  super.init(coder: aDecoder)
}

这段代码保证当视图控制器初始化的时候创建AVAudioplayer的实例。如果你直到Objective-c,那么在初始化最后调用super.init()看起来很奇怪。Swift中,初始化必须保证在调用父类的初始化前所有的实例变量已经初始化完成,在这之后,才能安全的调用实例或者父类方法。注意,带缺省值的实例变量(如isBugDead)不需要在初始化中设置。

最后,在handleTap()设置isBugDeadtrue的代码后面添加:

squishPlayer.play()

这样就可以在适当的时候播放音效。建造并运行,保证声音打开,现在你可以挤压虫子,并得到重要的音乐满意度。

何去何从

这是包含了本教程全部代码的最终工程

你可以考虑根据目前所学到的添加更多的动画。例如,挤死虫子后关闭野餐篮子(如果你像我一样,看到虫子在食物旁边玩耍就很恶心)。你还可以修改打开篮子和餐巾的时机,这样只有你触摸后它才会打开。

通篇教程中你只用到了animateWithDuration(duration, delay, options, animations, completion)函数,下面列出了一起其他的动画函数:

  1. animateWithDuration(duration, animations)
  2. animateWithDuration(duration, animations, completion)
  3. animatKeyframesWithDuration(duration, delay, options, animations, completion)
  4. performSystemAnimation(animation, onViews, options, animations, completion)
  5. animateWithDuration(duration, delay, usingSpringWithDamping, initialSprintVelocity, options, animations, completion)

前两个和本教程中用到的方法相同,只是少了delay和options参数。第三个是基于关键帧的动画,它使能了如何动画的大部分控制。第四个用来执行系统提供的动画。最后一个用来创建基于弹簧的运动曲线的动画。换句话说,它向尾端反弹。

现在你直到了UIView动画的基本用法,你还可以查看View Programming Guide for iOSAnimations章节来获取更多有用的信息。

你在项目中如何运用UIView动画或者Core Animation?我很乐意通过本文的回复聆听!

标签: Swift

?>