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


137

我的-drawInContext:方法中有一个包含一些复杂绘图代码的层。我试图尽量减少需要做的绘图,所以我使用-setNeedsDisplayInRect:来仅更新已更改的零件。这很出色。但是,当图形系统更新我的图层时,它会使用淡入淡出功能从旧图像过渡到新图像。我希望它可以立即切换。

我尝试使用CATransaction关闭操作并将持续时间设置为零,但都无法正常工作。这是我正在使用的代码:

[CATransaction begin];
[CATransaction setDisableActions: YES];
[self setNeedsDisplayInRect: rect];
[CATransaction commit];

我应该改用CATransaction上的其他方法吗(我也尝试了-setValue:forKey:和kCATransactionDisableActions,结果相同)。


您可以在下一个运行循环中执行此操作: dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
Hashem Aboonajmi 2015年

1
我发现以下许多答案对我有用。Apple的“ 更改图层的默认行为”文档也很有帮助,该文档详细描述了隐式操作决策过程。
ɲeuroburɳ

Answers:


172

您可以通过在图层上设置动作字典来执行此操作,以将其[NSNull null]作为动画返回给相应的关键点。例如,我用

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

layer.actions = newActions;

禁用在我的某一层中插入或更改子层时的淡入/淡出动画,以及该层的大小和内容的变化。我相信contents关键是您要寻找的那个,以防止更新图形上的淡入淡出。


迅捷版:

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

24
为防止在更换框架时发生移动,请使用@"position"键。
mxcl 2011年

11
@"hidden"如果要以这种方式切换图层的可见性并希望禁用不透明度动画,也请务必在动作字典中添加该属性。
安德鲁

1
@BradLarson这就是我想出了一些后,挣扎着(我推翻了同样的想法actionForKey:,而不是),发现fontSizecontentsonLayoutbounds。似乎您可以指定setValue:forKey:方法中可以使用的任何键,实际上是指定复杂的键路径bounds.size
pqnet 2012年

11
实际上有这些“特殊”的字符串常量不是代表一个属性(如kCAOnOrderOut为@“onOrderOut”)在这里充分证明:developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/...
帕特里克Pijnappel

1
@Benjohn只有没有相应属性的键才定义常量。BTW,链接似乎死了,这里的新网址:developer.apple.com/library/mac/documentation/Cocoa/Conceptual/...
帕特里克Pijnappel

89

也:

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

//foo

[CATransaction commit];

3
您可以替换//foo[self setNeedsDisplayInRect: rect]; [self displayIfNeeded];来回答原始问题。
2011年

1
谢谢!这也让我在自定义视图上设置了动画标记。方便在表视图单元格中使用(单元格重复使用可能会导致滚动时出现一些令人毛骨悚然的动画)。
Joe D'Andrea

3
对我而言,这会导致性能问题,设置动作的效果更好
Pascalius

26
速记:[CATransaction setDisableActions:YES]

7
万一有人(例如我)感到困惑,在@titaniumdecoy注释中添加注释[CATransaction setDisableActions:YES]是该[CATransaction setValue:forKey:]行的简写。您仍然需要begincommit行。
Hlung 2014年

31

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

目标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:相同。
Ben Sinclair 2014年

3
这是我在Swift中工作的最简单的解决方案!
Jambaman 2015年

到目前为止,@ Andy的评论是最好,最简单的方法!
Aᴄʜᴇʀᴏɴғᴀɪʟ

23

除了Brad Larson的回答:对于自定义图层(由您创建),您可以使用委托而不是修改图层的actions字典。这种方法更具动态性,并且性能可能更高。而且它允许禁用所有隐式动画,而不必列出所有可设置动画的关键帧。

不幸的是,不可能使用UIViews作为自定义层委托,因为每个人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:]调用放在a内是没有用的CATransaction,因为实际的重绘可能(可能会)稍后发生。好的方法是使用自定义属性,如本答案所述


这对我不起作用。看这里。
aleclarson 2014年

嗯 我从未有过这种方法的任何问题。链接的问题中的代码看起来还不错,并且可能是其他代码引起的。
skozin 2014年

啊,我看到您已经整理出CALayer阻止noImplicitAnimations工作的错误。也许您应该将自己的答案标记为正确,并解释该层出了什么问题?
skozin

我只是在测试错误的CALayer实例(当时我有两个实例)。
aleclarson

1
不错的解决方案...但是NSNull没有实现CAAction协议,这不是只有可选方法的协议。这段代码也会崩溃,您甚至无法将其转换为Swift。更好的解决方案:使您的对象符合CAAction协议(使用runActionForKey:object:arguments:不执行任何操作的空方法)并返回self而不是[NSNull null]。效果相同但安全(肯定不会崩溃),并且也可以在Swift中使用。
Mecki '16

9

这是一个更有效的解决方案,类似于已接受的答案,但适用于Swift。在某些情况下,这比每次修改值都要创建事务要好,这是性能方面的关注,正如其他人所提到的,例如,以60fps左右拖动图层位置的常见用例。

