如何创建一个非常酷的3D效果菜单
http://www.cocoachina.com/ios/20150603/11992.html
原文地址在这里.原文
去年,读者们投票选出了Top5的iOS7最佳动画,当然也很想看到有关这些动画如何实现的教程。这次,我们将会实现Taasky这个app的3D效果的侧滑菜单。
这篇教程比较适合开发经验比较丰富的开发者。因为这篇教程涵盖Autolayout,UIScrollView,viewcontroller容器还有CoreAnimation。这些对于初学者来说都比较陌生,所以如果你之前没有接触过的话阅读起来会有点困难。
开始
首先下载一个我们的初始项目。地址在这里
下载之后打开他,运行起来。
第一个页面和点击Cell之后进入的第二个页面是这样的。

第一个页面是一个继承自UITableViewController的Controller,名字叫做MenuViewController,从名字也能看出来了,这将会是我们的侧滑菜单。我们的TableView中使用的Cell是我们自定义的Cell,叫做MenuItemCell。每个Cell都是可以点击的,点击之后进入的是另一个界面,叫做DetailViewController,里面只有一张和点击Cell匹配的同一种背景色和图片。

例如点击绿色的cell
现在这个app距离我们的完成形态还有不少距离。但是耐心跟着教程走是肯定可以完成的。
首先我们需要按照下面几个步骤来。
首先现在的app实际上是两个页面,由navigationController来控制两个controller的切换。我们第一步要做的就是利用Autolayout和viewcontroller container这两个特性,把这两个viewcontroller合二为一放在一个容器里,而这个容器我们会用scrollview来充当。
第二步是添加一个button来控制显示和隐藏我们的菜单。
第三步实现我们菜单的3D化,就像Taasky这个APP里面的菜单一样。
最后一步,你要将菜单动画和scrollView的offset结合起来。
废话不多说,我们新建一个Viewcontroller,用来当做ViewController容器,名字就叫ContainerViewController.确保是继承自UIViewController。语言选择swift。

同样的在storyboard里也拉出一个ViewController,并把class改成我们的ContainerViewController。Storyboard ID改成ContainerVC.

选择view,并且把背景色改成黑色.

ok,拉一个UIScrollview到我们的view上.并且把垂直和水平滚动条隐藏掉.把Delays Content Touches也取消掉.如图.

右键单击我们的scrollview,把delegate设置为我们的ContainerViewController.

给我们的scrollview添加约束.很简单的约束,上下左右与父view间距为0.

设置contentView
然后托一个view到我们的scrollview上,并且把size和背景色设置如图的值.

把我们的view的Document Label设置为ContentView,用来和其他的view区别.
然后给我们的contentView添加约束.

然后把我们的Trailing这个约束的constant改为0.
这时候Xcode会出现红色的警告,是因为我们的约束没有添加完成,因为你如果不给scrollview的contentview设置宽高的话,scrollview是没办法确定自己的contentsize的.
所以我们这样设置.

把我们的ContentView的宽高设置为和ContainerViewController的view的宽高一致.
然后修改如下约束.

把constant改为80的意思就是,我们的Contentview的宽一直是底层view宽度+80(这80就是给我们的侧边栏准备的.).
添加Menu和Detail Container Views
从storyboard找到一个叫做ContainerView的控件,相信这个控件很多人并没有用过.这个控件就是在storyboard中为某个ViewController添加一个childViewController用的.
首先,拖一个ContainerView到我们的ContentView,宽高改为(80,600),然后Document里的label改为Menu Container View.

然后,再拖一个ContainerView到我们的ContentView,并且把size和Document里的label改为下图所示的数值.

拖完之后我们的ContentView就会长成这样.

ContainerView有一个特性,就是你一旦拖出一个ContainerView,那么xcode会自动帮你生成一个他的子ViewController.如图.

显然,系统帮我们生成的这两个ViewController对我们来说是没用的,因为我们已经有了MenuController和DetailController,所以删掉他们.
删掉之后,给我们的两个ContainerView分别添加约束.Menu ContainerView的约束如下.

DetailContainerView的约束如下.

我们刚才删除了系统帮我们生成的childViewController,现在我们需要手动添加.
首先把我们的InitController改成我们的ContainerViewController.

然后右键点击Menu ContainerView,拖一根线到我们的Navigation Controller.然后在弹出框中选择embed.

一旦线拖好之后,我们的storyboard看起来是这样子的.

肯定要改一改.首先把MenuController里的Cell里的UIImageView的width改成80.

然后,把MenuViewController和DetailViewController中间代表push的那个segue删掉.
然后为我们的DetailViewController生成一个自己的navigationController.

选择我们刚刚生成的navigationController,把我们的navagationbar改为如下.

