MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C动画与视图控制器转换技巧

2021-11-256.3k 阅读

Objective-C 动画基础

核心动画框架(Core Animation)

在 Objective-C 中,核心动画框架(Core Animation)是实现动画效果的基础。它提供了一种高效的方式来创建复杂的动画,并且能与视图层次结构紧密结合。Core Animation 是一个基于图层(CALayer)的动画框架,视图(UIView)实际上是对 CALayer 的封装,这意味着我们既可以通过 UIView 的动画方法,也可以直接操作 CALayer 来实现动画。

例如,要创建一个简单的 CALayer 并添加到视图中:

#import <QuartzCore/QuartzCore.h>

// 创建一个新的图层
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 200, 200);
layer.backgroundColor = [UIColor redColor].CGColor;

// 将图层添加到视图的图层上
[self.view.layer addSublayer:layer];

基本动画类型

  1. 位置动画(CABasicAnimation) 位置动画可以改变图层的位置。假设我们有一个图层,想要让它从当前位置移动到另一个位置:
CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
positionAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(300, 300)];
positionAnimation.duration = 2.0;
[layer addAnimation:positionAnimation forKey:@"positionAnimation"];

这里,我们创建了一个基于 position 键路径的基本动画,toValue 设定了动画结束时的位置,duration 定义了动画持续时间。

  1. 缩放动画 缩放动画通过改变图层的 transform.scale 来实现。比如,将一个图层放大两倍:
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.toValue = @2.0;
scaleAnimation.duration = 1.5;
[layer addAnimation:scaleAnimation forKey:@"scaleAnimation"];
  1. 旋转动画 旋转动画则是利用 transform.rotation 键路径。下面的代码实现了一个图层顺时针旋转 360 度:
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAnimation.toValue = @(M_PI * 2);
rotationAnimation.duration = 3.0;
[layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];

动画组(CAAnimationGroup)

有时候,我们需要同时执行多个动画,这就用到了动画组。例如,同时执行位置和缩放动画:

CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
positionAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(300, 300)];
positionAnimation.duration = 2.0;

CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.toValue = @2.0;
scaleAnimation.duration = 2.0;

CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = @[positionAnimation, scaleAnimation];
animationGroup.duration = 2.0;
[layer addAnimation:animationGroup forKey:@"groupAnimation"];

在这个例子中,CAAnimationGroup 包含了位置动画和缩放动画,它们会同时开始并持续相同的时间。

UIView 动画

简单的 UIView 动画方法

UIView 类提供了一些便捷的类方法来创建动画,这些方法基于 Core Animation 进行了封装,使用起来更加简单。例如,改变视图的背景颜色:

[UIView animateWithDuration:1.0 animations:^{
    self.view.backgroundColor = [UIColor blueColor];
}];

这段代码在 1 秒内将视图的背景颜色从当前颜色渐变到蓝色。animateWithDuration:animations: 方法接受两个参数,第一个是动画持续时间,第二个是一个包含需要动画的属性变化的块。

复杂 UIView 动画

除了简单的属性变化,UIView 动画还能实现更复杂的效果,比如淡入淡出和移动同时进行。

[UIView animateWithDuration:2.0 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
    self.view.alpha = 0.5;
    self.view.frame = CGRectMake(150, 150, self.view.bounds.size.width, self.view.bounds.size.height);
} completion:^(BOOL finished) {
    if (finished) {
        NSLog(@"动画完成");
    }
}];

这里,animateWithDuration:delay:options:animations:completion: 方法允许我们设置动画延迟、动画曲线(这里是 UIViewAnimationOptionCurveEaseInOut,表示缓入缓出),并且在动画完成时执行一个完成块。

关键帧动画(UIViewKeyframeAnimation)

关键帧动画允许我们定义一系列的关键帧,每个关键帧有特定的时间和属性值。例如,创建一个沿着特定路径移动的动画:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(100, 100)];
[path addLineToPoint:CGPointMake(200, 200)];
[path addLineToPoint:CGPointMake(300, 100)];

NSArray *keyTimes = @[@0.0, @0.5, @1.0];
NSArray *values = @[
    [NSValue valueWithCGPoint:path.currentPoint],
    [NSValue valueWithCGPoint:[path pointAtIndex:1]],
    [NSValue valueWithCGPoint:[path pointAtIndex:2]]
];

