如何创建一个非常酷的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: UITableViewDelegate override 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: ContainerViewController func 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: - UIScrollViewDelegate func 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 * multiplier let fraction = 1.0 - offset menuContainerView.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 的模型例子 ...
随机推荐
- 新的开始 | Arthas GitHub Star 破万后的回顾和展望
一切新的开始,都始于一个里程碑. 2月20日上午,Java 开源诊断工具 Arthas 的 GitHub Star 突破10000,距离开源后的第一个Release 版发布仅 147 天. 从中,我们 ...
- 整体二分(模板) 求区间第k小
整体二分,将询问与初值一起放入一个结构体里,然后每次二分判断询问在哪边,树状数组维护,时间复杂度O((n+Q)lognlogMAX_a[i] 代码 #include<iostream> # ...
- 使用Jest进行单元测试
Jest是Facebook推出的一款单元测试工具. 安装 npm install --save-dev jest ts-jest @types/jest 在package.json中添加脚本: “te ...
- python邮件发送:普通文本、html、添加附件
# -*- coding: utf-8 -*- # @Time : 2019/9/19 13:46 # @Author : HuangWenjun # @Email : 350920551@qq.co ...
- Leetcode8.String to Integer (atoi)字符串转整数(atoi)
实现 atoi,将字符串转为整数. 该函数首先根据需要丢弃任意多的空格字符,直到找到第一个非空格字符为止.如果第一个非空字符是正号或负号,选取该符号,并将其与后面尽可能多的连续的数字组合起来,这部分字 ...
- Linux系统下实现远程连接MySQL数据库的方法教程
1.在服务器端开启远程访问首先进入mysql数据库,然后输入下面两个命令: grant all privileges on *.* to 'root'@'%' identified by 'passw ...
- Hdu 3887树状数组+模拟栈
题目链接 Counting Offspring Time Limit: 15000/5000 MS (Java/Others) Memory Limit: 32768/32768 K (Java ...
- SSH 相关基础
检查是否安装: sudo apt-cache policy openssh-client sudo apt-cache policy openssh-server 也可直接用 sudo apt-ca ...
- Leetcode674.Longest Continuous Increasing Subsequence最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续的的递增序列. 示例 1: 输入: [1,3,5,4,7] 输出: 3 解释: 最长连续递增序列是 [1,3,5], 长度为3. 尽管 [1,3,5,7] 也 ...
- Linux常用命令4 帮助命令
1.帮助命令:man 命令英文原意:manual 命令所在路径:/usr/bin/man 执行权限:所有用户 语法:man [命令或配置文件] 功能描述:获得命令或者配置文件的帮助信息 例如:m ...