Как выполнить блокировку, ожидающую завершения анимации?
Я реализую панель состояния, которая анимируется с помощью пользовательского ввода.
Эти анимации заставляют его подниматься или опускаться на определенную величину (скажем, на 50 единиц) и являются результатом нажатия кнопки. Есть две кнопки. Увеличение и уменьшение.
Я хочу выполнить блокировку на панели состояния, чтобы за один раз ее мог изменить только один поток. Проблема в том, что я зашел в тупик.
Я предполагаю, потому что отдельный поток выполняет блокировку, которая удерживается другим потоком. Но эта блокировка уступит место, когда анимация завершится. Как реализовать блокировку, которая заканчивается, когда завершается [UIView AnimateWithDuration]?
Интересно, если NSConditionLock
это путь, но я хочу использовать NSLocks, если это возможно, чтобы избежать ненужной сложности. Что вы порекомендуете?
(В конце концов, я хочу, чтобы анимации "встали в очередь", пока пользовательский ввод продолжается, но сейчас я просто хочу, чтобы блокировка работала, даже если она сначала блокирует ввод.)
(Хм, подумайте об этом, есть только один [UIView AnimateWithDuration], запущенный одновременно для того же самого UIView. Второй вызов прервет первый, в результате чего обработчик завершения будет запущен сразу для первого. Возможно, вторая блокировка запускается до того, как У первого есть шанс разблокировать. Как лучше всего справиться с блокировкой в этом случае? Возможно, мне стоит вернуться к Grand Central Dispatch, но я хотел посмотреть, есть ли более простой способ.)
В ViewController.h я заявляю:
NSLock *_lock;
В ViewController.m у меня есть:
В loadView:
_lock = [[NSLock alloc] init];
Остальная часть ViewController.m (соответствующие части):
-(void)tryTheLockWithStr:(NSString *)str
{
LLog(@"\n");
LLog(@" tryTheLock %@..", str);
if ([_lock tryLock] == NO)
{
NSLog(@"LOCKED.");
}
else
{
NSLog(@"free.");
[_lock unlock];
}
}
// TOUCH DECREASE BUTTON
-(void)touchThreadButton1
{
LLog(@" touchThreadButton1..");
[self tryTheLockWithStr:@"beforeLock"];
[_lock lock];
[self tryTheLockWithStr:@"afterLock"];
int changeAmtInt = ((-1) * FILLBAR_CHANGE_AMT);
[self updateFillBar1Value:changeAmtInt];
[UIView animateWithDuration:1.0
delay:0.0
options:(UIViewAnimationOptionTransitionNone|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction)
animations:
^{
LLog(@" BEGIN animationBlock - val: %d", self.fillBar1Value)
self.fillBar1.frame = CGRectMake(FILLBAR_1_X_ORIGIN,FILLBAR_1_Y_ORIGIN, self.fillBar1Value,30);
}
completion:^(BOOL finished)
{
LLog(@" END animationBlock - val: %d - finished: %@", self.fillBar1Value, (finished ? @"YES" : @"NO"));
[self tryTheLockWithStr:@"beforeUnlock"];
[_lock unlock];
[self tryTheLockWithStr:@"afterUnlock"];
}
];
}
-(void)updateFillBar1Value:(int)changeAmt
{
self.prevFillBar1Value = self.fillBar1Value;
self.fillBar1Value += changeAmt;
if (self.fillBar1Value < FILLBAR_MIN_VALUE)
{
self.fillBar1Value = FILLBAR_MIN_VALUE;
}
else if (self.fillBar1Value > FILLBAR_MAX_VALUE)
{
self.fillBar1Value = FILLBAR_MAX_VALUE;
}
}
Выход:
Чтобы воспроизвести инструкции: нажмите "Уменьшить" один раз
touchThreadButton1..
попробуйте TheLock beforeLock.. бесплатно.
tryTheLock afterLock.. ЗАБЛОКИРОВАНО. BEGIN animationBlock - значение: 250 END animationBlock - значение: 250 - завершено: ДА
попробуй заблокировать до разблокировки.. ЗАБЛОКИРОВАН.
попробуйте theLock после разблокировки.. бесплатно.
Вывод: это работает как ожидалось.
-
Выход:
Чтобы воспроизвести инструкции: быстро нажмите "Уменьшить" дважды (прерывая первоначальную анимацию).
touchThreadButton1..
попробуйте TheLock beforeLock.. бесплатно.
tryTheLock afterLock.. ЗАБЛОКИРОВАНО. НАЧАТЬ анимациюБлок - значение: 250 TouchThreadButton1..
попробуйте TheLock beforeLock.. ЗАБЛОКИРОВАНО. * - [NSLock lock]: deadlock ('(null)') * Сбой _NSLockError() для отладки.
Заключение. Ошибка тупика. Пользовательский ввод заморожен.
1 ответ
Внизу, в моем исходном ответе, я описываю способ достижения запрошенной функциональности (если вы запускаете анимацию, пока предыдущая анимация еще выполняется, поставьте в очередь эту последующую анимацию, чтобы запускать ее только после завершения текущих).
Хотя я сохраню это для исторических целей, я мог бы предложить совершенно другой подход. В частности, если вы нажмете кнопку, которая должна привести к анимации, но предыдущая анимация все еще выполняется, я бы вместо этого предложил удалить старую анимацию и немедленно запустить новую анимацию, но сделать это таким образом, чтобы новая анимация начинается с того места, где остановился текущий.
В версиях iOS, предшествующих iOS 8, проблема заключается в том, что если вы запускаете новую анимацию в то время, когда другая находится в процессе выполнения, ОС неловко сразу же перескакивает туда, где закончилась бы текущая анимация, и запускает новую анимацию оттуда.
Типичное решение в версиях iOS до 8 будет:
захватить
presentationLayer
анимированного представления (это текущее состояниеCALayer
изUIView
... если вы посмотрите наUIView
пока идет анимация, вы увидите окончательное значение, и нам нужно получить текущее состояние);захватить текущее значение анимированного значения свойства из этого
presentationLayer
;удалить анимацию;
сбросить анимированное свойство к "текущему" значению (чтобы оно не переходило в конец предыдущей анимации перед началом следующей анимации);
инициировать анимацию до "нового" значения;
Так, например, если вы анимируете изменение
frame
который может быть в процессе анимации, вы можете сделать что-то вроде:CALayer *presentationLayer = animatedView.layer.presentationLayer; CGRect currentFrame = presentationLayer.frame; [animatedView.layer removeAllAnimations]; animatedView.frame = currentFrame; [UIView animateWithDuration:1.0 animations:^{ animatedView.frame = newFrame; }];
Это полностью устраняет всю неловкость, связанную с постановкой в очередь "следующей" анимации, запускаемой после завершения "текущей" анимации (и других анимаций в очереди). Вы также получите гораздо более отзывчивый пользовательский интерфейс (например, вам не нужно ждать завершения предыдущих анимаций, прежде чем начнется желаемая анимация пользователя).
В iOS 8 этот процесс бесконечно проще, где, если вы запустите новую анимацию, она будет часто запускать анимацию не только из текущего значения анимированного свойства, но и определять скорость, с которой эта анимация в данный момент анимируется. свойство изменяется, что приводит к плавному переходу между старой анимацией и новой анимацией.
Для получения дополнительной информации об этой новой функции iOS 8, я бы посоветовал вам обратиться к видео WWDC 2014 Построение прерывистых и гибких взаимодействий.
Для полноты изложения я оставлю свой первоначальный ответ ниже, так как он пытается точно использовать функциональность, изложенную в вопросе (просто используется другой механизм, чтобы гарантировать, что основная очередь не заблокирована). Но я бы действительно посоветовал рассмотреть возможность остановки текущей анимации и запуска новой таким образом, чтобы она начиналась с того места, где могла закончиться какая-либо анимация в процессе.
Оригинальный ответ:
Я бы не рекомендовал оборачивать анимацию в NSLock
(или семафор, или любой другой подобный механизм), потому что это может привести к блокировке основного потока. Вы никогда не хотите блокировать основной поток. Я думаю, что ваша интуиция об использовании последовательной очереди для изменения размера операций является многообещающей. И вы, вероятно, хотите операцию "изменения размера", которая:
инициирует
UIView
анимация в главной очереди (все обновления пользовательского интерфейса должны происходить в главной очереди); а такжев блоке завершения анимации завершите операцию (мы не завершаем операцию до тех пор, пока другие операции из очереди не начнутся до тех пор, пока эта операция не будет завершена).
Я мог бы предложить операцию изменения размера:
SizeOperation.h:
@interface SizeOperation : NSOperation
@property (nonatomic) CGFloat sizeChange;
@property (nonatomic, weak) UIView *view;
- (id)initWithSizeChange:(NSInteger)change view:(UIView *)view;
@end
SizingOperation.m:
#import "SizeOperation.h"
@interface SizeOperation ()
@property (nonatomic, readwrite, getter = isFinished) BOOL finished;
@property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
@end
@implementation SizeOperation
@synthesize finished = _finished;
@synthesize executing = _executing;
- (id)initWithSizeChange:(NSInteger)change view:(UIView *)view
{
self = [super init];
if (self) {
_sizeChange = change;
_view = view;
}
return self;
}
- (void)start
{
if ([self isCancelled] || self.view == nil) {
self.finished = YES;
return;
}
self.executing = YES;
// note, UI updates *must* take place on the main queue, but in the completion
// block, we'll terminate this particular operation
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[UIView animateWithDuration:2.0 delay:0.0 options:kNilOptions animations:^{
CGRect frame = self.view.frame;
frame.size.width += self.sizeChange;
self.view.frame = frame;
} completion:^(BOOL finished) {
self.finished = YES;
self.executing = NO;
}];
}];
}
#pragma mark - NSOperation methods
- (void)setExecuting:(BOOL)executing
{
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)setFinished:(BOOL)finished
{
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
@end
Затем определите очередь для этих операций:
@property (nonatomic, strong) NSOperationQueue *sizeQueue;
Убедитесь, что создали экземпляр этой очереди (как последовательная очередь):
self.sizeQueue = [[NSOperationQueue alloc] init];
self.sizeQueue.maxConcurrentOperationCount = 1;
И тогда, все, что делает рассматриваемый вид, будет расти:
[self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:+50.0 view:self.barView]];
И все, что сокращает видимость, сделало бы:
[self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:-50.0 view:self.barView]];
Надеюсь, это иллюстрирует идею. Есть всевозможные уточнения:
Я сделал анимацию очень медленной, поэтому я мог легко поставить в очередь целую кучу, но вы, вероятно, использовали бы гораздо более короткое значение;
Если вы используете автоматическое расположение, вы бы отрегулировали ограничение ширины
constant
и в блоке анимации вы бы выполнитьlayoutIfNeeded
) вместо того, чтобы регулировать рамку напрямую; а такжеВозможно, вы захотите добавить проверки, чтобы не выполнять смену кадра, если ширина достигла некоторых максимальных / минимальных значений.
Но ключ в том, что использование блокировок для управления анимацией изменений пользовательского интерфейса нецелесообразно. Вам не нужно ничего, что могло бы блокировать основную очередь на что-либо, кроме нескольких миллисекунд. Анимационные блоки слишком длинные, чтобы предусмотреть блокировку основной очереди. Поэтому используйте очередь последовательных операций (и если у вас есть несколько потоков, которые должны инициировать изменения, они просто добавят операцию в одну и ту же очередь общих операций, тем самым автоматически координируя изменения, инициированные из всех видов различных потоков).