UIView的基于块的动画方法的内部实现

11
自从 iOS 4 推出以来,我一直在思考 UIView 的基于块的动画方法的内部实现。特别是我想了解在执行动画块之前和之后捕获所有相关层状态更改所使用的 Objective C 的神秘功能。
观察到黑盒实现,我得出结论它需要捕获动画块中修改的所有图层属性的初始状态,以创建所有相关 CAAnimations。我猜它没有对整个视图层次结构进行快照,因为那样会非常低效。在运行时,动画块是不透明的代码块,因此我认为它不能直接分析其中的内容。它是否用某种记录版本替换了 CALayer 的属性设置器的实现?或者这种属性更改的记录支持是否已经深入嵌入到 CALayers 的某个地方?
为了将问题概括一下,是否可能使用某些 Objective C 黑魔法创建类似的基于块的 API 来记录状态更改,还是需要知道并访问在块中被更改的对象的内部?

2
视图动画实际上只是隐式层动画的一个子集。隐式层动画的工作方式如我在这里描述的那样:http://www.apeth.com/iOSBook/ch17.html#_actions_2 - matt
2个回答

21

这实际上是一个非常优雅的解决方案,它建立在视图是层的委托并且独立图层会隐式地在属性更改时进行动画的事实基础之上。

恰好我几天前在 NSConference 上做了一场关于此的 BLITZ 演讲,并在 GitHub 上发布了我的幻灯片,并尝试将我在演示说明中说的写下来。

尽管如此:这是一个非常有趣的问题,我很少看到有人问到。它可能有些广泛,但我真的很喜欢好奇心


iOS 4 之前已经存在 UIView 动画

自从它们在 iOS 4 中引入以来,我一直在想 UIView 基于 block 的动画方法的内部实现方式。

UIView 动画在 iOS 4 之前已经存在,但采用不同的风格(已不再推荐使用,因为使用起来更加繁琐)。例如,可以像这样使用延迟动画视图的位置和颜色。 免责声明:我没有运行过这段代码,因此可能存在错误。

// Setup
static void *myAnimationContext = &myAnimationContext;
[UIView beginAnimations:@"My Animation ID" context:myAnimationContext];
// Configure
[UIView setAnimationDuration:1.0];
[UIView setAnimationDelay:0.25];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];

// Make changes
myView.center = newCenter;
myView.backgroundColor = newColor;

// Commit
[UIView commitAnimations];

视图层协同非常优雅

特别是我想了解Objective C使用了哪些神秘的功能来捕获动画块执行前后的所有相关层状态更改。

实际上是相反的。视图建立在层之上,它们密切合作。当您在视图上设置属性时,它会设置对应的层属性。例如,您可以看到视图甚至没有自己的frame、bounds或position变量

观察黑盒实现,我得出结论,它需要捕获动画块中修改的所有层属性的前状态,以创建所有相关的CAAnimations。

它不需要这样做,而且这就是一切变得非常优雅的地方。每当层属性更改时,层都会查找要执行的操作(一个更通用的术语为动画)。由于在视图上设置大多数属性实际上是在层上设置属性,因此您隐式地设置了一堆层属性。

层寻找操作的第一个位置是询问层代理(文档记录了视图是层的代理)。这意味着当层属性更改时,层会要求视图为每个属性更改提供动画对象。因此,视图无需跟踪任何状态,因为层具有状态,当属性更改时,层会要求视图提供动画。

实际上,这并不完全正确。视图需要跟踪一些状态,例如:您是否在块内部,动画持续时间等。

您可以想象API看起来像这样。

注意:我不知道实际实现做了什么,这显然是一个证明观点的巨大简化

// static variables since this is a class method
static NSTimeInterval _durationToUseWhenAsked;
static BOOL _isInsideAnimationBlock;

// Oversimplified example implementation of how it _could_ be done
+ (void)animateWithDuration:(NSTimeInterval)duration
                 animations:(void (^)(void))animations
{
    _durationToUseWhenAsked = duration;
    _isInsideAnimationBlock = YES;
    animations();
    _isInsideAnimationBlock = NO;
}

// Running the animations block is going to change a bunch of properties
// which result in the delegate method being called for each property change
- (id<CAAction>)actionForLayer:(CALayer *)layer
                        forKey:(NSString *)event
{
    // Don't animate outside of an animation block
    if (!_isInsideAnimationBlock)
        return (id)[NSNull null]; // return NSNull to don't animate

    // Only animate certain properties
    if (![[[self class] arrayOfPropertiesThatSupportAnimations] containsObject:event])
        return (id)[NSNull null]; // return NSNull to don't animate

    CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:event];
    theAnimation.duration = _durationToUseWhenAsked;

    // Get the value that is currently seen on screen
    id oldValue = [[layer presentationLayer] valueForKeyPath:event];
    theAnimation.fromValue = oldValue;
    // Only setting the from value means animating form that value to the model value

    return theAnimation;
}

它是否用某种重新编码版本替换了CALayer上属性设置器的实现?
不是(见上文)
或者,这个属性更改的支持是否在CALayer的深处内置?
是的,有点像(见上文)
创建类似的API
为了将问题概括一下,使用某些Objective C黑魔法创建类似的基于块的API来记录状态更改是否可能,或者这是否依赖于知道并具有访问正在块中更改的对象的内部?
如果您想根据属性更改提供自己的动画,则绝对可以创建类似的基于块的API。如果您查看我在NSConference上展示的检查UIView动画的技术(直接询问图层的actionForLayer:forKey:和使用layerClass创建记录所有addAnimation:forKey:信息的图层),那么您应该能够学习足够多关于视图如何使用图层来创建这个抽象。
我不确定记录状态更改是否是您的最终目标。如果您只想执行自己的动画API,那么就不需要这样做。如果您真的想这样做,那么您可能可以,但是可用于动画的通信基础设施(视图和图层之间的委托方法和回调)将不如动画丰富。

3
谢谢你的详细解释! - Palimondo

6

David的回答非常棒。你应该接受它作为最终答案。

我有一个小贡献。我在我的一个github项目中创建了一个名为“Sleuthing UIView Animations”的markdown文件。(链接) 它更详细地介绍了如何监视系统响应UIView动画时创建的CAAnimation对象。该项目名为KeyframeViewAnimations。(链接)

它还展示了记录提交UIView动画时创建的CAAnimations的工作代码。

值得一提的是,正是David建议我使用这种技术。


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