【译】仿Taasky的3D翻转菜单动画实现
最终效果
![最终效果](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/runfinal.gif)
最终效果
开始
![Starter Project效果](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/starterMasterDetail-383x320.png)
Starter Project效果
这个教程将详细的介绍实现步骤,具体步骤如下:
-
整个教程将使用自动布局来实现,需要将现在的主页push到详情页的这种模式改为横向ScrollView滚动的模式。
- 需要在左上角添加显示/隐藏菜单栏的按钮。
- 接下来,需要实现类似于Taasky的菜单栏的3D翻转效果。
-
最后,需要将菜单栏的翻转效果与按钮的旋转效果结合起来。
![视图范围](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/diagramMenuDetail1-225x320.png)
视图范围
用户可以通过左右滑动来显示/隐藏菜单栏。在上图中,紫色方框是当菜单栏显示的时候,屏幕所显示的部分,绿色方框是当菜单栏隐藏时,屏幕所显示的内容。当菜单栏打开时,显示的是一个局部侧滑边栏。
刚才提到的SwiftSideNav是一个使用自动布局完成例子,但是今天的教程将会直接在StoryBoard中嵌入菜单与详情页,完成后的结构如图:
![最终结构图](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/sbContainers-700x445.png)
最终结构图
为UIScrollView添加约束
菜单栏与详情。整个视图层级应该是这样的:
- 有一个根控制器,并将UIScrollView添加到根控制器上;
- 然后添加一个UIView到UIScrollView上,暂且叫它”Content View”;
-
添加两个子容器到”Content View”上,然后分别把菜单栏和详情页填到到子容器中。
![创建容器](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/newFileContainerVC-480x303.png)
创建容器
打开Main.StoryBoard,从Object Library中拖一个View Controller到画布中,并在Identity Inspector(Xcode右边栏的第三个选项)中设置自定义类为ContainerViewController,唯一标识为ContainerVC。
![绑定类,修改唯一标识](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/sbIdInsp-700x116.png)
绑定类,修改唯一标识
![设置背景色](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbBkgdBlack-700x154.png)
设置背景色
创建UIScrollView
- 拖一个UIScrollView到ContainerViewController中,并让它填充整个ContentViewController;
- 取消横、纵滚动条的显示(Shows Horizontal Indicator与Shows Vertical Indicator);
-
取消Delays Content Touches,让响应事件及时传递给子控件,防止响应延迟;
将ContainerViewController设置为UIScrollView的delegate(按住control键,从UIScrollView拖到UIViewController)。
![关联代理](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbSetDelegate-480x289.png)
关联代理
- 找到右下角的Pin按钮,打开添加约束的弹窗;
-
- 添加上下左右四个约束,并确保约束的值都是0;
-
点击Add 4 Constraints按钮,将约束加上。
![给UIScrollView添加约束](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbAutoScrollView-386x500.png)
给UIScrollView添加约束
仍然选中UIScrollView,打开Size Inspector选项(Xcode右边栏倒数第二个选项),确认一下约束是否与教程所加的约束相同:
- Trailing Space to: Superview
- Leading Space to: Superview
-
Top Space to
-
Bottom Space to: Bottom Layout Guide
![检查约束](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbSizeInspScrollView-152x320.png)
检查约束
如果有类似16这样的数字出现,说明在添加约束的时候,没有取消Constrain to margins。解决方案:删除UIScrollView的约束,重新添加一次,注意记得取消Constrain to margins。
在添加或修改约束以后,可能需要根据新的约束来展示frame。这个需求可以通过右下角Resolve Auto Layout Issues弹出框(Pin按钮旁边)的Update Frames选项来解决。
创建Content View
接下来需要做的是添加一个Content View到UIScrollView上,并添加响应约束,这些约束对设置UIScrollView的contentSize很重要。在下一节中,要将添加菜单栏与详情两个容器视图添加到Content View上。
拖一个新的View到UIScrollView上,让它自动填满整个父视图,并将背景色设置为Default。
![设置宽与背景色](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbSizeInspView-480x190.png)
设置宽与背景色
译者注:上图中,作者将Width设置为了680,但在文字描述中并未提到。这个宽度可加可不加,因为之后界面上的宽度其实全都要通过约束来表现。
打开Pin弹出框,给Content View添加相对于父视图的约束。
![给Content View添加约束](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/sbAutoContentView-210x320.png)
给Content View添加约束
在Size Inspector中,确保Trailing Space的约束值为0。
![将右边距约束改为0](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbSizeInspContentView2.png)
将右边距约束改为0
译者注:这里有点不明白作者的操作,直接在添加约束的时候,将右边距设置为0就可以了,但是作者却先将宽设置为680,然后添加约束的时候也是添加的-80,最后又改成0。感觉有点多此一举,还请大神们解答下。
现在之所以有警告是因为StoryBoard需要Content View的高和宽来设置contentSize。添加相对于Scroll View的父容器的这些约束,可以使之适配各种设备与横竖屏。
![添加等宽等高约束](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/sbAutoContainerViewEqWidth.png)
添加等宽等高约束
然后将Equal Width约束改为80。
![加80像素的约束](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/sbSizeInspContentView-275x320.png)
加80像素的约束
将约束改为80的用意是:这样Content View就会比View宽80,就可以刚好装下菜单栏。同时,可以发现之前的警告也不存在了——干得漂亮。
添加菜单栏与详情界面
首先,创建Menu Container View:拖一个Container View到Content View上,在Size Inspector中,将其宽度设为80,然后在Identity Inspector中将Document\Label设置为Menu Container View:
![设置宽度,添加描述Label](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/sbContainerMenuUtil-480x296.png)
设置宽度,添加描述Label
高度应该是默认的600,不过如果你不确定,也可以设置一下。
译者注:这里作者只提到设置宽度,但根据后文的一些描述,这里应该还需要将x设置为0,y设置为0。
然后,添加Detail Container View:新拖一个Container View到Content View上,并放置在menu container的右边。打开Size Inspector,并设置以下值:
- X: 80
- Y: 0
- Width: 600
- Height: 600
![设置frame,添加描述Label](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/sbContainerDetailUtil-480x297.png)
设置frame,添加描述Label
这样,Menu View与Detail View的宽度就和Content View相等了。
![容器](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/sbContainerViews1-306x320.png)
容器
当添加完这两个自控制器后(container view),可在在IB中看到系统已经默认添加了contained view controller,但是我们需要用到已存在的Menu View与Detail View,所以需要在document outline或者画布中删除这两个控制器。
![删除系统自动关联的ViewController](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbDeleteContainerVCs-675x500.png)
删除系统自动关联的ViewController
译者注:这个教程中的操作,都在文章开头下载的那个Demo中进行(称为starter project),所以,已存在的Menu View与Detail View就是指starter project中的控制器视图。
给Menu Container View添加宽为80,上下左右边距为0,共5个约束。
![添加5个约束](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/sbAutoMenu-211x320.png)
添加5个约束
给Detail Container View添加上、右、下边距为0,共3个约束;注意不要添加左边距约束,否则会和Menu Container View的右边距约束重复。
![添加三个约束](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbAutoDetail-211x320.png)
添加三个约束
给两个Container View添加的约束都要确保Constrain to margins为取消。
嵌入Menu与Detail View Controllers
这一节中,将会把starter project中的menu和detail view controllers拆开,然后分别嵌入Menu Container View和Detail Container View。
首先,将Container View Controller设为入口控制器:将原本指向Navigation Controller的入口箭头指向Container View Controller:
![设置入口控制器](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/sbMoveStartArrow-480x306.png)
设置入口控制器
接下来,按住Control键,从Menu Container View拖到Navigation Controller,在弹出框中选择embed。
![关联菜单视图](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/sbEmbedMenu-343x320.png)
关联菜单视图
当Menu Container View嵌入Navigation Controller之后,相关的视图的宽度就都缩小到了80:
![关联以后,控制器宽度变为80](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/sbEmbedResult-433x320.png)
关联以后,控制器宽度变为80
现在来调整一下menu与detail:首先,将table view cell中imageView的宽度调整为80:
![调整cell宽度](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/sbSizeInspImage-700x289.png)
调整cell宽度
![给详情页添加导航栏](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/sbEmbedDetailNavController-241x320.png)
给详情页添加导航栏
现在detail应该加在了Navigation Controller上,同时拥有了黑色的导航栏。
选中这个新的Navigation Controller的导航栏,打开Attributes Inspector,选择Style为Blank,取消Translucent,并将Bar Tint设置为Black Color:
![设置导航栏样式与背景色](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/sbNavBarAttr.png)
设置导航栏样式与背景色
译者注:如果不好选中导航栏,可以在document outline中进行选择,注意不要选错了。
这个设置完以后,新加的导航栏就和第一个导航栏的样式一致了。
![选中Adjust Scroll View Insets](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/sbNavControllerLayout-163x320.png)
选中Adjust Scroll View Insets
Adjust Scroll View Insets的选中是为了将视图从Navigation Bar下面开始显示,而不会被Navigation Bar盖住
最后,将Detail View Controller嵌入Detail Container View:按住Control键,从Detail Container View拖到Detail View Controller的Navigation Controller,在弹出框中选择embed:
![嵌入详情页](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/sbEmbedDetail-321x320.png)
嵌入详情页
运行程序,左右拖动视图,就可以显示/隐藏菜单栏了。你是否注意到,在拖动Scroll View是,可以超出左右边界,而且还可以停止滚动,显示部分菜单这个问题?
![当前效果图](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/runScrollView0a.gif)
当前效果图
为了修复这个问题,需要在Scroll View的Attributes Inspector中修改一下属性:
- 选中Scrolling\Paging Enabled属性,这样就可以…(译者注:原谅我找不到好的词语来表达原词了,原文中用到的是“snaps”,大概就是有一种惯性、巧劲的感觉,具体Paging Enabled的效果相信大家都懂的,就不说了);
- 取消Bounce\Bounces属性,防止左右滚出边距(译者注:即没有回弹效果)。
译者注:原文的动图没有截导航栏部分,我在跟着做的时候,发现菜单的导航栏上有白边,原因是Menu Container View与Detail Container View的背景色为默认色的原因,设置为黑色即可。
这时,在运行程序,就不会出现左右滑出边界的情况了,并且,也不会出现菜单栏可以显示一部分的情况。但是,在向左滑动,试图隐藏菜单栏时,又出现了新问题。
![Paging Enable问题](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/runScrollview0b.gif)
Paging Enable问题
现在我们需要先解决另外一个问题:详情页是空白的,并且在点击菜单栏的时候,没有任何事件触发。
![详情页没有切换](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/runScrollview1.gif)
详情页没有切换
这完全在意料之中,因为还没有代码对container views进行关联。
对容器进行编码
在开始之前,先将MenuViewController.swift中的viewDidLoad()复制到DetailViewController.swift中,如下:
override func viewDidLoad() {
super.viewDidLoad()
// Remove the drop shadow from the navigation bar
navigationController!.navigationBar.clipsToBounds = true
}
这句代码的目的是不显示导航栏下面的那条阴影,虽然是个小细节,但就是这些细节使我们的App更加优雅。
当用户选中某个单元格时, MenuViewController必须要设置DetailViewController中的menuItem属性,但是这两个类已经没有直接联系了。所以,这两个类之间的通信将有ContainerViewController来代替。
在ContainerViewController.swift的顶部添加DetailViewController属性:
private var detailViewController: DetailViewController?
在ContainerViewController.swift中实现prepareForSegue(_:sender:)这个方法,并添加以下代码:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// 译者注:这里的"DetailViewSegue"设置接下来的描述中讲解(就是这段代码下面的这段话)
if segue.identifier == "DetailViewSegue" {
let navigationController = segue.destinationViewController as! UINavigationController
detailViewController = navigationController.topViewController as? DetailViewController
}
}
segue.identifier是啥?在将DetailViewController嵌入到容器时,需要在Attributes inspector中将Storyboard Embed Segue的Identifier设置为DetailViewSegue。
![设置Segue唯一标识](http://cdn5.raywenderlich.com/wp-content/uploads/2015/06/sbsegueID-700x372.png)
设置Segue唯一标识
然后,声明menuItem属性,并在didSet中,在它进行赋值时,也给DetailViewController中的menuItem属性进行赋值。
var menuItem: NSDictionary? {
didSet {
if let detailViewController = detailViewController {
detailViewController.menuItem = menuItem
}
}
}
这已不再是table view cell与content view之间的交互了,而是用户选择菜单时,MenuViewController需要做出响应。
译者注:上面这句话纠结好久,翻译出来还是不通顺,原话是这样的:There’s no longer a segue from a table view cell to the content view, but MenuViewController needs to respond when the user selects an item.
// MARK: UITableViewDelegate
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// 译者注:取消单元格选中
tableView.deselectRowAtIndexPath(indexPath, animated: true)
// 译者注:从menuItems数组中取出menuItem字典,里面的值是根据MenuItems.plist生成的,包含小图,大图,背景色
let menuItem = menuItems[indexPath.row] as! NSDictionary
// 译者注:当前的navigationController!.parentViewController就是ContainerViewController,因为navigationController的视图是作为80像素的Container View添加在ContainerViewController中的
(navigationController!.parentViewController as! ContainerViewController).menuItem = menuItem
}
这里,只是简单将基于选择的单元行设置了ContainerViewController的menuItem属性。这将使属性的didSet方法触发(就是上面将menuItem赋值给detailViewController.menuItem的那段代码)。
最后,在MenuViewController.swift的viewDidLoad()方法中,添加以下代码:
(navigationController!.parentViewController as! ContainerViewController).menuItem =
(menuItems[0] as! NSDictionary)
这句代码将在App第一次加载的时候给detail view添加对应的图片。
运行程序,可以看到程序加载以后,详情页已经有了图片,菜单栏正常显示,并且在切换菜单的时候,详情页也能显示对应的图片:
![运行效果](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/runScrollView2.gif)
运行效果
为了达到菜单栏点击以后应该自动隐藏的目的,应该在单元行点击以后,设置Scroll View的水平位移为菜单栏的宽度,这样就可以完全展示出详情页。
首先,需要将Scroll View与Container View关联。
建立scrollView与ContainerViewController.swift的关联:在Storyboard的document outline中,选中Scroll View,打开Assistant Editor,按住Control键,从Scroll View拖到ContainerViewController.swift。然后在弹出框的Name一栏中将Scroll View命名为scrollView。
![关联Scroll View](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/sbConnectScrollView-513x500.png)
关联Scroll View
同样的步骤,按住Control键,从Menu Container View拖到ContainerViewController.swift,创建一个menuContainerView。
![关联菜单视图](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/sbConnectMenuContainerView-499x500.png)
关联菜单视图
// MARK: ContainerViewController
func hideOrShowMenu(show: Bool, animated: Bool) {
let menuOffset = CGRectGetWidth(menuContainerView.bounds)
scrollView.setContentOffset(show ? CGPointZero : CGPoint(x: menuOffset, y: 0), animated: animated)
}
menuOffset的值就是Menu Container View宽度为80,如果show为true,scrollView的偏移量就为0,这是菜单栏就会显示,同样,scrollView的偏移量为80,菜单栏就会隐藏。
var menuItem: NSDictionary? {
didSet {
hideOrShowMenu(false, animated: true)
// ...
在用户点击单元行之后,应该关闭菜单栏,所以show置为false。
同样,在viewDidLoad() 中调用hideOrShowMenu(_:animated:)方法,让程序启动时,菜单栏处于隐藏状态:
override func viewDidLoad() {
super.viewDidLoad()
hideOrShowMenu(false, animated: false)
}
运行程序,这时详情页显示的是笑脸那张图,并且菜单栏处于隐藏状态。滑出菜单,选择某个单元行,可以看到菜单栏会隐藏,并且详情页能显示出对应的图片与背景色。
![当前效果](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/runScrollView3.gif)
当前效果
但是,之前因为Paging Enable出现的问题依然存在:如果滑出菜单栏,不选择单元行,然后滑动隐藏菜单栏时,菜单栏又会弹出来。这个问题可以通过实现UIScrollViewDelegate中的一个方法来解决。
在ContainerViewController类声明的地方,遵守UIScrollViewDelegate协议:
class ContainerViewController: UIViewController, UIScrollViewDelegate {
然后,在ContainerViewController添加UIScrollViewDelegate的代理方法:
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(scrollView: UIScrollView) {
/*
Fix for the UIScrollView paging-related issue mentioned here:
http://stackoverflow.com/questions/4480512/uiscrollview-single-tap-scrolls-it-to-top
*/
scrollView.pagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width - CGRectGetWidth(scrollView.frame))
}
这将在Scroll View的偏移量与菜单栏宽度相等的时候,禁用Paging Enable;这样,当菜单栏完全隐藏的时候,就会保持隐藏状态。当用户滑出菜单时,Paging Enable又会启用,从而菜单能一次性滑出。
运行程序,可以发现,这个问题已经解决了。
![当前效果](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/runScrollView4.gif)
当前效果
看起来不错,但依然缺少一些东西。详情也的导航栏上少了汉堡按钮(The hamburger menu button,即有三条横线的菜单按钮),它能控制菜单的开关,并且还能随着菜单的显示而旋转。
我们需要菜单按钮是自定义视图,这样才可以在菜单栏显示/隐藏的时候进行相应的旋转动画。
创建一个HamburgerView.swift,继承自UIView。
在这个类中添加以下代码:
class HamburgerView: UIView {
let imageView: UIImageView! = UIImageView(image: UIImage(named: “Hamburger”))
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
required override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
// MARK: Private
private func configure() {
imageView.contentMode = UIViewContentMode.Center
addSubview(imageView)
}
}
上面的代码重写了两个初始化方法,并调用了configure()来将imageView加载到父视图上。
在DetailViewController.swift中添加hamburgerView属性:
var hamburgerView: HamburgerView?
在viewDidLoad()中,给hamburgerView创建实例,并将它作为Navigation Bar的left bar button,并添加一个点击手势。
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: “hamburgerViewTapped”)
hamburgerView = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
hamburgerView!.addGestureRecognizer(tapGestureRecognizer)
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: hamburgerView!)
hamburgerViewTapped() 方法需要触发ContainerViewController中的hideOrShowMenu(_:animated:)方法,但是问题是show应该传什么参数?所以ContainerViewController需要一个Bool值来记录菜单显示/隐藏的状态。
在ContainerViewController.swift中添加以下属性:
var showingMenu = false
初始时菜单的显示状态是false,重写viewDidLayoutSubviews()方法,在bounds改变的时候来显示/隐藏菜单。
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
hideOrShowMenu(showingMenu, animated: false)
}
你将不再需要viewDidLoad()方法,所以将它从ContainerViewController.swift中删除。
打开DetailViewController.swift,添加以下代码:
func hamburgerViewTapped() {
let navigationController = parentViewController as! UINavigationController
let containerViewController = navigationController.parentViewController as! ContainerViewController
containerViewController.hideOrShowMenu(!containerViewController.showingMenu, animated: true)
}
如果showingMenu的值为false,菜单栏为隐藏状态,这是,当用户点击按钮,hideOrShowMenu(:animated:)方法就会被调用,show参数传入true,然后菜单显示。相反,当菜单处于显示状态时,即showingMenu值为true,这时用户点击按钮,菜单栏就会隐藏。因此,需要更新ContainerViewController.swift中hideOrShowMenu(:animated:)方法的showingMenu属性。
在 hideOrShowMenu(_:animated:):中添加以下代码:
showingMenu = show
运行程序,尝试去滚动视图、点击菜单按钮、点击单元行这些操作。
![当前运行效果](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/runButton1a.gif)
当前运行效果
这存在一个问题:如果是采用滑出/滑入菜单栏的操作,那么菜单栏按钮要点两次,菜单栏才有响应。这是为什么?
![出现的问题](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/runButton1b.gif)
出现的问题
这个问题出现的原因为:在滚动的后,showingMenu没有更改,菜单滑出的时候showingMenu还是false。当第一次点击的时候,showingMenu的值设置为true,所以菜单还是显示状态。第二次点击的时候,才能将showingMenu置为false,从而隐藏菜单。
要解决这个问题,需要在ContainerViewController的UIScrollViewDelegate代理方法中将showingMenu设置一下,具体如下:
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let menuOffset = CGRectGetWidth(menuContainerView.bounds)
showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset)
println(“didEndDecelerating showingMenu \(showingMenu)”)
}
当滚动结束以后,如果Scroll View的偏移量与菜单栏的宽度相同(即,菜单栏处于隐藏状态),那就将showingMenu设置为false。相反,则置为true。
运行程序,向右滑动,当滚动停止时,查看控制台打印信息。它的调用取决于Scroll View的滚动速度。当我在模拟器上运行时,只有慢慢的滚动,才会调用这个方法,但是当我在真机上运行时,它又需要快速滚动才能调用。
所以,将这段代码放入到scrollViewDidScroll(_:)方法中,这样可以更好的响应滚动。这个方法在用户滚动时就会调用,这也更加的可靠。
接下来就可以给菜单按钮加旋转动画了,但在此之前,我们先给菜单栏加3D翻转动画。
给菜单栏加立体效果
这个超级炫酷的动画看起来就像开门和关门。与此同时,菜单按钮也应该跟随菜单的状态,有一个平滑的旋转效果。
为了达到这个效果,我们将计算菜单滑出的比例(后文称为fraction),从而得到按钮应该旋转的角度。
在ContainerViewController.swift中,添加一个私有方法来通过比例进行3D旋转动画:
func transformForFraction(fraction:CGFloat) -> CATransform3D {
var identity = CATransform3DIdentity
identity.m34 = -1.0 / 1000.0;
let angle = Double(1.0 - fraction) * -M_PI_2
let xOffset = CGRectGetWidth(menuContainerView.bounds) * 0.5
let rotateTransform = CATransform3DRotate(identity, CGFloat(angle), 0.0, 1.0, 0.0)
let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0)
return CATransform3DConcat(rotateTransform, translateTransform)
}
下面是transformForFraction(_:)这个方法的实现步骤:
-
- CATransform3DIdentity是一个4*4的矩阵,对角线是1,其他是0;
- CATransform3DIdentity的m34属性是这个矩阵中的第三列第四行,它控制着变换中的立体程度;
- CATransform3DRotate通过angle这个变量来控制y轴的旋转量:-90度呈现的是菜单栏垂直于屏幕的状态,0度呈现的是与xy轴平行的状态;
- rotateTransform的旋转变换,是根据m34矩阵与y轴旋转量来的;
- translateTransform设置了x轴的偏移量为菜单栏的一般;
-
CATransform3DConcat将rotateTransform与translateTransform联系起来,使得在做翻转动画时,也有偏移动画。
接下来,在scrollViewDidScroll(_:):中添加下面代码:
let multiplier = 1.0 / CGRectGetWidth(menuContainerView.bounds)
let offset = scrollView.contentOffset.x * multiplier
let fraction = 1.0 - offset
menuContainerView.layer.transform = transformForFraction(fraction)
menuContainerView.alpha = fraction
offset的值在0到1之间。当offset的值为0的时候,菜单栏处于显示状态;当offset为1的时候,菜单栏处于隐藏状态。
fraction是菜单显示部分所占的比例,范围在0到1之间,0是菜单栏完全隐藏,1是显示状态。
同时,fraction也用来调整菜单栏的alpha值,从暗到明,从隐藏到显示。
运行程序,滑动看一下这个3D效果,但是…菜单栏的变换关系有点问题(译者注:原文用的”hinge”这个词,这里的问题是菜单栏沿着中心轴在旋转),原因是菜单栏的锚点在中心点。
![当前效果](http://cdn1.raywenderlich.com/wp-content/uploads/2015/04/runwonkyhinge-180x320.png)
当前效果
为了让菜单栏沿着右边距旋转,需要在ContainerViewController.swift的viewDidLayoutSubviews()方法中添加以下方法:
menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
将锚点的x设为1,y设为0,这样就可以沿右边距旋转了。
运行程序,可以看到完美的3D旋转效果。
![当前效果](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/runPenultimate.gif)
当前效果
菜单按钮动画将是整个App的点睛之笔。
当菜单栏隐藏的时候,按钮处于原有状态,当菜单栏显示的时候,按钮旋转90度。
技术上来讲,是按钮的图片在旋转,但是实际看起来的就效果,就像是按钮在旋转。
在HamburgerView.swift中添加以下代码:
func rotate(fraction: CGFloat) {
let angle = Double(fraction) * M_PI_2
imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle))
}
现在菜单按钮可以平滑的旋转了。通过fraction来计算应该旋转的角度,其中M_PI_2常量是定义在math.h头文件中的,表示pi/2。
添加以下代码到scrollViewDidScroll(_:)方法中,这样旋转角度可以和滚动偏移量结合起来:
if let detailViewController = detailViewController {
if let rotatingView = detailViewController.hamburgerView {
rotatingView.rotate(fraction)
}
}
运行程序,可以看到动画已经能很好的结合起来了。
![最终效果](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/runfinal.gif)
最终效果
最后
可以在这里下载这个项目的最终版。
本文简单的实验了以下m34的3D旋转效果,如果想要了解更多3D变换,可以看Richard Turton的Visual Tool for CATransform3D。
维基百科上也有一些文章,使用图片很好的解释了立体视觉这个概念。
同时,你也可以思考一下,还有那些3D效果可以用在你的App中,来提高用户交互体验。就像是菜单按钮这样的微妙的动画,细节的处理能很好的提高用户体验。