Swift - 侧滑菜单的实现(样例2:仿QQ,菜单带缩放效果)
作者:hangge | 2016-01-25 08:45
(本文代码已升级至Swift4)
前面我写了一篇文章介绍如何实现侧滑菜单:Swift - 侧滑菜单的实现(样例1:主页向右滑动,露出下方菜单页)
其实现方式是,通过手势拖动主页面移动,从而露出下面的菜单页(其实后面的菜单页是固定不动的)。
下面演示另一种样式的实现(模仿手机QQ的侧滑菜单),主页面滑动停靠的过程中会逐渐缩小,同时菜单页也会逐渐移动放大,浮现出来。
(注:本文样例是基于前面文章的demo修改的,如果没阅读前文的话可以先去看下。为便于理解,下面将效果分两步实现。)
1,主页停靠侧边时尺寸逐渐缩小
(1)定义了新属性 minProportion,表示停靠时的缩小比例。在滑动时,再根据页面的位置实时计算出当前的缩放比例。
(2)在主页面与菜单页之间添加了个黑色遮罩层(blackCover), 初始化时是不透明的。随着菜单的展开透明度逐渐变为0。这样侧滑菜单有逐渐显示出来的效果。
ViewController.swift 代码如下(高亮处为修改过的地方):
import UIKit class ViewController: UIViewController { // 主页导航控制器 var mainNavigationController:UINavigationController! // 主页面控制器 var mainViewController:MainViewController! // 菜单页控制器 var menuViewController:MenuViewController? // 菜单页当前状态 var currentState = MenuState.Collapsed { didSet { //菜单展开的时候,给主页面边缘添加阴影 let shouldShowShadow = currentState != .Collapsed showShadowForMainViewController(shouldShowShadow: shouldShowShadow) } } // 菜单打开后主页在屏幕右侧露出部分的宽度 let menuViewExpandedOffset: CGFloat = 60 // 侧滑菜单黑色半透明遮罩层 var blackCover: UIView? // 最小缩放比例 let minProportion: CGFloat = 0.77 override func viewDidLoad() { super.viewDidLoad() //初始化主视图 mainNavigationController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "mainNavigaiton") as! UINavigationController view.addSubview(mainNavigationController.view) //指定Navigation Bar左侧按钮的事件 mainViewController = mainNavigationController.viewControllers.first as! MainViewController mainViewController.navigationItem.leftBarButtonItem?.action = #selector(showMenu) //添加拖动手势 let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) mainNavigationController.view.addGestureRecognizer(panGestureRecognizer) //单击收起菜单手势 let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture)) mainNavigationController.view.addGestureRecognizer(tapGestureRecognizer) } //导航栏左侧按钮事件响应 @objc func showMenu() { //如果菜单是展开的则会收起,否则就展开 if currentState == .Expanded { animateMainView(shouldExpand: false) }else { addMenuViewController() animateMainView(shouldExpand: true) } } //拖动手势响应 @objc func handlePanGesture(_ recognizer: UIPanGestureRecognizer) { switch(recognizer.state) { // 刚刚开始滑动 case .began: // 判断拖动方向 let dragFromLeftToRight = (recognizer.velocity(in: view).x > 0) // 如果刚刚开始滑动的时候还处于主页面,从左向右滑动加入侧面菜单 if (currentState == .Collapsed && dragFromLeftToRight) { currentState = .Expanding addMenuViewController() } // 如果是正在滑动,则偏移主视图的坐标实现跟随手指位置移动 case .changed: let screenWidth = view.bounds.size.width var centerX = recognizer.view!.center.x + recognizer.translation(in: view).x //页面滑到最左侧的话就不许要继续往左移动 if centerX < screenWidth/2 { centerX = screenWidth/2 } // 计算缩放比例 var proportion:CGFloat = (centerX - screenWidth/2) / (view.bounds.size.width - menuViewExpandedOffset) proportion = 1 - (1 - minProportion) * proportion // 执行视差特效 blackCover?.alpha = (proportion - minProportion) / (1 - minProportion) //主页面滑到最左侧的话就不许要继续往左移动 recognizer.view!.center.x = centerX recognizer.setTranslation(.zero, in: view) //缩放主页面 recognizer.view!.transform = CGAffineTransform.identity .scaledBy(x: proportion, y: proportion) // 如果滑动结束 case .ended: //根据页面滑动是否过半,判断后面是自动展开还是收缩 let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width animateMainView(shouldExpand: hasMovedhanHalfway) default: break } } //单击手势响应 @objc func handleTapGesture() { //如果菜单是展开的点击主页部分则会收起 if currentState == .Expanded { animateMainView(shouldExpand: false) } } // 添加菜单页 func addMenuViewController() { if (menuViewController == nil) { menuViewController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "menuView") as? MenuViewController // 插入当前视图并置顶 view.insertSubview(menuViewController!.view, at: 0) // 建立父子关系 addChildViewController(menuViewController!) menuViewController!.didMove(toParentViewController: self) // 在侧滑菜单之上增加黑色遮罩层,目的是实现视差特效 blackCover = UIView(frame: self.view.frame.offsetBy(dx: 0, dy: 0)) blackCover!.backgroundColor = UIColor.black self.view.insertSubview(blackCover!, belowSubview: mainNavigationController.view) } } //主页自动展开、收起动画 func animateMainView(shouldExpand: Bool) { // 如果是用来展开 if (shouldExpand) { // 更新当前状态 currentState = .Expanded // 动画 let mainPosition = view.bounds.size.width * (1+minProportion/2) - menuViewExpandedOffset doTheAnimate(mainPosition: mainPosition, mainProportion: minProportion, blackCoverAlpha: 0) } // 如果是用于隐藏 else { // 动画 doTheAnimate(mainPosition: view.bounds.size.width/2, mainProportion: 1, blackCoverAlpha: 1) { finished in // 动画结束之后更新状态 self.currentState = .Collapsed // 移除左侧视图 self.menuViewController?.view.removeFromSuperview() // 释放内存 self.menuViewController = nil; // 移除黑色遮罩层 self.blackCover?.removeFromSuperview() // 释放内存 self.blackCover = nil; } } } //主页移动动画、黑色遮罩层动画 func doTheAnimate(mainPosition: CGFloat, mainProportion: CGFloat, blackCoverAlpha: CGFloat, completion: ((Bool) -> Void)! = nil) { //usingSpringWithDamping:1.0表示没有弹簧震动动画 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: .curveEaseInOut, animations: { self.mainNavigationController.view.center.x = mainPosition self.blackCover?.alpha = blackCoverAlpha // 缩放主页面 self.mainNavigationController.view.transform = CGAffineTransform.identity.scaledBy(x: mainProportion, y: mainProportion) }, completion: completion) } //给主页面边缘添加、取消阴影 func showShadowForMainViewController(shouldShowShadow: Bool) { if (shouldShowShadow) { mainNavigationController.view.layer.shadowOpacity = 0.8 } else { mainNavigationController.view.layer.shadowOpacity = 0.0 } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } } // 菜单状态枚举 enum MenuState { case Collapsed // 未显示(收起) case Expanding // 展开中 case Expanded // 展开 }源码下载:hangge_1035.zip
2,侧滑展开时,菜单页逐渐放大
(1)菜单页的背景是透明的,背景图(大海背景)是添加在容器页(ViewController)里面的。
(2)菜单页的移动缩放原理和上面一样,也是根据滑动时,主页面的位置实时计算出菜单页的尺寸和位置。
(2)菜单页的移动缩放原理和上面一样,也是根据滑动时,主页面的位置实时计算出菜单页的尺寸和位置。
(3)由于侧滑菜单打开时是从屏幕外滑入的,新增属性 menuViewStartOffset 表示它的起始位置(超出屏幕多少距离)。
ViewController.swift 代码如下(高亮处为修改过的地方):
ViewController.swift 代码如下(高亮处为修改过的地方):
import UIKit class ViewController: UIViewController { // 主页导航控制器 var mainNavigationController:UINavigationController! // 主页面控制器 var mainViewController:MainViewController! // 菜单页控制器 var menuViewController:MenuViewController? // 菜单页当前状态 var currentState = MenuState.Collapsed { didSet { //菜单展开的时候,给主页面边缘添加阴影 let shouldShowShadow = currentState != .Collapsed showShadowForMainViewController(shouldShowShadow: shouldShowShadow) } } // 菜单打开后主页在屏幕右侧露出部分的宽度 let menuViewExpandedOffset: CGFloat = 60 // 侧滑开始时,菜单视图起始的偏移量 let menuViewStartOffset: CGFloat = 70 // 侧滑菜单黑色半透明遮罩层 var blackCover: UIView? // 最小缩放比例 let minProportion: CGFloat = 0.77 override func viewDidLoad() { super.viewDidLoad() //状态栏文字改成白色 UIApplication.shared.statusBarStyle = .lightContent; // 给根容器设置背景 let imageView = UIImageView(image: UIImage(named: "back")) imageView.frame = UIScreen.main.bounds self.view.addSubview(imageView) //初始化主视图 mainNavigationController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "mainNavigaiton") as! UINavigationController view.addSubview(mainNavigationController.view) //指定Navigation Bar左侧按钮的事件 mainViewController = mainNavigationController.viewControllers.first as! MainViewController mainViewController.navigationItem.leftBarButtonItem?.action = #selector(showMenu) //添加拖动手势 let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) mainNavigationController.view.addGestureRecognizer(panGestureRecognizer) //单击收起菜单手势 let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture)) mainNavigationController.view.addGestureRecognizer(tapGestureRecognizer) } //导航栏左侧按钮事件响应 @objc func showMenu() { //如果菜单是展开的则会收起,否则就展开 if currentState == .Expanded { animateMainView(shouldExpand: false) }else { addMenuViewController() animateMainView(shouldExpand: true) } } //拖动手势响应 @objc func handlePanGesture(_ recognizer: UIPanGestureRecognizer) { switch(recognizer.state) { // 刚刚开始滑动 case .began: // 判断拖动方向 let dragFromLeftToRight = (recognizer.velocity(in: view).x > 0) // 如果刚刚开始滑动的时候还处于主页面,从左向右滑动加入侧面菜单 if (currentState == .Collapsed && dragFromLeftToRight) { currentState = .Expanding addMenuViewController() } // 如果是正在滑动,则偏移主视图的坐标实现跟随手指位置移动 case .changed: let screenWidth = view.bounds.size.width var centerX = recognizer.view!.center.x + recognizer.translation(in: view).x //页面滑到最左侧的话就不许要继续往左移动 if centerX < screenWidth/2 { centerX = screenWidth/2 } // 计算缩放比例 let percent:CGFloat = (centerX - screenWidth/2) / (view.bounds.size.width - menuViewExpandedOffset) var proportion:CGFloat = (centerX - screenWidth/2) / (view.bounds.size.width - menuViewExpandedOffset) proportion = 1 - (1 - minProportion) * proportion // 执行视差特效 blackCover?.alpha = (proportion - minProportion) / (1 - minProportion) //主页面滑到最左侧的话就不许要继续往左移动 recognizer.view!.center.x = centerX recognizer.setTranslation(.zero, in: view) //缩放主页面 recognizer.view!.transform = CGAffineTransform.identity .scaledBy(x: proportion, y: proportion) //菜单视图移动 menuViewController?.view.center.x = screenWidth/2 - menuViewStartOffset * (1 - percent) //菜单视图缩放 let menuProportion = (1 + minProportion) - proportion menuViewController?.view.transform = CGAffineTransform.identity .scaledBy(x: menuProportion, y: menuProportion) // 如果滑动结束 case .ended: //根据页面滑动是否过半,判断后面是自动展开还是收缩 let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width animateMainView(shouldExpand: hasMovedhanHalfway) default: break } } //单击手势响应 @objc func handleTapGesture() { //如果菜单是展开的点击主页部分则会收起 if currentState == .Expanded { animateMainView(shouldExpand: false) } } // 添加菜单页 func addMenuViewController() { if (menuViewController == nil) { menuViewController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "menuView") as? MenuViewController //菜单页先缩小 menuViewController!.view.center.x = view.bounds.size.width/2 * (1-(1-minProportion)/2) - menuViewStartOffset menuViewController!.view.transform = CGAffineTransform.identity .scaledBy(x: minProportion, y: minProportion) // 插入当前视图 view.insertSubview(menuViewController!.view, belowSubview: mainNavigationController.view) // 建立父子关系 addChildViewController(menuViewController!) menuViewController!.didMove(toParentViewController: self) // 在侧滑菜单之上增加黑色遮罩层,目的是实现视差特效 blackCover = UIView(frame: self.view.frame.offsetBy(dx: 0, dy: 0)) blackCover!.backgroundColor = UIColor.black self.view.insertSubview(blackCover!, belowSubview: mainNavigationController.view) } } //主页自动展开、收起动画 func animateMainView(shouldExpand: Bool) { // 如果是用来展开 if (shouldExpand) { // 更新当前状态 currentState = .Expanded // 动画 let mainPosition = view.bounds.size.width * (1+minProportion/2) - menuViewExpandedOffset doTheAnimate(mainPosition: mainPosition, mainProportion: minProportion, menuPosition: view.bounds.size.width/2, menuProportion: 1, blackCoverAlpha: 0) } // 如果是用于隐藏 else { // 动画 let menuPosition = view.bounds.size.width/2 * (1-(1-minProportion)/2) - menuViewStartOffset doTheAnimate(mainPosition: view.bounds.size.width/2, mainProportion: 1, menuPosition: menuPosition, menuProportion: minProportion, blackCoverAlpha: 1) { finished in // 动画结束之后更新状态 self.currentState = .Collapsed // 移除左侧视图 self.menuViewController?.view.removeFromSuperview() // 释放内存 self.menuViewController = nil; // 移除黑色遮罩层 self.blackCover?.removeFromSuperview() // 释放内存 self.blackCover = nil; } } } //主页移动动画、黑色遮罩层动画 func doTheAnimate(mainPosition: CGFloat, mainProportion: CGFloat, menuPosition: CGFloat, menuProportion: CGFloat, blackCoverAlpha: CGFloat, completion: ((Bool) -> Void)! = nil) { //usingSpringWithDamping:1.0表示没有弹簧震动动画 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: .curveEaseInOut, animations: { self.mainNavigationController.view.center.x = mainPosition self.blackCover?.alpha = blackCoverAlpha // 缩放主页面 self.mainNavigationController.view.transform = CGAffineTransform.identity.scaledBy(x: mainProportion, y: mainProportion) // 菜单页移动 self.menuViewController?.view.center.x = menuPosition // 菜单页缩放 self.menuViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: menuProportion, y: menuProportion) }, completion: completion) } //给主页面边缘添加、取消阴影 func showShadowForMainViewController(shouldShowShadow: Bool) { if (shouldShowShadow) { mainNavigationController.view.layer.shadowOpacity = 0.8 } else { mainNavigationController.view.layer.shadowOpacity = 0.0 } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } } // 菜单状态枚举 enum MenuState { case Collapsed // 未显示(收起) case Expanding // 展开中 case Expanded // 展开 }源码下载:hangge_1035.zip
全部评论(7)
代码1似乎有问题看了很久不对,照着代码2写 正常
站长回复:代码1我试了下没问题啊?你那边是什么现象。
站长 ,能给一份swift4.0的版本吗?我自己改完以后发现滑动就会报错
站长回复:文章代码现已更新,你可以再看下。
站长,我MainViewController放了个按钮push到下一个界面,点击导航栏返回按钮回来不到了
站长回复:只要自定义一下返回按钮,并添加相关的返回事件即可。具体参考我之前的文章:Swift - 修改导航栏“返回”按钮文字,图标
站长,我做个登录界面,登录完之后跳转到这个有侧栏的界面,但是传值的时候出现:Presenting view controllers on detached view controllers is discouraged 。这怎么回事
站长回复:提供的信息太少,不太确定是什么问题。
站长,我在MainViewController里面放了一个按钮,push到下一个视图控制器(OneViewController),在OneViewController这个视图控制器里面我还是可以进行侧滑,我想让它在OneViewController这个视图控制器里面不能侧滑该怎么办?
站长回复:你可以在拖动手势响应中加个判断,如果不是根视图则不执行侧滑。
当我用代码实现了相应的效果后,发现可能是因为父子关系的问题,出现了一些莫名其妙的问题,比如连TableView 都不能点击了,你有什么方法吗?
站长回复:不知道你的代码是什么样的,所以这个只能你自己找找看,我也帮不上什么忙了。
关注站长 学到了太多的东西
站长回复:欢迎常来看看