UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)

版本记录

版本号 时间
V1.0 2019.05.16 星期四

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)

开始

首先看下写作环境

Swift 4.2, iOS 12, Xcode 10

许多iOS应用程序需要一个菜单来在视图之间导航或让用户做出选择。 一种常用的设计是侧面菜单。

您可以使用简单的表单轻松制作侧边菜单,但是如何在UI中引入一些乐趣呢? 你想在用户的脸上露出微笑,并一次又一次地将它们带回你的应用程序。 实现此目的的一种方法是创建3D侧边栏动画。

在本教程中,您将学习如何通过操纵CALayer属性来创建3D侧边栏动画来为一些UIView元素设置动画。 这个动画的灵感来自一个名为TaaskyTo-Do应用程序。

在本教程中,您将使用以下元素:

  • Storyboards
  • Auto Layout constraints
  • UIScrollView
  • View controller containment
  • Core Animation

打开名为TaskChooser的入门项目。

想象一下,您正在创建一个与您的同事或朋友谈判活动的基本应用程序。如果你在里面竖起大拇指,如果你不能成功,请大拇指向下。你甚至可以因天气恶劣而下降。

花点时间看一下这个项目。你会看到它是一个标准的Xcode Master-Detail模板应用程序,它显示了一个图像表。

  • MenuViewController:一个UITableViewController,它使用自定义表格视图单元MenuItemCell来设置每个单元格的背景颜色。它还有一个图像。
  • MenuDataSource:实现UITableViewDataSource以从MenuItems.json提供表数据的对象。这些数据可能来自生产情况下的服务器。
  • DetailViewController:使用与您选择的单元格相同的背景颜色显示大图像。

构建并运行应用程序。您应该看到启动项目加载了7行颜色和图标:

使用菜单显示您选择的选项:

这是功能性的,但外观和感觉相当普通。 你希望你的应用程序既令人惊喜又高兴!

在本教程中,您将把Master-Detail应用程序重构为水平滚动视图。 您将在容器视图中嵌入masterdetail视图。

接下来,您将添加一个按钮来显示或隐藏菜单。 然后,您将在菜单上添加整齐的3D折叠效果。

作为此3D动画侧边栏的最后一步,您将同步旋转菜单按钮以显示或隐藏菜单。

您的第一个任务是将MenuViewControllerDetailViewController转换为滑出侧边栏,其中滚动视图包含菜单和详细视图并排。


Restructuring Your Storyboard

在重建菜单之前,您需要进行一些拆卸。

Project导航器的Views文件夹中打开Main.storyboard。 你可以看到由segues连接的UINavigationControllerMenuViewControllerDetailViewController

1. Deleting the Old Structure

导航控制器场景(Navigation Controller Scene)不会激发快乐。 选择该场景并将其删除。 接下来,选择MenuViewControllerDetailViewController之间的segue并删除它。

完成后,开始工作。

2. Adding a New Root Container

由于UINavigationController消失了,您不再拥有项目中视图控制器的顶级容器。 你现在就加一个。

Project导航器中选择Views文件夹。 按Command-N将新文件添加到项目中。 然后:

  • 1) 选择iOS▸CocoaTouch Class。 点击Next
  • 2) 将类命名为RootViewController
  • 3) 确保RootViewControllerUIViewController的子类。
  • 4) 确保未选中Also create XIB file
  • 5) 语言应该是Swift

再次打开Main.storyboard

使用快捷键Command-Shift-L打开对象库,并将UIViewController的实例拖到故事板。

从对象层次结构中选择View Controller Scene,然后打开Identity inspector。 将Class区域设置为RootViewController

接下来,打开Attributes inspector,然后选中Is Initial View Controller框。

3. Adding Identifiers to View Controllers

由于MenuViewControllerDetailViewController不再通过segues连接,因此您需要一种从代码中访问它们的方法。 因此,您的下一步是提供一些标识符来执行此操作。

从对象层次结构中选择Menu View Controller Scene。 打开Identity inspector并将Storyboard ID设置为MenuViewController

这个字符串可以是任何合理的值,但一个易于记忆的技术是使用类的名称。

接下来,从Object层次结构中选择Detail View Controller Scene并执行相同的操作。 将Storyboard ID设置为DetailViewController

这就是你需要在Main.storyboard中做的所有事情。 本教程的其余部分将在代码中。


Creating Contained View Controllers

在本节中,您将创建一个UIScrollView并向该滚动视图添加两个容器。 容器将保存MenuViewControllerDetailViewController

