CRAnimation

如何创建一个3D侧边栏动画

2017-03-10  本文已影响96人  好尼桑

原攻略来自于:How To Create a Cool 3D Sidebar Animation Like in Taasky

本篇文章为我自己的总结、翻译版,你可以点击上面的链接找到原文(如果你不是简书看到的,可以点击这里查看我的原文)

在今天的文章中,由于我的英文能力有限,就不会对原文进行逐句翻译了,我会以自己的理解来讲解这篇文章的精髓,教你一步步来实现最终的效果

另外由于我个人的能力有限,可能有些地方出现遗漏或者理解出错,欢迎指正下,共同学习。
最终效果:

屏幕快照
添加菜单和详情的Container Views

拖拽一个UIContainer View到Content View上,在属性检测器中设置宽度为80,并且给 Document\Label设置为Menu Container View,如下:


不要在意图中的600,因为Xcode版本的问题,现在的Xcode8已经不是默认600X600了,我们要设置的是高度和Content View一样,宽度为80,左上角对齐父视图就对了
添加详情Container View:同样再拖拽一个UIContainer View放在菜单Container View右边,给它的Document\Label 设置为Detail Container View



这个详情Container View的宽度和父视图的宽度是一样的,现在你应该得到如下的视图:



添加Container View 会自带一个控制器,把它们删掉:
现在来设置布局吧:

对于Menu Container View:相对于它的父视图以及详情 Container View共5个约束:



对于 Detail Container View:添加了3个约束:



通过移动箭头改一下 Initial view controller

把菜单控制器和详情控制器嵌套进来:选中Menu Container View拖拽到Navigation Controller,并选择embed



设置好嵌套之后,你的storyboard会变成如下:

嵌套进来的控制器统统都收缩到80的宽度了。
调整一下menu view controller上UIImageView的宽度

删掉 menu和 detail 之间的 segue,并给detail view controller添加一个Navigation Controller 通过上面菜单栏的按钮:Editor\Embed In\Navigation Controller



给这个新的navigation controller 设置如下属性:



在属性检测器中设置View Controller\Layout\Adjust Scroll View Insets选中(这个会避免内容被bar覆盖):

我们要让Detail View Controller嵌套在Detail Container View中,


现在你就可以运行一下了,运行结果应该是这个样子:



现在我们可以随意滑动ScrollView了, 接下来我们要修改一下只让它显示整个侧边菜单和隐藏侧边菜单, 并且让它不能滑出边界

  1. 在ScrollView的属性检测器中选择Scrolling\Paging Enabled
  2. 不要选择Bounce\Bounces

再次build&run 你会发现:


但是还有一点点问题,当你试图隐藏菜单框的时候,它有时又重新弹回并显示出来了,这是一个 paging的问题,详细的你可以参考这里: this problem discussion on StackOverflow.
先留着这个问题,我们先来解决点击左侧按钮没有相应的事件变化的问题(和我们最上面演示的图作比较)

这或许会使你感到惊讶,因为我们并没有改变任何相关的代码,接着往下看:
先修改一下细节的问题:
在 MenuViewController.swift的 viewDidLoad() 加入以下代码:
override func viewDidLoad() {
  super.viewDidLoad()
  // Remove the drop shadow from the navigation bar
  navigationController!.navigationBar.clipsToBounds = true
}

这能消除掉navigation bar下极小的细缝,虽然是个很小的细节,但是也能对整个APP填色不少。

当我们点击MenuViewController中的cell的时候 应该设置DetailViewController 的 menuItem 属性,但是现在DetailViewController已经不直接和MenuViewController连接了,所以没什么反应
ContainerViewController将会扮演MenuViewController和DetailViewController之间的协调者的角色
给ContainerViewController.swift添加一个属性:
private var detailViewController: DetailViewController?
在ContainerViewController.swift中实现 prepareForSegue(_:sender:) 方法:

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "DetailViewSegue" {
        let navigationController = segue.destination as! UINavigationController
        detailViewController = navigationController.topViewController as? DetailViewController
        }
}

在将DetailViewController嵌套进container View中的时候会生成一条Storyboard Embed Segue的线,你选中这条线,并设置它的Identifier为DetailViewSegue:



在ContainerViewController中声明一个menuItem属性,并设置属性观测器:

var menuItem: NSDictionary? {
  didSet {
    if let detailViewController = detailViewController {
      detailViewController.menuItem = menuItem
    }
  }
}

由于MenuViewController 和DetailViewController之间已经没有segue连接了,但是当选中MenuViewController中的cell的时候仍然需要作出相应,我们把事件的相应从prepareForSegue(:sender:)移动到tableView(:didDeselectRowAtIndexPath:)中。

删掉prepareForSegue(_:sender:)中的代码,改成如下:

// MARK: UITableViewDelegate
 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        tableView.deselectRow(at: indexPath, animated: true)
        let menuItem = menuItems[indexPath.row] as! NSDictionary
        (navigationController!.parent as! ContainerViewController).menuItem = menuItem
    }

这样我们就在选中menu中的cell的时候设置ContainerViewController的menuItem属性了,并且触发了属性观测器从而设置了DetailViewController的menuItem属性

我们在MenuViewController.swift中的viewDidLoad()添加:

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

这是为了app第一次启动的时候给DetailViewController设置默认的图片
现在运行app,会发现如下效果了:


现在我们要显示和隐藏左侧菜单

当我们选中菜单的时候,我们要隐藏掉菜单
为了实现这个目的,我们要设置Scroll View 的content
在ContainerViewController.swift 中连接 Scroll View并命名为scrollView
具体操作如下:



