iOS不改变frame,能写出一个位移动画

时间:2024-06-14 11:44:27

在iOS开发中,如果不想通过直接修改视图的frame属性来实现位移动画,有十多种方法,总结如下:

方法一:使用CABasicAnimation

可以使用Core Animation的CABasicAnimation类或UIView动画块来实现。下面分别展示这两种方法。

1.CABasicAnimation

CABasicAnimation可以用于对视图的图层属性进行动画化。通过更改图层的position属性,可以实现位移动画而不直接修改视图的frame

示例代码:

#import <QuartzCore/QuartzCore.h>

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

// 创建位移动画
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.fromValue = [NSValue valueWithCGPoint:view.layer.position];
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(300, 300)];
animation.duration = 1.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

// 应用动画到图层
[view.layer addAnimation:animation forKey:@"positionAnimation"];

// 最终位置
view.layer.position = CGPointMake(300, 300);

     2. CAKeyframeAnimation

CAKeyframeAnimation允许我们通过指定一系列的关键帧来实现复杂的动画效果。它可以用于改变图层的position属性,从而实现视图的移动。

示例代码:

#import <QuartzCore/QuartzCore.h>

// 创建一个视图
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

// 创建关键帧动画
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];

// 设置起点和终点
CGPoint startPoint = view.layer.position;
CGPoint endPoint = CGPointMake(300, 300);

// 创建路径
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, startPoint.x, startPoint.y);
CGPathAddLineToPoint(path, NULL, endPoint.x, endPoint.y);

// 将路径赋值给动画
animation.path = path;
animation.duration = 2.0;

// 添加动画到图层
[view.layer addAnimation:animation forKey:@"moveAnimation"];

// 释放路径
CGPathRelease(path);

分析

  1. 创建视图:首先创建一个视图并添加到当前视图控制器的视图中。

  2. 创建关键帧动画:使用CAKeyframeAnimation创建一个关键帧动画,并指定要改变的属性为position

  3. 设置路径:创建一个路径,从起点到终点。路径可以是直线、曲线等,这里我们使用直线。

  4. 将路径赋值给动画:将创建的路径赋值给动画的path属性,并设置动画的持续时间。

  5. 添加动画到图层:将动画添加到视图的图层中。

方法二:使用UIView动画块

使用UIView动画块可以更方便地实现位移动画,且不需要直接操作图层。

示例代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

// 使用UIView动画块
[UIView animateWithDuration:1.0
                      delay:0
                    options:UIViewAnimationOptionCurveEaseInOut
                 animations:^{
                     // 改变视图的中心点
                     view.center = CGPointMake(300, 300);
                 }
                 completion:nil];

注意事项

  1. 动画完成后的位置:

    • 在使用CABasicAnimation时,需要在动画结束后手动设置图层的最终位置,因为动画只是改变了图层的“表现”而不是实际的属性值。
    • 在使用UIView动画块时,动画结束后视图的位置会被自动更新。
  2. 动画的keyPath:

    • CABasicAnimation中,keyPath决定了动画作用的属性。对于位移动画,通常使用"position""transform.translation"
  3. 图层的隐式动画:

    • UIView实际上是CALayer的封装,所有视图的动画都是通过图层实现的。直接使用图层动画可以实现更精细的控制。

通过这两种方法,可以在不直接修改视图frame的情况下实现位移动画,从而实现更灵活和精细的动画效果。根据实际需求选择合适的方法,可以使动画代码更简洁和易于维护。

方法三: 利用锚点 

利用锚点(anchorPoint)来实现位移动画是一种较为高级的技术。锚点定义了视图旋转和缩放的中心点,通过改变锚点,同时调整视图的position属性,可以实现视图的位移动画,而不直接修改视图的frame

以下是使用anchorPoint结合position属性实现位移动画的示例代码:

示例代码

#import <QuartzCore/QuartzCore.h>

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

// 保存当前的中心点
CGPoint originalCenter = view.center;

// 目标位置
CGPoint targetCenter = CGPointMake(300, 300);

// 计算新锚点和新位置
CGPoint newAnchorPoint = CGPointMake((targetCenter.x - view.frame.origin.x) / view.frame.size.width,
                                     (targetCenter.y - view.frame.origin.y) / view.frame.size.height);