然后把MenuViewController的navigationbar也改成一样的参数.并且把View Controller\Layout\Adjust Scroll View Insets选中.

ok,按照刚才拉MenuContainerView的方式拉一下DetailContainerView.

这样,我们的ContainerViewController就拥有两个childViewController了.
运行一下.试试效果.

看起来不错.但是有个问题.使劲往右拉的话,左边会拉出来一片黑色的区域.这显然不是我们想要的.
所以在Storyboard中找到我们的ScrollView.
1.选中Paging Enabled.
2.取消Bounce\Bounces的选中状态.
再运行一次.向右拉,这次menu显示正确了.不会在左边漏出一大段黑色的空间.但是每次我们试图隐藏menu的时候它又会弹回来.(实际上我按照教程做到这的时候并没有发生这种情况,菜单是可以隐藏的.)
第二个问题是,点击侧边栏,detailContainerView并不会发生变化.这很正常,因为你还没写代码呢.
修改我们的代码
首先,把MenuViewController.swift里的这些代码拷贝到我们的DetailViewController中.
|
1
2
3
4
5
|
override func viewDidLoad() { super.viewDidLoad() // Remove the drop shadow from the navigation bar navigationController!.navigationBar.clipsToBounds = true} |
这个的作用是消除navigationbar下面的一条特别细的线.
每次选择一个MenuViewController里面的一个tableviewCell的时候,相应的我们应该设置DetailViewController里面的menuItem属性.但是现在我们的MenuViewController和DetailViewController还没有关联起来.所以我们会利用ContainerViewController来建立两个controller之间的联系.
在ContainerViewController里添加这么一个属性.
|
1
|
private var detailViewController: DetailViewController? |
然后override我们的ContainerViewController里的prepareForSegue(_:sender:)方法.
|
1
2
3
4
5
6
|
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "DetailViewSegue" { let navigationController = segue.destinationViewController as! UINavigationController detailViewController = navigationController.topViewController as? DetailViewController }} |
别忘了设置我们的segue.identifier.如图所示.

然后再添加一个menuItem的属性到ContainerViewController里,并且监听如果menuItem被设置,那么让detailViewController的menuItem相应的也改变.
|
1
2
3
4
5
6
7
|
var menuItem: NSDictionary? { didSet { if let detailViewController = detailViewController { detailViewController.menuItem = menuItem } }} |
然后,到我们的MenuViewController里,先删除prepareForSegue这个方法,因为这个方法是以前MenuViewController和DetailViewController有直接关联的时候才有用的,现在这个方法显然已经没有意义了.
我们要做的就是在MenuViewController里的tableview 的delegate里添加以下的内容.
|
1
2
3
4
5
6
|
// MARK: UITableViewDelegateoverride func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { tableView.deselectRowAtIndexPath(indexPath, animated: true) let menuItem = menuItems[indexPath.row] as! NSDictionary (navigationController!.parentViewController as! ContainerViewController).menuItem = menuItem} |
然后再在ViewDidLoad()方法里加入以下内容,确保第一次进入页面的时候默认选择的是第一个Cell.
|
1
2
|
(navigationController!.parentViewController as! ContainerViewController).menuItem = (menuItems[0] as! NSDictionary) |
运行一下.效果如下.

显示和隐藏我们的Menu
现在我们点击cell虽然DetailViewController的内容可以正确显示,但是菜单并不能自动隐藏.所以我们首先要实现的是点击菜单之后菜单自动隐藏.
要实现这个效果,首先要把我们的ContainerViewController里的scrollView和MenuContainerView拖线拖到我们的ContainerViewController里.
如图.


然后给ContainerViewController.swift添加一个新的方法.
|
1
|
hideOrShowMenu(_:animated:) |
|
1
2
3
4
5
|
// MARK: ContainerViewControllerfunc hideOrShowMenu(show: Bool, animated: Bool) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) scrollView.setContentOffset(show ? CGPointZero : CGPoint(x: menuOffset, y: 0), animated: animated)} |
然后在MenuItem的didSet里加入这个方法,意思就是每次设置menuItem的时候都会自动调用这个方法.
|
1
2
3
4
|
override func viewDidLoad() { super.viewDidLoad() hideOrShowMenu(false, animated: false)} |
运行一下.

原文中提到了这时候菜单还是存在回弹和收不回去的问题,实际上在我做的时候并没有出现这种情况.所以如果你们做的时候如果出现了回弹.那么需要在ContainerViewController里实现UIScrollView的这个Delegate.
|
1
2
3
4
5
6
7
8
|
// MARK: - UIScrollViewDelegatefunc scrollViewDidScroll(scrollView: UIScrollView) { /* Fix for the UIScrollView paging-related issue mentioned here: */ scrollView.pagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width - CGRectGetWidth(scrollView.frame))} |
然后运行,这时候应该没问题了.