1. Creating a Scroll View

您的第一步是创建UIScrollView

Project导航器中打开RootViewController.swift。 删除Xcode从RootViewController内部提供的所有内容。

RootViewController上面添加此扩展:

extension UIView {
  func embedInsideSafeArea(_ subview: UIView) {
    addSubview(subview)
    subview.translatesAutoresizingMaskIntoConstraints = false
    subview.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor)
      .isActive = true
    subview.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor)
      .isActive = true
    subview.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor)
      .isActive = true
    subview.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
      .isActive = true
  }
}

这是一个帮助方法,您将在本教程中使用几次。 代码将传入的视图添加为子视图,然后添加四个约束以将子视图粘贴到其自身内。

接下来在文件末尾添加此扩展名:

extension RootViewController: UIScrollViewDelegate {
}

您将需要监听UIScrollView以进行更改。 该操作稍后在本教程中进行,因此此扩展目前为空。

最后,在RootViewController中插入以下代码:

// 1
lazy var scroller: UIScrollView = {
  let scroller = UIScrollView(frame: .zero)
  scroller.isPagingEnabled = true
  scroller.delaysContentTouches = false
  scroller.bounces = false
  scroller.showsHorizontalScrollIndicator = false
  scroller.delegate = self
  return scroller
}()
// 2
override func viewDidLoad() {
  super.viewDidLoad()
  view.backgroundColor = UIColor(named: "rw-dark")
  view.embedInsideSafeArea(scroller)
}
// 3
override var preferredStatusBarStyle: UIStatusBarStyle {
  return .lightContent
}

以下是您在此代码中所做的事情:

  • 1) 首先,创建一个UIScrollView。 您希望启用分页,以便内容在滚动视图内以原子单位移动。 您已禁用delayedContentTouches,以便内部控制器能够快速响应用户触摸。 bounces设置为false,因此您不会从滚动条获得弹性感。 然后,将RootViewController设置为scroll view的代理。
  • 2) 在viewDidLoad()中,您可以使用之前添加的帮助方法设置背景颜色并将scroll view嵌入根视图中。
  • 3) 对preferredStatusBarStyle的覆盖允许状态栏在深色背景上显示为浅色。

构建并运行您的应用程序,以显示它在此重构后正确启动:

由于您尚未将按钮和内容添加到新的RootViewController,因此您应该只能看到已设置的深色背景。 别担心,您将在下一节中将它们添加回来。

2. Creating Containers

现在,您将创建UIView实例,它们将充当MenuViewControllerDetailViewController的容器。 然后,您将它们添加到scroll view

RootViewController的顶部添加这些属性:

let menuWidth: CGFloat = 80.0

var menuContainer = UIView(frame: .zero)
var detailContainer = UIView(frame: .zero)

接下来,将此方法添加到RootViewController

func installMenuContainer() {
  // 1
  scroller.addSubview(menuContainer)
  menuContainer.translatesAutoresizingMaskIntoConstraints = false
  menuContainer.backgroundColor = .orange
  
  // 2
  menuContainer.leadingAnchor.constraint(equalTo: scroller.leadingAnchor)
    .isActive = true
  menuContainer.topAnchor.constraint(equalTo: scroller.topAnchor)
    .isActive = true
  menuContainer.bottomAnchor.constraint(equalTo: scroller.bottomAnchor)
    .isActive = true
  
  // 3
  menuContainer.widthAnchor.constraint(equalToConstant: menuWidth)
    .isActive = true
  menuContainer.heightAnchor.constraint(equalTo: scroller.heightAnchor)
    .isActive = true
}

以下是您使用此代码所做的事情:

  • 1) 添加menuContainer作为scroller的子视图,并为其添加临时颜色。 在开发过程中使用非品牌颜色是了解开发过程中工作进展的好方法。
  • 2) 接下来,将menuContainer的顶部和底部固定到scroll view的相同边缘。
  • 3) 最后,将width设置为80.0的常量值,并将容器的高度固定为scroll view的高度。

接下来,将以下方法添加到RootViewController

