Отключение неявной анимации в - [CALayer setNeedsDisplayInRect:] - PullRequest
128 голосов
/ 11 февраля 2010

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

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

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

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

Ответы [ 14 ]

165 голосов
/ 11 февраля 2010

Вы можете сделать это, установив словарь действий на слое так, чтобы он возвращал [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(),
    ]
90 голосов
/ 30 марта 2011

Также:

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

//foo

[CATransaction commit];
29 голосов
/ 08 июля 2014

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

Objective-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()
23 голосов
/ 19 апреля 2013

В дополнение к ответ Брэда Ларсона : для пользовательских слоев (которые вы создали) вы можете использовать делегирование вместо изменения словаря actions слоя. Этот подход является более динамичным и может быть более производительным. И это позволяет отключить все неявные анимации без перечисления всех анимируемых ключей.

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

8 голосов
/ 30 октября 2015

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

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

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

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

7 голосов
/ 04 декабря 2013

На основании ответа Сэма и трудностей Саймона ... добавьте ссылку на делегат после создания 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];

}
7 голосов
/ 21 мая 2013

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

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

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

5 голосов
/ 24 марта 2015

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

[CATransaction setDisableActions:YES];

Swift:

CATransaction.setDisableActions(true)
3 голосов
/ 10 апреля 2018

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

CATransaction.setDisableActions(true)
2 голосов
/ 14 января 2015

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