禁用-[CALayer setNeedsDisplayInRect:]中的隐式动画

143

我有一个含有一些复杂绘图代码的层(layer),其-drawInContext:方法中。我试图最小化需要绘制的量,所以我使用-setNeedsDisplayInRect:来更新仅更改的部分,这非常有效。然而,当图形系统更新我的层时,它正在使用交叉淡入淡出(cross-fade)过渡从旧图像转换到新图像。我希望它能立即切换。

我尝试使用CATransaction来关闭动画效果并将持续时间设置为零,但都不起作用。这是我正在使用的代码:

[CATransaction begin];
[CATransaction setDisableActions: YES];
[self setNeedsDisplayInRect: rect];
[CATransaction commit];
有没有在CATransaction上应该使用的不同方法(我还尝试过使用kCATransactionDisableActions的-setValue:forKey:方法,结果相同)?

你可以在下一个运行循环中执行它:dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ }); - Hashem Aboonajmi
1
我发现下面许多答案适合我。还有一篇对我很有帮助的是苹果公司的 更改图层的默认行为 文档,其中详细描述了隐式动作决策过程。 - ɲeuroburɳ
这是一个与此问题重复的问题:https://dev59.com/8W025IYBdhLWcg3wvIco#54656717 - Ryan Francesconi
15个回答

180

你可以通过将图层的 actions 字典设置为返回适当键的 [NSNull null] 动画来实现此操作。例如,我使用:

NSDictionary *newActions = @{
    @"onOrderIn": [NSNull null],
    @"onOrderOut": [NSNull null],
    @"sublayers": [NSNull null],
    @"contents": [NSNull null],
    @"bounds": [NSNull null]
};

layer.actions = newActions;

如何禁用在图层中插入或更改子图层时以及更改图层大小和内容时淡入/淡出动画效果。我相信你需要使用contents关键字来阻止更新绘制时的交叉淡入淡出效果。


Swift 版本:

let newActions = [
        "onOrderIn": NSNull(),
        "onOrderOut": NSNull(),
        "sublayers": NSNull(),
        "contents": NSNull(),
        "bounds": NSNull(),
    ]

26
为了防止更改框架时的移动,请使用@"position"键。 - mxcl
12
如果您通过切换图层的可见性来隐藏图层,并希望禁用不透明度动画,请确保在操作字典中也添加@"hidden"属性。 - user836263
14
这些“特殊”字符串实际上有常量来表示它们不代表属性(例如kCAOnOrderOut表示@"onOrderOut"),在这里有详细的文档:https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CoreAnimation_guide/Articles/Actions.html#//apple_ref/doc/uid/TP40006095-SW1 - Patrick Pijnappel
2
@Benjohn 只有那些没有相应属性的键才定义了常量。顺便说一下,链接似乎已经失效了,这是新的 URL:https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/ReactingtoLayerChanges/ReactingtoLayerChanges.html#//apple_ref/doc/uid/TP40004514-CH7-SW1 - Patrick Pijnappel
1
这对我不起作用。无法取消我的点赞。请参见下面的mxcl答案。那个有效。 - Sam Soffes
显示剩余10条评论

90

另外:

[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

//foo

[CATransaction commit];

3
你可以用 [self setNeedsDisplayInRect: rect]; [self displayIfNeeded]; 替换 //foo 以回答原来的问题。 - Karoy Lorentey
1
谢谢!这让我也可以在自定义视图上设置一个动画标志。在表视图单元格中使用非常方便(因为单元格重用可能会导致滚动时出现一些奇怪的动画效果)。 - Joe D'Andrea
3
对我来说,这会导致性能问题,设置操作更有效率。 - Pascalius
26
速记:[CATransaction setDisableActions:YES]翻译:禁用动画效果。 - titaniumdecoy
7
补充@titaniumdecoy的评论,以防有人感到困惑(像我一样),[CATransaction setDisableActions:YES]是仅需要[CATransaction setValue:forKey:]这行代码的缩写。您仍然需要使用begincommit代码块。 - Hlung

32

当您改变图层的属性时,通常会创建一个隐式事务对象来动画显示更改。如果您不想动画显示该更改,则可以通过创建显式事务并将其kCATransactionDisableActions属性设置为true来禁用隐式动画。

Objective-C

[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
// change properties here without animation
[CATransaction commit];

迅捷

CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// change properties here without animation
CATransaction.commit()

6
setDisableActions: 做了同样的事情。 - pronebird
3
这是我在Swift中找到的最简单的解决方案! - Jambaman
@Andy的评论是迄今为止最好、最简单的方法! - Aᴄʜᴇʀᴏɴғᴀɪʟ

23
Brad Larson的答案之外:对于自定义图层(由您创建),您可以使用委托而不是修改图层的actions字典。这种方法更加动态,可能更具性能。它还允许禁用所有隐式动画,而无需列出所有可动画的键。
不幸的是,不可能将UIView用作自定义图层委托,因为每个UIView已经是其自己图层的委托。但是,您可以使用像这样的简单助手类:
@interface MyLayerDelegate : NSObject
    @property (nonatomic, assign) BOOL disableImplicitAnimations;
@end

@implementation MyLayerDelegate

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
    if (self.disableImplicitAnimations)
         return (id)[NSNull null]; // disable all implicit animations
    else return nil; // allow implicit animations

    // you can also test specific key names; for example, to disable bounds animation:
    // if ([event isEqualToString:@"bounds"]) return (id)[NSNull null];
}

@end

使用方法(在视图内):

MyLayerDelegate *delegate = [[MyLayerDelegate alloc] init];

// assign to a strong property, because CALayer's "delegate" property is weak
self.myLayerDelegate = delegate;

self.myLayer = [CALayer layer];
self.myLayer.delegate = delegate;

// ...

self.myLayerDelegate.disableImplicitAnimations = YES;
self.myLayer.position = (CGPoint){.x = 10, .y = 42}; // will not animate

// ...

self.myLayerDelegate.disableImplicitAnimations = NO;
self.myLayer.position = (CGPoint){.x = 0, .y = 0}; // will animate

有时候,将视图控制器作为视图自定义子层的代理很方便;在这种情况下,不需要帮助类,您可以直接在控制器中实现actionForLayer:forKey:方法。
重要提示:不要尝试修改UIView底层图层的代理(例如启用隐式动画)-会发生糟糕的事情 :)
注意:如果您想要对层进行动画处理(而不是禁用动画),则将[CALayer setNeedsDisplayInRect:]调用放在CATransaction中没有用,因为实际的重绘可能会(并且很可能会)在稍后发生。好的方法是使用自定义属性,如在这个答案中所述。

