Вимкнення неявної анімації в - [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

1
Нижче я знайшов багато відповідей, щоб працювати на мене. Також корисним є документ Apple « Зміна поводження за замовчуванням шару» , який детально описує процес неявного дії.
ɲeuroburɳ

Відповіді:


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

11
Також не забудьте додати @"hidden"властивість до словника дій, якщо ви перемикаєте видимість шару таким чином і хочете вимкнути анімацію непрозорості.
Андрій

1
@BradLarson це та ж сама ідея , я придумав після того, як деякі намагається (я скасовую actionForKey:замість цього), виявляючи fontSize, contents, onLayoutі bounds. Схоже, ви можете вказати будь-який ключ, який ви могли використовувати в setValue:forKey:методі, фактично вказуючи складні шляхи ключів, як-от bounds.size.
pqnet

11
Насправді є константи цих "спеціальних" рядків, що не представляють властивості (наприклад, kCAOnOrderOut для @ "onOrderOut"), добре задокументовані тут: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Патрік Pijnappel

1
@Benjohn Визначаються лише ключі, які не мають відповідного властивості. До речі, посилання начебто мертве, ось нова URL-адреса: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Патрік Пійнаппель

89

Також:

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

//foo

[CATransaction commit];

3
Ви можете замінити //fooз [self setNeedsDisplayInRect: rect]; [self displayIfNeeded];відповіді на вихідний питання.
Карой Лорентей

1
Дякую! Це дозволяє мені встановити анімований прапор і на моєму власному перегляді. Зручний для використання в комірці перегляду таблиці (де повторне використання комірок може призвести до потрійних анімацій під час прокрутки).
Джо Д'Андреа

3
Приводить до проблем, пов'язаних із виконанням роботи, ефективніші налаштування дій
Паскалій

26
Скорочення:[CATransaction setDisableActions:YES]
титандекой

7
Додаючи до коментаря @titaniumdecoy, на всякий випадок, якщо хтось заплутався (як я), [CATransaction setDisableActions:YES]це скорочення для лише [CATransaction setValue:forKey:]рядка. Вам ще потрібні beginі commitлінії.
Хлунг

31

Коли ви змінюєте властивість шару, CA зазвичай створює неявний об'єкт транзакції, щоб анімувати зміни. Якщо ви не хочете анімувати зміни, ви можете відключити неявну анімацію, створивши явну транзакцію і встановивши її властивість kCATransactionDisableActions на істинне .

Ціль-С

[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: робить те ж саме.
Бен Сінклер

3
Це було найпростішим рішенням, з яким я працював у Свіфті!
Джамбаман

Коментар @Andy - це найкращий і найпростіший спосіб зробити це!
1818

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:]виклик всередині а CATransaction, оскільки фактичне перемальовування може (і, ймовірно, буде) траплятися іноді пізніше. Хороший підхід - використовувати власні властивості, як описано в цій відповіді .


Це не працює для мене. Дивіться тут.
aleclarson

Хммм. У мене ніколи не було проблем з таким підходом. Код у пов'язаному питанні виглядає нормально, і, ймовірно, проблема викликана деяким іншим кодом.
skozin

Ах, я бачу, що ви вже розібралися, що це неправильно CALayerзаважало noImplicitAnimationsпрацювати. Можливо, ви повинні позначити власну відповідь правильною та пояснити, що було з цим шаром?
skozin

Я просто тестував неправильний CALayerекземпляр (у мене було два на той час).
aleclarson

1
Приємне рішення ... але NSNullне реалізує CAActionпротокол, і це не той протокол, який має лише необов'язкові методи. Цей код також виходить з ладу, і ви навіть не можете його перекласти на швидкий. Краще рішення: Зробіть об'єкт відповідним CAActionпротоколу (з порожнім runActionForKey:object:arguments:методом, який нічого не робить) і поверніться selfзамість [NSNull null]. Той самий ефект, але безпечний (не вийде з ладу напевно), а також працює у Swift.
Мецькі

9

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

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

Дивіться документи Apple, як розв’язуються дії шару . Реалізація делегата пропустила б ще один рівень у каскаді, але в моєму випадку це було занадто безладним через застереження про необхідність делегата встановити на пов'язаний UIView .

Редагувати: Оновлено завдяки коментатору, який вказує, що NSNullвідповідає CAAction.


Не потрібно створювати NullActionдля Swift, NSNullвідповідає CAActionвже тому, щоб ви могли робити те саме, що ви робите в цілі C: layer.action = ["position": NSNull ()]
user5649358

Я об'єднав свою відповідь з цим , щоб виправити свою надихаючу CATextLayer stackoverflow.com/a/5144221/816017
Ерік Živković

Це було чудовим виправленням моєї проблеми, необхідної для обходу затримки «анімації» при зміні кольору ліній CALayer в моєму проекті. Дякую!!
PlateReverb

Коротке і солодке! Чудове рішення!
Девід Н

7

На основі відповіді Сема та труднощів Саймона ... додайте посилання делегата після створення CSShapeLayer:

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

... в іншому місці файлу "m" ...

По суті такий же, як у Сама, не маючи можливості перемикатися через користувальницьку компоновку змінних "disabledImplicitAnimations". Більше "жорсткого" підходу.

- (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;   
}

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


5

Щоб вимкнути неявні анімаційні шари у Swift

CATransaction.setDisableActions(true)

Дякую за цю відповідь. Я спершу спробував використовувати, disableActions()як здається, це робить те саме, але насправді це отримати поточне значення. Я думаю, що це @discardableтеж позначено , що робить це важче помітити. Джерело: developer.apple.com/documentation/quartzcore/catransaction/…
Остін

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

Якщо вам колись знадобиться дуже швидкий (але, правда, хакі) виправлення, можливо, варто просто зробити (Swift):

let layer = CALayer()

// set other properties
// ...

layer.speed = 999

3
Будь ласка, ніколи цього не робіть
m1h4

@ m1h4 спасибі за це - будь ласка, поясніть, чому це погана ідея
Martin CR

3
Тому що, якщо потрібно вимкнути неявну анімацію, існує механізм для цього (або трансакція з тимчасово відключеними діями, або явне встановлення порожніх дій на шар). Просто встановлення швидкості анімації на щось, сподіваємось, досить високе, щоб воно здавалося миттєвим, спричиняє навантаження непотрібних накладних робочих характеристик (про які автор згадує, що стосуються його) та потенціалу для різних умов гонки (малюнок все ще робиться в окремий буфер для анімовані на дисплеї пізніше - точніше, для вашого випадку вище, на 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 Ах, я думаю, ти маєш рацію. Не знали стільки в серпні. Чи слід видалити цю відповідь?
Warpling

:-) Я ніколи не впевнений, вибачте! Зауваження так чи інакше повідомляють про невизначеність, тому, мабуть, це добре.
Бенджон

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, іноді трапляються збої. (Можливо, найвищий результат прокрутки закликається рекурсивно.)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.