func installDetailContainer() {
  //1
  scroller.addSubview(detailContainer)
  detailContainer.translatesAutoresizingMaskIntoConstraints = false
  detailContainer.backgroundColor = .red
  
  //2
  detailContainer.trailingAnchor.constraint(equalTo: scroller.trailingAnchor)
    .isActive = true
  detailContainer.topAnchor.constraint(equalTo: scroller.topAnchor)
    .isActive = true
  detailContainer.bottomAnchor.constraint(equalTo: scroller.bottomAnchor)
    .isActive = true
  
  //3
  detailContainer.leadingAnchor
    .constraint(equalTo: menuContainer.trailingAnchor)
    .isActive = true
  detailContainer.widthAnchor.constraint(equalTo: scroller.widthAnchor)
    .isActive = true
}
  • 1) 与installMenuContainer类似,您将detailContainer作为子视图添加到scroll view
  • 2) 顶部,底部和右侧边缘固定到它们各自的scroll view边缘。 detailContainerleading edge连接到menuContainer
  • 3) 最后,容器的宽度始终与scroll view的宽度相同。

要让UIScrollView滚动其内容,它需要知道该内容有多大。 您可以通过使用UIScrollViewcontentSize属性或隐式定义内容的大小来实现。

在这种情况下,内容大小由五件事隐式定义:

  • 1) menu container高度==scroll view高度
  • 2) detail container的后缘固定到menu container的前缘
  • 3) menu container的宽度== 80
  • 4) detail container的宽度==scroll view的宽度
  • 5) 外部detail and menu container的边缘锚定到scroller的边缘

最后要做的是使用这两种方法。 在viewDidLoad()的末尾添加这些行:

installMenuContainer()
installDetailContainer()

构建并运行您的应用程序,看看一些糖果色的奇迹。 您可以拖动内容以隐藏橙色菜单容器。 您已经可以看到成品开始形成。

3. Adding Contained View Controllers

您正在构建创建界面所需的视图堆栈。 下一步是在您创建的容器中安装MenuViewControllerDetailViewController

你仍然想要一个导航栏,因为你想要一个放置菜单显示按钮的地方。 将此扩展添加到RootViewController.swift的末尾:

extension RootViewController {
  func installInNavigationController(_ rootController: UIViewController)
    -> UINavigationController {
      let nav = UINavigationController(rootViewController: rootController)
      
      //1
      nav.navigationBar.barTintColor = UIColor(named: "rw-dark")
      nav.navigationBar.tintColor = UIColor(named: "rw-light")
      nav.navigationBar.isTranslucent = false
      nav.navigationBar.clipsToBounds = true
      
      //2
      addChild(nav)
      
      return nav
  }
}

以下是此代码中发生的情况:

  • 1) 此方法采用视图控制器,将其安装在UINavigationController中,然后设置导航栏的视觉样式。
  • 2) 视图控制器包含的最重要部分是addChild(nav)。 这会将UINavigationController安装为RootViewController的子视图控制器。 这意味着在iPad上旋转或拆分视图导致的特征变化等事件可以在层次结构中向下传播给子节点。

接下来,在installInNavigationController(_ :)之后将此方法添加到同一扩展中以帮助安装MenuViewControllerDetailViewController

func installFromStoryboard(_ identifier: String,
                           into container: UIView)
  -> UIViewController {
    guard let viewController = storyboard?
      .instantiateViewController(withIdentifier: identifier) else {
        fatalError("broken storyboard expected \(identifier) to be available")
    }
    let nav = installInNavigationController(viewController)
    container.embedInsideSafeArea(nav.view)
    return viewController
}

此方法从故事板中实例化视图控制器,警告开发人员故事板中断。

然后,代码将视图控制器放在UINavigationController中,并将该导航控制器嵌入到容器中。

接下来,在主类中添加这些属性以跟踪MenuViewControllerDetailViewController

var menuViewController: MenuViewController?
var detailViewController: DetailViewController?

然后在viewDidLoad()的末尾插入这些行:

menuViewController = 
  installFromStoryboard("MenuViewController", 
                        into: menuContainer) as? MenuViewController

detailViewController = 
  installFromStoryboard("DetailViewController",
                        into: detailContainer) as? DetailViewController

在这个片段中,您实例化了MenuViewControllerDetailViewController并保留了对它们的引用,因为稍后您将需要它们。

构建并运行应用程序,您将看到菜单可见,虽然比以前更瘦。

这些按钮不会导致DetailViewController更新,因为segue不再存在。 你将在下一节中解决这个问题。

您已完成本教程的视图包含部分。 现在你可以进入真正有趣的东西了。


Reconnect Menu and Detail Views

在你进行拆除横冲直撞之前,在MenuViewController中选择一个表格单元格会触发一个segue,它将选定的MenuItem传递给DetailViewController

它很便宜而且完成了工作,但是有一个小问题。 该模式需要MenuViewController了解DetailViewController

