iOS学习笔记(12)-自己动手写RESideMenu

很多app都实现了类似RESideMenu的效果,RESideMenu是Github上面一个stars数超过5000的作品,作为iOS初学者,我下载试用了并读了下源码,大致理清了思路,很多东西看着是明白了,真正写的时候容易卡壳,所以,自己动手来实现一次吧,代码都是来自RESideMenu的源码,有简化功能和少许改动。代码地址:,可以checkout出不同tag的代码来根据本文实现相关功能,最新的tag是最终完整版本。

RESideMenu的效果演示如下:


图1 RESideMenu最终效果图

1 初始化

先来创建几个View Controller(VC),后面实现需要。如图所示,storyboard中有6个VC。其中Root VC是init VC,用于加载待显示的VC,Left Menu和Right Menu就是左右侧边栏,而First和Second就是点击侧边栏的第一二个条目显示的视图控制器了。First和Second的VC嵌入到了Navigation Bar里面,所以还有个Navigation VC。

iOS学习笔记(12)-自己动手写RESideMenu_第1张图片
图2 视图控制器列表

最初的核心代码也很简单,我们只需要依次将左右侧边栏的VC以及显示内容的VC添加到Root VC中即可。这里我们创建了两个视图menuViewContainer和contentViewContainer,分别用于添加左右侧边栏的视图和中间的内容视图。

注意,Root VC是我们自定义的一个容器视图控制器(Container View Controller),这是iOS5之后才支持的功能。所谓的容器视图控制器,是指一个View Controller显示的某部分内容属于另一个View Controller,那么这个View Controller就是一个Container,比如UIKit中的UINavigationController,UITabBarController等。Root VC是Container,而Left Menu VC和Right Menu VC以及Content VC都会添加到Root VC中,它们的视图也会添加到Root VC的视图中,所以它们都是Root VC的子视图控制器。

这里看一下添加子视图控制器的方法,其他的代码参见rp-a这个tag。下面的四行代码是添加一个子视图控制器的套路,移除一个子视图控制器后面用到的时候再讲,是三步,也是按套路写就行了,这里有篇文章很详细的描述了这个原理。

  • 1)调用addChildViewController添加子视图控制器。在这个方法调用前,iOS会自动给你调用子视图控制器的willMoveToParentViewController方法。因为在容器视图控制器添加子视图控制器的视图之前,必须先添加子视图控制器。
    1. 设置子视图控制器的视图frame。这里设置的是Root View的大小,也就是整个屏幕大小。
    1. 在容器视图控制器的视图中加入子视图控制器的视图。
    1. 调用子视图控制器的didMoveToParentViewController方法。这个是iOS的规定,虽然你不调用该方法可能也没有问题,但是从规则上来说,这个方法必须调用以通知子视图控制器自己已经被加入到容器视图控制器中。stackoverflow这个问题的答案也简要说明了为什么要调用这个方法。
- (void)addChildViewController:(UIView *)container childViewController:(UIViewController *)childViewController {
    [self addChildViewController:childViewController]; //1
    childViewController.view.frame = self.view.bounds; //2
    [container addSubview:childViewController.view]; //3
    [childViewController didMoveToParentViewController:self]; //4
}

2 实现左侧栏的动画效果

所谓的动画效果都是一个个效果的叠加,我们看到的左侧栏的动画效果是点击Left按钮,动画效果是左侧栏慢慢显示,内容视图则往右移动并缩放了。点击左侧栏的菜单项,会显示对应的VC的视图。这节就来实现这个效果。

2.1 实现显示/隐藏左侧栏的动画

左侧栏的内容就是一个简单的Table View,里面的内容是手动添加的几个菜单项,点击第一项和第二项可以分别显示First VC的视图和Second VC的视图。我们现在的视图结构是Root VC的视图里面添加了一个背景图,然后其上是视图menuViewContainer,里面添加了左右侧边栏的子视图,再是视图contentViewContainer,添加的是要显示的内容视图。

先修改RESideMenu.m,加入一个contentButton,在显示侧边栏的时候,将这个button加入到内容视图,点击内容视图就可以隐藏侧边栏了。另外需要加入几个新的属性,用于缩放内容视图,背景图,以及侧边栏等。接着重点来了,显示和隐藏侧边栏的方法,核心代码如下:

- (void)showLeftMenuViewController {
    if (!self.leftMenuViewController) {
        return;
    }
    
    //添加一个按钮,这样点击的时候可以显示内容视图,在隐藏侧边栏的时候需要移除这个按钮。
    //这个按钮点击绑定的事件就是隐藏侧边栏。注意到在初始化的时候加的代码:
    // [button addTarget:self action:@selector(hideMenuViewController) forControlEvents:UIControlEventTouchUpInside];
    [self addContentButton];

    //调用beginAppearanceTransition时为了触发viewWillAppear和
    //viewDidAppear,如果不调用该方法则不会触发相关方法调用。
    [self.leftMenuViewController beginAppearanceTransition:YES animated:YES];
    self.leftMenuViewController.view.hidden = NO;
    self.rightMenuViewController.view.hidden = YES;
    
    [UIView animateWithDuration:self.animationDuration animations:^{
        //设置内容视图的缩放比例为0.7
        self.contentViewContainer.transform = CGAffineTransformMakeScale(self.contentViewScaleValue, self.contentViewScaleValue);
        
        //修改内容视图的center,这样内容视图会右移,左侧留出空间显示侧栏。
        self.contentViewContainer.center = CGPointMake((UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]) ? self.contentViewInLandscapeOffsetCenterX + CGRectGetWidth(self.view.frame) : self.contentViewInPortraitOffsetCenterX + CGRectGetWidth(self.view.frame)), self.contentViewContainer.center.y);
        
        //设置menuViewContainer和contentViewContainer的透明度。
        self.menuViewContainer.alpha = 1.0f;
        self.contentViewContainer.alpha = self.contentViewFadeOutAlpha;
        
        //复位背景图和菜单栏的缩放比例。在隐藏左侧栏的方法中,我们会放大背景图和左侧栏。
        self.backgroundImageView.transform = CGAffineTransformIdentity;
        self.menuViewContainer.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        [self.leftMenuViewController endAppearanceTransition];
        self.leftMenuVisible = YES;
        self.rightMenuVisible = NO;
    }];
}

看代码功能不复杂,首先是在内容视图添加了一个按钮,为了点击内容视图隐藏侧边栏。另外,隐藏右侧边栏的视图,显示左侧边栏视图。在动画里面就是左侧边栏显示的动画呈现,因为在隐藏侧边栏的时候,侧边栏放大了1.5倍的,而背景图放大了1.7倍,因此需要还原回来。而内容视图则需要缩小为原来的0.7倍大小。

接下来看下因此侧边栏的方法。其实就是一个相反的过程,将边栏放大到1.5倍,背景图放大到1.7倍,内容视图恢复到原来的正常大小。