现在在ContainerViewController.swift 添加 hideOrShowMenu(_:animated:)方法

// MARK: ContainerViewController
func hideOrShowMenu(show: Bool, animated: Bool) {
  let menuOffset = menuContainerView.bounds.width
  scrollView.setContentOffset(show ? CGPoint.zero : CGPoint(x: menuOffset, y: 0), animated: animated)
}

menuOffset 的值是 80 ,当true的时候,那么origin就是(0,0),这个时候菜单是可见的,同理,当origin时(80,0)的时候菜单是隐藏的

在ContainerViewController的menuItem属性观测器中添加

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

运行app,将会出现如下效果:



好了这个时候我们来解决paging 的问题吧(滑动来隐藏侧边栏的时候,侧边栏会弹出来的BUG)
我们将通过遵守UIScrollViewDelegate协议来解决这个问题
给ContainerViewController添加协议:

class ContainerViewController: UIViewController, 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.isPagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width - scrollView.frame.width)
}

对于这个问题的详细探究就不在本文的范围之内了,有兴趣的同学可以进入这个链接看看:这里

Bulid&Run,你会发现解决了这个问题:


添加左上角的按钮

通过点击按钮来隐藏和展示左侧栏,创建一个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)
  }
 
}

我们来给DetailViewController.swift添加一个属性:
var hamburgerView: HamburgerView?
在 viewDidLoad()中创建hamburgerView并赋在navigation bar上

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:)方法,但是我们应该传入什么值呢?

我们给ContainerViewController添加一个bool类型的属性来记录左侧菜单是否显示

在ContainerViewController.swift下面添加属性:
var showingMenu = false
我们重写viewDidLayoutSubviews() 来控制展示或者隐藏菜单栏,这样的好处是旋转的时候也能及时响应:

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
      hideOrShowMenu(show: showingMenu, animated: false)
}

那么我们就不需要ContainerViewController.swift中的viewDidLoad()了,删了它们吧
别忘了我们还要在DetailViewController.swift中添加响应事件:

    func hamburgerViewTapped() {
        let navigationController = parent as! UINavigationController
        let containerViewController = navigationController.parent as! ContainerViewController
        containerViewController.hideOrShowMenu(show: !containerViewController.showingMenu, animated: true)
    }

每当我们点击的时候需要通过!containerViewController.showingMenu来控制是否显示,就像button的选中非选中那样。

我们要在hideOrShowMenu方法中及时修改一下我们的showingMenu的状态,在hideOrShowMenu方法下面添加如下:
showingMenu = show
B&R(Build & Run)你会看到如下的效果:

还有一个问题就是当你滑动展示菜单栏或者隐藏菜单栏的时候,再去点击左上角的button来响应事件需要点击两次,这是为什么呢?

这是因为我们滑动ScrollView的时候 并没有更新showingMenu
为了修正这个问题,你需要实现UIScrollViewDelegate另一个方法
在ContainerViewController中添加:

func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
  let menuOffset = CGRectGetWidth(menuContainerView.bounds)
  showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset)
  println(“didEndDecelerating showingMenu \(showingMenu)”)
}

当ScrollView的content offset等于菜单栏宽度(80)的时候,也就是菜单栏是隐藏的,设置showingMenu为false,反之同理

B&R 让我们来看看当停下来的时候是否会如预期那样?看起来和期望好像有一点点的差池,这点差池有点依赖于滑动的速度,当我在模拟器上测试的时候,只有滚动很慢的情况下达到预期,但是在真实设备上只有滚动很快才会达到预期。

好吧,那就把上面的代码全部都移动到scrollViewDidScroll(_:)中,这个方法会不断的调用,相对来说更可靠点


添加3D效果

首先要添加透视效果:
在ContainerViewController.swift中添加如下代码:

  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 = menuContainerView.bounds.width * 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(_:):的作用:

注意: m34 通常是1除以一个值来表示,这个值表达的含义是你在z轴上观察x-y平面的位置(单位是像素),负数表明观察者是在屏幕前,而正数表示观察者在屏幕后面。

在观察者和观察的对象之间画线形成3D透视效果。观察者移动的越远,那么透视效果越不明显。你可以试试改变值1000到500或者2000来看看菜单栏会发生什么样的变化.

在scrollViewDidScroll(_:):下添加如下代码:

 let multiplier = 1.0 / menuContainerView.bounds.width
        let offset = scrollView.contentOffset.x * multiplier
        let fraction = 1.0 - offset
        menuContainerView.layer.transform = transformForFraction(fraction: fraction)
        menuContainerView.alpha = fraction

值是从0到1的,0表示完全显示,1表示完全隐藏菜单栏
这样的话fraction 完全依赖于已经显示的菜单栏的宽度来改变其值(0~1),同时我们还通过fraction来调整菜单栏的alpha 来改变它的明暗情况

B&R 我们可以看到我们的3D效果了


很显然还有一点错误,我们的连接点好像出问题了,这是因为view默认状态的anchorPoint是view的中心

我们来修改anchorPoint使其在右侧边缘中心:

在ContainerViewController.swift的viewDidLayoutSubviews()中添加如下code:
menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
B&R 你会看到:

还有一件事:

我们来给左上角的按钮(以下称MenuBtn)添加动画吧
在HamburgerView.swift中添加:
func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle)) }
在ContainerViewController.swift中的scrollViewDidScroll(_:)中添加如下代码:

if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) } }
B&R

最终效果最终效果

Cool!

上一篇下一篇

猜你喜欢

热点阅读