当你遇到项目中需要有侧滑功能时(现在很多app都有,不知道是谁模仿谁!),你是否在惆怅,为毛你写的没有某社交软件的好,今天我们就来学习,并制作一个属于自己的侧滑菜单,同样,我们还是介绍下本篇博客的结构(容我装个逼,一天不装浑身难受!!):
我记得之前有个大牛给我说过一个原则whh(自创的)原则,也就是why、how、how,相信大家都应该明白我要说什么了吧。之所以我们要做这个东西,很大部分上就是因为项目需要(这不废话吗,谁没事和自己过不去?),其次就是对自我的一种提高。
看过我之前写的博客的人应该知道这里我要干嘛了:
当然,这里的页面是我截的图,实际上是两个tableView,这个页面我也实现过,不信你看(有空和大家分享一下):
聪明的大家应该能够很清楚的知道这就是新建一个容器视图控制器,然后里面放了两个viewCongtroller,然后就是手势的交互了。恭喜你,答对了,但是我们究竟会使用到那些手势呢?UIPanGestureRecognizer、UITapGestureRecognizer,但是仅仅这两个手势就行了吗?如果你不怕麻烦,你可以用一个pan手势实现,但是里面的判断可能就比较多了,这里我们还会使用到一种手势UIScreenEdgePanGestureRecognizer,我给他取名叫边缘拖拽手势。在它的api中,我们可以看到,它继承于pan,但是多了这么一个属性public var edges: UIRectEdge(不好意思,swift的,难得改了。)其中UIRectEdge的解释如下:
public struct UIRectEdge : OptionSetType {
public init(rawValue: UInt)
public static var None: UIRectEdge { get }
public static var Top: UIRectEdge { get }
public static var Left: UIRectEdge { get }
public static var Bottom: UIRectEdge { get }
public static var Right: UIRectEdge { get }
public static var All: UIRectEdge { get }
}
不用我多说,大家应该知道我们为什么要用这个手势了吧(因为整个控制器只有在屏幕左边缘才能滑动)。
说了那么多,我们还是用代码来实现吧:
首先我们新建一个类,取名叫RLSideslipController,继承于UIViewController,用它做我们的容器视图控制器。
在RLSideslipController.h中:
#define rScreenHeight [UIScreen mainScreen].bounds.size.height
#define rScreenWidth [UIScreen mainScreen].bounds.size.width
#define rOffSet rScreenWidth / 4
#import <UIKit/UIKit.h>
@interface RLSideslipController : UIViewController
/* *初始化方法 *main 最中间的视图控制器 *left 向右滑动时的视图控制器 * */
- (instancetype)initWithMainViewController:(UIViewController *)main
leftViewController:(UIViewController *)left;
@end
看起来我们的.h很简单,当然如果你要有动画的回调等等方法,你可以自行添加代理或者block作为回调。接下来我们看看.m如何实现:
#define rMainFrame CGRectMake(0,0,rScreenWidth,rScreenHeight)
#define rLeftFrame CGRectMake(-rOffSet,0,rScreenWidth,rScreenHeight)
#define rVelocityRatio rOffSet / (rScreenWidth - rOffSet)
#import "RLSideslipController.h"
@interface RLSideslipController ()
@property (nonatomic, strong) UIViewController * mainVC;
@property (nonatomic, strong) UIViewController * leftVC;
@property (nonatomic, strong) UIView * maskView;
//左侧边缘手势
@property (nonatomic, strong) UIScreenEdgePanGestureRecognizer * edge;
//右滑手势
@property (nonatomic, strong) UIPanGestureRecognizer * pan;
//点击手势
@property (nonatomic, strong) UITapGestureRecognizer * tap;
@end
从上面,我们可以看出我们主要有三种手势tap、pan、edge,他们分别用来干嘛的?
首先我们还是把初始化方法和viewDiload方法实现了:
#pragma mark - initalizer
- (instancetype)initWithMainViewController:(UIViewController *)main leftViewController:(UIViewController *)left {
if (self = [super init]) {
self.mainVC = main;
self.leftVC = left;
}
return self;
}
#pragma mark - load
- (void)viewDidLoad {
[super viewDidLoad];
//设置两个视图的frame
self.mainVC.view.frame = rMainFrame;
self.leftVC.view.frame = rLeftFrame;
//将两个视图添加进来,并且main在上层
[self.view addSubview:self.leftVC.view];
[self.view addSubview:self.mainVC.view];
//初始添加侧滑手势
[self.view addGestureRecognizer:self.edge];
}
当然属性肯定用Getter方法加载咯:
#pragma mark - Getters
- (UIScreenEdgePanGestureRecognizer *)edge {
if (!_edge) {
_edge = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToEdge:)];
_edge.edges = UIRectEdgeLeft;//设置只有在左侧边缘滑动时才响应
}
return _edge;
}
- (UIPanGestureRecognizer *)pan {
if (!_pan) {
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToPan:)];
}
return _pan;
}
- (UITapGestureRecognizer *)tap {
if (!_tap) {
_tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToTap:)];
}
return _tap;
}
- (UIView *)maskView {
if (!_maskView) {
_maskView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, rScreenWidth, rScreenHeight)];
_maskView.backgroundColor = [UIColor clearColor];
}
return _maskView;
}
现在我们来思考,我们通过手势的响应方法可以得到当前滑动的距离:CGPoint transformPoint = [sender translationInView:self.view];我们拿到这个距离能够做些什么东西呢?实际上聪明的人们肯定已经知道我们的实现就是通过不断改变的transformPoint的x值来动态的改变视图的center的x值,并且我们根据需求,可以得出mainVC的view.center.x存在这么一个不等式:rScreenWidth * 3 / 2 - rOffSet(这是目的位置的x值) >= mainCenterX >= rScreenWidth / 2(这是起始位置的x值),leftVC的view.center.x存在这么一个不等式:rScreenWidth / 2 >= mainCenterX >= rScreenWidth / 2 - rOffSet,这里我们需要注意一点,transformPoint.x不管是谁,都是一样的,如果我们分别用两个控制器的中心点的横坐标加上transformPoint.x的话会出现一个问题,因为两个视图的总共位移距离不一样,而加的偏移量又一致的话,会导致一个先停下来,一个后停下来,就不漂亮了,所以会他们的移动会有一定的比值进行:也就是我们宏定义的rVelocityRatio。只需要将leftVC改变的偏移量乘以它就可以保证动画都在执行,具体如下:
#pragma mark - responds
- (void)respondsToEdge:(UIScreenEdgePanGestureRecognizer *)sender {
CGPoint transformPoint = [sender translationInView:self.view];
switch (sender.state) {
case UIGestureRecognizerStateBegan: {
[self.maskView addGestureRecognizer:self.tap];
[self.mainVC.view addSubview:self.maskView];
}
break;
case UIGestureRecognizerStateChanged: {
//不等式rScreenWidth * 3 / 2 - rOffSet >= mainCenterX >= rScreenWidth / 2
CGFloat mainCenterX = MIN(rScreenWidth * 3 / 2 - rOffSet, MAX(rScreenWidth / 2, rScreenWidth / 2 + transformPoint.x));
//不等式rScreenWidth / 2 >= mainCenterX >= rScreenWidth / 2 - rOffSet
CGFloat leftCenterX = MIN(rScreenWidth / 2, MAX(rScreenWidth / 2 - rOffSet, rScreenWidth / 2 - rOffSet + transformPoint.x * rVelocityRatio));
self.mainVC.view.center = CGPointMake(mainCenterX, rScreenHeight / 2);
self.leftVC.view.center = CGPointMake(leftCenterX, rScreenHeight / 2);
}
break;
case UIGestureRecognizerStateEnded: {
CGFloat mainCurrentCenterX = self.mainVC.view.center.x;//当前主视图的位置
CGFloat allOffSet = rScreenWidth - rOffSet;//主视图应该移动的偏移量
CGFloat ratio = (mainCurrentCenterX - rScreenWidth / 2) / allOffSet;//当前完成了得百分比
__weak typeof(self)weakSelf = self;
if (ratio >= 0.6) {
//完成显示的剩余动画
[self showWithRatio:ratio completion:^{
[weakSelf.view removeGestureRecognizer:weakSelf.edge];
[weakSelf.mainVC.view addGestureRecognizer:weakSelf.pan];
}];
}else {
//完成消失的剩余动画
[self dismissWithRatio:ratio completion:^{
[weakSelf.maskView removeGestureRecognizer:weakSelf.tap];
[weakSelf.maskView removeFromSuperview];
}];
}
}
break;
default:
break;
}
}
- (void)respondsToPan:(UIPanGestureRecognizer *)sender {
CGPoint transformPoint = [sender translationInView:self.view];
//如果向右滑动,则直接返回
if (transformPoint.x > 0) {
return;
}
switch (sender.state) {
case UIGestureRecognizerStateChanged: {
//不等式rScreenWidth * 3 / 2 - rOffSet >= mainCenterX >= rScreenWidth / 2
CGFloat mainCenterX = MIN(rScreenWidth * 3 / 2 - rOffSet, MAX(rScreenWidth / 2, rScreenWidth * 3 / 2 - rOffSet + transformPoint.x));
//不等式rScreenWidth / 2 >= mainCenterX >= rScreenWidth / 2 - rOffSet
CGFloat leftCenterX = MIN(rScreenWidth / 2, MAX(rScreenWidth / 2 - rOffSet, rScreenWidth / 2 + transformPoint.x * rVelocityRatio));
self.mainVC.view.center = CGPointMake(mainCenterX, rScreenHeight / 2);
self.leftVC.view.center = CGPointMake(leftCenterX, rScreenHeight / 2);
}
break;
case UIGestureRecognizerStateEnded: {
CGFloat mainCurrentCenterX = self.mainVC.view.center.x;//当前主视图的位置
CGFloat allOffSet = rScreenWidth - rOffSet;//主视图应该移动的偏移量
CGFloat ratio = (mainCurrentCenterX - rScreenWidth / 2) / allOffSet;//当前完成了得百分比
if (ratio < 0.4) {
__weak typeof(self)weakSelf = self;
[self dismissWithRatio:ratio completion:^{
[weakSelf.view addGestureRecognizer:weakSelf.edge];
[weakSelf.mainVC.view removeGestureRecognizer:weakSelf.pan];
[weakSelf.maskView removeGestureRecognizer:weakSelf.tap];
[weakSelf.maskView removeFromSuperview];
}];
}else {
[self showWithRatio:1 - ratio completion:nil];
}
}
break;
default:
break;
}
}
- (void)respondsToTap:(UITapGestureRecognizer *)sender {
__weak typeof(self)weakSelf = self;
[self dismissWithRatio:1 completion:^{
[weakSelf.view addGestureRecognizer:weakSelf.edge];
[weakSelf.mainVC.view removeGestureRecognizer:weakSelf.pan];
[weakSelf.maskView removeGestureRecognizer:weakSelf.tap];
[weakSelf.maskView removeFromSuperview];
}];
}
#pragma mark - show or dismiss
- (void)showWithRatio:(CGFloat)ratio completion:(void(^)(void))comple {
__weak typeof(self)weakSelf = self;
[UIView animateWithDuration:1 * (1 - ratio) animations:^{
weakSelf.mainVC.view.center = CGPointMake(rScreenWidth * 3 / 2 - rOffSet, rScreenHeight / 2);
weakSelf.leftVC.view.center = CGPointMake(rScreenWidth / 2, rScreenHeight / 2);
} completion:^(BOOL finished) {
if (comple) {
comple();
}
}];
}
- (void)dismissWithRatio:(CGFloat)ratio completion:(void(^)(void))comple {
__weak typeof(self)weakSelf = self;
[UIView animateWithDuration:1 * ratio animations:^{
weakSelf.mainVC.view.center = CGPointMake(rScreenWidth / 2, rScreenHeight / 2);
weakSelf.leftVC.view.center = CGPointMake(rScreenWidth / 2 - rOffSet, rScreenHeight / 2);
} completion:^(BOOL finished) {
if (comple) {
comple();
}
}];
}
看起来好大一堆,不要怕,实际上pan和edge手势的方法可以整合到一起,但是这里我为了大家看起来不是特别容易混淆,分开写了。为什么我们在手势的end时要加上一个动画呢,而且动画时间还是根据动画的完成度来设置的呢?主要就是为了我们比较理想的实现中途停止用户交互后的操作和执行完毕后的手势变化,不管怎么样,都会在手势结束后执行show or dismiss方法。
好了,今天就到这里。温故而知新,我们还是来回顾一下今天的内容,主要新加了一个edge手势和一些计算,这里我巧妙的运用了取最大和最小值的方法写出了两个不等式(这里可以理解下)。整个侧滑菜单做完了,我们又可以拿着这个去装逼了,因为他是属于我们自己的!!!哈哈哈