动画特效十一:侧边栏效果

本人录制技术视频地址:https://edu.csdn.net/lecturer/1899 欢迎观看。

好久没有进行 "动画特效" 这一系列了。今天和大家继续分享动画特效:“侧边栏效果”。由于侧边栏效果的动画效果各式各样了,所以我大致分为三种进行说明;

样式一:

动画特效十一:侧边栏效果_第1张图片


这个样式的动画效果还是比较容易处理的。主要是通过计算view的宽度或者位置来实现。我这样说,也许有点模糊了。让我们来分析程序的架构模式。

动画特效十一:侧边栏效果_第2张图片


动画特效十一:侧边栏效果_第3张图片


默认情况下是"图一"模式:

1.  "Screen area" 就是移动设备屏幕显示区域。并且它的根控制器就是 "ContainerViewController"。

2.  SliderViewController是 ContainerViewController的子控制器,它的坐标的x,y均为0,高度为屏幕的高度,有自己的宽度。

3. CenterViewController是 ContainerViewController的子控制器,它的坐标的x值就是SliderViewController的View的宽度,y为0,宽高大小等于屏幕的宽高大小。

代码设计思路:

就以点击白色按钮的效果进行说明(注:实际应用中,又将CenterViewController进行导航栏一层的封装,而上面的白色按钮是导航栏的leftBarButtonItem)。

当你点击白色按钮的时候,就来判断当前CenterViewController所在的导航控制器的View的x值。

1. 如果等于0(动画效果就是要从"图二" 变成 "图一"); 

2. 如果等于侧边栏的宽度(动画效果就是要从"图一" 变成 "图二"); 

不管动画是从谁执行到谁,均是同时改变两个子控制器的View的x值来实现的。

核心代码如下:

- (void)toggleSideMenu {
    CGFloat ratio = self.centerNavVC.view.x / self.baseWidth;
    BOOL isOpen = ratio == 1.0;
    CGFloat toggleProgress = isOpen ? 0.0 : 1.0;
    [UIView animateWithDuration:0.25 animations:^{
        [self setToPercent:toggleProgress];
    }];
}

- (void)setToPercent:(CGFloat)progress {
    self.slideMenuVC.view.x = (progress - 1) * self.baseWidth;
    self.centerNavVC.view.x = progress * self.baseWidth;
}

注:由于这样的侧边栏效果相对而言比较简单,我并没有用代码进行详细的讲解;后面的两个侧边栏效果我会结合代码进行比较详细的说明。


样式二:

动画特效十一:侧边栏效果_第4张图片


首先,我们来分析程序的架构模式。如下图:

动画特效十一:侧边栏效果_第5张图片


同样的道理,SliderViewController和CenterViewController均是ContainerViewController的子控制器。不同的是先将SliderViewController的View添加到ContainerViewController的View上面,再将CenterViewController的View添加到ContainerViewController的View上面,然后在点击白色按钮或者手势拖拽的时候,SliderViewController的View逐渐变大并且向左移动,直到某个位置的时候,SliderViewController的View "满高度"显示(即SliderViewController的View的高度等于屏幕的高度),而CenterViewController的x进行一定距离的移动。

根控制器ContainerViewController 代码分析:

1. ViewDidLoad进行初始化工作,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
    CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;
    
    // Slide VC
    self.slideMenuVC = [[SlideMenuViewController alloc] init];
    self.slideMenuVC.view.frame = CGRectMake(0, 0, kSliderViewWidth, screenHeight);
    self.cover = [[UIView alloc] init];
    self.cover.frame = self.slideMenuVC.view.bounds;
    self.cover.backgroundColor = [UIColor blackColor];
    self.cover.alpha = kDefaultCoverAlpha;
    [self.slideMenuVC.view addSubview:self.cover];
    
    [self addChildViewController:self.slideMenuVC];
    [self.view addSubview:self.slideMenuVC.view];
    self.slideMenuVC.delegate = self;
    self.slideMenuVC.centerNavViewController = self.centerNavVC;
    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(kDefaultScale, kDefaultScale);
    CGAffineTransform translationTransform = CGAffineTransformMakeTranslation(kDefaultTranslation, 0);
    self.slideMenuVC.view.transform = CGAffineTransformConcat(scaleTransform, translationTransform);
    
    // Center VC
    CenterViewController *centerVC = [[CenterViewController alloc] init];
    self.centerNavVC = [[UINavigationController alloc] initWithRootViewController:centerVC];
    self.centerNavVC.view.frame = CGRectMake(0, 0, screenWidth, screenHeight);
    [self addChildViewController:self.centerNavVC];
    [self.view addSubview:self.centerNavVC.view];

    // Gesture
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
    [self.view addGestureRecognizer:panGesture];
}