// 创建位移动画
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.fromValue = [NSValue valueWithCGPoint:view.layer.position];
animation.toValue = [NSValue valueWithCGPoint:targetCenter];
animation.duration = 1.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

// 应用动画到图层
[view.layer addAnimation:animation forKey:@"positionAnimation"];

// 更改锚点后,重新设置视图的中心点
view.layer.anchorPoint = newAnchorPoint;
view.layer.position = targetCenter;

解释

  1. 保存当前中心点:

    • 在更改anchorPoint之前,保存视图当前的center,以便在更改锚点后重新调整视图的位置。
  2. 计算新锚点和新位置:

    • 计算新锚点值。新锚点的计算基于目标位置相对于视图原始位置的比例。
  3. 创建位移动画:

    • 使用CABasicAnimation创建一个position属性的动画,从当前的position到目标位置。
  4. 应用动画到图层:

    • 将动画应用到视图的图层。
  5. 更改锚点后,重新设置视图的中心点:

    • 更改锚点为新计算的锚点,然后更新图层的position到目标位置。

注意事项

  1. 动画完成后的位置:

    • 在动画完成后,需要手动修改视图的anchorPointposition,以确保视图在最终位置正确显示。
  2. 锚点和位置的关系:

    • 更改锚点会影响视图的position属性,因此在更改锚点后,需要重新计算和设置视图的位置。
  3. 复杂性:

    • 使用锚点进行位移动画比直接修改frame或使用UIView动画块要复杂,适用于需要更精细控制动画和视图位置的情况。

通过这种方法,可以利用锚点实现位移动画,从而在不直接修改视图frame的情况下实现视图的移动。这种方法提供了更大的灵活性,特别是在需要精确控制视图位置和动画效果时。

方法四: 使用定时器(Timer)

可以使用NSTimer或者GCD的定时器来实现动画,通过定时器周期性地更新视图的位置来实现位移动画。

示例代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

CGPoint startPoint = view.center;
CGPoint endPoint = CGPointMake(300, 300);
NSTimeInterval duration = 2.0; // 动画持续时间
NSTimeInterval interval = 0.01; // 定时器触发间隔

__block NSTimeInterval elapsedTime = 0;

// 使用GCD定时器
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
    elapsedTime += interval;
    if (elapsedTime >= duration) {
        dispatch_source_cancel(timer);
        view.center = endPoint;
    } else {
        CGFloat t = elapsedTime / duration;
        view.center = CGPointMake(startPoint.x + (endPoint.x - startPoint.x) * t,
                                  startPoint.y + (endPoint.y - startPoint.y) * t);
    }
});
dispatch_resume(timer);

方法五:使用CADisplayLink 

CADisplayLink是一个高效的定时器,可以在屏幕刷新时调用指定的方法。通过它可以实现更平滑的动画效果。

示例代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

CGPoint startPoint = view.center;
CGPoint endPoint = CGPointMake(300, 300);
NSTimeInterval duration = 2.0; // 动画持续时间

__block NSTimeInterval startTime = CACurrentMediaTime();

// 使用CADisplayLink
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAnimation:)];
displayLink.userInfo = @{@"view": view,
                         @"startPoint": [NSValue valueWithCGPoint:startPoint],
                         @"endPoint": [NSValue valueWithCGPoint:endPoint],
                         @"duration": @(duration),
                         @"startTime": @(startTime)};
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

- (void)updateAnimation:(CADisplayLink *)displayLink {
    NSDictionary *userInfo = displayLink.userInfo;
    UIView *view = userInfo[@"view"];
    CGPoint startPoint = [userInfo[@"startPoint"] CGPointValue];
    CGPoint endPoint = [userInfo[@"endPoint"] CGPointValue];
    NSTimeInterval duration = [userInfo[@"duration"] doubleValue];
    NSTimeInterval startTime = [userInfo[@"startTime"] doubleValue];
    
    NSTimeInterval currentTime = CACurrentMediaTime();
    NSTimeInterval elapsedTime = currentTime - startTime;
    
    if (elapsedTime >= duration) {
        view.center = endPoint;
        [displayLink invalidate];
    } else {
        CGFloat t = elapsedTime / duration;
        view.center = CGPointMake(startPoint.x + (endPoint.x - startPoint.x) * t,
                                  startPoint.y + (endPoint.y - startPoint.y) * t);
    }
}

