zoukankan      html  css  js  c++  java
  • 【译】仿Taasky的3D翻转菜单动画实现

    最终效果

    最终效果

    最终效果

    开始

    首先下载并打开一个事先搭好架子的Demo,然后来分析一下。这个Demo包含一个主页和详情页,其中MenuViewController继承自UITableViewController,它主要用于展示左边侧栏,自定义的MenuItemCell中设置了每一个菜单的图标和颜色。DetailViewController为详情页,显示了每个cell点击后,对应的颜色和图标。

    Starter Project效果

    Starter Project效果

    这个教程将详细的介绍实现步骤,具体步骤如下:

    • 整个教程将使用自动布局来实现,需要将现在的主页push到详情页的这种模式改为横向ScrollView滚动的模式。

    • 需要在左上角添加显示/隐藏菜单栏的按钮。
    • 接下来,需要实现类似于Taasky的菜单栏的3D翻转效果。
    • 最后,需要将菜单栏的翻转效果与按钮的旋转效果结合起来。

    首要任务是把菜单栏改成滑出的效果,UIScrollView中需要包含菜单栏和详情页面,就像是 Swift Scroll View School Part 13这个视频教程中介绍的SwiftSideNav一样。示意图如下:

    视图范围

    视图范围

    用户可以通过左右滑动来显示/隐藏菜单栏。在上图中,紫色方框是当菜单栏显示的时候,屏幕所显示的部分,绿色方框是当菜单栏隐藏时,屏幕所显示的内容。当菜单栏打开时,显示的是一个局部侧滑边栏。

    如果对UIScrollView不熟悉,请先学习Part 1Part2一系列教程,它们讲介绍UIScrollView的工作流程。

    刚才提到的SwiftSideNav是一个使用自动布局完成例子,但是今天的教程将会直接在StoryBoard中嵌入菜单与详情页,完成后的结构如图:

    最终结构图

    最终结构图

    为UIScrollView添加约束

    这个部分会使用到控制器约束等知识(在iOS5时推出),如果还不熟悉,可以先学习iOS 5 By Tutorials书中的第18章UIViewController Containment。

    菜单栏与详情。整个视图层级应该是这样的:

    • 有一个根控制器,并将UIScrollView添加到根控制器上;
    • 然后添加一个UIView到UIScrollView上,暂且叫它”Content View”;
    • 添加两个子容器到”Content View”上,然后分别把菜单栏和详情页填到到子容器中。

    创建一个名为ContainerViewController的控制机,继承自UIViewController,语言选择Swift

    创建容器

    创建容器

    打开Main.StoryBoard,从Object Library中拖一个View Controller到画布中,并在Identity Inspector(Xcode右边栏的第三个选项)中设置自定义类为ContainerViewController,唯一标识为ContainerVC

    绑定类,修改唯一标识

    绑定类,修改唯一标识

    接下来,在 Attributes Inspector(Xcode右边栏的第四个选项)中将ContainerViewController的view背景色设置为黑色。

    设置背景色

    设置背景色

    创建UIScrollView

    接着,需要添加UIScrollView到ContainerViewController中,并添加约束。具体操作如下:

    • 拖一个UIScrollView到ContainerViewController中,并让它填充整个ContentViewController
    • 取消横、纵滚动条的显示(Shows Horizontal IndicatorShows Vertical Indicator);
    • 取消Delays Content Touches,让响应事件及时传递给子控件,防止响应延迟;

    ContainerViewController设置为UIScrollView的delegate(按住control键,从UIScrollView拖到UIViewController)。

    关联代理

    关联代理

    接下来是给UIScrollView添加约束的步骤:

    • 找到右下角的Pin按钮,打开添加约束的弹窗;
    • 取消Constrain to margins,防止系统自动加左右边距;

    • 添加上下左右四个约束,并确保约束的值都是0;
    • 点击Add 4 Constraints按钮,将约束加上。

    给UIScrollView添加约束

    给UIScrollView添加约束

    仍然选中UIScrollView,打开Size Inspector选项(Xcode右边栏倒数第二个选项),确认一下约束是否与教程所加的约束相同:

    • Trailing Space to: Superview
    • Leading Space to: Superview
    • Top Space to: Superview

    • Bottom Space to: Bottom Layout Guide

    检查约束

    检查约束

    如果有类似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

    设置宽与背景色

    设置宽与背景色

    译者注:上图中,作者将Width设置为了680,但在文字描述中并未提到。这个宽度可加可不加,因为之后界面上的宽度其实全都要通过约束来表现。

    选中刚加上的这个View,打开Identity Inspector,将DocumentLabel设置为Content View。这个名字将会在document outline(IB左边视图层级栏)显示出来,可以很容易追踪到这个View,并且在之后加与它相关的约束的时候,这个名字也会显示出来。

    在给Content View添加约束的过程中,可以看到警告,不过不用惊慌,这些警告在这一节完之前能得到解决。

    打开Pin弹出框,给Content View添加相对于父视图的约束。

    给Content View添加约束

    给Content View添加约束

    在Size Inspector中,确保Trailing Space的约束值为0。

    将右边距约束改为0

    将右边距约束改为0

    译者注:这里有点不明白作者的操作,直接在添加约束的时候,将右边距设置为0就可以了,但是作者却先将宽设置为680,然后添加约束的时候也是添加的-80,最后又改成0。感觉有点多此一举,还请大神们解答下。

    现在之所以有警告是因为StoryBoard需要Content View的高和宽来设置contentSize。添加相对于Scroll View的父容器的这些约束,可以使之适配各种设备与横竖屏。

    在document outline中,按住Control键,从Content View拖到View(Scroll View的父容器)。按住Shift键,选择Equal WidthsEqual Heights

    添加等宽等高约束

    添加等宽等高约束

    然后将Equal Width约束改为80。

    加80像素的约束

    加80像素的约束

    将约束改为80的用意是:这样Content View就会比View宽80,就可以刚好装下菜单栏。同时,可以发现之前的警告也不存在了——干得漂亮。

    添加菜单栏与详情界面

    现在Scroll View上有Content View可以作为菜单栏和详情页的容器,然后将菜单栏与详情页(后文中称为Menu View与Detail View)嵌入Content View,这样便可以创建一个可以滑动显示.隐藏的Menu View。

    首先,创建Menu Container View:拖一个Container ViewContent View上,在Size Inspector中,将其宽度设为80,然后在Identity Inspector中将DocumentLabel设置为Menu Container View

    设置宽度,添加描述Label

    设置宽度,添加描述Label

    高度应该是默认的600,不过如果你不确定,也可以设置一下。

    译者注:这里作者只提到设置宽度,但根据后文的一些描述,这里应该还需要将x设置为0,y设置为0。

    然后,添加Detail Container View:新拖一个Container ViewContent View上,并放置在menu container的右边。打开Size Inspector,并设置以下值:

    • X: 80
    • Y: 0
    • Width: 600
    • Height: 600

    接下来,打开Identity Inspector,将DocumentLabel设置为Detail Container View

    设置frame,添加描述Label

    设置frame,添加描述Label

    这样,Menu ViewDetail View的宽度就和Content View相等了。

    容器

    容器

    当添加完这两个自控制器后(container view),可在在IB中看到系统已经默认添加了contained view controller,但是我们需要用到已存在的Menu View与Detail View,所以需要在document outline或者画布中删除这两个控制器。

    删除系统自动关联的ViewController

    删除系统自动关联的ViewController

    译者注:这个教程中的操作,都在文章开头下载的那个Demo中进行(称为starter project),所以,已存在的Menu View与Detail View就是指starter project中的控制器视图。

    接下来,需要给两个container view添加约束。

    Menu Container View添加宽为80,上下左右边距为0,共5个约束。

    添加5个约束

    添加5个约束

    Detail Container View添加上、右、下边距为0,共3个约束;注意不要添加左边距约束,否则会和Menu Container View的右边距约束重复。

    添加三个约束

    添加三个约束

    给两个Container View添加的约束都要确保Constrain to margins为取消。

    嵌入Menu与Detail View Controllers

    这一节中,将会把starter project中的menu和detail view controllers拆开,然后分别嵌入Menu Container ViewDetail Container View

    首先,将Container View Controller设为入口控制器:将原本指向Navigation Controller的入口箭头指向Container View Controller

    设置入口控制器

    设置入口控制器

    接下来,按住Control键,从Menu Container View拖到Navigation Controller,在弹出框中选择embed

    关联菜单视图

    关联菜单视图

    Menu Container View嵌入Navigation Controller之后,相关的视图的宽度就都缩小到了80:

    关联以后,控制器宽度变为80

    关联以后,控制器宽度变为80

    现在来调整一下menu与detail:首先,将table view cell中imageView的宽度调整为80

    调整cell宽度

    调整cell宽度

    然后,删除menu与detail之间的push连线。选中Detail View Controller,然后在Xcode菜单中选择EditorEmbed InNavigation Controller

    给详情页添加导航栏

    给详情页添加导航栏

    现在detail应该加在了Navigation Controller上,同时拥有了黑色的导航栏。

    选中这个新的Navigation Controller的导航栏,打开Attributes Inspector,选择StyleBlank,取消Translucent,并将Bar Tint设置为Black Color

    设置导航栏样式与背景色

    设置导航栏样式与背景色

    译者注:如果不好选中导航栏,可以在document outline中进行选择,注意不要选错了。

    这个设置完以后,新加的导航栏就和第一个导航栏的样式一致了。

    接着,打开Detail View Controller控制器的Attributes Inspector,确保View ControllerLayoutAdjust Scroll View Insets是处于选中状态。

    选中Adjust Scroll View Insets

    选中Adjust Scroll View Insets

    Adjust Scroll View Insets的选中是为了将视图从Navigation Bar下面开始显示,而不会被Navigation Bar盖住

    最后,将Detail View Controller嵌入Detail Container View:按住Control键,从Detail Container View拖到Detail View ControllerNavigation Controller,在弹出框中选择embed

    嵌入详情页

    嵌入详情页

    运行程序,左右拖动视图,就可以显示/隐藏菜单栏了。你是否注意到,在拖动Scroll View是,可以超出左右边界,而且还可以停止滚动,显示部分菜单这个问题?

    当前效果图

    当前效果图

    为了修复这个问题,需要在Scroll View的Attributes Inspector中修改一下属性:

    • 选中ScrollingPaging Enabled属性,这样就可以…(译者注:原谅我找不到好的词语来表达原词了,原文中用到的是“snaps”,大概就是有一种惯性、巧劲的感觉,具体Paging Enabled的效果相信大家都懂的,就不说了);
    • 取消BounceBounces属性,防止左右滚出边距(译者注:即没有回弹效果)。

    译者注:原文的动图没有截导航栏部分,我在跟着做的时候,发现菜单的导航栏上有白边,原因是Menu Container View与Detail Container View的背景色为默认色的原因,设置为黑色即可。

    这时,在运行程序,就不会出现左右滑出边界的情况了,并且,也不会出现菜单栏可以显示一部分的情况。但是,在向左滑动,试图隐藏菜单栏时,又出现了新问题。

    Paging Enable问题

    Paging Enable问题

    本来滚动隐藏的菜单栏又弹了出来。这个问题在StackOverflow上讨论过,我们将在关联Scroll View的时候再去解决它。

    现在我们需要先解决另外一个问题:详情页是空白的,并且在点击菜单栏的时候,没有任何事件触发。

    详情页没有切换

    详情页没有切换

    这完全在意料之中,因为还没有代码对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 SegueIdentifier设置为DetailViewSegue

    设置Segue唯一标识

    设置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.

    删除MenuViewControllerprepareForSegue(:sender:)部分的代码,并且添加下面这段代码,注意,在选择方法的时候,不要选成tableView(:didDeselectRowAtIndexPath:)而造成麻烦。

    // 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
    }
    

    这里,只是简单将基于选择的单元行设置了ContainerViewControllermenuItem属性。这将使属性的didSet方法触发(就是上面将menuItem赋值给detailViewController.menuItem的那段代码)。

    最后,在MenuViewController.swiftviewDidLoad()方法中,添加以下代码:

    (navigationController!.parentViewController as! ContainerViewController).menuItem = 
      (menuItems[0] as! NSDictionary)
    

    这句代码将在App第一次加载的时候给detail view添加对应的图片。

    运行程序,可以看到程序加载以后,详情页已经有了图片,菜单栏正常显示,并且在切换菜单的时候,详情页也能显示对应的图片:

    运行效果

    运行效果

    显示/隐藏菜单

    为了达到菜单栏点击以后应该自动隐藏的目的,应该在单元行点击以后,设置Scroll View的水平位移为菜单栏的宽度,这样就可以完全展示出详情页。

    首先,需要将Scroll ViewContainer View关联。

    建立scrollView与ContainerViewController.swift的关联:在Storyboard的document outline中,选中Scroll View,打开Assistant Editor,按住Control键,从Scroll View拖到ContainerViewController.swift。然后在弹出框的Name一栏中将Scroll View命名为scrollView

    关联Scroll View

    关联Scroll View

    这里,我设置了ViewAssistant EditorAssistant Editors on Bottom,所以Assistant Editor就会在Xcode窗口的底部(译者注:默认是在右边),这样我就不用在拖动关联时,跨越整个画布了。

    同样的步骤,按住Control键,从Menu Container View拖到ContainerViewController.swift,创建一个menuContainerView

    关联菜单视图

    关联菜单视图

    然后,给ContainerViewController.swift添加hideOrShowMenu(_:animated:)这个方法:

    // 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,如果showtrue,scrollView的偏移量就为0,这是菜单栏就会显示,同样,scrollView的偏移量为80,菜单栏就会隐藏。

    现在,在menuItem的didSet中调用hideOrShowMenu(_:animated:)方法:

    var menuItem: NSDictionary? {
      didSet {
        hideOrShowMenu(false, animated: true)
        // ...
    

    在用户点击单元行之后,应该关闭菜单栏,所以show置为false

    同样,在viewDidLoad() 中调用hideOrShowMenu(_:animated:)方法,让程序启动时,菜单栏处于隐藏状态:

    override func viewDidLoad() {
      super.viewDidLoad()
      hideOrShowMenu(false, animated: false)
    }
    

    运行程序,这时详情页显示的是笑脸那张图,并且菜单栏处于隐藏状态。滑出菜单,选择某个单元行,可以看到菜单栏会隐藏,并且详情页能显示出对应的图片与背景色。

    当前效果

    当前效果

    但是,之前因为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又会启用,从而菜单能一次性滑出。

    运行程序,可以发现,这个问题已经解决了。

    当前效果

    当前效果

    看起来不错,但依然缺少一些东西。详情也的导航栏上少了汉堡按钮(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 Barleft bar button,并添加一个点击手势。

    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: “hamburgerViewTapped”)
    hamburgerView = HamburgerView(frame: CGRect(x: 0, y: 0,  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.swifthideOrShowMenu(:animated:)方法的showingMenu属性。

    hideOrShowMenu(_:animated:):中添加以下代码:

    showingMenu = show
    

    运行程序,尝试去滚动视图、点击菜单按钮、点击单元行这些操作。

    当前运行效果

    当前运行效果

    这存在一个问题:如果是采用滑出/滑入菜单栏的操作,那么菜单栏按钮要点两次,菜单栏才有响应。这是为什么?

    出现的问题

    出现的问题

    这个问题出现的原因为:在滚动的后,showingMenu没有更改,菜单滑出的时候showingMenu还是false。当第一次点击的时候,showingMenu的值设置为true,所以菜单还是显示状态。第二次点击的时候,才能将showingMenu置为false,从而隐藏菜单。

    要解决这个问题,需要在ContainerViewControllerUIScrollViewDelegate代理方法中将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(_:)这个方法的实现步骤:

    • 在菜单完全隐藏时,fraction = 0,完全显示时,fraction = 1;

    • CATransform3DIdentity是一个4*4的矩阵,对角线是1,其他是0;
    • CATransform3DIdentitym34属性是这个矩阵中的第三列第四行,它控制着变换中的立体程度;
    • CATransform3DRotate通过angle这个变量来控制y轴的旋转量:-90度呈现的是菜单栏垂直于屏幕的状态,0度呈现的是与xy轴平行的状态;
    • rotateTransform的旋转变换,是根据m34矩阵与y轴旋转量来的;
    • translateTransform设置了x轴的偏移量为菜单栏的一般;
    • CATransform3DConcatrotateTransformtranslateTransform联系起来,使得在做翻转动画时,也有偏移动画。

    接下来,在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”这个词,这里的问题是菜单栏沿着中心轴在旋转),原因是菜单栏的锚点在中心点。

    当前效果

    当前效果

    为了让菜单栏沿着右边距旋转,需要在ContainerViewController.swiftviewDidLayoutSubviews()方法中添加以下方法:

    menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
    

    将锚点的x设为1,y设为0,这样就可以沿右边距旋转了。

    运行程序,可以看到完美的3D旋转效果。

    当前效果

    当前效果

    最后一件事:给菜单按钮加旋转动画

    菜单按钮动画将是整个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)
      }
    }
    

    运行程序,可以看到动画已经能很好的结合起来了。

    最终效果

    最终效果

    最后

    可以在这里下载这个项目的最终版。

    本文简单的实验了以下m34的3D旋转效果,如果想要了解更多3D变换,可以看Richard Turton的Visual Tool for CATransform3D

    维基百科上也有一些文章,使用图片很好的解释了立体视觉这个概念。

    同时,你也可以思考一下,还有那些3D效果可以用在你的App中,来提高用户交互体验。就像是菜单按钮这样的微妙的动画,细节的处理能很好的提高用户体验。

  • 相关阅读:
    jsp设置footer底部内容
    dashboard项目心得:
    深度和广度优先算法
    一个action读取另一个action里的session
    算法笔记-0302
    JAVA基础---面向对象
    Flutter 读写本地文件
    Dart 处理json,built_value库
    Flutter 页面入栈和出栈
    web项目如何使用Material Icons
  • 原文地址:https://www.cnblogs.com/yujidewu/p/5741159.html
Copyright © 2011-2022 走看看