本人录制技术视频地址:https://edu.csdn.net/lecturer/1899 欢迎观看。
好久没有进行 "动画特效" 这一系列了。今天和大家继续分享动画特效:“侧边栏效果”。由于侧边栏效果的动画效果各式各样了,所以我大致分为三种进行说明;
样式一:
这个样式的动画效果还是比较容易处理的。主要是通过计算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);
}
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];
}
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];
}
我这里定义的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;
}
- (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);
}
讲解了这么多,我们现在看看手势操作之后的效果。
但是大家注意到:这里是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);
注意到,在慢速拖拽的时候,可以看到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];
}
}
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];
}
}];
}
至此,整个的侧边栏动画效果就算总结完毕了。