这意味着MenuViewControllerDetailViewController紧密绑定。 如果您不再想使用DetailViewController来显示菜单选项的结果,会发生什么?

作为优秀的开发人员,您应该寻求减少系统中的紧密绑定量。 您现在将设置一个新模式。

1. Creating a Delegate Protocol

首先要做的是在MenuViewController中创建一个委托协议,它允许您传达菜单选择更改。

Project导航器中找到MenuViewController.swift并打开该文件。

由于您不再使用segue,您可以继续删除prepare(for:sender :)

接下来,在MenuViewController类声明之上添加此协议定义:

protocol MenuDelegate: class {
  func didSelectMenuItem(_ item: MenuItem)
}

接下来,在MenuViewController的主体中插入以下代码:

//1
weak var delegate: MenuDelegate?

override func tableView(_ tableView: UITableView,
                        didSelectRowAt indexPath: IndexPath) {
  //2
  let item = datasource.menuItems[indexPath.row]
  delegate?.didSelectMenuItem(item)
  
  //3
  DispatchQueue.main.async {
    tableView.deselectRow(at: indexPath, animated: true)
  }
}

这是代码的作用:

  • 1) 在第一个代码片段中,您声明了一个感兴趣的各方可以采用的协议。 在MenuViewController中,您声明了一个weak delegate属性。 在协议引用中使用weak有助于避免创建保留周期。
  • 2) 接下来,实现UITableViewDelegate方法tableView(_:didSelectRowAt :)将选定的MenuItem传递给委托。
  • 3) 最后一个声明是取消选择单元格并删除其突出显示的整体操作。

2. Implementing the MenuDelegate Protocol

您现在可以实现您创建的协议,以将选择更改发送到DetailViewController

打开RootViewController.swift并将此扩展名添加到文件末尾:

extension RootViewController: MenuDelegate {
  func didSelectMenuItem(_ item: MenuItem) {
    detailViewController?.menuItem = item
  }
}

此代码声明RootViewController采用MenuDelegate。 当您选择一个菜单项时,RootViewController通过将选定的MenuItem传递给实例来告诉DetailViewController该更改。

最后,在viewDidLoad()的末尾插入此行:

menuViewController?.delegate = self

这告诉MenuViewController RootViewController是委托。

构建并运行应用程序。 您的菜单选择现在将更改DetailViewController的内容。 竖起大拇指。


Controlling the Scroll View

到现在为止还挺好。 你的菜单工作,应用程序看起来更好。

但是,您还会注意到手动滚动菜单不会持续很长时间。 菜单总是反弹回视图。

滚动视图属性isPagingEnabled会导致该效果,因为您已将其设置为true。 你现在就解决这个问题。

仍然在RootViewController中工作,在下面添加以下行:menuWidth:CGFloat = 80.0

lazy var threshold = menuWidth/2.0

在这里,您可以选择一个任意点,菜单将选择隐藏或显示自己。 你使用lazy,因为你正在计算相对于menuWidth的值。

RootViewController中找到extension RootViewController: UIScrollViewDelegate并在扩展中插入此代码:

//1
func scrollViewDidScroll(_ scrollView: UIScrollView) {
  let offset = scrollView.contentOffset
  scrollView.isPagingEnabled = offset.x < threshold
}
//2
func scrollViewDidEndDragging(_ scrollView: UIScrollView,
                              willDecelerate decelerate: Bool) {
  let offset = scrollView.contentOffset
  if offset.x > threshold {
    hideMenu()
  }
}
//3
func moveMenu(nextPosition: CGFloat) {
  let nextOffset = CGPoint(x: nextPosition, y: 0)
  scroller.setContentOffset(nextOffset, animated: true)
}
func hideMenu() {
  moveMenu(nextPosition: menuWidth)
}
func showMenu() {
  moveMenu(nextPosition: 0)
}
func toggleMenu() {
  let menuIsHidden = scroller.contentOffset.x > threshold
  if menuIsHidden {
    showMenu()
  } else {
    hideMenu()
  }
}

看看这段代码的作用:

  • 1) 第一个UIScrollViewDelegate方法,scrollViewDidScroll(_ :),非常有用。 它总是会告诉您何时更改了scroll viewcontentOffset。 您可以根据水平偏移量是否高于阈值threshold来设置isPagingEnabled
  • 2) 接下来,实现scrollViewDidEndDragging(_:willDecelerate :)以检测滚动视图上的抬起触摸。 只要内容偏移量大于阈值,就隐藏菜单;否则分页效果将保持并显示菜单。
  • 3) 最后一种方法是帮助菜单将菜单设置到位置:显示,隐藏和切换。

