iOS CAKeyFrameAnimation在动画结束时出现闪烁问题的缩放

6
在另一个关键帧动画测试中,我将移动UIImageView(称为theImage)沿着贝塞尔曲线并在移动时缩放它,从而在路径结束时得到一个2倍大的图像。 我最初编写的代码包含以下元素来启动动画:
UIImageView* theImage = ....
float scaleFactor = 2.0;
....

theImage.center = destination;
theImage.transform = CGAffineTransformMakeScale(1.0,1.0);

CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:@"bounds.size"];
[resizeAnimation setToValue:[NSValue valueWithCGSize:CGSizeMake(theImage.image.size.height*scaleFactor, theImage.image.size.width*scaleFactor)]];
resizeAnimation.fillMode = kCAFillModeBackwards;
resizeAnimation.removedOnCompletion = NO;  

CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
pathAnimation.path = [jdPath path].CGPath;
pathAnimation.fillMode = kCAFillModeBackwards;
pathAnimation.removedOnCompletion = NO;

CAAnimationGroup* group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:pathAnimation, resizeAnimation, nil];
group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
group.removedOnCompletion = NO;
group.duration = duration;
group.delegate = self;

[theImage.layer addAnimation:group forKey:@"animateImage"];

然后,当动画完成时,我希望保留图像的较大尺寸,因此我实现了以下内容:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
   theImage.transform = CGAffineTransformMakeScale(scaleFactor,scaleFactor);
}

这一切都运作得...有点。问题在于,在动画结束时,theImage 会闪烁一下,仅此而已,但已足够让它看起来不好。我猜测这是动画结束时我将转换设置为新大小的过渡造成的。在尝试了上述稍微不同的形式后,我进行了实验,但仍然出现了相同的闪烁:
CAKeyframeAnimation *resizeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
NSValue* startSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, 1.0, 1.0, 1.0)];
NSValue* endSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, scaleFactor, scaleFactor, 1.0)]; 
NSArray* sizeKeys = [NSArray arrayWithObjects:startSizeKey, endSizeKey, nil];
[resizeAnimation setValues:sizeKeys];

....

theImage.transform = CGAffineTransformMakeScale(scaleFactor,scaleFactor);

但是当我在与原始大小相同时结束动画时,就没有闪烁问题:

CAKeyframeAnimation *resizeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
NSValue* startSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, 1.0, 1.0, 1.0)];
NSValue* middleSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, scaleFactor, scaleFactor, 1.0)];
NSValue* endSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, 1.0, 1.0, 1.0)]; 
NSArray* sizeKeys = [NSArray arrayWithObjects:startSizeKey, middleSizeKey, endSizeKey, nil];
[resizeAnimation setValues:sizeKeys];

....

theImage.transform = CGAffineTransformMakeScale(1.0,1.0);

我的重要问题是如何在没有闪烁的情况下动画化此图像,并在动画结束时得到不同的大小?

编辑于3月2日

我的初始测试是将图像放大。我尝试了缩小它(即scaleFactor = 0.4),闪烁更加明显,我看到的情况也更加明显。以下是事件的顺序:

  1. 在起始位置屏幕上绘制原始大小的图像。
  2. 随着图像沿路径移动,它平滑地缩小。
  3. 完全缩小的图像到达路径的末端。
  4. 然后以其原始大小绘制图像。
  5. 最后以其缩小的大小绘制图像。

因此,似乎是步骤4导致了我所看到的闪烁。


3月22日更新

我刚刚在GitHub上上传了一个演示项目,展示了沿着贝塞尔曲线移动对象的方法。代码可以在PathMove找到。

我还在我的博客中写了一篇关于这个主题的文章,链接为iOS中沿着贝塞尔曲线移动对象

3个回答