代码的主要工作

1) 初始化SliderViewController和CenterViewController两个子控制器,并且将各自的View添加到父控制器的View中。

2) 定义手势方法,用来执行滑动手势。

代码中用了很多宏定义,清单如下:

#define kDefaultCoverAlpha 0.9
#define kDefaultScale 0.6
#define kDefaultTranslation 50
#define kSliderViewWidth 120
kDefaultCoverAlpha 就是SliderViewController的View的遮罩层View的透明度;因为在手势滑动过程中,遮罩层的透明度会一直改变的,以达到阴影的效果。

kDefaultScale 和 kDefaultTranslation 分别是SliderViewController的View的初始状态下的缩放及平移大小。

下面三句代码就可以让SliderViewController的View初始状态时呈现为 "图三" 的状态:

CGAffineTransform scaleTransform = CGAffineTransformMakeScale(kDefaultScale, kDefaultScale);
CGAffineTransform translationTransform = CGAffineTransformMakeTranslation(kDefaultTranslation, 0);
self.slideMenuVC.view.transform = CGAffineTransformConcat(scaleTransform, translationTransform);
kSliderViewWidth 就是SliderViewController的View “满高度” 显示在屏幕上面的时候的宽度。

2. 白色按钮点击事件的处理相关代码:

- (void)toggleSideMenu {
    CGFloat ratio = self.centerNavVC.view.x / kSliderViewWidth;
    BOOL isOpen = ratio == 1.0;
    CGFloat toggleProgress = isOpen ? 0.0 : 1.0;
    [UIView animateWithDuration:1 animations:^{
        [self setToPercent:toggleProgress];
    }];
}

- (void)setToPercent:(CGFloat)progress {
    self.centerNavVC.view.x = progress * kSliderViewWidth;
    
    CGFloat alpha = kDefaultCoverAlpha * (1 - progress);
    self.cover.alpha = alpha;
    CGFloat scale = kDefaultScale + (1 - kDefaultScale) * progress;
    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scale, scale);
    CGFloat translation = kDefaultTranslation * (1 - progress);
    CGAffineTransform translationTransform = CGAffineTransformMakeTranslation(translation, 0);
    self.slideMenuVC.view.transform = CGAffineTransformConcat(scaleTransform, translationTransform);
}

这里主要的代码就是平移和缩放的处理,由于和在ViewDidload中初始化的操作基本类似,只是最终状态有所不同,我就不加说明了。

3. 手势的相关代码:

- (void)handleGesture:(UIPanGestureRecognizer *)pan {
    
    if(pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateEnded){
        CGFloat formerX = self.centerNavVC.view.x;
        if(formerX > kSliderViewWidth * 0.5 && formerX <= kSliderViewWidth){
            formerX = kSliderViewWidth;
        } else{
            formerX = 0;
        }
        
        CGFloat progress = formerX / kSliderViewWidth;
        [UIView animateWithDuration:1 animations:^{
            [self setToPercent:progress];
        }];
        
        return;
    }
    
    if(pan.state == UIGestureRecognizerStateBegan){
        // 一定要将首次按下的起始点记录下来,作为参考点
        self.relativeX = self.centerNavVC.view.x;
    }
    
    CGPoint translation = [pan translationInView:pan.view];
    CGFloat actualX = self.relativeX + translation.x;
    if (actualX >= kSliderViewWidth) {
        actualX = kSliderViewWidth;
    }
    
    if (actualX <= 0) {
        actualX = 0;
    }
    
    CGFloat progress = actualX / kSliderViewWidth;
    [self setToPercent:progress];
}

1. 手势结束或者终止的时候,判断CenterViewController的View的x值,如果小于 0.5 * kSliderViewWidth,动画执行到x = 0 的位置; 反之,动画执行到x = kSliderViewWidth的位置。

2. 手势刚刚开始的时候,一定要记录下CenterViewController的View的x值, 因为后面的移动的操作都是相对于它的,如果想仔细观察,大家可以打印它的值看看。

3. 代码中actualX的值就是手指当前所在屏幕上面的实时的x值,并且限定了手势动画的范围为0到kSliderViewWidth。


样式三

动画特效十一:侧边栏效果_第6张图片


功能分析:

1. 侧边栏有3D的透视效果。

2. 导航栏上面的白色按钮也会有旋转效果。

3. 点击侧边栏上面的Item,有收起侧边栏的动画。

注意:这里的架构模式为"图二"并不是"图一"。很多人也许会疑惑。看动画效果仿佛就是 "图一" 的设计。下面通过代码一一解释。

一、 侧边栏有3D的透视效果分析

