前面我寫了一篇文章介紹如何實現側滑菜單: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)
}
}
// 菜單打開後主頁在屏幕右側露出部分的寬度
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 表示它的起始位置(超出屏幕多少距離)。
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 // 展開
}