- (void)hideMenuViewController:(BOOL)animated {
    UIViewController *visibleMenuViewController = self.rightMenuVisible ? self.rightMenuViewController : self.leftMenuViewController;
    [visibleMenuViewController beginAppearanceTransition:NO animated:animated];
    
    self.leftMenuVisible = NO;
    self.rightMenuVisible = NO;

    //移除按钮,否则会影响原来的内容视图响应事件
    [self.contentButton removeFromSuperview];     
    __typeof (self) __weak weakSelf = self;
    void (^animationBlock)(void) = ^{
        __typeof (weakSelf) __strong strongSelf = weakSelf;
        if (!strongSelf) {
            return;
        }
        //恢复内容视图的缩放比例为1,重置frame。
        strongSelf.contentViewContainer.transform = CGAffineTransformIdentity;
        strongSelf.contentViewContainer.frame = strongSelf.view.bounds;
        
        //设置背景图和侧边栏的缩放比例
        strongSelf.backgroundImageView.transform = self.backgroundImageViewTransformation;
        strongSelf.menuViewContainer.transform = self.menuViewControllerTransformation;

        //设置侧边栏和内容视图的透明度值
        strongSelf.menuViewContainer.alpha = 0;
        strongSelf.contentViewContainer.alpha = 1;
        
    };
    void (^completionBlock)(void) = ^{
        [visibleMenuViewController endAppearanceTransition];
    };
    
    if (animated) {
        [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
        [UIView animateWithDuration:self.animationDuration animations:^{
            animationBlock();
        } completion:^(BOOL finished) {
            [[UIApplication sharedApplication] endIgnoringInteractionEvents];
            completionBlock();
        }];
    } else {
        animationBlock();
        completionBlock();
    }
}

隐藏侧边栏的有两个地方要注意下,一个是要记得移除contentButton,不然隐藏视图后你点击内容视图会没有反应了,因为点击到的都是这个添加的按钮。

另外一个是里面用到的__weak和 __strong语法。我们知道在Block中如果直接用self会导致循环引用,所以一般是用__weak类型的weakSelf,但是我们看RESideMenu源码发现其中用的是代码中这种方式,这是必要的,因为我们在Block中需要多次用到self的引用,如果只用一次,那么可以直接用weakSelf就行,如果是多次用到,需要用到代码中这种strongSelf。具体原因可以参考这篇文章。

然后修改原来的Category中的方法presentLeftMenuViewController,加入调用RESideMenu类的presentLeftMenuViewController方法调用,这样我们的主体代码就完成了,运行起来,效果如下:

iOS学习笔记(12)-自己动手写RESideMenu_第2张图片
图3 左侧栏效果

当然,我们会发现有个问题,就是第一次显示侧边栏的缩放时候,没有缩放效果,所以我们要加下初始的缩放效果。在显示左侧边栏之前先设置一下侧边栏和内容视图的缩放比例和透明度。在显示侧边栏之前先调用该方法,这样可以看到第一次显示侧边栏也有缩放效果了。代码见tag rp-b-1。

/**   
 * 放大背景图和侧边栏,这样第一次显示侧边栏的时候才会有缩小的动画效果。
 */
- (void)presentMenuViewContainerWithMenuViewController:(UIViewController *)menuViewController {
    self.backgroundImageView.transform = self.backgroundImageViewTransformation;
    self.menuViewContainer.transform = self.menuViewControllerTransformation;
    self.menuViewContainer.alpha = 0;
}

2.2 左侧栏菜单项点击效果

现在点击左侧栏的菜单项是没有反应的,这一节就来加入点击切换视图控制器效果。先完善下storyboard中的Second View Controller,在导航栏加入左右两个按钮,并与之前的First View Controller的两个按钮绑定同一个事件,这样切换到Second View Controller后,也可以点击导航栏按钮显示侧边栏。

接下来就是在LeftMenuViewController.m中加入菜单项点击事件了。代码如下:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    switch (indexPath.row) {
        case 0:
            [self.sideMenuViewController setContentViewController:[[UINavigationController alloc] initWithRootViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"firstViewController"]]
                                                         animated:YES];
            [self.sideMenuViewController hideMenuViewController];
            break;
        case 1:
            [self.sideMenuViewController setContentViewController:[[UINavigationController alloc] initWithRootViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"secondViewController"]]
                                                         animated:YES];
            [self.sideMenuViewController hideMenuViewController];
            break;
        default:
            break;
    }
}

这里只对第一个和第二个菜单项做了处理,其他的类似。这里用到了一个sideMenuViewController属性,这是为了获取最顶层的Root VC,这个属性值获取可以通过UIViewController+RESideMenu.m中的函数获取,即一直向上找parentViewController(在addChildViewController时建立了父子视图控制器之间的关联),直到找到Root VC为止(因为Root VC继承自RESideMenu)。

- (RESideMenu *)sideMenuViewController
{
    UIViewController *iter = self.parentViewController;
    while (iter) {
        if ([iter isKindOfClass:[RESideMenu class]]) {
            return (RESideMenu *)iter;
        } else if (iter.parentViewController && iter.parentViewController != iter) {
            iter = iter.parentViewController;
        } else {
            iter = nil;
        }
    }
    return nil;
}

这个点击菜单项显示对应的视图控制器主要是分两步,第一步是设置新的内容视图控制器,第二步是隐藏菜单栏。设置内容视图控制器的代码如下:

    1. 如果内容视图控制器没有变,则什么都不做。
    1. 如果不需要动画效果,则直接替换contentViewController即可。
    1. 如果要设置动画效果,则先将新的contentViewController添加到Root VC中,然后设置新的contentViewController的透明度为0,最终隐藏老的contentViewController,并设置新的contentViewController,透明度的变化可以造成一种渐隐的动画效果。这里的hideViewController实质上是移除了子视图控制器,与之前提到的添加子视图控制器是一个相反的过程。如果这里不移除老的contentViewConroller,显示上也没有问题,只是你在调试器里面可以看到视图层次在一层层叠加,徒增资源消耗,所以一定要记得移除。
    1. 移除子视图控制器分为三步:首先是调用willMoveToParentViewController:nil通知子视图控制器即将移除。然后是将视图从superview中移除。最后调用removeFromParentViewController移除当前视图控制器,当然,ios在这个方法调用之后接着会自动调用子视图控制器的didMoveToParentViewController:nil方法。由于我们这里的First VC和Second VC都是嵌入在Navigation VC里面的,所以在First或者Second VC里面是看不到对应的方法调用。
