Отключение неявных анимаций в - [CALayer setNeedsDisplayInRect:]

В методе -drawInContext: у меня есть слой с некоторым сложным кодом чертежа. Я пытаюсь свести к минимуму количество чертежа, которое мне нужно сделать, поэтому я использую -setNeedsDisplayInRect: обновлять только измененные части. Это прекрасно работает. Однако, когда графическая система обновляет мой слой, он переходит от старого к новому изображению с помощью перекрестного затухания. Я бы хотел, чтобы он мгновенно переключился.

Я попытался использовать CATransaction, чтобы отключить действия и установить длительность в ноль, и ни одна из них не работает. Вот код, который я использую:

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

Есть ли другой метод для CATransaction, который я должен использовать вместо этого (я также попытался -setValue: forKey: с kCATransactionDisableActions, тот же результат).

Ответ 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(),
    ]

Ответ 2

также:

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

//foo

[CATransaction commit];

Ответ 3

При изменении свойства слоя CA обычно создает неявный объект транзакции для анимации изменения. Если вы не хотите анимировать изменение, вы можете отключить неявные анимации, создав явную транзакцию и установив для свойства 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()

Ответ 4

В дополнение к ответ Брэда Ларсона: для пользовательских слоев (созданных вами) вы можете использовать делегирование вместо изменения словаря 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 "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, потому что фактическое перерисовка может (и, вероятно, будет) происходить иногда позже. Хороший подход заключается в использовании пользовательских свойств, как описано в этом ответе.

Ответ 5

Здесь более эффективное решение, похожее на принятый ответ, но для Swift. В некоторых случаях это будет лучше, чем создание транзакции каждый раз, когда вы изменяете значение, что является проблемой производительности, как уже упоминалось в других, например, в общем случае перетаскивания позиции слоя со скоростью 60 кадров в секунду.

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

См. Документы Apple, чтобы узнать, как решаются действия слоя. Реализация делегата пропустила бы еще один уровень в каскаде, но в моем случае это было слишком запутанно из-за предостережения о том, что делегату необходимо установить соответствующий ассоциированный UIView.

Редактировать: Обновлено благодаря комментатору, указывающему, что NSNull соответствует CAAction.

Ответ 6

Собственно, я не нашел ни одного ответа, чтобы быть правильным. Метод, который решает проблему для меня, таков:

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

Тогда вы можете использовать любую логику в нем, чтобы отключить определенную анимацию, но так как я хотел удалить их все, я вернул nil.

Ответ 7

На основе ответа Сэма и трудностей Саймона... добавьте ссылку делегата после создания CSShapeLayer:

CAShapeLayer *myLayer = [CAShapeLayer layer];
myLayer.delegate = self; // <- set delegate here, it magic.

... в другом месте в файле "m"...

По существу то же самое, что и Сэм без возможности переключения с помощью настраиваемой переменной "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

Обнаружен более простой метод отключения действия внутри CATransaction который внутренне вызывает setValue:forKey: для ключа kCATransactionDisableActions:

[CATransaction setDisableActions:YES];

Swift:

CATransaction.setDisableActions(true)

Ответ 9

Чтобы отключить неявную анимацию слоя в Swift

CATransaction.setDisableActions(true)

Ответ 10

Добавьте это в свой собственный класс, где вы используете метод -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];
}

Ответ 11

Обновлено для быстрого и отключения только одной неявной анимации свойств в 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)
    }
}

Ответ 12

Как и в iOS 7, существует удобный метод, который выполняет только это:

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

Ответ 13

Чтобы отключить раздражающую (размытую) анимацию при изменении свойства строки 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 столько, сколько хотите =)

Вдохновленный этим и этот ответ.

Ответ 14

Попробуйте это.

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

Внимание

Если вы установите делегат экземпляра UITableView, иногда произойдет сбой. (Вероятно, scrollview hittest называется рекурсивно.)

Ответ 15

Если вам понадобится очень быстрое (но, по общему признанию, хакерское) исправление, возможно, стоит просто сделать (Swift):

let layer = CALayer()

// set other properties
// ...

layer.speed = 999