标题党,动画知识只是在写转场动画的时候顺便提一下。不敢说很棒,但是不可缺少的知识点全都有。
现在,转场动画的文章有好多,但大部分都是在给鱼而不是给渔,所以,今天恰好看到一些相关的知识顺便给大家献丑。 包教会!
首先看一下视图出现的代码, 模态与推出:
[self.navigationController pushViewController:[UIViewController new] animated:YES];
[self presentViewController:[UIViewController new] animated:YES completion:nil];
当你想使用视图转场动画的时候, 首先要明确你是用模态还是推出。然后重载方法。
- 推出(这方法名的确符合OC风格-。-)
- (nullable id )
navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id ) animationController;
- (nullable id )
navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC;
- 模态
- (nullable id )
animationControllerForPresentedController: (UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController: (UIViewController *)source;
- (nullable id )
animationControllerForDismissedController: (UIViewController *)dismissed;
- (nullable id )
interactionControllerForPresentation:(id )animator;
- (nullable id )
interactionControllerForDismissal:(id ) animator;
二种转场中出现了UIViewControllerInteractiveTransitioning
与 UIViewControllerAnimatedTransitioning
从字面意思我们可以看出来,前者是负责交互的, 后者是负责动画的。 所以我们要改变转场动画, 要自己去实现UIViewControllerAnimatedTransitioning
<这是非交互的转场,指的是完全按照系统指定的切换机制,用户无法中途取消或者控制进度切换>, 实现有交互的转场就要用UIViewControllerInteractiveTransitioning
来搞了。 我们先来说AnimatedTrans。
UIViewControllerAnimatedTransitioning
先来看下这个协议中有什么方法:
@protocol UIViewControllerAnimatedTransitioning
// This is used for percent driven interactive transitions, as well as for container controllers that have companion animations that might need to
// synchronize with the main animation.
- (NSTimeInterval)transitionDuration:(nullable id )transitionContext;
// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id )transitionContext;
@optional
// This is a convenience and if implemented will be invoked by the system when the transition context's completeTransition: method is invoked.
- (void)animationEnded:(BOOL) transitionCompleted;
@end
二个必须实现的方法, 还有一个可选的, 报告动作结束的。 为了保证我们在VC中代码的高度可读性,我们创建一个新的类,实现这个协议中的方法,然后返回这个类的对象。(由于NC, VC只是重载的方法不同,下边就以VC为例说明)。
- 创建 AnimatedVC 继承于 NSObject并实现协议
#import
#import
@interface AnimatedVC : NSObject
@end
- 在.M中实现方法(下边的说明我在代码注释中写明)
//
// AnimatedVC.m
// Created by Dylan on 16/3/29.
//
#import "AnimatedVC.h"
@interface AnimatedVC ()
@property ( nonatomic, strong ) id transitionContext;
@end
@implementation AnimatedVC
- (NSTimeInterval)transitionDuration:(nullable id )transitionContext {
// 动画的时间 duration
return .3;
}
// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id )transitionContext {
self.transitionContext = transitionContext;
// 动画 开始
// [step.1] 获取到当前VC 目标VC UITransitionContextFromViewControllerKey UITransitionContextToViewControllerKey
UIViewController * fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController * toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// [step.2] 获取当前的容器视图
UIView * contentView = [transitionContext containerView];
// 添加原本与目标的视图
[contentView addSubview:fromVC.view];
[contentView addSubview:toVC.view];
// 设置目标视图大小
toVC.view.frame = CGRectMake(0, fromVC.view.frame.size.height, fromVC.view.frame.size.width, 200);
// [step.3] 动画的处理 <如果你是从视图上的某一个View扩展到一个新的视图, 这个时候需要给目标VC的View加上蒙版, 对蒙版进行动画操作。>
// 下边的操作比较通用
/*!
* `[CABasicAnimation animationWithKeyPath:@"frame"];` keyPath 为支持Animation的属性名称
* 详见这篇文章: `http://blog.csdn.net/majiakun1/article/details/46426727`
*/
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
/*!
* 动画的初始值, 这里的类型是id.
*/
// animation.fromValue = [NSValue valueWithCGRect:nil];
/*!
* 动画的结束值, 这里的类型是id. 当然还有byValue 动画的参考值,这里我们把y上移动200
*/
animation.toValue = @(-200);
/*!
* 动画持续时间.
*/
animation.duration = .2;
/*!
* 动画函数, 这里有4种效果。
* - kCAMediaTimingFunctionLinear选项创建了一个线性的计时函数,同样也是CAAnimation的timingFunction属性为空时候的默认函数。线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),但是默认来说它看起来很奇怪,因为对大多数的动画来说确实很少用到。
* - kCAMediaTimingFunctionEaseIn常量创建了一个慢慢加速然后突然停止的方法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的发射。
* - kCAMediaTimingFunctionEaseOut则恰恰相反,它以一个全速开始,然后慢慢减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声。
* - kCAMediaTimingFunctionEaseInEaseOut创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。如果只可以用一种缓冲函数的话,那就必须是它了。那么你会疑惑为什么这不是默认的选择,实际上当使用UIView的动画方法时,他的确是默认的,但当创建CAAnimation的时候,就需要手动设置它了。
*/
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
/*
* 下边2行代码, 是否在结束时移除动画效果, presentingLayer
*/
animation.removedOnCompletion = false;
animation.fillMode = kCAFillModeForwards;
animation.delegate = self;
[toVC.view.layer addAnimation:animation forKey:@"frameAnimation"];}
// This is a convenience and if implemented will be invoked by the system when the transition context's completeTransition: method is invoked.
- (void)animationEnded:(BOOL) transitionCompleted {
// 动画结束, 完成转场
[self.transitionContext completeTransition:YES];
}
@end
至此,一个简单的转场动画就OK了。 效果很简单,就是想ActionSheet一样从底部弹出来。
当然,这里的动画随你搞,View随你改变。知道了原理,相必大家就会用了,直接去网上搞个pop或者canvas的动画库、或者自己去写动画,总之、一切随你所愿。当然模态的话,dismissedController就是你要给定的消失动画需要覆写的方法。
UIPercentDrivenInteractiveTransition
- 有手势交互的动画,比如说,拖动控制这个
vc
一点点变化。
这里我留下伏笔UIPercentDrivenInteractiveTransition
CAAnimation
其实看到最后, 今天主要是想聊聊动画,平时不管是开发一些框架性的东西还是开发一些业务相关的东西,很少解除到动画。
一般的来说:
一个复杂的动画 最终都将变成 几个简单的动画。
一个复杂的动画效果 最终都将变成简单的数学计算。
比如说我们模拟一个弹簧的效果
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) UIImageView *ballView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//add ball image view
UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
self.ballView = [[UIImageView alloc] initWithImage:ballImage];
[self.containerView addSubview:self.ballView];
//animate
[self animate];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//replay animation on tap
[self animate];
}
- (void)animate
{
//reset ball to top of screen
self.ballView.center = CGPointMake(150, 32);
//create keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 1.0;
animation.delegate = self;
animation.values = @[
[NSValue valueWithCGPoint:CGPointMake(150, 32)],
[NSValue valueWithCGPoint:CGPointMake(150, 268)],
[NSValue valueWithCGPoint:CGPointMake(150, 140)],
[NSValue valueWithCGPoint:CGPointMake(150, 268)],
[NSValue valueWithCGPoint:CGPointMake(150, 220)],
[NSValue valueWithCGPoint:CGPointMake(150, 268)],
[NSValue valueWithCGPoint:CGPointMake(150, 250)],
[NSValue valueWithCGPoint:CGPointMake(150, 268)]
];
animation.timingFunctions = @[
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]
];
animation.keyTimes = @[@0.0, @0.3, @0.5, @0.7, @0.8, @0.9, @0.95, @1.0];
//apply animation
self.ballView.layer.position = CGPointMake(150, 268);
[self.ballView.layer addAnimation:animation forKey:nil];
}
@end
其实一个效果被拆分成了若干个细分的动作。
再比如说 我们看到手机QQ的消息数量气泡可以拖动,其实这个动画可以慢慢的简化为几个小的组合动画,然后通过取到关键的点与控制点来控制小球的动画。
就像我们模拟一个小球的变换一样。 实际上,这个小球由4段弧线组成。
UIBezierPath * rectPath = [UIBezierPath bezierPath];
[rectPath moveToPoint:A];
[rectPath addCurveToPoint:B controlPoint1:A_1 controlPoint2:A_2];
[rectPath addCurveToPoint:C controlPoint1:B_1 controlPoint2:B_2];
[rectPath addCurveToPoint:D controlPoint1:C_1 controlPoint2:C_2];
[rectPath addCurveToPoint:A controlPoint1:D_1 controlPoint2:D_2];
[[UIColor redColor] setStroke];
[[UIColor redColor] setFill];
[rectPath fill];
[rectPath stroke];
每一段弧线之间又有2个控制点,实际上我们就是控制这些点的动作就可以简单的恩完成这个动画。
下边附上代码,我就懒得写了(这些东西也都是慢慢的学习一些碎片化的知识。但是需要整理。我也在之前的博客中发出了原pdf,大家可以下载学习,感谢yang
)。顺便附上一个非常好的效果 Loading
以下是变化小球的代码, 很简单一看就懂。
.h
#import
typedef NS_OPTIONS(NSInteger, BZDirection) {
BZDirection_D = 0,
BZDirection_B = 1,
};
@interface BZView : UIView
@property ( nonatomic, assign ) CGFloat progress;
@property ( nonatomic, assign ) BZDirection direction;
@property ( nonatomic, assign ) CGRect boxFrame;
@end
.m
//
// BZView.m
// AnimationDemo
//
// Created by Dylan on 16/3/28.
// Copyright © 2016年 Dylan. All rights reserved.
//
#import "BZView.h"
#define BoxSize 90 /* 外框大小 */
#define AnimatedWidth [UIScreen mainScreen].bounds.size.width - BoxSize /* 限制活动范围 */
@implementation BZView
- (instancetype) initWithFrame: (CGRect) frame {
self = [super initWithFrame:frame];
if ( self ) {
self.backgroundColor = [UIColor whiteColor];
self.progress = 0.5;
}
return self;
}
- (void) drawRect: (CGRect) rect {
// Draw
// Dash
CGFloat dashArray[3];
dashArray[0] = 3;
dashArray[1] = 3;
dashArray[2] = 3;
// 外边辅助边框
UIBezierPath * boxPath = [UIBezierPath bezierPathWithRect:_boxFrame];
[[UIColor greenColor] setStroke];
boxPath.lineWidth = 1;
[boxPath setLineDash:dashArray count:3 phase:1];
[boxPath stroke];
// 控制点的偏移量
CGFloat offSet = BoxSize / 3.6;
// 每次变化progress后, 偏移量的计算
CGFloat moveOffSet = (BoxSize * .2) * fabs( self.progress - .5 ) * 2;
// 外部矩形的X, Y
CGFloat boxX = self.boxFrame.origin.x;
CGFloat boxY = self.boxFrame.origin.y;
// 外部矩形的中心
CGPoint boxCenter = CGPointMake(boxX + BoxSize / 2., boxY + BoxSize / 2.);
// 圆
CGPoint A = CGPointMake(boxCenter.x, boxY + moveOffSet);
CGPoint B = CGPointMake(self.direction == BZDirection_D ? boxX + BoxSize : boxX + BoxSize + moveOffSet * 2., boxCenter.y);
CGPoint C = CGPointMake(boxCenter.x, boxY + BoxSize - moveOffSet);
CGPoint D = CGPointMake(self.direction == BZDirection_D ? boxX - moveOffSet * 2 : boxX, boxCenter.y);
// 控制点
CGPoint A_1 = CGPointMake(A.x + offSet, A.y);
CGPoint A_2 = CGPointMake(B.x, self.direction == BZDirection_D ? B.y - offSet : B.y - offSet + moveOffSet);
CGPoint B_1 = CGPointMake(B.x, self.direction == BZDirection_D ? B.y + offSet : B.y + offSet - moveOffSet);
CGPoint B_2 = CGPointMake(C.x + offSet, C.y);
CGPoint C_1 = CGPointMake(C.x - offSet, C.y);
CGPoint C_2 = CGPointMake(D.x, self.direction == BZDirection_D ? D.y + offSet - moveOffSet : D.y + offSet);
CGPoint D_1 = CGPointMake(D.x, self.direction == BZDirection_D ? D.y - offSet + moveOffSet : D.y - offSet);
CGPoint D_2 = CGPointMake(A.x - offSet, A.y);
// 圆形
UIBezierPath * rectPath = [UIBezierPath bezierPath];
[rectPath moveToPoint:A];
[rectPath addCurveToPoint:B controlPoint1:A_1 controlPoint2:A_2];
[rectPath addCurveToPoint:C controlPoint1:B_1 controlPoint2:B_2];
[rectPath addCurveToPoint:D controlPoint1:C_1 controlPoint2:C_2];
[rectPath addCurveToPoint:A controlPoint1:D_1 controlPoint2:D_2];
[[UIColor redColor] setStroke];
[[UIColor redColor] setFill];
[rectPath fill];
[rectPath stroke];
// 控制点 连接
UIBezierPath * control = [UIBezierPath bezierPath];
[control moveToPoint:A];
[control addLineToPoint:A_1];
[control addLineToPoint:A_2];
[control addLineToPoint:B];
[control addLineToPoint:B_1];
[control addLineToPoint:B_2];
[control addLineToPoint:C];
[control addLineToPoint:C_1];
[control addLineToPoint:C_2];
[control addLineToPoint:D];
[control addLineToPoint:D_1];
[control addLineToPoint:D_2];
[control addLineToPoint:A];
[control setLineDash:dashArray count:3 phase:1];
[[UIColor blueColor] setStroke];
[control stroke];
[control appendPath:({
UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:A radius:3 startAngle:0 endAngle:M_PI * 4 clockwise:YES];
[[UIColor purpleColor] setFill];
[path fill];
path;
})];
}
- (void) setProgress: (CGFloat) progress {
// 设置方向
if ( progress <= 0.5 ) {
_direction = BZDirection_B;
NSLog(@"B 点 移动");
} else {
_direction = BZDirection_D;
NSLog(@"D 点 移动");
}
_progress = progress;
// 重置外部边框大小
CGFloat x = self.center.x - BoxSize / 2. + ( progress - .5 ) * ( AnimatedWidth - BoxSize );
CGFloat y = self.center.y - BoxSize / 2.;
_boxFrame = CGRectMake(x, y, BoxSize, BoxSize);
[self setNeedsDisplay];
}
@end