/**
 * 设置新的内容视图控制器
 */
- (void)setContentViewController:(UIViewController *)contentViewController animated:(BOOL)animated {
    if (self.contentViewController == contentViewController) { //1
        return;
    }
    
    if (!animated) { //2
        self.contentViewController = contentViewController; 
    } else {  //3
        [self addChildViewController:contentViewController]; 
        contentViewController.view.alpha = 0;
        contentViewController.view.frame = self.contentViewContainer.bounds;
        [self.contentViewContainer addSubview:contentViewController.view];
        [UIView animateWithDuration:self.animationDuration animations:^{
            contentViewController.view.alpha = 1;
        } completion:^(BOOL finished) {
            [self hideViewController:self.contentViewController];
            self.contentViewController = contentViewController;
            [contentViewController didMoveToParentViewController:self];
        }];
    }
}

/**
 * 移除view controller,也是规定的套路。
 */
- (void)hideViewController:(UIViewController *)viewController
{
    [viewController willMoveToParentViewController:nil];
    [viewController.view removeFromSuperview];
    [viewController removeFromParentViewController];
}

到此,点击菜单项切换视图控制器的功能也完成了,完整代码参见 rp-b-2。

3 横竖屏支持

checkout到tag位rp-b-2代码版本,先旋转屏幕到横屏,然后点击"Left“按钮,可以发现显示错乱了,如图4所示。

iOS学习笔记(12)-自己动手写RESideMenu_第3张图片
图4 横屏样式错乱

原因也很简单,因为旋转屏幕,视图的宽高没有改变,导致了错乱。在RESideMenu.m中加入旋转屏幕的支持,代码如下:

- (BOOL)shouldAutorotate
{
    return self.contentViewController.shouldAutorotate; //contentViewController支持旋转,如果不想支持,返回NO即可。
}

/**
 屏幕旋转时需要重新设置内容视图的frame和侧边栏的bounds。
 */
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    //只有当侧边栏显示的情况下才需要修改内容视图,这个时候侧边栏缩放是1,只需要设置bounds即可。
    if (self.leftMenuVisible) {
        self.menuViewContainer.bounds = self.view.bounds;
        
        //设置新的frame。注意重新设置frame之前需要先将transform置为CGAffineTransformIdentity,否则无效。
        //文档中设置frame属性时有提到: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
        self.contentViewContainer.transform = CGAffineTransformIdentity;
        self.contentViewContainer.frame = self.view.bounds;
        
        //根据新的frame重新缩放和重新设置center
        self.contentViewContainer.transform = CGAffineTransformMakeScale(self.contentViewScaleValue, self.contentViewScaleValue);
        CGPoint center = CGPointMake((UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation) ? self.contentViewInLandscapeOffsetCenterX + CGRectGetWidth(self.view.frame) : self.contentViewInPortraitOffsetCenterX + CGRectGetWidth(self.view.frame)), self.contentViewContainer.center.y);
        self.contentViewContainer.center = center;
    }
}

除了这个还不够,还要修改下之前的presentMenuViewContainerWithMenuViewController方法,加入对旋转屏幕的支持,改成这样:

/**   
 * 放大背景图和侧边栏,这样第一次显示侧边栏的时候才会有缩小的动画效果。
 * 1,2,3,4是因为旋转屏幕之后,需要重新设置frame大小。
 */
- (void)presentMenuViewContainerWithMenuViewController:(UIViewController *)menuViewController {
    self.backgroundImageView.transform = CGAffineTransformIdentity; //1
    self.backgroundImageView.frame = self.view.bounds; //2
    self.backgroundImageView.transform = self.backgroundImageViewTransformation;
    
    self.menuViewContainer.transform = CGAffineTransformIdentity; //3
    self.menuViewContainer.frame = self.view.bounds; //4
    self.menuViewContainer.transform = self.menuViewControllerTransformation;
    self.menuViewContainer.alpha = 0;
}