根控制器ContainerViewController 代码分析:

1. ViewDidLoad进行初始化工作,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
    CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;
    
    // Center VC
    CenterViewController *centerVC = [[CenterViewController alloc] init];
    self.centerNavVC = [[UINavigationController alloc] initWithRootViewController:centerVC];
    self.centerNavVC.view.frame = CGRectMake(0, 0, screenWidth, screenHeight);
    [self addChildViewController:self.centerNavVC];
    [self.view addSubview:self.centerNavVC.view];
    
    // Slide VC
    self.slideMenuVC = [[SlideMenuViewController alloc] init];
    self.slideMenuVC.view.frame = CGRectMake(-kSliderViewWidth, 0, kSliderViewWidth, screenHeight);
    [self addChildViewController:self.slideMenuVC];
    [self.view addSubview:self.slideMenuVC.view];
    self.slideMenuVC.centerNavViewController = self.centerNavVC;

    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
    [self.view addGestureRecognizer:panGesture];
}

看起来和方式二的初始化代码很像,只是注意到SliderViewController的View的层次关系是在CenterViewController的View的前面的,并且它的View的x初始值是 -kSliderViewWidth。

我这里定义的kSliderViewWidth的宏如下面:

#define kSliderViewWidth 120
2. 手势的代码直接复用方式二的代码,只是手势中调用的setToPercent: 方法有所改变。

- (void)setToPercent:(CGFloat)progress {
    self.slideMenuVC.view.layer.transform = [self menuTransformForPercent:progress];
    self.coverView.alpha = 1 - progress;
    [self.slideMenuVC.view addSubview:self.coverView];
    self.centerNavVC.view.x = progress * kSliderViewWidth;
}

方法中的最后一句代码依旧是CenterViewController的View的拖拽效果。第2,3句代码是为了调节遮罩的程度。SliderViewController的View显示出来的越多,遮罩程度越轻;显示出来的越少,遮罩程度越重;下面的代码就是coverView的懒加载:

- (UIView *)coverView {
    if (!_coverView) {
        _coverView = [[UIView alloc] init];
        _coverView.userInteractionEnabled = NO;
        _coverView.frame = self.slideMenuVC.view.bounds;
        _coverView.backgroundColor = [UIColor blackColor];
    }
    return _coverView;
}
setToPercent: 代码的第一句就是透视动画执行的关键部分。而且这个效果只作用于SliderViewController的View上面,由于是3D动画,所以transform变换是layer的操作。具体调用的menuTransformForPercent: 方法实现如下:

- (CATransform3D)menuTransformForPercent:(CGFloat)percent {
    CATransform3D identity = CATransform3DIdentity;
    identity.m34 = -1.0 / 1000;
    
    CGFloat remainingPercent = 1.0 - percent;
    CGFloat angle = remainingPercent * (-M_PI_2); 
    
    CATransform3D rotationTransform = CATransform3DRotate(identity, angle, 0, 1, 0);
    CATransform3D translationTransform = CATransform3DMakeTranslation(percent * kSliderViewWidth, 0, 0);
    return CATransform3DConcat(rotationTransform, translationTransform);
}

代码中使用了单位矩阵的m34属性,关于这个属性不明白的,请参照《CATransform3D 特效详解》。简言之,透视效果就是依据 "近大远小" 这一视觉特性,使人感觉空间是3D的呈现效果。而SliderViewController的View不但有旋转动画,也有平移动画。旋转的角度是90°,平移的大小是kSliderViewWidth。而且这两个动画是同时执行的,所以使用CATransform3DConcat将两个动画"合并"起来。注意代码中,旋转角度的初始值是 -M_PI_2, 即它是垂直面向我们的,也就是看不见 (Just Imagine)。

讲解了这么多,我们现在看看手势操作之后的效果。

动画特效十一:侧边栏效果_第7张图片


 但是大家注意到:这里是3D旋转效果,而不是2D平移。3D的旋转必须考虑到旋转轴(就是应该绕着哪里进行旋转)。在执行手势操作的时候,两个View之间会出现间隙,并且间隙距离有时候大,有时候小。导致出现这种现象的原因就是SliderViewController的View所绕的旋转轴不正确。默认情况,View所绕着的旋转轴是正中心的垂直轴,因为他们的锚点是(0.5, 0.5)。 但如果想旋转的时候,想让SliderViewController的View右边一直紧挨着 CenterViewController的View的左边,应该设置旋转轴是SliderViewController的View的右边,即改变它的锚点为(1, 0.5) 即可。转换效果图如下:

动画特效十一:侧边栏效果_第8张图片

所以在ViewDidload代码中,加入下面一句代码:

self.slideMenuVC.view.layer.anchorPoint = CGPointMake(1, 0.5);
我们再看看运行效果:

动画特效十一:侧边栏效果_第9张图片


此时,虽然两者之间有空白的间隙,但是注意到不管怎么移动,两只之间的空隙的距离是保持一致的,也就是说设置锚点值为(1, 0.5) 是起作用的,让其绕着右边进行旋转。而这个空白间隙,我们很容易想象到,是因为没有设置position造成的。所以在ViewDidload代码中加入下面的代码,一切就OK了。

self.slideMenuVC.view.layer.position = CGPointMake(0, screenHeight * 0.5);


3. 消除锯齿现象

注意到,在慢速拖拽的时候,可以看到SliderViewController中的View的各个Item交界处有一些锯齿现象。大图如下:

动画特效十一:侧边栏效果_第10张图片

这个时候,我们可以进行光栅化处理。

Core Animation 在动画过程中,会连续重绘View上面的所有内容并且不停的计算所有移动元素的透视大小等。而光栅化处理,会告诉Core Animation缓存layer的所有内容为一个Image,然后执行的动画效果其实是在处理这个缓存的Image,不需要进行View的重绘工作了。这样处理:1. 提高了动画执行的效率;2. 消除了锯齿现象。

所以我们可以在UIGestureRecognizerStateBegan手势开始的代码中,加入下面两句,防止锯齿现象。

self.slideMenuVC.view.layer.shouldRasterize = YES;
self.slideMenuVC.view.layer.rasterizationScale = [UIScreen mainScreen].scale;
当然,缓存成图片也是一个很耗内存的操作,所以,我们应该在动画结束之后,立刻取消光栅化,释放内存。所以在手势Cancel或者End的时候,加入以下代码:

self.slideMenuVC.view.layer.shouldRasterize = NO;

二、 导航栏上面的白色按钮也会有旋转效果

大家注意到:导航控制器上面的白色按钮的动画,有一种旋转的现象,但又不是普通的绕着x或者y轴的旋转。因为它是绕着x,y轴的斜线方向上面的旋转。

动画特效十一:侧边栏效果_第11张图片

旋转完180°之后,这个横条就会变成竖条(Just Imagine)。

但由于这个按钮是导航控制器的leftBarButtonItem,是导航控制器的一部分;当你进行3D旋转的时候,会改变导航控制器上面本来UI元素的层级关系,所以当按钮旋转到导航栏Title的后面的时候,Title显示,当旋转到导航栏Title的前面的时候,Title会被遮挡;因为在旋转过程中,Title会一直闪烁。解决的办法就是旋转button自己内容的imageView而不是button本身。因为button是相对于导航栏的,而button中的imageView是相对于button的,imageView的旋转不会影响到导航栏本身。所以在setToPercent: 中追加以下代码可以完成旋转工作。

CenterViewController *centerVC = [self.centerNavVC.childViewControllers firstObject];
UIButton *toggleButton = centerVC.flipButton;
// 不能直接旋转Button,因为它是导航条的一部分,会影响title的显示
toggleButton.imageView.layer.transform = CATransform3DMakeRotation(progress * M_PI, 1, 1, 0);
最终旋转的流程图如下:

动画特效十一:侧边栏效果_第12张图片


三、 点击侧边栏上面的Item,有收起侧边栏的动画

这个操作就非常简单了,可以使用block或者代理来完成。我是使用代理来实现的。

1. 在SliderViewController中定义代理quickView代理,并且在点击侧边栏每一项的时候调用代理方法。代码如下:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSArray *items = [MenuItemsHelper sharedMenuItems].items;
    MenuItem *item = items[indexPath.row];
    CenterViewController *centerVC = [self.centerNavViewController.childViewControllers firstObject];
    centerVC.item = item;
    if ([self.delegate respondsToSelector:@selector(quickView:)]) {
        [self.delegate quickView:self];
    }
}

2. ContainerViewController成为代理并且实现代理方法。

self.slideMenuVC.delegate = self;
- (void)quickView:(SlideMenuViewController *)slideMenuVC {
    [self toggleSideMenu];
}
- (void)toggleSideMenu {
    CGFloat ratio = self.centerNavVC.view.x / kSliderViewWidth;
    BOOL isOpen = ratio == 1.0;
    CGFloat toggleProgress = isOpen ? 0.0 : 1.0;
    [UIView animateWithDuration:0.25 animations:^{
        [self setToPercent:toggleProgress];
    } completion:^(BOOL finished) {
        if (isOpen) {
            [self.coverView removeFromSuperview];
        }
    }];
}

至此,整个的侧边栏动画效果就算总结完毕了。

你可能感兴趣的:(Core,Animation)