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
官方教程
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
,获益匪浅。