[UIView animateKeyframesWithDuration:3.0 delay:0 options:UIViewKeyframeAnimationOptionCalculationModeCubic animations:^{
    [UIView addKeyframeWithRelativeStartTime:0 relativeDuration:0.33 animations:^{
        self.view.center = [values[0] CGPointValue];
    }];
    [UIView addKeyframeWithRelativeStartTime:0.33 relativeDuration:0.33 animations:^{
        self.view.center = [values[1] CGPointValue];
    }];
    [UIView addKeyframeWithRelativeStartTime:0.66 relativeDuration:0.34 animations:^{
        self.view.center = [values[2] CGPointValue];
    }];
} completion:nil];

在这段代码中,我们首先定义了一个贝塞尔路径,然后设置了关键时间点和对应的值。通过 animateKeyframesWithDuration:delay:options:animations:completion: 方法以及 addKeyframeWithRelativeStartTime:relativeDuration:animations: 方法来实现关键帧动画。

视图控制器转换中的动画

基本视图控制器转换

在 Objective-C 中,视图控制器之间的转换是常见操作,并且可以添加动画效果。例如,使用 presentViewController:animated:completion: 方法来呈现一个新的视图控制器:

SecondViewController *secondVC = [[SecondViewController alloc] init];
[self presentViewController:secondVC animated:YES completion:nil];

这里,animated:YES 表示以动画形式呈现视图控制器,默认的动画是从底部滑入。

自定义视图控制器转换动画

  1. 实现 UIViewControllerAnimatedTransitioning 协议 要实现自定义的视图控制器转换动画,我们需要创建一个类来实现 UIViewControllerAnimatedTransitioning 协议。假设我们要实现一个从右侧滑入的动画:
#import <UIKit/UIKit.h>

@interface SlideInFromRightAnimator : NSObject <UIViewControllerAnimatedTransitioning>

@end

@implementation SlideInFromRightAnimator

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.5;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    
    UIView *containerView = [transitionContext containerView];
    CGRect screenBounds = [UIScreen mainScreen].bounds;
    toViewController.view.frame = CGRectMake(screenBounds.size.width, 0, screenBounds.size.width, screenBounds.size.height);
    
    [containerView addSubview:toViewController.view];
    
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        toViewController.view.frame = screenBounds;
        fromViewController.view.frame = CGRectMake(-screenBounds.size.width, 0, screenBounds.size.width, screenBounds.size.height);
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
    }];
}

@end

在这个类中,transitionDuration: 方法定义了动画持续时间,animateTransition: 方法实现了具体的动画逻辑。

  1. 设置转换代理 在发起转换的视图控制器中,我们需要设置转换代理来使用自定义的动画。例如:
#import "SlideInFromRightAnimator.h"

@interface FirstViewController : UIViewController <UIViewControllerTransitioningDelegate>

@end

@implementation FirstViewController

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"showSecondVC"]) {
        SecondViewController *secondVC = segue.destinationViewController;
        secondVC.transitioningDelegate = self;
        secondVC.modalPresentationStyle = UIModalPresentationCustom;
    }
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return [[SlideInFromRightAnimator alloc] init];
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return [[SlideInFromRightAnimator alloc] init];
}

@end

prepareForSegue:sender: 方法中,我们设置了目标视图控制器的 transitioningDelegate 为当前视图控制器,并设置 modalPresentationStyleUIModalPresentationCustomanimationControllerForPresentedController:presentingController:sourceController:animationControllerForDismissedController: 方法返回我们自定义的动画控制器实例。

交互式视图控制器转换动画

  1. 实现 UIPercentDrivenInteractiveTransition 类 交互式视图控制器转换动画允许用户通过手势等交互方式来控制转换过程。我们需要创建一个类继承自 UIPercentDrivenInteractiveTransition。例如,通过拖动视图来控制视图控制器的转换:
#import <UIKit/UIKit.h>

@interface InteractiveTransition : UIPercentDrivenInteractiveTransition

@property (nonatomic, assign) BOOL shouldCompleteTransition;

@end

@implementation InteractiveTransition

- (void)updateInteractiveTransition:(CGFloat)percentComplete {
    if (self.shouldCompleteTransition) {
        [super updateInteractiveTransition:percentComplete];
    } else {
        [super updateInteractiveTransition:1 - percentComplete];
    }
}

- (void)finishInteractiveTransition {
    if (self.shouldCompleteTransition) {
        [super finishInteractiveTransition];
    } else {
        [super cancelInteractiveTransition];
    }
}