8
使用核心动画(Core Animation)来动画化视图的层会有一些棘手的问题。这里有几个让人困惑的事情:
- 在层上设置动画不会改变层的属性,而是在应用动画时替换原始“模型层”并更改“呈现层”的属性,呈现层会在应用动画期间显示在屏幕上。 - 更改层的属性通常会向层添加隐式动画,其中属性名称作为动画的键。因此,如果要显式地动画化属性,则通常需要将该属性设置为其最终值,然后添加一个以属性名称作为键的动画来覆盖隐式动画。 - 视图通常禁用其层上的隐式动画。它还以其他某种神秘的方式操纵其层的属性。
另外,动画化视图的边界(bounds)以将其缩放,但最后切换为比例变换也是令人困惑的。
我认为做到你想要的最简单方法是尽可能使用UIView的动画化方法,并仅在关键帧动画中使用核心动画。在让UIView添加自己的动画后,您可以将关键帧动画添加到视图的层中,您的关键帧动画将覆盖UIView添加的动画。
对我来说,这很有效:
- (IBAction)animate:(id)sender {
    UIImageView* theImage = self.imageView;
    CGFloat scaleFactor = 2;
    NSTimeInterval duration = 1;

    UIBezierPath *path = [self animationPathFromStartingPoint:theImage.center];
    CGPoint destination = [path currentPoint];

    [UIView animateWithDuration:duration animations:^{
        // UIView will add animations for both of these changes.
        theImage.transform = CGAffineTransformMakeScale(scaleFactor, scaleFactor);
        theImage.center = destination;

        // Prepare my own keypath animation for the layer position.
        // The layer position is the same as the view center.
        CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        positionAnimation.path = path.CGPath;

        // Copy properties from UIView's animation.
        CAAnimation *autoAnimation = [theImage.layer animationForKey:@"position"];
        positionAnimation.duration = autoAnimation.duration;
        positionAnimation.fillMode = autoAnimation.fillMode;

        // Replace UIView's animation with my animation.
        [theImage.layer addAnimation:positionAnimation forKey:positionAnimation.keyPath];
    }];
}

这个解决方案到目前为止是最好的。然而(接下来就是“但是”),我也在尝试解决我的另一个动画问题(https://dev59.com/DWox5IYBdhLWcg3wDgIz)。那么,我该如何无缝地将前置和后置旋转动作添加到这个解决方案中呢?我已经尝试添加了一个“完成”块,但结果是出现了我想要避免的终端闪烁。 - Peter M
嗯,您已经为那个问题奖励了悬赏金。您没有找到满意的答案吗? - rob mayoff
对于另一个问题,我将采用预先/后旋转图像的方法。但这与试图将所有所需的动画合并在一起的问题是分开的。顺便说一下,我不知道为什么你的答案会被标记为负面的。我很喜欢它的简洁和整洁。 - Peter M
我已经回答了你的另一个问题,使用了扩展版本的这段代码,其中使用了kCAAnimationRotateAuto并解释了如何避免旋转开始和结束时的闪烁。我还在github上放了一个测试项目:https://github.com/mayoff/stackoverflow-kCAAnimationRotateAuto。 - rob mayoff
感谢您在回答中付出的努力。我不知道发生了什么事情导致SO悬赏消失了,但我想我可能回到这个问题的时间太晚了,所以现在显然我只能授予您答案 :-(。 - Peter M
感谢您的详细解释。我很欣赏这样的回答,而不仅仅是“代码块”。 - psy

4
我这周在试验CAAnimations时注意到动画结束时出现了闪烁。具体来说,我将圆形变成正方形,同时也改变了填充颜色。
每个CAAnimation都有一个属性叫做removedOnCompletion,默认值为YES。这意味着当动画完成时,动画(如转场、缩放、旋转等)将消失,您将得到原始图层。
既然您已经将removedOnCompletion属性设为NO,我建议尝试使用CATransactions来执行动画,而不是使用代理和animationDidStop...
[CATransaction begin];
[CATransaction setDisableActions:YES];
[CATransaction setCompletionBlock: ^{ theImage.transform = ...}];

// ... CAAnimation Stuff ... //

[CATransaction commit];

根据以下链接的建议,您应该在创建动画之前调用事务完成块:http://zearfoss.wordpress.com/2011/02/24/core-animation-catransaction-protip/ 下面是我的一个方法示例:
[CATransaction begin];
CABasicAnimation *animation = ...;
animation.fromValue = ...;
animation.toValue = ...;
[CATransaction setCompletionBlock:^ { self.shadowRadius = _shadowRadius; }];
[self addAnimation:animation forKey:@"animateShadowOpacity"];
[CATransaction commit];

我创建了这个动画,它在我的电脑上完美运行,没有任何问题:

设置和触发是我在一个窗口中自定义的方法,我在鼠标按下时触发动画。

UIImageView *imgView;
UIBezierPath *animationPath;

-(void)setup {
    canvas = (C4View *)self.view;
    imgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"img256.png"]];
    imgView.frame = CGRectMake(0, 0, 128, 128);
    imgView.center = CGPointMake(384, 128);
    [canvas addSubview:imgView];

}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [UIImageView animateWithDuration:2.0f animations:^{
        [CATransaction begin];        
        CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        pathAnimation.duration = 2.0f;
        pathAnimation.calculationMode = kCAAnimationPaced;
        animationPath = [UIBezierPath bezierPath];
        [animationPath moveToPoint:imgView.center];
        [animationPath addLineToPoint:CGPointMake(128, 512)];
        [animationPath addLineToPoint:CGPointMake(384, 896)];
        pathAnimation.path = animationPath.CGPath;
        pathAnimation.fillMode = kCAFillModeForwards;
        pathAnimation.removedOnCompletion = NO;
        [imgView.layer addAnimation:pathAnimation forKey:@"animatePosition"];
        [CATransaction commit];

        CGFloat scaleFactor = 2.0f;
        CGRect newFrame = imgView.frame;
        newFrame.size.width *= scaleFactor;
        newFrame.size.height *= scaleFactor;
        newFrame.origin = CGPointMake(256, 0);
        imgView.frame = newFrame;
        imgView.transform = CGAffineTransformRotate(imgView.transform,90.0*M_PI/180);

    }];
}