这样调整就完成了。注意到现在我只对左侧栏进行了设置,并没有考虑右侧的情况,等下一节处理右侧栏的时候再加入相应的处理代码。最终效果如图5所示,代码见rp-c。

图5 加入旋转屏幕支持

4 加入右侧边栏

有了前面的基础,加入右侧边栏过程就比较简单了。需要修改RESideMenu.m文件,加入显示右侧边栏的代码:

- (void)showRightMenuViewController
{
    if (!self.rightMenuViewController) {
        return;
    }
    [self.rightMenuViewController beginAppearanceTransition:YES animated:YES];
    self.leftMenuViewController.view.hidden = YES;
    self.rightMenuViewController.view.hidden = NO;
    [self addContentButton];
    
    [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
    [UIView animateWithDuration:self.animationDuration animations:^{
        self.contentViewContainer.transform = CGAffineTransformMakeScale(self.contentViewScaleValue, self.contentViewScaleValue);
        self.contentViewContainer.center = CGPointMake((UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]) ? -self.contentViewInLandscapeOffsetCenterX : -self.contentViewInPortraitOffsetCenterX), self.contentViewContainer.center.y);
        
        self.menuViewContainer.alpha = 1;
        self.contentViewContainer.alpha = self.contentViewFadeOutAlpha;
        self.menuViewContainer.transform = CGAffineTransformIdentity;
        self.backgroundImageView.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        [self.rightMenuViewController endAppearanceTransition];
        self.rightMenuVisible = YES;
        self.leftMenuVisible = NO;
        [[UIApplication sharedApplication] endIgnoringInteractionEvents];
    }];
}

然后修改UIViewController+RESideMenu.m,修改显示右侧边栏的方法:

- (IBAction)presentRightMenuViewController:(id)sender
{
    [self.sideMenuViewController presentRightMenuViewController];
}

另外,还要修改前一节里面旋转屏幕时设置内容视图的willAnimateRotationToInterfaceOrientation方法中的center赋值代码,因为显示左侧栏和右侧栏的center位置不一样,代码修改如下:

 CGPoint center;
 if (self.leftMenuVisible) {
     center = CGPointMake((UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation) ? self.contentViewInLandscapeOffsetCenterX + CGRectGetWidth(self.view.frame) : self.contentViewInPortraitOffsetCenterX + CGRectGetWidth(self.view.frame)), self.contentViewContainer.center.y);
 } else {
     center = CGPointMake((UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation) ? -self.contentViewInLandscapeOffsetCenterX : -self.contentViewInPortraitOffsetCenterX), self.contentViewContainer.center.y);
 }

最后在SecondViewController.m中加入菜单项点击事件处理,跟FirstViewController.m中的一样,这里就不贴代码了。如此,右侧边栏显示功能完成,完整代码见rp-d。

5 加入拖拽手势

看演示可以知道除了点击可以显示隐藏菜单栏,拖拽手势也是要支持的。毕竟,手势操作在iPhone中是很常用的。首先在RESideMenu.h中加入UIGestureRecognizerDelegate代理,然后在RESideMenu.m中加入手势监听方法addPanGesture,并在viewDidLoad方法中调用,核心方法就是下面的panGestureRecognized方法了。

- (void)addPanGesture {
    self.view.multipleTouchEnabled = NO;
    UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureRecognized:)];
    panGestureRecognizer.delegate = self;
    [self.view addGestureRecognizer:panGestureRecognizer];
}

