【问题标题】:Disabling implicit animations in -[CALayer setNeedsDisplayInRect:]在 -[CALayer setNeedsDisplayInRect:] 中禁用隐式动画
【发布时间】:2011-01-15 16:39:18
【问题描述】:

我在它的 -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(), ^{ });
  • 我在下面找到了许多适合我的答案。 Apple 的 Changing a Layer’s Default Behavior 文档也很有帮助,该文档详细描述了隐式操作决策过程。
  • 这是一个重复的问题:stackoverflow.com/a/54656717/5067402

标签: iphone ios core-animation calayer


【解决方案1】:

您可以通过设置图层上的操作字典来返回 [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(),
    ]

【讨论】:

  • 要在更改框架时防止移动,请使用@"position" 键。
  • 如果您以这种方式切换图层的可见性并希望禁用不透明动画,请务必在动作字典中添加@"hidden" 属性。
  • @BradLarson 这与我经过一番挣扎后想出的想法相同(我取代了actionForKey:),发现了fontSizecontentsonLayoutbounds。似乎您可以指定可以在 setValue:forKey: 方法中使用的任何键,实际上是指定复杂的键路径,例如 bounds.size
  • 这些“特殊”字符串实际上有一些不代表属性的常量(例如,@"onOrderOut" 的 kCAOnOrderOut)在此处详细记录:developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
  • @Benjohn 只有没有对应属性的键才定义了常量。顺便说一句,链接似乎已失效,这是新的 URL:developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
【解决方案2】:

还有:

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

//foo

[CATransaction commit];

【讨论】:

  • 您可以将//foo替换为[self setNeedsDisplayInRect: rect]; [self displayIfNeeded];来回答原问题。
  • 谢谢!这让我也可以在我的自定义视图上设置一个动画标志。方便在表格视图单元格中使用(其中单元格重用可能会导致滚动时出现一些幻觉动画)。
  • 对我来说导致性能问题,设置操作更高效
  • 速记:[CATransaction setDisableActions:YES]
  • 添加到@titaniumdecoy 评论,以防万一有人感到困惑(比如我),[CATransaction setDisableActions:YES] 只是[CATransaction setValue:forKey:] 行的简写。您仍然需要 begincommit 行。
【解决方案3】:

当您更改层的属性时,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()

【讨论】:

  • setDisableActions: 也一样。
  • 这是我在 Swift 中得到的最简单的解决方案!
  • @Andy 的评论是迄今为止最好和最简单的方法!
【解决方案4】:

除了Brad Larson's answer:用于自定义层(由您创建)you can use delegation,而不是修改层的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:] 调用放在CATransaction 中是没有用的,因为实际重绘可能(并且可能会)有时会在以后发生。好的方法是使用自定义属性,如in this answer 所述。

【讨论】:

  • 这对我不起作用。 See here.
  • 嗯。我对这种方法从来没有任何问题。链接问题中的代码看起来没问题,可能问题是由其他代码引起的。
  • 啊,我看到你已经解决了 CALayer 错误导致 noImplicitAnimations 无法工作。也许您应该将自己的答案标记为正确并解释该层出了什么问题?
  • 我只是用错误的CALayer 实例进行测试(当时我有两个)。
  • 不错的解决方案...但是NSNull 没有实现CAAction 协议,这不是只有可选方法的协议。这段代码也崩溃了,你甚至不能把它翻译成 swift。更好的解决方案:使您的对象符合CAAction 协议(使用一个不执行任何操作的空runActionForKey:object:arguments: 方法)并返回self 而不是[NSNull null]。效果相同但安全(肯定不会崩溃)并且在 Swift 中也可以使用。
【解决方案5】:

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

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

请参阅 Apple 的文档以获取 how layer actions are resolved。实现委托将在级联中再跳过一个级别,但在我的情况下,由于caveat about the delegate needing to be set to the associated UIView,这太混乱了。

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

【讨论】:

  • 无需为 Swift 创建 NullActionNSNull 已经符合 CAAction你可以在目标 C 中做同样的事情: layer.actions = [ "position" : NSNull() ]
  • 我把你的答案和这个结合起来修复我的动画 CATextLayer stackoverflow.com/a/5144221/816017
  • 这很好地解决了我在项目中更改 CALayer 线条颜色时需要绕过“动画”延迟的问题。谢谢!!
  • 又短又甜!很好的解决方案!
【解决方案6】:

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

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

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

【讨论】:

  • 这对我有用。不要忘记继承CALayer 来覆盖该方法。
【解决方案7】:

根据 Sam 的回答和 Simon 的困难...在创建 CSSShapeLayer 后添加委托引用:

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

}

【讨论】:

    【解决方案8】:

    在 Swift 中禁用隐式层动画

    CATransaction.setDisableActions(true)
    

    【讨论】:

    【解决方案9】:

    找到了一种更简单的方法来禁用 CATransaction 内部的操作,该方法在内部调用 setValue:forKey: 以获得 kCATransactionDisableActions 键:

    [CATransaction setDisableActions:YES];
    

    斯威夫特:

    CATransaction.setDisableActions(true)
    

    【讨论】:

      【解决方案10】:

      更新了 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)
          }
      }
      

      【讨论】:

        【解决方案11】:

        将此添加到您正在实现 -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];
        }
        

        【讨论】:

          【解决方案12】:

          如果您需要一个非常快速的(但公认的 hacky)修复,那么它可能值得做(Swift):

          let layer = CALayer()
          
          // set other properties
          // ...
          
          layer.speed = 999
          

          【讨论】:

          • 请永远不要这样做
          • @m1h4 谢谢你 - 请解释为什么这是一个坏主意
          • 因为如果需要关闭隐式动画,有一种机制可以做到这一点(临时禁用操作的 ca 事务或显式将空操作设置到图层上)。只是将动画速度设置为希望足够高以使其看起来即时会导致负载不必要的性能开销(原作者提到与他有关)和各种竞争条件的可能性(绘图仍然在单独的缓冲区中完成稍后动画到显示器中 - 准确地说,对于上述情况,在 0.25/999 秒后)。
          • 很遗憾view.layer?.actions = [:] 并没有真正起作用。设置速度很难看,但很有效。
          【解决方案13】:

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

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

          【讨论】:

          • 我不相信这个方法会阻塞CALayer animations
          • @Benjohn 啊,我认为你是对的。八月份的时候不知道。我应该删除这个答案吗?
          • :-) 我也不确定,抱歉!无论如何,cmets 都会传达不确定性,所以可能没问题。
          【解决方案14】:

          要在更改 CATextLayer 的字符串属性时禁用烦人(模糊)的动画,您可以这样做:

          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 =)

          灵感来自 thisthis 的回答。

          【讨论】:

            【解决方案15】:

            试试这个。

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

            警告

            如果你设置 UITableView 实例的委托,有时会发生崩溃。(可能是滚动视图的 hittest 被递归调用。)

            【讨论】:

              猜你喜欢
              • 2014-11-07
              • 2011-08-15
              • 1970-01-01
              • 2016-08-10
              • 2011-03-16
              • 1970-01-01
              • 2016-05-16
              • 1970-01-01
              • 2011-04-22
              相关资源
              最近更新 更多