根据我的上述代码,每个单独的动画以及整个组已经将removedOnCompletion设置为NO。然而,这些动画似乎并没有被保留。如果我不在animationDidStop:中执行变换,则会显示原始图像。 - Peter M
是的,抱歉我错过了removedOnCompletion(那是昨晚太晚了)...我编辑了我的答案并包含了另一种技术... - C4 - Travis
你的意思是在你说“CAAnimation Stuff”的地方插入[theImage.layer addAnimation:group forKey:@"animateImage"];吗? - Peter M
好的,请试一下。这个星期我一直在处理事务和动画,它们都很简单,所以我在CATransaction的begin和commit调用中构建了这些动画。我已经编辑了上面的答案,向你展示了我如何创建其中一个事务。 - C4 - Travis
这仍然在最后给我闪烁。 - Peter M
我在我的答案中添加了更多细节。这个动画沿着所需的路径移动,缩放imageView,并旋转imageView,而在动画结束时不会出现任何闪烁。缩放动画是在原地发生的(即,如果您删除关键帧动画,它看起来像是imageView的大小加倍,中心位置保持不变)。 - C4 - Travis

4
如果终端状态是这样指定的,以至于它本身创建了一个隐式动画,CAAnimation在结束时会闪烁。要记住,CAAnimation是为了可视化转换而对对象属性进行的临时调整。当动画完成后,如果图层的状态仍是原始起始状态,则会出现所谓的“闪烁”,直到你在animationDidStop:方法中设置最终图层状态。

此外,你的动画正在调整图层的bounds.size属性,因此你应该设置你的最终状态,而不是使用变换调整作为你的最终状态。你也可以将transform属性作为动画中的动画属性,而不是bounds.size

为了解决这个问题,在分配动画之后,立即将图层的永久状态更改为所需的终端状态,这样当动画完成时就不会出现闪烁,但要注意在动画开始之前不要触发隐式动画。具体来说,在你的动画设置结束时,你应该这样做:

UIImageView* theImage = ....
float scaleFactor = 2.0;
....

theImage.center = destination;
theImage.transform = CGAffineTransformMakeScale(1.0,1.0);

CGSize finalSize = CGSizeMake(theImage.image.size.height*scaleFactor, theImage.image.size.width*scaleFactor);
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:@"bounds.size"];
[resizeAnimation setToValue:[NSValue valueWithCGSize:finalSize]];
resizeAnimation.fillMode = kCAFillModeBackwards;
resizeAnimation.removedOnCompletion = NO;  

CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
pathAnimation.path = [jdPath path].CGPath;
pathAnimation.fillMode = kCAFillModeBackwards;
pathAnimation.removedOnCompletion = NO;

CAAnimationGroup* group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:pathAnimation, resizeAnimation, nil];
group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
group.removedOnCompletion = NO;
group.duration = duration;
group.delegate = self;

[theImage.layer addAnimation:group forKey:@"animateImage"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
theImage.bounds = CGRectMake( theImage.bounds.origin.x, theImage.bounds.origin.y, finalSize.width, finalSize.height );
[CATransaction commit];

然后在您的animationDidStop:方法中移除变换调整。


你说的有道理,但是当我添加了你建议的代码后,我没有得到想要的结果。现在图像的情况是以缩放版本开始,然后变得更大,最后恢复到预期的缩放图像大小。但没有闪烁!!!顺便说一下,当你说“移除变换调整”时,我相信你的意思是从animationDidStop:方法中删除theImage.transform = CGAffineTransformMakeScale(scaleFactor, scaleFactor);这一行。移除一个变换可能被解读为对以前所做的操作进行反转:D - Peter M
等等,我明白了。我没有仔细看你的代码。你在动画中没有使用transform属性。你也不应该在最终状态中使用它。我会编辑我的答案来弥补这个问题。 - kamprath
theImage.bounds.size = finalSize 这段代码有一个小问题。XCode 报告了 Expression is not assignable 的错误。 - Peter M
抱歉,我总是忘记了在stackoverflow上进行自由编码时(没有编译器和自动补全)需要固定格式。 - kamprath
通过这个,我可以让图像以最终大小开始。 - Peter M
谢谢!将图层属性设置为“toValue”(最终值)可以消除闪烁问题。 - Honghao Z

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接