- (void)cancelInteractiveTransition {
    if (self.shouldCompleteTransition) {
        [super cancelInteractiveTransition];
    } else {
        [super finishInteractiveTransition];
    }
}

@end
  1. 添加手势识别器并关联交互 在视图控制器中,我们添加手势识别器并与交互过渡关联起来。例如,添加一个平移手势:
#import "InteractiveTransition.h"

@interface FirstViewController : UIViewController <UIViewControllerTransitioningDelegate, UIGestureRecognizerDelegate>

@property (nonatomic, strong) InteractiveTransition *interactiveTransition;

@end

@implementation FirstViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    [self.view addGestureRecognizer:panGesture];
    panGesture.delegate = self;
}

- (void)handlePan:(UIPanGestureRecognizer *)gesture {
    CGPoint translation = [gesture translationInView:self.view];
    CGFloat progress = fabs(translation.x) / self.view.bounds.size.width;
    progress = MIN(1.0, MAX(0.0, progress));
    
    switch (gesture.state) {
        case UIGestureRecognizerStateBegan: {
            self.interactiveTransition = [[InteractiveTransition alloc] init];
            [self performSegueWithIdentifier:@"showSecondVC" sender:self];
            break;
        }
        case UIGestureRecognizerStateChanged: {
            [self.interactiveTransition updateInteractiveTransition:progress];
            break;
        }
        case UIGestureRecognizerStateEnded: {
            if (progress > 0.5) {
                self.interactiveTransition.shouldCompleteTransition = YES;
                [self.interactiveTransition finishInteractiveTransition];
            } else {
                self.interactiveTransition.shouldCompleteTransition = NO;
                [self.interactiveTransition cancelInteractiveTransition];
            }
            self.interactiveTransition = nil;
            break;
        }
        default:
            break;
    }
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return [[SlideInFromRightAnimator alloc] init];
}

- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator {
    return self.interactiveTransition;
}

@end

handlePan: 方法中,我们根据手势的状态和移动距离来控制交互过渡的进度。interactionControllerForPresentation: 方法返回我们的交互过渡实例,以便系统在需要时调用交互相关的方法。

动画性能优化

减少重绘

  1. 不透明视图设置 确保视图的 opaque 属性设置正确。如果视图是不透明的,将 opaque 设置为 YES,这样系统在绘制时可以更高效地处理,因为不需要考虑透明度。例如:
self.view.opaque = YES;
  1. 避免频繁更新属性 避免在动画过程中频繁更新那些会触发重绘的属性。例如,尽量减少对 backgroundColor 等属性的重复设置,除非必要。如果确实需要更新,可以将多个更新合并在一个动画块中。

硬件加速

  1. 使用离屏渲染 某些情况下,我们可以利用离屏渲染来提高动画性能。通过设置图层的 shouldRasterize 属性为 YES,可以让图层在一个单独的缓冲区中渲染,然后再合成到屏幕上。例如:
layer.shouldRasterize = YES;
layer.rasterizationScale = [UIScreen mainScreen].scale;

需要注意的是,离屏渲染会消耗额外的内存,所以要谨慎使用。

  1. 利用 GPU 加速 Core Animation 会自动利用 GPU 进行一些动画的渲染,尤其是对于基于 transform 的动画,如缩放、旋转等。尽量使用这些基于 transform 的属性来实现动画,而不是改变视图的 framebounds,因为改变 framebounds 会触发重新布局和重绘,而 transform 操作可以在 GPU 上高效执行。

优化动画时长和帧率

  1. 合适的动画时长 选择合适的动画时长很重要。过短的动画时长可能导致动画看起来卡顿,而过长的动画时长可能会让用户感到厌烦。一般来说,简单的过渡动画可以设置在 0.2 - 0.5 秒之间,复杂的动画可以适当延长,但也不宜过长。

  2. 保持帧率 尽量保持动画的帧率在 60fps。为了实现这一点,要避免在动画过程中执行复杂的计算或 I/O 操作。如果有必要,可以将这些操作放在后台线程中执行,但要注意线程安全问题,确保与动画相关的属性更新在主线程中进行。

通过以上这些技巧和方法,我们可以在 Objective - C 开发中实现丰富多样且性能优良的动画效果以及视图控制器转换动画,为用户带来更加流畅和美观的体验。无论是简单的 UI 动画还是复杂的视图控制器交互转换,都可以通过合理运用这些技术来实现。同时,在开发过程中要不断进行测试和优化,以确保动画在不同设备和场景下都能有良好的表现。