自定义转场动画
两套体系
- Modal present / dismiss:协议
UIViewControllerTransitioningDelegate - Navigation push / pop:协议
UINavigationControllerDelegate
核心动画协议:UIViewControllerAnimatedTransitioning(动画本体)
交互手势可选:UIViewControllerInteractiveTransitioning(侧滑交互式转场)
实现一个简单的Model present/dismiss动画
//A->B页面
// 设置代理为self.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
SecondViewController *s = [[SecondViewController alloc] init];
s.modalPresentationStyle = UIModalPresentationFullScreen;
s.transitioningDelegate = self;
[self presentViewController:s animated:YES completion:nil];
}
UIViewControllerTransitioningDelegate这个协议,返回一个处理转场动画的对象。
@interface ViewController () <UIViewControllerTransitioningDelegate>
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
MyAnimation *animation =[[MyAnimation alloc] init];
animation.isPresent = true;
return animation;
}
处理转场动画的这个对象需要遵守UIViewControllerAnimatedTransitioning。
@interface MyAnimation : NSObject <UIViewControllerAnimatedTransitioning>
// 代理对象无法区分是dismiss还是present。如果要用同一个对象。就需要添加一个字段进行区分。
@property (nonatomic) bool isPresent;
@end
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{
return 5;
}
// This method can only be a no-op if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
// 获取 fromVC 和 toVC
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 获取容器视图
UIView *containerView = [transitionContext containerView];
// 这里的if-else的代码都一样,只是为了区分。顺便方便打印不同UIViewController.view的地址。
// 实际开发中,可以根据需要确定是否用同一个动画。
if (self.isPresent){
NSLog(@"from %p", fromVC.view);
NSLog(@"to %p", toVC.view);
NSArray *subView = containerView.subviews;
for (int i=0;i< subView.count;i ++){
NSLog(@"%p %d", subView[i], i);
}
// fromVC默认就添加到了containerView
toVC.view.alpha = 0;
// 将目标视图加入容器
[containerView addSubview:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}else{
// 退场动画
NSLog(@"from %p", fromVC.view);
NSLog(@"to %p", toVC.view);
NSArray *subView = containerView.subviews;
for (int i=0;i< subView.count;i ++){
NSLog(@"%p %d", subView[i], i);
}
// fromVC.view自动添加到了containerView .fromVC就是要退出的View
toVC.view.alpha = 0;
// 将目标视图加入容器
[containerView addSubview:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
}
同理,dismiss的转场动画也需要设置代理.
@interface SecondViewController () <UIViewControllerTransitioningDelegate>
@end
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor greenColor];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.transitioningDelegate = self;
[self dismissViewControllerAnimated:YES completion:nil];
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismisse {
MyAnimation *animation = [[MyAnimation alloc] init];
animation.isPresent = false;
return animation;
}
实现一个简单的navigation的push和pop动画。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
self.navigationController.delegate = self;
}
@interface ViewController () <UINavigationControllerDelegate>
需要注意的是,如果是大小变化。
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC{
MyAnimation *animation = [[MyAnimation alloc] init];
if (operation == UINavigationControllerOperationPush){
animation.isPush = YES;
}else{
animation.isPush = false;
}
return animation;
}
第二个页面pop的时候,会回调根视图控制器的代理方法。不需要再次delegate=self。
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
// 获取 fromVC 和 toVC
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 获取容器视图
UIView *containerView = [transitionContext containerView];
if (self.isPush) {
// fromVC.view会自动添加到当前视图
[containerView addSubview:toVC.view];
toVC.view.alpha = 0;
[UIView animateWithDuration:5 animations:^{
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
} else {
// fromVC.view会自动添加到当前视图
[containerView insertSubview:toVC.view aboveSubview:fromVC.view];
toVC.view.alpha = 0;
[UIView animateWithDuration:5 animations:^{
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
}
}
hero动画。
掌握前面的知识点,就可以做hero动画。核心就是snapshotViewAfterScreenUpdates:NO。
将fromVC.view.hidden = yes。
主要,如果只是大小的变化,直接用snapshotViewAfterScreenUpdates就行。但是如果有颜色的变化。则需要在上面覆盖一个视图才能做动画。
结束后,记得恢复原来的状态。
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
// 获取 fromVC 和 toVC
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 获取容器视图
UIView *containerView = [transitionContext containerView];
if (self.isPush) {
ViewController *fromC = (ViewController *)fromVC;
SecondViewController *toC = (SecondViewController *)toVC;
UIView *fromAnimationView = [fromC animationView];
UIView *toAnimationView = [toC animationView];
// 这个UIView不能修改颜色了
UIView *copyFromView = [fromAnimationView snapshotViewAfterScreenUpdates:NO];
copyFromView.frame = CGRectMake(100, 200, 300, 300);
[containerView addSubview:toC.view];
[containerView addSubview:copyFromView];
// 先隐藏之前的视图
fromAnimationView.hidden = YES;
toAnimationView.hidden = YES;
toVC.view.alpha = 0;
// copyFromView的无法做backgrounColor动画
// copyFromView.backgroundColor = [UIColor greenColor];
UIView *bgView = [[UIView alloc] initWithFrame:copyFromView.bounds];
bgView.backgroundColor = [UIColor greenColor];
[copyFromView addSubview:bgView];
[UIView animateWithDuration:5 animations:^{
toVC.view.alpha = 1;
bgView.backgroundColor = [UIColor redColor];
copyFromView.frame = CGRectMake(100, 200, 100, 100);
bgView.frame = CGRectMake(0, 0, 100, 100);
} completion:^(BOOL finished) {
// 恢复原来的
fromAnimationView.hidden = NO;
toAnimationView.hidden = NO;
[transitionContext completeTransition:finished];
}];
} else {
// todo
}
}