1. 前言
前段時間做 Android 開發時使用系統控件 DrawerLayout
輕松實現了左右側欄,最近做 iOS 開發時恰好需要用到,本想著跟 Android 一樣會有系統級的控件提供,誰料 Apple 并不提倡使用「側欄」的交互模式,未提供相關控件。由于項目需要用到左右側欄,擺在面前的只有兩個選擇:使用第三方開源庫,或自己造輪子。
首先,我們先看看使用 Android 系統控件實現的側欄效果:
Figure 1.1 : DrawerLayout
實現效果
從圖1.1中可以看出,DrawerLayout
大致實現了下述幾種交互:
- 左側欄未彈出的情況下:
- 點擊左上角 Menu 按鈕,左側欄彈出,同時添加黑色半透明 View 對主界面進行遮蓋
- 手指從屏幕左方邊緣向右滑動,左側欄相應右移,黑色背景 View 的透明度隨右移距離增加而變深;若右劃距離不超過側欄寬度一半,手指松開后側欄收回;若右劃距離超過側欄一半,則側欄彈出 。( 此功能在模擬器上未能觸發,需在真機上重現 )
- 左側欄已彈出的情況下:
- 選中左側欄中的某個 Item ,左側欄收回
- 點擊黑色半透明 View,左側欄收回
- 手指向左滑動,左側欄相應進行左劃,黑色背景 View 的透明度隨左移距離增加而變淺;若左劃距離不超過側欄寬度一半,手指松開后側欄回到原位置;若左劃距離超過側欄寬度一半,左側欄收回
在下載嘗試了幾款 iOS 側欄開源庫后,發現基本上都不能滿足 DrawerLayout
的相同交互,而且大部分都是使用純代碼實現,與公司 iOS 項目遵循的 storyboard 優先原則有所違背,最終決定造個輪子,讓 iOS 及 Android app 的 UX/UI 盡可能一致。
1.1 開發環境
- macOS Sierra : 10.12.1 (16B2555)
- Xcode : 8.1 (8B62)
- Objective-C
- Android Studio : 2.2.2
1.2 工具
- Keynote
- GIPHY Capture
- MWeb
- IconJar
- Sketch
1.3 完整工程
Talk is cheap, show me the code!
DrawerLayoutDemo
1.4 最終效果
Figure 1.2 : iOS 頁面結構
Figure 1.3 : iOS DrawerLayoutDemo
實現效果
2. 實現過程
2.1 思路
Figure 2.1 : DrawerLayout
實現方式
從 Figure 2.1 中可以看出,在 Activity
( 相當于 iOS 的ViewController ) 中,包含了三個 RelativeLayout
,分別代表左側欄、主頁面和右側欄,其中左側欄和右側欄的默認起始坐標均處于屏幕可視范圍外,所以對用戶來說,左右側欄在彈出時才加載顯示,但事實上在 Activity
加載時,左右側欄已經加載了,只是顯示位置在屏幕范圍外而已。
同理,在 iOS 中,我們可以在 ViewController
中添加三個 Container View
,分別對應左側欄、主頁面和右側欄,并實現
- 在
ViewController
加載時,將左側欄和右側欄的frame
均設置在屏幕范圍外來達到側欄「隱藏」效果 - 在側欄彈出和收回時,增加頁面平移動畫,實現彈出和收回的動畫效果
- 在手指滑動時,捕捉滑動手勢,實現頁面隨手指移動的動畫效果
- 在
ViewController
中,增加backgroundView
,使其層級處于主頁面view
之上,側欄view
之下,實現黑色半透明背景
2.2 Container View
介紹
Container view controllers are a way to combine the content from multiple view controllers into a single user interface. Container view controllers are most often used to facilitate navigation and to create new user interface types based on existing content.
Examples of container view controllers in UIKit include UINavigationController, UITabBarController, and UISplitViewController, all of which facilitate navigation between different parts of your user interface.
根據蘋果的官方介紹,Container View
主要用于將多個頁面的內容整合到一個頁面,同時,每個 Container View
均對應一個獨立的 View Controller
,將每個 Container View
的功能解耦,避免主 View Controller
過于臃腫。這樣看來,Container View
用于實現 DrawerLayout
最合適不過。
2.3 代碼框架搭建
Figure 2.2 : 代碼框架
2.3.1 Storyboard
搭建
根據圖2.2,我們在 Main.storyboard
中
- 創建一個
ContainersViewController
,作為RootViewController
-
ContainersViewController
里面放置三個與屏幕同樣大小的Container View
,分別對應 LeftMenuViewController
MainViewController
RightMenuViewController
- 新建一個與屏幕同樣大小的
View
,作為backgroundView
,設置Background = Black Color; Alpha = 0.5
考慮到使用系統內建的 Navigation Bar
,以及 MainViewController
里面通常都會有一些 push navigation
的頁面跳轉需求,故通過 Editor
-> Embed in
-> Navigation Controller
給 MainViewController
增加一個 Navigation Controller
作為 parent controller
,同理,可使用相同方式添加 Tab Bar Controller
。
2.3.2 代碼目錄搭建
對應 Main.storyboard
中的頁面,新建
- ContainerViewController.h & .m
- MainViewController.h & .m
- LeftMenuViewController.h & .m
- RightMenuViewController.h & .m
做好必要的 AutoLayout
設置,以及 ViewController
映射后,我們在 ContainerViewController.m
的 viewDidLayoutSubviews
中增加少量代碼,編譯運行看看頁面架構是否符合需求。
@interface ContainerViewController ()
@property (weak, nonatomic) IBOutlet UIView *leftMenuContainerView;
@property (weak, nonatomic) IBOutlet UIView *rightMenuContainerView;
@property (weak, nonatomic) IBOutlet UIView *mainContainerView;
@property (weak, nonatomic) IBOutlet UIView *backgroundView;
@end
...
@implementation ContainerViewController
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
/* 測試代碼,待刪*/
//獲取 ContainersView 的 frame
CGRect windowFrame = self.view.frame;
//將 LeftMenuContainerView 的 x 軸起始坐標左移出屏幕左側
[self.leftMenuContainerView setFrame:CGRectMake (100.0 - windowFrame.size.width, 0, self.leftMenuContainerView.frame.size.width, self.leftMenuContainerView.frame.size.height)];
//將 RightMenuContainerView 的 x 軸起始坐標右移到屏幕右側左方
[self.rightMenuContainerView setFrame:CGRectMake (windowFrame.size.width - 100.0, 0, self.rightMenuContainerView.frame.size.width, self.rightMenuContainerView.frame.size.height)];
//將 backGroundView 顏色設置為黑色,透明度設置為 50%
[self.backgroundView setBackgroundColor:[UIColor blackColor]];
[self.backgroundView setAlpha:0.5];
/* 測試代碼,待刪*/
}
@end
運行效果如下:
Figure 2.3 : 代碼框架運行效果
2.4 ViewController
代碼實現
對章節 1 中描述的交互需求進行分解,我們可以得到每個 ViewController
需要實現的功能
ContainersViewController
- 頁面初始化時設置
LeftMenuContainerView
及RightMenuContainerView
的初始位置為屏幕兩側;backgroundView
的默認狀態為alpha = 0.0, hidden = YES
-
LeftMenuContainerView
彈出及收回 -
RightMenuContainerView
彈出及收回 - 左側屏幕邊緣滑入手勢捕捉,
LeftMenuContainerView
隨手勢在 x 軸上平移,松手時判斷需彈出或收回;backgroundVIew
透明度隨手勢漸變 - 右側屏幕邊緣滑入手勢捕捉,
RightMenuContainerView
隨手勢在 x 軸上平移,松手時判斷需彈出或收回;backgroundVIew
透明度隨手勢漸變 - 當
LeftMenuContainerView
已彈出時,屏蔽右側屏幕邊緣滑入手勢捕捉,收回后重新開啟;同理,RightMenuContainerView
彈出后,屏蔽左側屏幕邊緣滑入手勢捕捉,收回后重新開啟 MainViewController
- 點擊
Navigation Bar
上的LeftMenu
按鈕后,「通知」ContainersViewController
彈出左側欄 - 點擊
Navigation Bar
上的RightMenu
按鈕后,「通知」ContainersViewController
彈出右側欄 LeftMenuViewController
-
View
的右側設置一個全透明的transparentView
,用于「透視」ContainersViewController
上的backgroundView
- 點擊右側的
transparentView
,「通知」ContainersViewController
收回左側欄 - 點擊
LeftMenuViewController
上的「項目」,「通知」ContainersViewController
收回左側欄 - 捕捉滑動手勢,
LeftMenuViewController
隨手勢在 x 軸上平移,松手時判斷需恢復到彈出狀態,還是通知ContainersViewController
收回左側欄 RightMenuViewController
-
View
的左側設置一個全透明的transparentView
,用于「透視」ContainersViewController
上的backgroundView
- 點擊左側的
transparentView
,「通知」ContainersViewController
收回右側欄 - 點擊
RightMenuViewController
上的「項目」,「通知」ContainersViewController
收回右側欄 - 捕捉滑動手勢,
RightMenuViewController
隨手勢在 x 軸上平移,松手時判斷需恢復到彈出狀態,還是通知ContainersViewController
收回右側欄
講到這里,相信大家都可以明顯地感受到
Container View
的好處。通過使用Container View
對ViewController
的功能進行解耦,在避免產生單個臃腫ViewController
的同時,又能很好地實現復雜的單頁面功能;同時對多尺寸、橫豎屏的適配也更靈活方便,推薦大家多使用。
這里插播一下,上面功能分析提到的「通知」,有很多種實現方式,包括但不限于 NSNotificationCenter
,Delegate
,函數調用
。本教程的「通知」使用的是 函數調用
的方式。
「萬事俱備,只欠東風」,功能分解完畢,接下來只需逐個擊破!
2.5 ContainersViewController
首先,記得將章節 2.3.2 中的測試代碼刪除。
- 頁面初始化時設置
LeftMenuContainerView
及RightMenuContainerView
的初始位置為屏幕兩側;backgroundView
的默認狀態為alpha = 0.0, hidden = YES
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self viewItemInitial];
}
//... other functions ...
- (void)viewItemInitial {
//設置 backgroundView 初始隱藏狀態及透明度
[self.backgroundView setAlpha:0.0];
[self.backgroundView setHidden:YES];
CGRect windowFrame = self.view.frame;
CGFloat startX = 0.0;
//設置 leftMenuContainerView 初始位置
startX = -windowFrame.size.width;
[self.leftMenuContainerView setFrame:CGRectMake(startX,
self.leftMenuContainerView.frame.origin.y,
self.leftMenuContainerView.frame.size.width,
self.leftMenuContainerView.frame.size.height)];
//設置 rightMenuContainerView 初始位置
startX = windowFrame.size.width;
[self.rightMenuContainerView setFrame:CGRectMake(startX,
self.rightMenuContainerView.frame.origin.y,
self.rightMenuContainerView.frame.size.width,
self.rightMenuContainerView.frame.size.height)];
}
-
backgroundView
漸隱及漸顯動畫;需暴露接口供其他ViewController
使用
- (void) showBackgroundView {
[self.backgroundView setHidden:NO];
[UIView animateWithDuration:self.bgViewAnimationDuration animations:^{
[self.backgroundView setAlpha:self.bgViewFinalAlpha];
}];
}
- (void) dismissBackgroundView {
[UIView animateWithDuration:self.bgViewAnimationDuration animations:^{
[self.backgroundView setAlpha:0.0];
} completion:^(BOOL finished) {
[self.backgroundView setHidden:YES];
}];
}
-
LeftMenuContainerView
彈出及收回;RightMenuContainerView
彈出及收回動畫;需暴露接口供其他ViewController
使用
#pragma public function
- (void)showLeftMenu {
[self showMenu:self.leftMenuContainerView menuType:MENU_TYPE_LEFT_MENU];
}
- (void)dismissLeftMenu {
[self dismissMenu:self.leftMenuContainerView menuType:MENU_TYPE_LEFT_MENU];
}
- (void)showRightMenu {
[self showMenu:self.rightMenuContainerView menuType:MENU_TYPE_RIGHT_MENU];
}
- (void)dismissRightMenu {
[self dismissMenu:self.rightMenuContainerView menuType:MENU_TYPE_RIGHT_MENU];
}
#pragma private function
- (void)showMenu:(UIView *) view menuType:(MENU_TYPE) menuType {
CGFloat finalX = 0.0;
if (menuType == MENU_TYPE_UNKNOWN) {
return;
}
[UIView animateWithDuration:self.menuAnimationDuration animations:^{
[view setFrame:CGRectMake(finalX,
view.frame.origin.y,
view.frame.size.width,
view.frame.size.height)];
}];
}
- (void)dismissMenu:(UIView *) view menuType:(MENU_TYPE) menuType {
CGRect windowFrame = self.view.frame;
CGFloat finalX = 0.0;
if (menuType == MENU_TYPE_LEFT_MENU) {
finalX = 0 - windowFrame.size.width;
}
else if (menuType == MENU_TYPE_RIGHT_MENU) {
finalX = windowFrame.size.width;
}
else {
return;
}
[UIView animateWithDuration:self.menuAnimationDuration animations:^{
[view setFrame:CGRectMake(finalX,
view.frame.origin.y,
view.frame.size.width,
view.frame.size.height)];
}];
}
- 雙側屏幕邊緣滑入手勢捕捉,
LeftMenuContainerView
或RightMenuContainerView
隨手勢在 x 軸上平移,松手時判斷需彈出或收回;backgroundVIew
透明度隨手勢漸變
- (void)gestureRecognizerInitial {
self.screenEdgePanGestureRecognizerLeft = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgePanGestureRecognizerHandler:)];
[self.screenEdgePanGestureRecognizerLeft setEdges:UIRectEdgeLeft];
self.screenEdgePanGestureRecognizerRight = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgePanGestureRecognizerHandler:)];
[self.screenEdgePanGestureRecognizerRight setEdges:UIRectEdgeRight];
[self.view addGestureRecognizer:self.screenEdgePanGestureRecognizerLeft];
[self.view addGestureRecognizer:self.screenEdgePanGestureRecognizerRight];
}
- (void)screenEdgePanGestureRecognizerHandler:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
if ((gestureRecognizer.edges == UIRectEdgeLeft) || (gestureRecognizer.edges == UIRectEdgeRight)) {
//獲取手指相對于屏幕的坐標
CGPoint gesturePoint = [gestureRecognizer locationInView:self.view];
CGFloat windowWidth = self.view.frame.size.width;
//滑動開始,保存初始坐標
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
self.panGestureStartPointX = gesturePoint.x;
[self.backgroundView setHidden:NO];
}
//滑動過程中,動態改變 menuView 位置及 backgroundView 透明度
else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
CGFloat deltaX = 0;
//計算手指相對起始位置的滑動距離
deltaX = (gestureRecognizer.edges == UIRectEdgeLeft) ?
(gesturePoint.x - self.panGestureStartPointX) : (self.panGestureStartPointX - gesturePoint.x);
//如果滑動距離是負數,則說明手指滑動方向與側欄彈出反向相反,無需處理
if (deltaX > 0.0) {
CGFloat newPointX = 0.0;
CGFloat newBgAlpha = 0.0;
UIView *menuView = nil;
if (gestureRecognizer.edges == UIRectEdgeLeft) {
newPointX = -windowWidth + deltaX;
newBgAlpha = (newPointX + windowWidth) / windowWidth * self.bgViewFinalAlpha;
menuView = self.leftMenuContainerView;
}
else {
newPointX = windowWidth - deltaX;
newBgAlpha = (windowWidth - newPointX) / windowWidth * self.bgViewFinalAlpha;
menuView = self.rightMenuContainerView;
}
//更新 menuView 顯示位置
[menuView setFrame:CGRectMake(newPointX, menuView.frame.origin.y, menuView.frame.size.width, menuView.frame.size.height)];
//更新 backgroundView 透明度
[self.backgroundView setAlpha:newBgAlpha];
}
}
//滑動結束后,判斷該彈出還是收回 menuView
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//計算 menuView 的最終位移
CGFloat viewOffset = (gestureRecognizer.edges == UIRectEdgeLeft) ?
(self.leftMenuContainerView.frame.origin.x + windowWidth) : (windowWidth - self.rightMenuContainerView.frame.origin.x);
//彈出/收回側欄
if (viewOffset > self.minOffset) {
(gestureRecognizer.edges == UIRectEdgeLeft) ? ([self showLeftMenu]) : ([self showRightMenu]);
[self showBackgroundView];
}
else {
(gestureRecognizer.edges == UIRectEdgeLeft) ? ([self dismissLeftMenu]) : ([self dismissRightMenu]);
[self dismissBackgroundView];
}
}
}
}
- 當
LeftMenuContainerView
已彈出時,屏蔽右側屏幕邊緣滑入手勢捕捉,收回后重新開啟;同理,RightMenuContainerView
彈出后,屏蔽左側屏幕邊緣滑入手勢捕捉,收回后重新開啟
使用 addGestureRecognizer
和 removeGestureRecognizer
,在彈出/收回側欄時對手勢捕捉進行使能/禁止
- (void)enableEdgePanGestureRecognizer {
[self.view addGestureRecognizer:self.screenEdgePanGestureRecognizerLeft];
[self.view addGestureRecognizer:self.screenEdgePanGestureRecognizerRight];
}
- (void)disableEdgePanGestureRecognizer {
[self.view removeGestureRecognizer:self.screenEdgePanGestureRecognizerLeft];
[self.view removeGestureRecognizer:self.screenEdgePanGestureRecognizerRight];
}
- (void)showLeftMenu {
[self showMenu:self.leftMenuContainerView menuType:MENU_TYPE_LEFT_MENU];
[self disableEdgePanGestureRecognizer];
}
- (void)dismissLeftMenu {
[self dismissMenu:self.leftMenuContainerView menuType:MENU_TYPE_LEFT_MENU];
[self enableEdgePanGestureRecognizer];
}
- (void)showRightMenu {
[self showMenu:self.rightMenuContainerView menuType:MENU_TYPE_RIGHT_MENU];
[self disableEdgePanGestureRecognizer];
}
- (void)dismissRightMenu {
[self dismissMenu:self.rightMenuContainerView menuType:MENU_TYPE_RIGHT_MENU];
[self enableEdgePanGestureRecognizer];
}
2.6 MainViewController
MainViewController
只做兩件事情,「通知」ContainersViewController
彈出/收回 LeftMenuContainerView
/RightMenuContainerView
2.6.1 storyboard
實現
添加 LeftMenu
和 RightMenu
兩個 Bar Button Item
到 Navigation Bar
,并將 Button Action
關聯到 MainViewController
中。
Figure 2.4 : MainViewController
頁面
2.6.2 代碼實現
在章節 2.4 中提到,本 demo 中「通知」的方式使用的是函數調用,所以在 MainViewController
中,當用戶點擊 LeftMenu
和 RightMenu
Button時,需要通過調用 ContainersViewController
暴露出來的函數實現左右側欄的顯示。
從 storyboard
中可知, self.parentViewController
獲取到的是 navigationController
,self.parentViewController.parentViewController
獲取到的便是 ContainersViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.containerViewController = (ContainerViewController *) self.parentViewController.parentViewController;
}
- (IBAction)leftMenuButtonAction:(UIBarButtonItem *)sender {
[self.containerViewController showLeftMenu];
[self.containerViewController showBackgroundView];
}
- (IBAction)rightMenuButtonAction:(UIBarButtonItem *)sender {
[self.containerViewController showRightMenu];
[self.containerViewController showBackgroundView];
}
2.6.3 LeftMenuViewController
-
View
的右側設置一個全透明的transparentView
,用于「透視」ContainersViewController
上的backgroundView
Figure 2.5 : LeftMenuViewController
頁面
- 點擊右側的
transparentView
,「通知」ContainersViewController
收回左側欄;點擊LeftMenuViewController
上的「項目」,「通知」ContainersViewController
收回左側欄
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.containerViewController = (ContainerViewController *)self.parentViewController;
[self gestureRecognizerInitial];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)gestureRecognizerInitial {
UITapGestureRecognizer *transparentViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(transparentViewTapHandler:)];
[self.transparentView addGestureRecognizer:transparentViewTapGestureRecognizer];
UITapGestureRecognizer *bookViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(transparentViewTapHandler:)];
[self.booksView addGestureRecognizer:bookViewTapGestureRecognizer];
UITapGestureRecognizer *tagViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(transparentViewTapHandler:)];
[self.tagView addGestureRecognizer:tagViewTapGestureRecognizer];
}
- (void)transparentViewTapHandler:(UITapGestureRecognizer *)gestureRecognizer {
[self.containerViewController dismissLeftMenu];
[self.containerViewController dismissBackgroundView];
}
- 捕捉滑動手勢,
LeftMenuViewController
隨手勢在 x 軸上平移,松手時判斷需恢復到彈出狀態,還是通知ContainersViewController
收回左側欄
- (void)gestureRecognizerInitial {
......
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureRecognizerHandler:)];
[self.view addGestureRecognizer:panGestureRecognizer];
}
- (void)panGestureRecognizerHandler:(UIPanGestureRecognizer *)gestureRecognizer {
//獲取手指相對于屏幕的坐標
CGPoint gesturePoint = [gestureRecognizer locationInView:self.containerViewController.view];
CGFloat windowWidth = self.containerViewController.view.frame.size.width;
//滑動開始,保存初始坐標
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
self.panGestureStartPointX = gesturePoint.x;
}
//滑動過程中,動態改變 menuView 位置及 backgroundView 透明度
else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
CGFloat deltaX = 0;
//計算手指相對起始位置的滑動距離
deltaX = self.panGestureStartPointX - gesturePoint.x;
//如果滑動距離是負數,則說明手指滑動方向與側欄回收方向相反,無需處理
if (deltaX > 0.0) {
CGFloat newPointX = 0.0;
CGFloat newBgAlpha = 0.0;
CGFloat bgViewFinalAlpha = [self.containerViewController getBgViewFinalAlphaValue];
newPointX = -deltaX;
newBgAlpha = (newPointX + windowWidth) / windowWidth * bgViewFinalAlpha;
//更新 menuView 顯示位置
CGRect newFrame = CGRectMake(newPointX, self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
[self.containerViewController modifyMenuViewFrame:newFrame menuType:MENU_TYPE_LEFT_MENU];
//更新 backgroundView 透明度
[self.containerViewController modifyBackgroundViewAlpha:newBgAlpha];
}
}
//滑動結束后,判斷該彈出還是收回 menuView
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
CGFloat minOffset = [self.containerViewController getMinOffset];
//計算 menuView 的最終位移
CGFloat viewOffset = -self.containerViewController.leftMenuContainerView.frame.origin.x;
//彈出/收回側欄
if (viewOffset > minOffset) {
[self.containerViewController dismissLeftMenu];
[self.containerViewController dismissBackgroundView];
}
else {
[self.containerViewController showLeftMenu];
[self.containerViewController showBackgroundView];
}
}
}
2.6.4 RightMenuViewController
實現方式與 LeftMenuViewController 相同,只是在拖拽手勢處理時坐標計算有少許變化。
- (void)panGestureRecognizerHandler:(UIPanGestureRecognizer *)gestureRecognizer {
//獲取手指相對于屏幕的坐標
CGPoint gesturePoint = [gestureRecognizer locationInView:self.containerViewController.view];
CGFloat windowWidth = self.containerViewController.view.frame.size.width;
//滑動開始,保存初始坐標
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
self.panGestureStartPointX = gesturePoint.x;
}
//滑動過程中,動態改變 menuView 位置及 backgroundView 透明度
else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
CGFloat deltaX = 0;
//計算手指相對起始位置的滑動距離
deltaX = gesturePoint.x - self.panGestureStartPointX;
//如果滑動距離是負數,則說明手指滑動方向與側欄回收方向相反,無需處理
if (deltaX > 0.0) {
CGFloat newPointX = 0.0;
CGFloat newBgAlpha = 0.0;
CGFloat bgViewFinalAlpha = [self.containerViewController getBgViewFinalAlphaValue];
newPointX = deltaX;
newBgAlpha = (windowWidth - newPointX) / windowWidth * bgViewFinalAlpha;
//更新 menuView 顯示位置
CGRect newFrame = CGRectMake(newPointX, self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
[self.containerViewController modifyMenuViewFrame:newFrame menuType:MENU_TYPE_RIGHT_MENU];
//更新 backgroundView 透明度
[self.containerViewController modifyBackgroundViewAlpha:newBgAlpha];
}
}
//滑動結束后,判斷該彈出還是收回 menuView
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
CGFloat minOffset = [self.containerViewController getMinOffset];
//計算 menuView 的最終位移
CGFloat viewOffset = self.containerViewController.rightMenuContainerView.frame.origin.x;
//彈出/收回側欄
if (viewOffset > minOffset) {
[self.containerViewController dismissRightMenu];
[self.containerViewController dismissBackgroundView];
}
else {
[self.containerViewController showRightMenu];
[self.containerViewController showBackgroundView];
}
}
}
寫了這么多,終于接近尾聲!
3. 坑!
-
章節 2.6.3 & 2.6.4,為何要在
MenuViewController
中設計transparentView
用于「透視」ContainersViewController
的黑色半透明背景,而不直接將MenuContainerView
的寬度固定為有效內容寬度,而非全屏幕?- 假設
MenuViewController
寬度不是全屏幕,但使用了NavigationController
,在調用pushViewController
后,新頁面寬度將和MenuViewController
一致,不能全屏顯示。所以這個地方的實現邏輯需要根據項目實際需求修改。
- 假設
-
若
MainViewController
中存在Scroll View
,屏幕邊緣滑入不能觸發側欄打開- 在
MainViewController
中調用下述代碼,讓ContainersViewController
的手勢優先級更高
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.containersViewController.screenEdgePanGesture];
- 在
若
MainViewController
,LeftMenuViewController
或RightMenuViewController
存在頁面跳轉,在跳轉后必須禁止ContainersViewController
的UIScreenEdgePanGestureRecognizer
,否則頁面跳轉后仍能通過屏幕邊緣滑入手勢彈出側欄如需要同時使用
TabBarController
,只需在MainViewController
的NavigationController
前添加一個TabBarController
即可
4. 寫在最后
對于雖說側欄只是一個很舊的,甚至不被蘋果提倡的功能,不過通過這次「造」輪子,也算比較深入地了解了 Container View
,獲益匪淺。