(void)panGestureRecognized:(UIPanGestureRecognizer *)recognizer {
    CGPoint point = [recognizer translationInView:self.view];
    if (recognizer.state == UIGestureRecognizerStateBegan) { //1
        //originalPoint的x值为当前中心点距离原来中心点的x轴距离,y值类似。
        self.originalPoint = CGPointMake(self.contentViewContainer.center.x - CGRectGetWidth(self.contentViewContainer.bounds) / 2.0,
                                         self.contentViewContainer.center.y - CGRectGetHeight(self.contentViewContainer.bounds) / 2.0);
        
        self.backgroundImageView.transform = CGAffineTransformIdentity;
        self.backgroundImageView.frame = self.view.bounds;
        
        self.menuViewContainer.transform = CGAffineTransformIdentity;
        self.menuViewContainer.frame = self.view.bounds;
    }
    
    if (recognizer.state == UIGestureRecognizerStateChanged) { //2
        CGFloat delta;
        if (self.leftMenuVisible || self.rightMenuVisible) {
            delta = fabs(self.originalPoint.x != 0 ? (point.x + self.originalPoint.x) / self.originalPoint.x : 0);
        } else {
            delta = fabs(point.x / self.view.frame.size.width);
        }
        
        CGFloat backgroundViewScale = self.backgroundImageViewScaleValue - ((self.backgroundImageViewScaleValue-1) * delta);
        CGFloat menuViewScale = self.menuViewScaleValue - ((self.menuViewScaleValue-1) * delta);
        
        //为了向右拖动的时候背景图的缩放比例不小于1,否则背景不能充满屏幕看起来不美观。
        if (backgroundViewScale < 1) {
            self.backgroundImageView.transform = CGAffineTransformIdentity;
        } else {
            self.backgroundImageView.transform = CGAffineTransformMakeScale(backgroundViewScale, backgroundViewScale);
        }

        self.menuViewContainer.transform = CGAffineTransformMakeScale(menuViewScale, menuViewScale);
        self.menuViewContainer.alpha = delta;
        self.contentViewContainer.alpha = 1 - (1 - self.contentViewFadeOutAlpha) * delta;
        
        //如果缩放比例大于1,则设置为相反的值。比如放大1.2倍则设置成缩小为0.8倍。
        CGFloat contentViewScale = 1 - ((1 - self.contentViewScaleValue) * delta);
        if (contentViewScale > 1) {
            contentViewScale = (1 - (contentViewScale - 1));
        }
        
        self.contentViewContainer.transform = CGAffineTransformMakeScale(contentViewScale, contentViewScale);
        self.contentViewContainer.transform = CGAffineTransformTranslate(self.contentViewContainer.transform, point.x, 0);
        
        //注意这里左右view的可见设置,不设置则在拖动的时候会在某个时刻同时出现左右侧边栏的。
        self.leftMenuViewController.view.hidden = self.contentViewContainer.frame.origin.x < 0;
        self.rightMenuViewController.view.hidden = self.contentViewContainer.frame.origin.x > 0;
    }
    
    if (recognizer.state == UIGestureRecognizerStateEnded) { //3
        if (self.panMinimumOpenThreshold > 0 && (
                (self.contentViewContainer.frame.origin.x < 0 && self.contentViewContainer.frame.origin.x > -((NSInteger)self.panMinimumOpenThreshold)) ||
                (self.contentViewContainer.frame.origin.x > 0 && self.contentViewContainer.frame.origin.x < self.panMinimumOpenThreshold))) {
            [self hideMenuViewController];
        }
        else if (self.contentViewContainer.frame.origin.x == 0) {
            [self hideMenuViewController:NO];
        }
        else {
            if ([recognizer velocityInView:self.view].x > 0) {
                if (self.contentViewContainer.frame.origin.x < 0) {
                    [self hideMenuViewController];
                } else {
                    if (self.leftMenuViewController) {
                        [self showLeftMenuViewController];
                    }
                }
            } else {
                if (self.contentViewContainer.frame.origin.x < 20) {
                    if (self.rightMenuViewController) {
                        [self showRightMenuViewController];
                    }
                } else {
                    [self hideMenuViewController];
                }
            }
        }
    }
}

