如何创建一个非常酷的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距离我们的完成形态还有不少距离。但是耐心跟着教程走是肯定可以完成的。

首先我们需要按照下面几个步骤来。

  1. 首先现在的app实际上是两个页面,由navigationController来控制两个controller的切换。我们第一步要做的就是利用Autolayout和viewcontroller container这两个特性,把这两个viewcontroller合二为一放在一个容器里,而这个容器我们会用scrollview来充当。
  2. 第二步是添加一个button来控制显示和隐藏我们的菜单。
  3. 第三步实现我们菜单的3D化,就像Taasky这个APP里面的菜单一样。
  4. 最后一步,你要将菜单动画和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:

  http://stackoverflow.com/questions/4480512/uiscrollview-single-tap-scrolls-it-to-top

  */

  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)

}

上面的方法就是计算菜单显示的部分和旋转角度的关系.

  1. fraction当menu完全隐藏的时候是0,完全显示的时候是1.
  2. CATransform3DIdentity代表原始的Transform.
  3. CATransform3DIdentity’s m34这个值代表view的perspective.(设置了他旋转的时候才会有3D效果)
  4. 利用CATransform3DRotate来实现菜单的旋转效果.并且是绕Y轴旋转.-90度的时候代表与平面向内垂直(所以你看不到).0度的时候水品展开.
  5. translateTransform负责menu在旋转的时候同时位移到正确的位置.
  6. 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有疑问.那么请在这里浏览相关信息.

Perspective)

有任何疑问可以留言.

时间: 2024-11-10 14:56:11

如何创建一个非常酷的3D效果菜单的相关文章

一个炫酷的Actionbar效果

今天在网上看到一个炫酷的Actionbar效果,一个老外做的DEMO,目前很多流行的app已经加入了这个效果. 当用户初始进入该界面的时候,为一个透明的 ActiionBar ,这样利用充分的空间显示大图片,如果用户滚动页面需要查看内容的时候,则大图收缩到 ActionBar 中. 源码下载:http://files.cnblogs.com/sage-blog/notboringactionbar.zip 参考:http://flavienlaurent.com/blog/2013/11/20/

用 CSS3 创建一个漂亮的多种色彩的菜单

1. [图片] thumb.png ?2. [代码][HTML]代码 <!DOCTYPE html><html lang="en" >    <head>        <meta charset="utf-8" />        <title>CSS3 multicolor menu | Script Tutorials</title>        <link href="

Android 3D滑动菜单完全解析,实现推拉门式的立体特效

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/10471245 在上一篇文章中,我们学习了Camera的基本用法,并借助它们编写了一个例子,实现了类似于API Demos里的图片中轴旋转功能.不过那个例子的核心代码是来自于API Demos中带有的Rotate3dAnimation这个类,是它帮助我们完成了所有的三维旋转操作,所有Matrix和Camera相关的代码也是封装在这个类中. 这样说来的话,大家心里会不会痒痒的呢?虽然

使用Javascript来创建一个响应式的超酷360度全景图片查看幻灯效果

360度的全景图片效果常常可以用到给客户做产品展示,今天这里我们推荐一个非常不错的来自Robert Pataki的360全景幻灯实现教程,这里教程中将使用javascript来打造一个超酷的全景幻灯实现,相信大家一定会喜欢的! 在这个教程中没有使用到任何插件,我们将使用HTML,css和javascript来实现,当然,也使用是jQuery这个框架! 如何实现? 我们将使用预先按照360生成的图片进行轮播来实现动画展示效果.包含了180个图片.所以加载时间可能比较长. 代码实现 我们将在css代

Unity 3D使用GameObject创建一个简单的可移动物体

于Unity 3D游戏的开发.游戏脚本需要3D模拟组合,该脚本将被写入阻力3D为了达到效果对象. 以下是一个小实例,使用Unity 3D实现一个可控制移动的小人.小人能够向前.向后.向左和向右移动. 1.通过 File - > New Scene 创建一个场景: 2.点击Create -> Create Empty 创建一个GameObject,它即是游戏对象: 3.在Inspector 面板里将此对象的名字改为 "hero" (记得按回车键),等一下再脚本中将通过此名字来

Unity CG 写一个超酷的 ray-marching(shader纯代码写3D)

Unity CG 写一个超酷的 ray-marching(shader纯代码写3D) 1.其实自从看了http://www.shadertoy.com(inigo quilez为其主创始人)上的shader后,让我感到很高兴 2.更重要的是自从我接触了一个叫 inigo quilez 的shader技术后,让我觉得shader情感更深的浓厚了 3.http://www.iquilezles.org/ 哈哈,当然给大家一个崇拜的机会吧,你一定会学到你想学到的技术和秘密 哈哈,邪恶的专栏地址放送,一

Android 从无到有打造一个炫酷的进度条效果

从无到有打造一个炫酷的进度条效果

实现一个图片轮播-3d播放效果

前言:最近在做一个音乐播放器,首页要做一个图片轮播,看了bootstrap的carousel插件以及移动端的swipe.js库,都是平面图片轮播的效果,所以自己想着实现类似网易云app里那种3d图片轮播的效果,所以写下此文 使用方法: 首先,引入Swipe.js和Swipe.css 定义轮播列表ul[data-ride="swipe"],然后每一个轮播项都加上类 class="item" : 1 <!DOCTYPE html> 2 <html la

Quick-cocos2d-x3.3 Study (十二)--------- 创建一个圆形的物体并带有物理引擎效果

创建带物理效果的心心图片 先创建一个物理场景 1 local GameScene = class("GameScene", function ( ) 2 -- body 3 -- return display.newScene("GameScene") 4 -- 创建一个带物理效果的场景 5 return display.newPhysicsScene("GameScene") 6 end) 在这个物理世界中添加物理心心 1 -- 获取场景中绑定