构建并运行您的应用程序。 现在,尝试拖动scroll view,看看会发生什么。 越过阈值时,菜单会弹出或关闭:


Adding a Menu Button

在本节中,您将向导航栏添加汉堡(burger)按钮,这样您的用户就不必拖动来显示和隐藏菜单。

因为您想稍后为此按钮设置动画,所以这需要是UIView而不是基于图像的UIBarButton

1. Creating a Hamburger View

Project导航器中选择Views文件夹,然后添加一个新的Swift文件。

  • 1) 选择iOS▸CocoaTouch Class。 点击Next
  • 2) 将类命名为HamburgerView
  • 3) 确保HamburgerViewUIView的子类。
  • 4) 语言应该是Swift

打开HamburgerView.swift并使用以下代码替换HamburgerView类中的所有内容:

//1
let imageView: UIImageView = {
  let view = UIImageView(image: UIImage(imageLiteralResourceName: "Hamburger"))
  view.contentMode = .center
  return view
}()
//2
required override init(frame: CGRect) {
  super.init(frame: frame)
  configure()
}
required init?(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
  configure()
}
private func configure() {
  addSubview(imageView)
}

这是你在这里做的事情:

  • 1) 首先,使用库中的资源创建UIImageView
  • 2) 然后添加该图像视图,为两种可能的init方法创建路径。

2. Installing the Hamburger View

现在您有了一个视图,您可以将其安装在属于DetailViewController的导航栏中。

再次打开RootViewController.swift并在主RootViewController类的顶部插入此属性:

var hamburgerView: HamburgerView?

接下来将此扩展名附加到文件末尾:

extension RootViewController {
  func installBurger(in viewController: UIViewController) {
    let action = #selector(burgerTapped(_:))
    let tapGestureRecognizer = UITapGestureRecognizer(target: self,
                                                      action: action)
    let burger = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
    burger.addGestureRecognizer(tapGestureRecognizer)
    viewController.navigationItem.leftBarButtonItem
      = UIBarButtonItem(customView: burger)
    hamburgerView = burger
  }
  @objc func burgerTapped(_ sender: Any) {
    toggleMenu()
  }
}

最后将此语句添加到viewDidLoad()的底部:

if let detailViewController = detailViewController {
  installBurger(in: detailViewController)
}

这组代码为汉堡按钮提供了一个实例变量,因为您很快就想要为它设置动画。然后,您可以创建一个方法,以在任何视图控制器的导航栏中安装汉堡。

方法installBurger(in :)在视图中创建一个tap方法,调用方法burgerTapped(_ :)

请注意,您必须使用@objc注释burgerTapped(_ :),因为您在此处使用Objective-C运行时。此方法根据当前状态切换菜单。

然后使用此方法在属于DetailViewControllerUINavigationBar中安装按钮。从体系结构的角度来看,DetailViewController不知道这个按钮,也不需要处理任何菜单状态操作。你保持责任分离。

就这些。在构建对象堆栈时,使3D侧边栏动画生动的步骤变得越来越少。

构建并运行您的应用程序。你会看到你现在有一个汉堡按钮可以切换菜单。


Adding Perspective to the Menu

为了回顾你到目前为止所做的事情,你已经将Master-Detail应用程序重构为可行的侧面菜单式应用程序,用户可以拖动或使用按钮来显示和隐藏菜单。

现在,为您的下一步:菜单的动画版本应该看起来像一个面板打开和关闭。 菜单按钮将在菜单打开时顺时针旋转,在菜单关闭时逆时针旋转。

为此,您将计算可见的菜单视图的分数,然后使用它来计算菜单的旋转角度。

1. Manipulating the Menu Layer

仍在RootViewController.swift中,将此扩展名添加到文件中:

extension RootViewController {
  func transformForFraction(_ fraction: CGFloat, ofWidth width: CGFloat)
    -> CATransform3D {
      //1
      var identity = CATransform3DIdentity
      identity.m34 = -1.0 / 1000.0
      
      //2
      let angle = -fraction * .pi/2.0
      let xOffset = width/2.0 + width * fraction/4.0
      
      //3
      let rotateTransform = CATransform3DRotate(identity, angle, 0.0, 1.0, 0.0)
      let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0)
      return CATransform3DConcat(rotateTransform, translateTransform)
  }
}

