好久没有进行 "动画特效" 这一系列了。今天和大家继续分享动画特效:“侧边栏效果”。由于侧边栏效果的动画效果各式各样了,所以我大致分为三种进行说明;
样式一:
这个样式的动画效果还是比较容易处理的。主要是通过计算view的宽度或者位置来实现。我这样说,也许有点模糊了。让我们来分析程序的架构模式。
默认情况下是"图一"模式:
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; }
注:由于这样的侧边栏效果相对而言比较简单,我并没有用代码进行详细的讲解;后面的两个侧边栏效果我会结合代码进行比较详细的说明。
样式二:
首先,我们来分析程序的架构模式。如下图:
同样的道理,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。
样式三
功能分析:
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)。
讲解了这么多,我们现在看看手势操作之后的效果。
但是大家注意到:这里是3D旋转效果,而不是2D平移。3D的旋转必须考虑到旋转轴(就是应该绕着哪里进行旋转)。在执行手势操作的时候,两个View之间会出现间隙,并且间隙距离有时候大,有时候小。导致出现这种现象的原因就是SliderViewController的View所绕的旋转轴不正确。默认情况,View所绕着的旋转轴是正中心的垂直轴,因为他们的锚点是(0.5, 0.5)。 但如果想旋转的时候,想让SliderViewController的View右边一直紧挨着
CenterViewController的View的左边,应该设置旋转轴是SliderViewController的View的右边,即改变它的锚点为(1, 0.5) 即可。转换效果图如下:
所以在ViewDidload代码中,加入下面一句代码:
self.slideMenuVC.view.layer.anchorPoint = CGPointMake(1, 0.5);
我们再看看运行效果:
此时,虽然两者之间有空白的间隙,但是注意到不管怎么移动,两只之间的空隙的距离是保持一致的,也就是说设置锚点值为(1, 0.5) 是起作用的,让其绕着右边进行旋转。而这个空白间隙,我们很容易想象到,是因为没有设置position造成的。所以在ViewDidload代码中加入下面的代码,一切就OK了。
self.slideMenuVC.view.layer.position = CGPointMake(0, screenHeight * 0.5);
3. 消除锯齿现象
注意到,在慢速拖拽的时候,可以看到SliderViewController中的View的各个Item交界处有一些锯齿现象。大图如下:
这个时候,我们可以进行光栅化处理。
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轴的斜线方向上面的旋转。
旋转完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);
最终旋转的流程图如下:
三、 点击侧边栏上面的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]; } }]; }
至此,整个的侧边栏动画效果就算总结完毕了。
版权声明:本文为博主原创文章,未经博主允许不得转载。