添加我们的汉堡按钮
新建一个类继承自UIView,起名叫做HamburgerView.swift.
然后修改内容如下.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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) }} |
然后在我们的DetailViewController里,把他加进去.先添加一个属性
|
1
|
var hamburgerView: HamburgerView? |
然后在viewDidLoad()里添加如下代码.
|
1
2
3
4
|
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’s hideOrShowMenu(_:animated:),但是现在缺少一个布尔值来表示菜单是否处于打开状态.所以我们为ContainerViewController添加一个布尔值用来记录菜单的状态.
|
1
|
var showingMenu = false |
然后override viewDidLayoutSubviews()方法.加入如下代码.
|
1
2
3
4
|
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() hideOrShowMenu(showingMenu, animated: false)} |
这会在ContainerViewController的布局每次发生变化的时候调用hideorShow方法.
然后打开DetailViewController,添加我们的点击事件.
|
1
2
3
4
5
|
func hamburgerViewTapped() { let navigationController = parentViewController as! UINavigationController let containerViewController = navigationController.parentViewController as! ContainerViewController containerViewController.hideOrShowMenu(!containerViewController.showingMenu, animated: true)} |
现在点击汉堡按钮,已经能够打开菜单了,但是再次点击应该是关闭菜单,然后并没有效果,原因很简单,你没有跟新showingMenu的值,所以在我们的hideOrShowMenu方法里加入showingMenu = show.
再试一下.

ok了.
然而,问题依然没有结束.
当你滑动打开菜单的时候,需要点击汉堡菜单两次才能关闭菜单.这是因为你滑动打开菜单的时候并没有更新showingMenu的值.所以,需要在UIScrollviewDelegate里更新我们的showingMenu.
|
1
2
3
4
5
|
func scrollViewDidEndDecelerating(scrollView: UIScrollView) { let menuOffset = CGRectGetWidth(menuContainerView.bounds) showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset) println(“didEndDecelerating showingMenu \(showingMenu)”)} |
运行一下,注意一下console,当你快速滑动的时候是没问题的,但是缓慢滑动的时候这个方法似乎不响应.所以这个方法并不靠谱.
我们把代码移到另一个代理方法scrollViewDidScroll(_:)里.
再次运行.

应该没问题了.
给我们的菜单添加透视效果
实际上完整的效果华丽就华丽在菜单出现的方式并不是水平的,而是以3D旋转的效果出现的.要实现这个效果我们必须计算菜单显示的比例和菜单旋转角度之间的关系.如下所示.
|
1
2
3
4
5
6
7
8
9
|
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)} |
上面的方法就是计算菜单显示的部分和旋转角度的关系.
fraction当menu完全隐藏的时候是0,完全显示的时候是1.
CATransform3DIdentity代表原始的Transform.
CATransform3DIdentity’s m34这个值代表view的perspective.(设置了他旋转的时候才会有3D效果)
利用CATransform3DRotate来实现菜单的旋转效果.并且是绕Y轴旋转.-90度的时候代表与平面向内垂直(所以你看不到).0度的时候水品展开.
translateTransform负责menu在旋转的时候同时位移到正确的位置.
CATransform3DConcat负责把位置的transform和旋转的transform结合起来.
现在在我们的scrollViewDidScroll这个代理方法里加入以下代码.
|
1
2
3
4
5
|
let multiplier = 1.0 / CGRectGetWidth(menuContainerView.bounds)let offset = scrollView.contentOffset.x * multiplierlet fraction = 1.0 - offsetmenuContainerView.layer.transform = transformForFraction(fraction)menuContainerView.alpha = fraction |
运行一下.

效果似乎不太对.那是因为我们并没有设置menuContainerView的anchorpoint,现在的anchorPoint还是在view的中心点我们实际上的anchorpoint应该是在view中心最右的位置.所以在ContainerViewController的viewDidLayoutSubViews()里修改anchorPoint.
|
1
|
menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5) |
运行.

效果不错.
让汉堡按钮动起来.
我们只剩最后一个效果了,就是菜单出现的过程中,汉堡按钮也要转相应的角度.
在HamburgerView中添加下面的方法.
|
1
2
3
4
|
func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle))} |
然后在ContainerViewController里的scrollViewDidScroll()里添加以下代码.
|
1
2
3
4
5
|
if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) }} |
运行一下.