这是transformForFraction(_:ofWidth :)的逐个分析:

  • 1) CATransform3DIdentity是一个4×4矩阵,其中对角线为1,其他地方为零。 CATransform3DIdentitym34属性是第3行第4列中的值,它控制转换中的透视量。
  • 2) 角度和偏移量作为输入fraction的函数计算。 当fraction1.0时,菜单将完全隐藏,当它为0.0时,菜单将完全可见。
  • 3) 计算最终变换。 CATransform3DRotate使用angle来确定围绕y轴的旋转量:-90度使菜单垂直于视图的背面,0度渲染菜单与x-y平面平行,CATransform3DMakeTranslation将菜单移动到中心的右侧, 和CATransform3DConcat连接translateTransformrotateTransform,以便菜单在旋转时显示为侧滑。

注意:m34值通常计算为1除以表示观察者在z轴上的位置的数字,同时观察2D x-y平面。 负z值表示观察者在平面前面,而正z值表示观察者在平面后面。

在此viewer与平面中对象边缘之间绘制线条会产生3D透视效果。 当viewer移动得更远时,视角不太明显。 尝试更改1,0005002,000,以查看菜单的透视图如何更改。

接下来,将此扩展添加到RootViewController.swift

extension RootViewController {  
  //1
  func calculateMenuDisplayFraction(_ scrollview: UIScrollView) -> CGFloat {
    let fraction = scrollview.contentOffset.x/menuWidth
    let clamped = Swift.min(Swift.max(0, fraction), 1.0)
    return clamped
  }  
  //2
  func updateViewVisibility(_ container: UIView, fraction: CGFloat) {
    container.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
    container.layer.transform = transformForFraction(fraction,
                                                     ofWidth: menuWidth)
    container.alpha = 1.0 - fraction
  }  
}

此代码提供了一些用于打开和关闭菜单的帮助程序:

  • 1) calculateMenuDisplayFraction(_ :)将原始水平偏移量转换为相对于菜单宽度的1.0的fraction。 该值夹在0.0和1.0之间。
  • 2) updateViewVisibility(_:fraction :)将分数生成的变换应用于视图层。 anchorPoint是转换应用的铰链,因此CGPoint(x:1.0,y:0.5)表示右手边缘和垂直中心。

通过设置alpha,随着转换的进行,视图也会变暗。

现在,找到scrollViewDidScroll(_ :)并在方法的末尾插入这些行:

let fraction = calculateMenuDisplayFraction(scrollView)
updateViewVisibility(menuContainer, fraction: fraction)

构建并运行应用程序。 当您向左拖动详细视图时,菜单现在似乎在细节视图下折叠。


Rotating the Burger Button

在本教程中,您要做的最后一件事是在scroll view移动时使汉堡按钮看起来在屏幕上滚动。

打开HamburgerView.swift并将此方法插入到类中:

func setFractionOpen(_ fraction: CGFloat) {
  let angle = fraction * .pi/2.0
  imageView.transform = CGAffineTransform(rotationAngle: angle)
}

此代码将视图作为fraction的函数旋转。 当菜单完全打开时,视图旋转90度。

返回RootViewController.swift。 找到scrollViewDidScroll(_ :)并将此行追加到方法的末尾:

hamburgerView?.setFractionOpen(1.0 - fraction)

scroll view移动时,这会旋转汉堡按钮。

然后,因为启动应用程序时菜单已打开,所以将此行添加到viewDidLoad()的末尾以将菜单置于正确的初始状态:

hamburgerView?.setFractionOpen(1.0)

构建并运行您的应用程序。 滑动并点按菜单以查看动态和同步的3D侧边栏动画:

在本教程中,您用到了:

  • 视图控制器。
  • UIScrollView隐式内容大小。
  • 代理模式。
  • 透视随CATransform3Dm34而变化。

尝试使用m34值来查看它对转换的影响。 如果您想了解有关m34的更多信息,请阅读this xdPixel blog post

WikipediaPerspective页面有一些很好的照片,解释了视觉透视Perspective的概念。

另外,请考虑如何在自己的应用程序中使用此3D侧边栏动画,为用户交互添加一点生命。 令人惊讶的是,对菜单这么简单的东西的微妙影响可以增加整个用户体验。

后记

本篇主要讲述了基于CALayer属性的一种3D边栏动画的实现,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,100评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,862评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,993评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,309评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,303评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,421评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,830评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,501评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,689评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,506评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,564评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,286评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,826评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,875评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,114评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,705评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,269评论 2 341

推荐阅读更多精彩内容