// Disable implicit position animation.
layer.actions = ["position": NSNull()]      

有关如何解决图层操作的信息,请参阅Apple的文档。实现委托将在级联中再跳过一个级别,但是在我的情况下,由于需要将委托设置为关联的UIView,这一点太混乱

编辑:已更新,感谢评论者指出NSNull符合CAAction


无需NullAction为Swift 创建一个,已经NSNull遵循,CAAction因此您可以在目标C中进行相同的操作:layer.actions = [“ position”:NSNull()]
user5649358 2015年

我将您的答案与此答案相结合,以修复动画CATextLayer stackoverflow.com/a/5144221/816017
Erik Zivkovic

这对于解决我在项目中更改CALayer线的颜色时需要绕过“动画”延迟的问题是一个很好的解决方案。谢谢!!
PlateReverb

简短而甜蜜!很好的解决方案!
David H

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];

}

7

实际上,我没有找到正确的答案。为我解决问题的方法是这样的:

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

然后,您可以使用其中的任何逻辑来禁用特定的动画,但是由于我想将其全部删除,因此我返回nil。



5

发现到关闭动作更简单的方法内部CATransaction在内部调用setValue:forKey:kCATransactionDisableActions关键:

[CATransaction setDisableActions:YES];

迅速:

CATransaction.setDisableActions(true)

2

将其添加到要实现-drawRect()方法的自定义类中。更改代码以适应您的需求,对我来说,“不透明”起到了阻止交叉淡入淡出动画的作用。

-(id<CAAction>) actionForLayer:(CALayer *)layer forKey:(NSString *)key
{
    NSLog(@"key: %@", key);
    if([key isEqualToString:@"opacity"])
    {
        return (id<CAAction>)[NSNull null];
    }

    return [super actionForLayer:layer forKey:key];
}

1

如果您需要非常快速的修复程序(但是很容易破解),那么值得(快速)进行:

let layer = CALayer()

// set other properties
// ...

layer.speed = 999

3
请永远不要这样做ffs
m1h4

@ m1h4感谢那-请解释为什么这是一个坏主意
马丁CR

3
因为如果需要关闭隐式动画,则有一种机制可以执行此操作(具有临时禁用动作的ca事务或将空动作显式设置到层上)。只是将动画速度设置为希望高到足以使其看起来像瞬间一样,会导致不必要的性能开销(原始作者提到与他有关)的负担和潜在的各种竞态条件(绘制仍在单独的缓冲区中完成)稍后在动画中显示-确切地说,对于上述情况,在0.25 / 999秒后)。
m1h4

真是遗憾,view.layer?.actions = [:]但实际上并没有奏效。设置速度很难,但是可以。
tcurdt

1

已更新,可在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)
    }
}

0

iOS 7开始,有一种便捷的方法可以做到这一点:

[UIView performWithoutAnimation:^{
    // apply changes
}];

1
我认为该方法不会阻止CALayer动画
约翰

1
@Benjohn啊,我认为你是对的。在八月份了解的不多。我应该删除这个答案吗?
2013年

:-)我也不确定,对不起!无论如何,这些评论传达了不确定性,因此可能还可以。
约翰

0

要在更改CATextLayer的string属性时禁用烦人的(模糊的)动画,可以执行以下操作:

class CANullAction: CAAction {
    private static let CA_ANIMATION_CONTENTS = "contents"

    @objc
    func runActionForKey(event: String, object anObject: AnyObject, arguments dict: [NSObject : AnyObject]?) {
        // Do nothing.
    }
}

然后像这样使用它(不要忘记正确设置CATextLayer,例如正确的字体等):

caTextLayer.actions = [CANullAction.CA_ANIMATION_CONTENTS: CANullAction()]

您可以在此处查看我对CATextLayer的完整设置:

private let systemFont16 = UIFont.systemFontOfSize(16.0)

caTextLayer = CATextLayer()
caTextLayer.foregroundColor = UIColor.blackColor().CGColor
caTextLayer.font = CGFontCreateWithFontName(systemFont16.fontName)
caTextLayer.fontSize = systemFont16.pointSize
caTextLayer.alignmentMode = kCAAlignmentCenter
caTextLayer.drawsAsynchronously = false
caTextLayer.actions = [CANullAction.CA_ANIMATION_CONTENTS: CANullAction()]
caTextLayer.contentsScale = UIScreen.mainScreen().scale
caTextLayer.frame = CGRectMake(playbackTimeImage.layer.bounds.origin.x, ((playbackTimeImage.layer.bounds.height - playbackTimeLayer.fontSize) / 2), playbackTimeImage.layer.bounds.width, playbackTimeLayer.fontSize * 1.2)

uiImageTarget.layer.addSublayer(caTextLayer)
caTextLayer.string = "The text you want to display"

现在,您可以根据需要更新caTextLayer.string =)

灵感来自这个这个答案。


0

试试这个。

let layer = CALayer()
layer.delegate = hoo // Same lifecycle UIView instance.

警告

如果设置UITableView实例的委托,有时会发生崩溃。(可能滚动调用了scrollview的hittest。)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.