Perfect!
从这里获取最终的程序.
如果你对perspective有疑问.那么请在这里浏览相关信息.
有任何疑问可以留言.
如何创建一个非常酷的3D效果菜单的更多相关文章
- 一个炫酷的Actionbar效果
今天在网上看到一个炫酷的Actionbar效果,一个老外做的DEMO,目前很多流行的app已经加入了这个效果. 当用户初始进入该界面的时候,为一个透明的 ActiionBar ,这样利用充分的空间显示 ...
- 用 CSS3 创建一个漂亮的多种色彩的菜单
1. [图片] thumb.png 2. [代码][HTML]代码 <!DOCTYPE html><html lang="en" > <hea ...
- css3 3D效果
css3 3D变形 transfrom初学 这个礼拜学了css3 3d,感觉到css无穷的魅力,可以通过几个特定的代码符号创建出3D效果的页面. ___ 透视 一个元素需要一个透视点才能激活3D空间, ...
- css3 之炫酷的loading效果
css3 之炫酷的loading效果 今天实现了一个炫酷的loading效果,基本全用css来实现,主要练习一下css3的熟练运用 js需要引入jquery 只用到了一点点js 先看效果图 html: ...
- Android 3D滑动菜单完全解析,实现推拉门式的立体特效
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/10471245 在上一篇文章中,我们学习了Camera的基本用法,并借助它们编写了一 ...
- 使用Three.js网页引擎创建酷炫的3D效果的标签墙
使用Three.js引擎(这是开源的webgl三维引擎,gitgub)进行一个简单应用. 做一个酷炫的3d效果的标签墙(已经放在我的博客首页,大屏幕可见), 去我的博客首页看看实际效果 www.son ...
- 使用Javascript来创建一个响应式的超酷360度全景图片查看幻灯效果
360度的全景图片效果常常可以用到给客户做产品展示,今天这里我们推荐一个非常不错的来自Robert Pataki的360全景幻灯实现教程,这里教程中将使用javascript来打造一个超酷的全景幻灯实 ...
- WebGIS简单实现一个区域炫酷的3D立体地图效果
1.别人的效果 作为一个GIS专业的,做一个高大上的GIS系统一直是我的梦想,虽然至今为止还没有做出一个理想中的系统,但是偶尔看看别人做的,学习下别人的技术还是很有必要的.眼睛是最容易误导我们的,有时 ...
- 拜托,使用Three.js让二维图片具有3D效果超酷的好吗 💥
声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 背景 逛 sketchfab 网站的时候我看到有很多二维平面转 3D 的模型例子 ...
随机推荐
- Django项目:CRM(客户关系管理系统)--24--16PerfectCRM实现King_admin日期过滤
登陆密码设置参考 http://www.cnblogs.com/ujq3/p/8553784.html list_filter = ('date','source','consultant','con ...
- Python 易错点
1. Python查找一个变量时会按照“局部作用域”, “嵌套作用域”, “全局作用域”,“内置作用域”的顺序进行搜索. 在实际开发中,我们应该尽量减少对全局变量的使用,因为全局变量的作用域和影响过于 ...
- mongodb本地搭建过程
1.解压安装包后安装 安装时注意:1.选择customs 2.路径选择C盘以外的盘符 安装完成后: 2.在bin的同级目录下新建data.log文件夹 3.在data文件夹下新建db文件夹,在l ...
- Node.js Error: Cannot find module express的解决办法(转载)
1.全局安装express框架,cmd打开命令行,输入如下命令: npm install -g express express 4.x版本中将命令工具分出来,安装一个命令工具,执行命令: npm in ...
- [转]JS设计模式-单例模式(二)
单例模式是指保证一个类仅有一个实例,并提供一个访问它的全局访问点. 单例模式是一种常用的模式,有一些对象往往只需要一个,比如线程池.全局缓存.浏览器中的window对象等.在javaScript开发中 ...
- _mysql_exceptions.IntegrityError: (1062, "Duplicate entry, Python操作MySQL数据库,插入重复数据
[python] view plain copy sql = "INSERT INTO test_c(id,name,sex)values(%s,%s,%s)" param = ...
- bzoj3064/洛谷P4314 CPU监控【线段树】
好,长草博客被催更了[?] 我感觉这题完全可以当作线段树3 线段树2考加法和乘法标记的下放顺序,这道题更丧心病狂[?] 很多人可能跟我一样,刚看到这道题秒出思路:打一个当前最大值一个历史最大值不就完事 ...
- Promise对象和async函数
Promise对象 //1开始 function fna(){ console.log('1开始'); var p = new Promise(function(resolve, reject){ / ...
- linux文件系统配置文件
文件系统 内核提供了一个接口,用来显示一些它的数据结构,这些数据结构对于决定诸如使用的中断.初始化的设备和内存统计信息之类的系统参数可能很有用.这个接口是作为一个独立但虚拟的文件系统提供的,称为 /p ...
- Node.js调试技巧
1. console.log 跟前端调试相同,通过一步步打印相关变量进行代码调试 2. 使用Node.js内置的调试器 通过node debug xxx.js来进行调试: [root@~/wade/n ...