2013-08-24 3 views
5

Я реализую панель здоровья, которая анимируется с помощью пользовательского ввода.Как выполнить блокировку, ожидающую завершения анимации?

Эти анимации позволяют продвигаться вверх или вниз на определенную величину (например, 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; 
    } 
} 

Выход:

To-Воспроизведите Инструкции: Нажмите "Уменьшить" раз

touchThreadButton1 ..

tryTheLock beforeLock .. бесплатно.

tryTheLock последующийLock .. LOCKED. НАЧАТЬ animationBlock - Вэл: 250 END animationBlock - VAL: 250 - закончил: ДА

tryTheLock beforeUnlock .. LOCKED.

tryTheLock последующийUnlock .. свободный.

Заключение: Работает так, как ожидалось.

-

Выход:

To-Воспроизведите Инструкции: Нажмите "Уменьшить" дважды быстро (перебивает начальную анимацию) ..

touchThreadButton1 ..

tryTheLock передLock .. бесплатный.

tryTheLock последующийLock .. LOCKED. НАЧАТЬ animationBlock - VAL: 250 touchThreadButton1 ..

tryTheLock beforeLock .. LOCKED. * - [Блокировка NSLock]: тупик ('(null)') * Перерыв на _NSLockError() для отладки.

Заключение. Ошибка взаимоблокировки. Ввод пользователя заморожен.

ответ

8

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

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

  1. В версиях 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; 
    }]; 
    

    Это полностью устраняет все громоздкости связанный с очередью «следующей» анимации для запуска после «текущей» анимации (и других анимаций с очередью). В итоге вы получаете гораздо более отзывчивый интерфейс (например, вам не нужно ждать, пока предыдущие анимации не закончатся, прежде чем начнется нужная анимация пользователя).

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

    Для получения дополнительной информации об этой новой функции iOS 8, я предлагаю вам обратиться к видео WWDC 2014 Building Interruptible and Responsive Interactions.

Для полноты картины, я буду держать оригинальный ответ ниже, как он пытается точно решить функциональность, описанную в вопросе (просто использует другой механизм для обеспечения основной очереди не блокируется) , Но я бы действительно предложил рассмотреть возможность остановки текущей анимации и начать новую так, чтобы она начиналась с того места, где могла отсутствовать какая-либо анимация в процессе.


Оригинальный ответ:

я бы не рекомендовал оберточной анимацию в NSLock (или семафор, или любого другого аналогичного механизма), потому что может привести к блокировке основного потока. Вы никогда не хотите блокировать основной поток. Я думаю, что ваша интуиция об использовании последовательной очереди для операций изменения размеров является многообещающей. И вы, вероятно, хотите «изменение размера» операцию:

  • инициирует UIView анимации на главной очереди (все UI обновления должны иметь место на главной очереди); и

  • в блоке завершения анимации завершите операцию (мы до конца не завершаем операцию, чтобы другие операции в очереди не запускались до тех пор, пока это не закончится).

Я мог бы предложить операцию изменения размера:

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), а не корректировать рамку напрямую; и

  • Возможно, вы захотите добавить проверки, чтобы не выполнять смену кадра, если ширина достигла максимального/минимального значения.

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

+1

Спасибо, сэр! Я ценю, как вы четко демонстрируете NSOperationQueue. Инкапсуляция операции размера в своем классе довольно изящна. Он работает отлично. –

+0

@BlackOrchid FYI, я уверен, что вы давно прошли мимо этого вопроса, но я обновил его с помощью значительно более простого подхода. – Rob

+0

Жаль, что я не мог это сделать. :) –