这对我不起作用。[在这里看。](http://stackoverflow.com/questions/25830050/disabling-calayer-implicit-animations) - aleclarson
我只是在使用错误的CALayer实例进行测试(当时我有两个)。 - aleclarson
1
不错的解决方案...但是NSNull没有实现CAAction协议,也没有只有可选方法的协议。这段代码同样会崩溃,而且你甚至无法将其转换为Swift。更好的解决方案:使您的对象符合CAAction协议(使用一个空的runActionForKey:object:arguments:方法什么都不做)并返回self而不是[NSNull null]。相同的效果但更安全(肯定不会崩溃),并且在Swift中也可以工作。 - Mecki
@Mecki,这是不正确的。NSNull被允许作为返回值。请参阅actionForKey:方法参考(CALayerDelegateactionForLayer:forKey:文档重定向到此)。 - skozin
@Mecki 特别指出,它说:“委托必须执行以下操作之一:1)返回给定键的操作对象。2)如果不处理该操作,则返回NSNull对象。” 此外,NSNull确实符合CAAction,请参见其参考文档中它符合的协议列表(目前,CAAction是列表中的第一个协议)。 - skozin
显示剩余8条评论

9
这里提供一种更高效的解决方案,类似于被接受的答案,但适用于Swift。对于某些情况,它比每次修改值时创建交易更好,这是其他人已经提到的性能问题,例如在60fps下拖动图层位置的常见用例。
// Disable implicit position animation.
layer.actions = ["position": NSNull()]      

请参考苹果文档中关于图层行为解析的内容。实现委托将会在级联过程中再跳过一层,但在我的情况下,由于委托需要设置为相关联的UIView的限制,这会变得混乱不堪。
编辑:感谢评论者指出NSNull符合CAAction的要求,已进行更新。

Swift中无需创建NullAction,因为NSNull已经符合CAAction,所以您可以像在Objective-C中一样操作: layer.actions = [ "position" : NSNull() ] - user5649358
我将您的答案与此答案结合起来,以修复我的动画CATextLayer。https://dev59.com/O1TTa4cB1Zd3GeqPvLxe#5144221 - Erik Živković
这是一个很好的解决方案,可以帮助我解决在项目中更改CALayer线条颜色时需要绕过“动画”延迟的问题。谢谢! - PlateReverb
简洁明了!完美的解决方案! - David H

8

实际上,我没有发现任何答案是正确的。解决我的问题的方法是这样的:

- (id<CAAction>)actionForKey:(NSString *)event {   
    return nil;   
}

然后你可以在其中加入任何逻辑,以禁用特定的动画,但由于我想删除所有动画,所以我返回了 nil。


这对我有用。别忘了子类化CALayer以覆盖该方法。 - eonil

7
根据Sam的回答和Simon的困难,创建CSShapeLayer后添加委托引用:
CAShapeLayer *myLayer = [CAShapeLayer layer];
myLayer.delegate = self; // <- set delegate here, it's magic.

...在"m"文件的其他地方...

本质上与Sam的方法相同,不过没有通过自定义的“disableImplicitAnimations”变量安排来切换。更具有“硬连接”的方式。

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {

    // disable all implicit animations
    return (id)[NSNull null];

    // allow implicit animations
    // return nil;

    // you can also test specific key names; for example, to disable bounds animation:
    // if ([event isEqualToString:@"bounds"]) return (id)[NSNull null];

}

6
我发现一种更简单的方法来禁用在 CATransaction 内部调用 setValue:forKey: 来设置 kCATransactionDisableActions 键的操作:
[CATransaction setDisableActions:YES];

Swift:

CATransaction.setDisableActions(true)

6

如何在Swift中禁用隐式图层动画

CATransaction.setDisableActions(true)

谢谢你的回答。我最初尝试使用disableActions(),因为它听起来像是做相同的事情,但实际上是为了获取当前值。我认为这也标记为 @discardable,使得更难发现。来源:https://developer.apple.com/documentation/quartzcore/catransaction/1448276-disableactions - Austin

4

已更新以适用于Swift,仅在iOS而非MacOS中禁用一个隐式属性动画

// Disable the implicit animation for changes to position
override open class func defaultAction(forKey event: String) -> CAAction? {
    if event == #keyPath(position) {
        return NSNull()
    }
    return super.defaultAction(forKey: event)
}

下面是另一个例子,这次是消除两个隐式动画。

class RepairedGradientLayer: CAGradientLayer {

    // Totally ELIMINATE idiotic implicit animations, in this example when
    // we hide or move the gradient layer

    override open class func defaultAction(forKey event: String) -> CAAction? {
        if event == #keyPath(position) {
            return NSNull()
        }
        if event == #keyPath(isHidden) {
            return NSNull()
        }
        return super.defaultAction(forKey: event)
    }
}

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