Swift侧滑菜单实现仿QQ,菜单带缩放效果

作者:袖梨 2022-11-14

前面我写了一篇文章介绍如何实现侧滑菜单:Swift - 侧滑菜单的实现(样例1:主页向右滑动,露出下方菜单页)

其实现方式是,通过手势拖动主页面移动,从而露出下面的菜单页(其实后面的菜单页是固定不动的)。

下面演示另一种样式的实现(模仿手机QQ的侧滑菜单),主页面滑动停靠的过程中会逐渐缩小,同时菜单页也会逐渐移动放大,浮现出来。

(注:本文样例是基于前面文章的demo修改的,如果没阅读前文的话可以先去看下。为便于理解,下面将效果分两步实现。)

原文:Swift - 侧滑菜单的功能实现(样例2:仿QQ,菜单带缩放效果)

1,主页停靠侧边时尺寸逐渐缩小

(1)定义了新属性 minProportion,表示停靠时的缩小比例。在滑动时,再根据页面的位置实时计算出当前的缩放比例。
(2)在主页面与菜单页之间添加了个黑色遮罩层(blackCover), 初始化时是不透明的。随着菜单的展开透明度逐渐变为0。这样侧滑菜单有逐渐显示出来的效果。
原文:Swift - 侧滑菜单的功能实现(样例2:仿QQ,菜单带缩放效果) 原文:Swift - 侧滑菜单的功能实现(样例2:仿QQ,菜单带缩放效果) 原文:Swift - 侧滑菜单的功能实现(样例2:仿QQ,菜单带缩放效果)

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

// 菜单打开后主页在屏幕右侧露出部分的宽度
let menuViewExpandedOffset: CGFloat = 60

// 侧滑菜单黑色半透明遮罩层
var blackCover: UIView?

// 最小缩放比例
let minProportion: CGFloat = 0.77

override func viewDidLoad() {
super.viewDidLoad()

//状态栏文字改成白色
UIApplication.sharedApplication().statusBarStyle = .LightContent;

// 给根容器设置背景
let imageView = UIImageView(image: UIImage(named: "back"))
imageView.frame = UIScreen.mainScreen().bounds
self.view.addSubview(imageView)

//初始化主视图
mainNavigationController = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewControllerWithIdentifier("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: "handlePanGesture:")
mainNavigationController.view.addGestureRecognizer(panGestureRecognizer)

//单击收起菜单手势
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
action: "handlePanGesture")
mainNavigationController.view.addGestureRecognizer(tapGestureRecognizer)
}

//导航栏左侧按钮事件响应
func showMenu() {
//如果菜单是展开的则会收起,否则就展开
if currentState == .Expanded {
animateMainView(false)
}else {
addMenuViewController()
animateMainView(true)
}
}