代码比较多,我之前看的时间也比较长,由于近期身体出了点问题,导致一直没有写完这一节。在拖拽手势里面,分为好几个状态(开始,拖动中,结束)。分别对这几种状态来看看代码实现的功能。

  • 1)拖动开始的时候,先设置好背景图和菜单栏的transform和frame为初始值。并设置了一个originalPoint变量,这个变量是个CGPoint类型,它的x值是当前内容视图的中心点到内容视图默认中心点的距离,y值含义类似。这是什么意思呢?也就是说,如果当前左右菜单栏都是隐藏的,只有内容视图,那么这个originalPoint就是{0,0};如果菜单栏是显示的,比如左侧栏显示,那么这个时候内容视图center已经变化,这个originalPoint的x值就是新的center的值与原来默认的center的x值的距离。另外,函数translationInView是获取拖动后相对坐标轴的偏移量,比如向左拖动了多少点则x值为负值,向右拖动了多少点则x值为正值。注意是指从拖动开始拖动了多少点,不是当前手势的位置的坐标点。

  • 2)拖动进行中的时候,需要区分两种情况,一种是已经点开菜单栏,从菜单栏往隐藏菜单栏的方向拖,一种是没有点开菜单栏,往显示菜单栏的方向拖。那我们知道如果是从显示菜单栏的方向拖,是需要最终把内容视图缩放到0.7倍大小并且向右移动一定位置,之前点击显示左侧栏的时候是将center移动到了视图总宽度+30point的地方。这里我们不是直接移动center,而是通过CGAffineTransformTranslate方式来实现。

  • 2.1)首先是计算了一个delta值,这是计算已经移动了多少比例。接下来根据这个比例值来计算背景图,菜单栏以及内容视图分布的缩放比例是多少。由于是通过缩放和移动来实现拖动效果,这些缩放比例需要通过一个线性的公式计算,比如背景图的公式,可以参考下面的计算得到,菜单栏和内容视图的缩放比例公式同理。

  • 2.2) 另外注意一点就是内容视图的CGAffineTransformMakeScale和CGAffineTransformTranslate的变换时是加成的效果,也就是说,如果当前的缩放比例是0.8,而你translate的x值为50的话,实际是移动50*0.8=40个点。为什么要用加成呢,因为我们在改变scale的时候就已经修改了frame的值了,这里不用加成效果,那最终移动的距离就不对了。

先假定每次拖动一定delta值后,背景图的缩放比例为的对应关系为:
 backgroundViewScale = delta * a + b 
我们知道,在隐藏侧边栏的时候,delta为0,背景图缩放比例为self.backgroundImageViewScaleValue,而显示侧边栏的时候,背景图缩放比例为1,delta值为1。因此可以得到下面两个等式

self.backgroundImageViewScaleValue = 0 *a + b
1 = 1 * a + b;
于是得到 a = 1 - self.backgroundImageViewScaleValue, b=self.backgroundImageViewScaleValue
于是公式就是
backgroundViewScale = delta * (1 - self.backgroundImageViewScaleValue) + self.backgroundImageViewScaleValue
    1. 最后拖动停止的时候,需要根据拖动的方向以及当前内容视图位置来确定是显示左边栏,右边栏还是只显示内容视图。这个比较简单,有个变量panMinimumOpenThreshold说明下,这是显示侧边栏的一个阈值,内容视图的坐标的origin的x值在这个值以内则不显示侧边栏。另外一个就是,函数velocityInView返回的是一个手势移动的速度,是个CGPoint,有方向,向右为正值,左为负值。

完成这些后,运行代码,可以看到拖动效果了,如下:

图6 拖动效果图

认真的同学可能发现了,在拖动后松开的时候会有一个回弹,这个不是我们想要的,具体原因我也不是十分确定,似乎连续的缩放后再修改center会有这个问题,在显示侧边栏之前先重设一下内容视图的center为当前位置的center就可以解决这个问题。加入的函数如下,在函数showLeftMenuViewControllershowRightMenuViewController前面代码中加入resetContentViewScale函数调用,这个函数的作用其实就是重设了内容视图的frame,最终效果其实也就是重新设置了center,scale并没有变。这个函数起作用的具体原因我还尚不明确,有知道具体原因的同学麻烦告知一声,谢谢。

- (void)resetContentViewScale
{
    CGAffineTransform t = self.contentViewContainer.transform;
    CGFloat scale = sqrt(t.a * t.a + t.c * t.c);
    CGRect frame = self.contentViewContainer.frame;
    self.contentViewContainer.transform = CGAffineTransformIdentity;
    self.contentViewContainer.transform = CGAffineTransformMakeScale(scale, scale);
    self.contentViewContainer.frame = frame;
}

现在再运行,发现没有回弹了,效果自然很多。

图7 优化后的拖动效果

最终代码见rp-e。

6 总结

写这篇文章前前后后花了近两周的空余时间,代码都是来自RESideMenu,有问题还请大家指正。
代码地址: https://github.com/shishujuan/residemenu-practice

你可能感兴趣的:(iOS学习笔记(12)-自己动手写RESideMenu)