CADisplayLink:通过屏幕刷新时调用的方法,更新视图的位置实现动画。

这两种方法都提供了对动画过程的精细控制,但CADisplayLink通常更适合实现平滑动画,因为它与屏幕刷新频率同步。

 方法六:手势识别(Gesture Recognizers) 

虽然手势识别通常用于用户交互,但也可以用于自动移动视图。通过模拟手势,可以实现视图的动画效果。

示例代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

CGPoint endPoint = CGPointMake(300, 300);

// 创建一个手势识别器
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[view addGestureRecognizer:panGesture];

// 模拟手势移动
[UIView animateWithDuration:2.0
                 animations:^{
                     view.center = endPoint;
                 }];

// 手势处理方法
- (void)handlePan:(UIPanGestureRecognizer *)gesture {
    // 处理手势逻辑
}

方法七:KVO(Key-Value Observing)结合自定义动画逻辑 

通过KVO监控视图的位置变化,并在变化时执行自定义动画逻辑。

示例代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

// 添加KVO监听
[view addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionNew context:nil];

// 目标位置
CGPoint endPoint = CGPointMake(300, 300);

// 手动更新视图位置
dispatch_async(dispatch_get_main_queue(), ^{
    view.center = endPoint;
});

// KVO回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"center"]) {
        // 自定义动画逻辑
        CGPoint newCenter = [change[NSKeyValueChangeNewKey] CGPointValue];
        [UIView animateWithDuration:2.0 animations:^{
            ((UIView *)object).center = newCenter;
        }];
    }
}

方法八:UIKit Dynamics

UIKit Dynamics提供了一种物理引擎,可以用来实现基于物理的动画效果。

示例代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

// 创建动力学动画器
UIDynamicAnimator *animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];

// 创建重力行为
UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[view]];

// 创建碰撞行为
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[view]];
collision.translatesReferenceBoundsIntoBoundary = YES;

// 添加行为到动画器
[animator addBehavior:gravity];
[animator addBehavior:collision];

方法九:Core Graphics动画 

可以使用Core Graphics绘制视图,然后通过逐帧绘制实现动画效果。

示例代码:

@implementation CustomView

- (void)drawRect:(CGRect)rect {
    // 自定义绘制代码
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextFillRect(context, rect);
}

@end

CustomView *view = [[CustomView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:view];

CGPoint endPoint = CGPointMake(300, 300);

// 使用定时器逐帧绘制
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(updateView:) userInfo:@{@"view": view, @"endPoint": [NSValue valueWithCGPoint:endPoint]} repeats:YES];

// 更新视图位置
- (void)updateView:(NSTimer *)timer {
    NSDictionary *userInfo = timer.userInfo;
    CustomView *view = userInfo[@"view"];
    CGPoint endPoint = [userInfo[@"endPoint"] CGPointValue];
    
    // 计算新的位置
    CGFloat dx = (endPoint.x - view.center.x) * 0.1;
    CGFloat dy = (endPoint.y - view.center.y) * 0.1;
    view.center = CGPointMake(view.center.x + dx, view.center.y + dy);
    
    // 检查是否到达目标位置
    if (fabs(view.center.x - endPoint.x) < 1 && fabs(view.center.y - endPoint.y) < 1) {
        [timer invalidate];
    }
}

方法十: 使用CGAffineTransform进行平移 

CGAffineTransform提供了一种矩阵变换的方法,可以用来对视图进行平移、缩放、旋转等操作。通过transform属性,可以实现位移动画。

示例代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

CGPoint endPoint = CGPointMake(300, 300);
CGPoint translation = CGPointMake(endPoint.x - view.center.x, endPoint.y - view.center.y);

[UIView animateWithDuration:2.0 animations:^{
    view.transform = CGAffineTransformTranslate(view.transform, translation.x, translation.y);
}];

方法十一:Layer的position属性(不使用CABasicAnimation)  

虽然不能使用CABasicAnimation,但可以直接修改图层的position属性,并使用定时器或CADisplayLink来平滑过渡。

示例代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

CGPoint startPoint = view.layer.position;
CGPoint endPoint = CGPointMake(300, 300);
NSTimeInterval duration = 2.0; // 动画持续时间

__block NSTimeInterval startTime = CACurrentMediaTime();

CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateLayerPosition:)];
displayLink.userInfo = @{@"layer": view.layer,
                         @"startPoint": [NSValue valueWithCGPoint:startPoint],
                         @"endPoint": [NSValue valueWithCGPoint:endPoint],
                         @"duration": @(duration),
                         @"startTime": @(startTime)};
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

- (void)updateLayerPosition:(CADisplayLink *)displayLink {
    NSDictionary *userInfo = displayLink.userInfo;
    CALayer *layer = userInfo[@"layer"];
    CGPoint startPoint = [userInfo[@"startPoint"] CGPointValue];
    CGPoint endPoint = [userInfo[@"endPoint"] CGPointValue];
    NSTimeInterval duration = [userInfo[@"duration"] doubleValue];
    NSTimeInterval startTime = [userInfo[@"startTime"] doubleValue];
    
    NSTimeInterval currentTime = CACurrentMediaTime();
    NSTimeInterval elapsedTime = currentTime - startTime;
    
    if (elapsedTime >= duration) {
        layer.position = endPoint;
        [displayLink invalidate];
    } else {
        CGFloat t = elapsedTime / duration;
        layer.position = CGPointMake(startPoint.x + (endPoint.x - startPoint.x) * t,
                                     startPoint.y + (endPoint.y - startPoint.y) * t);
    }
}

方法十二:自定义动画引擎  

可以实现一个简单的自定义动画引擎,通过插值计算来逐帧更新视图的位置。

示例代码:

@interface CustomAnimator : NSObject

@property (nonatomic, weak) UIView *view;
@property (nonatomic) CGPoint startPoint;
@property (nonatomic) CGPoint endPoint;
@property (nonatomic) NSTimeInterval duration;
@property (nonatomic) NSTimeInterval startTime;
@property (nonatomic, strong) CADisplayLink *displayLink;

- (void)startAnimation;

@end

@implementation CustomAnimator

- (void)startAnimation {
    self.startTime = CACurrentMediaTime();
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAnimation)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)updateAnimation {
    NSTimeInterval currentTime = CACurrentMediaTime();
    NSTimeInterval elapsedTime = currentTime - self.startTime;
    
    if (elapsedTime >= self.duration) {
        self.view.center = self.endPoint;
        [self.displayLink invalidate];
    } else {
        CGFloat t = elapsedTime / self.duration;
        self.view.center = CGPointMake(self.startPoint.x + (self.endPoint.x - self.startPoint.x) * t,
                                       self.startPoint.y + (self.endPoint.y - self.startPoint.y) * t);
    }
}

@end

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];

CustomAnimator *animator = [[CustomAnimator alloc] init];
animator.view = view;
animator.startPoint = view.center;
animator.endPoint = CGPointMake(300, 300);
animator.duration = 2.0;
[animator startAnimation];

方法十三:  使用UIScrollViewcontentOffset属性 

通过将目标视图嵌入到一个UIScrollView中,并调整contentOffset来实现视图的移动。UIScrollView的平滑滚动机制可以用来模拟动画效果。

示例代码:

// 创建一个 UIScrollView
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
scrollView.backgroundColor = [UIColor clearColor];
scrollView.contentSize = CGSizeMake(self.view.bounds.size.width * 2, self.view.bounds.size.height);
[self.view addSubview:scrollView];

// 创建要移动的目标视图
UIView *targetView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
targetView.backgroundColor = [UIColor blueColor];
[scrollView addSubview:targetView];

// 设置起点和终点
CGPoint startPoint = targetView.frame.origin;
CGPoint endPoint = CGPointMake(300, 100);

// 初始位置(将scrollView的contentOffset设置为起点)
scrollView.contentOffset = startPoint;

// 使用 scrollView 的动画方法模拟移动
[UIView animateWithDuration:2.0 animations:^{
    scrollView.contentOffset = endPoint;
}];

分析

  1. 创建UIScrollView:首先创建一个UIScrollView并设置其contentSize大于屏幕的大小,以便有足够的空间进行平移。

  2. 添加目标视图:将目标视图添加到UIScrollView中,并设置其初始位置。

  3. 设置起点和终点:通过调整UIScrollViewcontentOffset属性来实现视图的移动。

  4. 动画实现:使用UIView的动画块来平滑地改变contentOffset,从而实现目标视图的移动。