仿QQ抽屉效果(OC版&swift版)
抽屉效果几乎在每一个APP中都能遇到。那么最为一个iOS开发人员怎么好意思不懂其中的原理呢。如果你愿意花上3分钟来学会,然后自己写一个抽屉,那么请你留下来。在这里我会贴上两份代码(一份OC、一份swift)
留下来.gif这里我们先看一下实现的效果。
QQDRAWER.gif
接下来就简单的说一下具体原理:
首先,self.window.rootViewController就是一个普通的控制器(UIViewController)。设置好根控制器后,另外要再创建两个控制器,一个是左侧的控制器(一般都是用UIViewController),一个是主控制器(UITabBarController)。然后我们按照顺序添加到self.window.rootViewController 上面,一定要注意添加的顺序(先添加左侧控制器,然后再添加主控制器)不然左侧控制器就会在最外面,盖住了主控制器。接着就是打开和抽屉效果的时候这里是通过改变view.tranform来达到效果。
如果文字没有能听懂我说的啥,那我就在用图片展示一下。
上面图片已经说明各个控制器的扮演角色和添加顺序。另外,为了模仿QQ的动画效果,这里还需要注意下左侧控制器的tranform。因为在打开抽屉效果的过程中,我们可以看到,左侧的控制器是从左侧滑下右侧的,所以添加左侧的控制器的时候就要把它放的靠左些。
下面我通过gif图片来展示一下添加的效果,争取让大家在没有敲代码的时候就明白要怎么去敲。
addAction.gif
待我们添加结束以后,当前屏幕上就只能看到绿色的这个视图了,紫色的这个视图实际上是已经偏出界面所以是看不到的。
到这里基本逻辑已经说完,下面就是代码实现
在AppDelegate里传入左侧控制器和主控制器创建抽屉,因为我们要在根控制器(这里称为抽屉控制器)里添加,所以要传过去
<pre>
// 创建左侧控制器
QQLeftTableViewController * leftViewController = [[QQLeftTableViewController alloc]init];
// 创建主控制器
QQMainTabBarController * mainViewController = [[QQMainTabBarController alloc]init];
// 传入左视图和主视图以及抽屉的最大宽度 创建抽屉
QQDrawerViewController *rootViewController = [QQDrawerViewController drawerWithLeftViewController:leftViewController andMainViewController:mainViewController andMaxWidth:300];
</pre>
下面我们看看在这个类方法里做了什么
<pre>
-
(instancetype)drawerWithLeftViewController:(UIViewController *)leftViewController andMainViewController:(UIViewController *)mainViewController andMaxWidth:(CGFloat)maxWidth{
QQDrawerViewController *drawerViewController = [[QQDrawerViewController alloc]init];
drawerViewController.mainViewController = (QQMainTabBarController *)mainViewController;
drawerViewController.leftViewController = (QQLeftTableViewController *)leftViewController;
drawerViewController.drawerMaxWidth = maxWidth;for (UIViewController *childViewController in mainViewController.childViewControllers) {
childViewController.view.backgroundColor = [UIColor whiteColor];
}
// 这里注意,在添加view过后,不要忘记把添加控制器,不然会出现未知错误(多种错误随机出现)
[drawerViewController.view addSubview:leftViewController.view];
[drawerViewController.view addSubview:mainViewController.view];
[drawerViewController addChildViewController:leftViewController];
[drawerViewController addChildViewController:mainViewController];
leftViewController.view.transform = CGAffineTransformMakeTranslation(-maxWidth, 0);
return drawerViewController;
}
</pre>
在创建主控制器(UITabBarController)里我抽取了一个添加子控制器的方法,另外这里通过一个简单的判断给消息控制器里添加了一个左侧导航按钮,用于在点击的时候打开抽屉
<pre>
/**
根据传入的标题和图片创建子控制器
@param childController 要添加的子控制器
@param title 标题
@param defaultImageName tabBarItem的默认图片
@param selectedImageName tabBarItem的选中图片
*/
-(void)addChildViewController:(UIViewController *)childController andTabTitle:(NSString *)title andDefaultImageName:(NSString *)defaultImageName andSelectedImageName:(NSString *)selectedImageName{
UINavigationController *NAVNC = [[UINavigationController alloc]initWithRootViewController:childController];
[self addChildViewController:NAVNC];
if ([title isEqualToString:@"消息"]) {
UISegmentedControl *segmentedControl = [[UISegmentedControl alloc]initWithItems:[NSArray arrayWithObjects:@"消息",@"电话",nil]];
segmentedControl.tintColor = [UIColor purpleColor];
segmentedControl.backgroundColor = [UIColor whiteColor];
segmentedControl.frame = CGRectMake([UIScreen mainScreen].bounds.size.width * 0.5 - 80, 20, 160, 30);
childController.navigationController.navigationBar.shadowImage = [UIImage imageNamed:@""];
segmentedControl.selected = YES;
segmentedControl.selectedSegmentIndex = 0;
childController.navigationItem.titleView = segmentedControl;
// 创建打开抽屉的按钮
childController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithImage:[UIImage imageNamed:@"run10"] style:UIBarButtonItemStylePlain target:self action:@selector(openDrawer)];
}
childController.navigationItem.title = title;
childController.tabBarItem.title = title;
[childController.tabBarItem setImage:[UIImage imageNamed:defaultImageName]];
[childController.tabBarItem setSelectedImage:[UIImage imageNamed:selectedImageName]];
}
</pre>
在左侧导航按钮的点击方法里调用抽屉控制器(也就是window的跟控制器)的打开抽屉方法
<pre>
/**
打开抽屉并传入打开抽屉的时长
*/
- (void)openDrawer{
// 通过单例创建,调用打开抽屉的具体实现效果
[[QQDrawerViewController shareDrawerViewController] openDrawerWithOpenDuration:0.2];
}
</pre>
在抽屉控制器里,通过改变view的transfrom实现打开抽屉的效果
<pre>
/**
打开抽屉效果
*/
-
(void)openDrawerWithOpenDuration:(CGFloat)openDuration{
[UIView animateWithDuration:openDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
// 因为在没打开抽屉的时候,主控制的tranform是处于为偏移状态,左侧控制器是向左偏移了maxWidth的宽度,所以
所以,这里要让主控制器右偏移,左侧控制器恢复就好
self.mainViewController.view.transform = CGAffineTransformMakeTranslation(self.drawerMaxWidth, 0);
self.leftViewController.view.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
// 在打开抽屉效果之后,我们往主控制器上添加一个遮罩按钮,用来点击的时候关闭抽屉效果
并且给遮罩按钮添加一个手势,实现通过拖动关闭抽屉
[self.mainViewController.view addSubview:self.coverButton];
[self addPanGestureRecognizerToView:self.coverButton];
}];
}
</pre>
点击遮罩按钮实现关闭抽屉和打开抽屉一样的原理,对于遮罩按钮的拖动手势一定要注意偏移量的计算,这里就通过一个图片来描述一下主控制器的偏移量计算,图片中offset是值的大小,没考虑正负,而在参与计算中,offsetX为负值,所以在计算的时候一定要注意
Paste_Image.png<pre>
/**
创建拖动手势,添加到覆盖按钮上
*/
- (void)addPanGestureRecognizerToView:(UIButton *)button{
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureRecognizer:)];
[button addGestureRecognizer:pan];
}
/**
按钮拖动手势的回调
*/
- (void)panGestureRecognizer:(UIPanGestureRecognizer *)pan{
CGFloat offsetX = [pan translationInView:pan.view].x;
if (pan.state == UIGestureRecognizerStateFailed || pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateEnded ) {
if (SCREENBOUNDS.size.width - self.drawerMaxWidth + ABS(offsetX) > SCREENBOUNDS.size.width * 0.5) {
[self closeDrawerWithOpenDuration:((self.drawerMaxWidth - ABS(offsetX)) / self.drawerMaxWidth) * 0.2];
}else{
[self openDrawerWithOpenDuration:(ABS(offsetX) / self.drawerMaxWidth) * 0.2];
}
}else if (pan.state == UIGestureRecognizerStateChanged && offsetX < 0 && offsetX > - self.drawerMaxWidth){
self.mainViewController.view.transform = CGAffineTransformMakeTranslation(self.drawerMaxWidth + offsetX, 0);
self.leftViewController.view.transform = CGAffineTransformMakeTranslation(offsetX, 0);
}
}
</pre>
此外,也可以通过边缘拖拽手势打开抽屉
<pre>
/**
创建边缘拖拽手势
*/
- (void)addScreenEdgePanGestureRecognizerToView:(UIView *)view{
UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc]initWithTarget:self action:@selector(screenEdgePanGestureRecognizer:)];
pan.edges = UIRectEdgeLeft;
[view addGestureRecognizer:pan];
}
/**
边缘拖拽手势的回调
*/
-
(void)screenEdgePanGestureRecognizer:(UIScreenEdgePanGestureRecognizer *)pan{
CGFloat OffsetX = [pan translationInView:pan.view].x;
if (pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateFailed || pan.state == UIGestureRecognizerStateEnded) {
if (OffsetX > SCREENBOUNDS.size.width * 0.5) { [self openDrawerWithOpenDuration:((self.drawerMaxWidth - OffsetX) / self.drawerMaxWidth) * 0.2]; }else{ [self closeDrawerWithOpenDuration:(OffsetX / self.drawerMaxWidth) * 0.2]; }
}else if(pan.state == UIGestureRecognizerStateChanged){
if (OffsetX > 0 && OffsetX < self.drawerMaxWidth) {
self.mainViewController.view.transform = CGAffineTransformMakeTranslation(OffsetX, 0);
self.leftViewController.view.transform = CGAffineTransformMakeTranslation(-self.drawerMaxWidth + OffsetX, 0);
}
}
}
</pre>
在打开抽屉后,对选择一些菜单的处理也是在抽屉控制器里和添加主控制一样添加上去,在添加的时候注意改版一下偏移量
<pre>
/**
选择左侧控制器后进行跳转
*/
-
(void)switchViewController:(UIViewController *)viewController{
[self.view addSubview:viewController.view];
[self addChildViewController:viewController];
self.destViewController = viewController;viewController.view.transform = CGAffineTransformMakeTranslation(SCREENBOUNDS.size.width, 0);
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
viewController.view.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
self.mainViewController.view.transform = CGAffineTransformIdentity;
[self.coverButton removeFromSuperview];
self.coverButton = nil;
}];
}
</pre>
然后在取消跳转控制器的时候移除控制器过后不要忘记置为nil。
thank you.jpg谢谢你赠送给我宝贵的时间,最后贴上代码地址:
OC版本: https://github.com/BlacksSky/QQDRAWER
swift版本:https://github.com/BlacksSky/QQDrawer_swift