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

140

У меня есть слой со сложным кодом рисования в методе -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
1
Я нашел много ответов ниже, чтобы работать на меня. Также полезен документ Apple Changing a Layer Default Behavior , в котором подробно описывается процесс принятия решения о неявных действиях.
euroburɳ
Это дублирующий вопрос к этому: stackoverflow.com/a/54656717/5067402
Райан Франческони,

Ответы:

174

Вы можете сделать это, установив для словаря действий на слое возврат [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(),
    ]
Брэд Ларсон
источник
25
Для предотвращения движения при смене рамки используйте @"position"клавишу.
mxcl
11
Также не забудьте добавить @"hidden"свойство в словарь действий, если вы переключаете видимость слоя таким образом и хотите отключить анимацию непрозрачности.
Эндрю
1
@BradLarson это та же самая идея , я придумал после того, как некоторые пытается (я отменяю actionForKey:вместо этого), обнаруживая fontSize, contents, onLayoutи bounds. Похоже, вы можете указать любой ключ, который вы могли бы использовать в setValue:forKey:методе, фактически указав сложные пути ключей, например bounds.size.
pqnet 08
13
На самом деле существуют константы для этих «специальных» строк, не представляющих свойство (например, kCAOnOrderOut для @ "onOrderOut"), хорошо задокументированные здесь: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Патрик Пийнаппель
1
@Benjohn Константы определены только для ключей, у которых нет соответствующего свойства. Кстати, ссылка кажется мёртвой, вот новый URL: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Патрик Пийнапель
89

Также:

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

//foo

[CATransaction commit];
mxcl
источник
3
Вы можете заменить //fooна, [self setNeedsDisplayInRect: rect]; [self displayIfNeeded];чтобы ответить на исходный вопрос.
Karoy Lorentey
1
Благодарность! Это также позволяет мне установить анимированный флаг в моем пользовательском представлении. Удобно для использования в ячейке табличного представления (где повторное использование ячейки может привести к некоторым странным анимациям при прокрутке).
Джо Д'Андреа,
3
Приводит к проблемам с производительностью для меня, настройки действий более производительны
Паскалиус 08
26
В сокращении:[CATransaction setDisableActions:YES]
titaniumdecoy
7
Добавление в комментарий @titaniumdecoy, на всякий случай, если кто-то запутался (например, я), [CATransaction setDisableActions:YES]является сокращением только для [CATransaction setValue:forKey:]строки. Вы все еще нужны beginи commitлиния.
Hlung
31

Когда вы изменяете свойство уровня, CA обычно создает неявный объект транзакции для анимации изменения. Если вы не хотите анимировать изменение, вы можете отключить неявную анимацию, создав явную транзакцию и установив для ее свойства kCATransactionDisableActions значение true .

Цель-C

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

Swift

CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// change properties here without animation
CATransaction.commit()
пользователь3378170
источник
6
setDisableActions: делает то же самое.
pronebird
3
Это было самое простое решение, которое я получил в Swift!
Jambaman
Комментарий @Andy - безусловно, лучший и самый простой способ сделать это!
Aᴄʜᴇʀᴏɴғᴀɪʟ
23

В дополнение к ответу Брэда Ларсона : для настраиваемых слоев (которые созданы вами) вы можете использовать делегирование вместо изменения 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,
Хммм. У меня никогда не было проблем с таким подходом. Код в связанном вопросе выглядит нормально, и, вероятно, проблема вызвана другим кодом.
skozin
Ах, я вижу, что вы уже разобрались, CALayerчто мешало noImplicitAnimationsработать неправильно . Может, стоит отметить свой ответ как правильный и объяснить, что не так с этим слоем?
skozin
Я просто тестировал не тот CALayerэкземпляр (у меня тогда было два).
aleclarson
1
Хорошее решение ... но NSNullне реализует CAActionпротокол, и это не протокол, который имеет только дополнительные методы. Этот код тоже дает сбой, и вы даже не можете перевести это на быстрый. Лучшее решение: сделайте ваш объект совместимым с CAActionпротоколом (с пустым runActionForKey:object:arguments:методом, который ничего не делает) и верните selfвместо [NSNull null]. Тот же эффект, но безопасный (точно не выйдет из строя), а также работает в Swift.
Mecki
9

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

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

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

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

Джаррод Смит
источник
Нет необходимости создавать NullActionдля Swift, уже NSNullсоответствует требованиям, CAActionпоэтому вы можете делать то же самое, что и в цели C: layer.actions = ["position": NSNull ()]
user5649358 07
Я объединил ваш ответ с этим, чтобы исправить мой анимированный CATextLayer stackoverflow.com/a/5144221/816017
Эрик Живкович
Это было отличным решением моей проблемы, связанной с необходимостью обхода задержки "анимации" при изменении цвета линий CALayer в моем проекте. Благодарность!!
PlateReverb 02
Коротко и мило! Отличное решение!
Дэвид Х
8

На самом деле, я не нашел ни одного правильного ответа. Для меня проблема была решена следующим образом:

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

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

Саймон
источник
Это сработало для меня. Не забудьте создать подкласс, CALayerчтобы переопределить метод.
eonil
7

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

CAShapeLayer *myLayer = [CAShapeLayer layer];
myLayer.delegate = self; // <- set delegate here, it's 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];

}
боб
источник
5

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

CATransaction.setDisableActions(true)
лапа
источник
Спасибо за этот ответ. Сначала я попробовал использовать, так disableActions()как это звучит так, как будто он делает то же самое, но на самом деле для получения текущего значения. Думаю, он тоже отмечен @discardable, поэтому его труднее обнаружить. Источник: developer.apple.com/documentation/quartzcore/catransaction/…
Остин,
5

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

[CATransaction setDisableActions:YES];

Swift:

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];
}
Камран Хан
источник
2

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

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

let layer = CALayer()

// set other properties
// ...

layer.speed = 999
Мартин ЧР
источник
3
Пожалуйста, никогда не делайте этого ffs
m1h4
@ m1h4, спасибо за это - объясните, пожалуйста, почему это плохая идея
Мартин ЧР
3
Потому что, если нужно отключить неявную анимацию, есть механизм для этого (либо транзакция ca с временно отключенными действиями, либо явная установка пустых действий на слой). Простая установка скорости анимации на что-то, как мы надеемся, достаточно высокое, чтобы она казалась мгновенной, вызывает массу ненужных накладных расходов на производительность (что, по словам оригинального автора, актуально для него) и возможность возникновения различных состояний гонки (рисунок все еще выполняется в отдельный буфер для быть анимированным на дисплее позже - а точнее, для вашего случая выше, через 0,25 / 999 сек.).
m1h4
Жаль, что view.layer?.actions = [:]это действительно не работает. Установка скорости некрасивая, но работает.
tcurdt
0

Начиная с iOS 7 есть удобный метод, который делает именно это:

[UIView performWithoutAnimation:^{
    // apply changes
}];
Коробление
источник
1
Я не верю, что этот метод блокирует анимацию CALayer .
Benjohn
1
@Benjohn А, я думаю, ты прав. В августе этого не знал. Следует ли мне удалить этот ответ?
Warpling
:-) Я тоже не уверен, извините! Комментарии все равно передают неопределенность, так что, вероятно, все в порядке.
Benjohn,
0

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

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

Эрик Живкович
источник
0

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

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

Предупреждение

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

Tueno
источник