//拖动手势响应
func handlePanGesture(recognizer: UIPanGestureRecognizer) {

switch(recognizer.state) {
// 刚刚开始滑动
case .Began:
// 判断拖动方向
let dragFromLeftToRight = (recognizer.velocityInView(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.translationInView(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(CGPointZero, inView: view)
//缩放主页面
recognizer.view!.transform =
CGAffineTransformScale(CGAffineTransformIdentity, proportion, proportion)

// 如果滑动结束
case .Ended:
//根据页面滑动是否过半,判断后面是自动展开还是收缩
let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width
animateMainView(hasMovedhanHalfway)
default:
break
}
}

//单击手势响应
func handlePanGesture() {
//如果菜单是展开的点击主页部分则会收起
if currentState == .Expanded {
animateMainView(false)
}
}

// 添加菜单页
func addMenuViewController() {
if (menuViewController == nil) {
menuViewController = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewControllerWithIdentifier("menuView") as? MenuViewController

// 插入当前视图并置顶
view.insertSubview(menuViewController!.view,
belowSubview: mainNavigationController.view)

// 建立父子关系
addChildViewController(menuViewController!)
menuViewController!.didMoveToParentViewController(self)

// 在侧滑菜单之上增加黑色遮罩层,目的是实现视差特效
blackCover = UIView(frame: CGRectOffset(self.view.frame, 0, 0))
blackCover!.backgroundColor = UIColor.blackColor()
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, mainProportion: minProportion, blackCoverAlpha: 0)
}
// 如果是用于隐藏
else {
// 动画
doTheAnimate(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.animateWithDuration(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 =
CGAffineTransformScale(CGAffineTransformIdentity,
mainProportion, 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)菜单页的移动缩放原理和上面一样,也是根据滑动时,主页面的位置实时计算出菜单页的尺寸和位置。
(3)由于侧滑菜单打开时是从屏幕外滑入的,新增属性 menuViewStartOffset 表示它的起始位置(超出屏幕多少距离)。

原文:Swift - 侧滑菜单的功能实现(样例2:仿QQ,菜单带缩放效果) 原文:Swift - 侧滑菜单的功能实现(样例2:仿QQ,菜单带缩放效果) 原文:Swift - 侧滑菜单的功能实现(样例2:仿QQ,菜单带缩放效果)


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

// 菜单打开后主页在屏幕右侧露出部分的宽度
let menuViewExpandedOffset: CGFloat = 60

// 侧滑开始时,菜单视图起始的偏移量
let menuViewStartOffset: CGFloat = 70

// 侧滑菜单黑色半透明遮罩层
var blackCover: UIView?

// 最小缩放比例
let minProportion: CGFloat = 0.77

override func viewDidLoad() {
super.viewDidLoad()

//状态栏文字改成白色
UIApplication.sharedApplication().statusBarStyle = .LightContent;

// 给根容器设置背景
let imageView = UIImageView(image: UIImage(named: "back"))
imageView.frame = UIScreen.mainScreen().bounds
self.view.addSubview(imageView)

//初始化主视图
mainNavigationController = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewControllerWithIdentifier("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: "handlePanGesture:")
mainNavigationController.view.addGestureRecognizer(panGestureRecognizer)

//单击收起菜单手势
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
action: "handlePanGesture")
mainNavigationController.view.addGestureRecognizer(tapGestureRecognizer)
}

//导航栏左侧按钮事件响应
func showMenu() {
//如果菜单是展开的则会收起,否则就展开
if currentState == .Expanded {
animateMainView(false)
}else {
addMenuViewController()
animateMainView(true)
}
}

//拖动手势响应
func handlePanGesture(recognizer: UIPanGestureRecognizer) {

switch(recognizer.state) {
// 刚刚开始滑动
case .Began:
// 判断拖动方向
let dragFromLeftToRight = (recognizer.velocityInView(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.translationInView(view).x
//页面滑到最左侧的话就不许要继续往左移动
if centerX < screenWidth/2 { centerX = screenWidth/2 }

// 计算缩放比例
let percent:CGFloat = (centerX - screenWidth/2) /
(view.bounds.size.width - menuViewExpandedOffset)
let proportion = 1 - (1 - minProportion) * percent

// 执行视差特效
blackCover?.alpha = (proportion - minProportion) / (1 - minProportion)

recognizer.view!.center.x = centerX
recognizer.setTranslation(CGPointZero, inView: view)
//缩放主页面
recognizer.view!.transform =
CGAffineTransformScale(CGAffineTransformIdentity, proportion, proportion)

//菜单视图移动
menuViewController?.view.center.x = screenWidth/2 -
menuViewStartOffset * (1 - percent)
//菜单视图缩放
let menuProportion = (1 + minProportion) - proportion
menuViewController?.view.transform = CGAffineTransformScale(
CGAffineTransformIdentity, menuProportion, menuProportion)

// 如果滑动结束
case .Ended:
//根据页面滑动是否过半,判断后面是自动展开还是收缩
let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width
animateMainView(hasMovedhanHalfway)
default:
break
}
}

//单击手势响应
func handlePanGesture() {
//如果菜单是展开的点击主页部分则会收起
if currentState == .Expanded {
animateMainView(false)
}
}

// 添加菜单页
func addMenuViewController() {
if (menuViewController == nil) {
menuViewController = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewControllerWithIdentifier("menuView") as? MenuViewController

menuViewController!.view.center.x = view.bounds.size.width/2
* (1-(1-minProportion)/2) - menuViewStartOffset
menuViewController!.view.transform = CGAffineTransformScale(
CGAffineTransformIdentity, minProportion, minProportion)

// 插入当前视图并置顶
view.insertSubview(menuViewController!.view,
belowSubview: mainNavigationController.view)

// 建立父子关系
addChildViewController(menuViewController!)
menuViewController!.didMoveToParentViewController(self)

// 在侧滑菜单之上增加黑色遮罩层,目的是实现视差特效
blackCover = UIView(frame: CGRectOffset(self.view.frame, 0, 0))
blackCover!.backgroundColor = UIColor.blackColor()
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, 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(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.animateWithDuration(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 =
CGAffineTransformScale(CGAffineTransformIdentity,
mainProportion, mainProportion)

// 菜单页移动
self.menuViewController?.view.center.x = menuPosition
// 菜单页缩放
self.menuViewController?.view.transform =
CGAffineTransformScale(CGAffineTransformIdentity,
menuProportion, 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 // 展开
}

相关文章

精彩推荐