2013-08-24 20 views
5

Wprowadzam pasek zdrowia, który animuje się za pomocą danych wprowadzanych przez użytkownika.Jak wykonać blokadę, która czeka aż animacja się zakończy?

Te animacje powodują zwiększenie lub zmniejszenie o określoną wartość (na przykład 50 jednostek) i są wynikiem naciśnięcia przycisku. Są dwa przyciski. Zwiększ i zmniejsz.

Chcę wykonać blokadę na pasku stanu, tak aby tylko jeden wątek mógł ją zmienić jednocześnie. Problem polega na tym, że dostaję impasu.

Zgaduję, ponieważ oddzielny wątek uruchamia blokadę, która jest utrzymywana przez inny wątek. Ale ta blokada ustąpi po zakończeniu animacji. Jak zaimplementować blokadę, która kończy się po zakończeniu [UIView AnimateWithDuration]?

Zastanawiam się, czy jest to możliwe, aby przejść na NSConditionLock, ale jeśli to możliwe, chcę użyć NSLocks, aby uniknąć niepotrzebnej złożoności. Co polecasz?

(W końcu chcę mieć animacje „kolejce”, podczas gdy pozwalając wejście użytkownik nadal, ale teraz po prostu chcę, aby uzyskać blokadę pracy, nawet jeśli wejście blokuje pierwszy).

(Hmm przyjść do myślę o tym, że jest tylko jedna [UIView AnimateWithDuration] działająca w tym samym czasie dla tego samego UIView. Drugie wywołanie przerywa pierwszą, powodując, że procedura zakończenia zostanie uruchomiona natychmiast dla pierwszego. Może druga blokada działa, zanim pierwsza ma szansę aby odblokować Jaki jest najlepszy sposób na zablokowanie w tym przypadku? Być może powinienem ponownie odwiedzić Grand Central Dispatch, ale chciałem sprawdzić, czy był prostszy sposób.)

W ViewController.h Deklaruję:

NSLock *_lock; 

W ViewController.m mam:

W loadview:

_lock = [[NSLock alloc] init]; 

Reszta ViewController.m (istotne części):

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

wyjściowa:

To-odtworzenia Instrukcje: Wybierz "Zmniejsz" raz

touchThreadButton1 ..

tryTheLock beforeLock .. darmo.

tryTheLock afterLock .. ZABLOKOWANE. BEGIN animationBlock - Val: 250 END animationBlock - Val: 250 - Ukończony TAK

tryTheLock beforeUnlock .. zablokowana.

tryTheLock afterUnlock .. za darmo.

Wniosek: Działa to zgodnie z oczekiwaniami.

-

wyjściowa:

To-odtworzenia Instrukcje: Wybierz "Zmniejsz" dwa razy szybko (przerywa początkową animację) ..

touchThreadButton1 ..

tryTheLock beforeLock .. za darmo.

tryTheLock afterLock .. ZABLOKOWANE. UŻYCIEM animationBlock - val 250 touchThreadButton1 ..

tryTheLock beforeLock .. zablokowane. * - [Blokada NSLock]: zakleszczenie ('(null)') * Przerwij _NSLockError() do debugowania.

Podsumowanie. Błąd impasu. Wprowadzane przez użytkownika jest zamrożone.

Odpowiedz

8

U dołu, w mojej oryginalnej odpowiedzi, opisuję sposób na osiągnięcie żądanej funkcjonalności (jeśli zainicjowałeś animację, podczas gdy poprzednia animacja nadal trwa, ustaw kolejkę w kolejnej animacji, aby rozpoczynała się dopiero po obecnych są skończone).

Chociaż zachowam to dla celów historycznych, mogę zaproponować zupełnie inne podejście. W szczególności, jeśli naciśniesz przycisk, który powinien spowodować animację, ale poprzednia animacja jest nadal w toku, sugerowałbym usunięcie starej animacji i natychmiast rozpocząć nową animację, ale rób to w taki sposób, aby nowy animacja odrywa się od miejsca, w którym zostało przerwane.

  1. w iOS wersjach poprzedzających iOS 8, wyzwaniem jest to, że jeśli zaczniesz nową animację podczas gdy inny jest w toku, OS niezręcznie natychmiast skacze do których prąd animacja skończyłaby i rozpoczyna nową animację z tam.

    Typowym rozwiązaniem w wersjach iOS przed 8 byłoby:

    • chwycić presentationLayer z animowanym widzenia (to jest obecny stan CALayer z UIView ...jeśli spojrzysz na UIView podczas animacji, zobaczysz ostateczną wartość i musimy pobrać aktualny stan);

    • pobrać bieżącą wartość animowanej wartości nieruchomości z tego presentationLayer;

    • usuń animacje;

    • resetuje animowaną właściwość do "bieżącej" wartości (tak, że nie wydaje się, aby przeskoczyła do końca poprzedniej animacji przed rozpoczęciem następnej animacji);

    • zainicjować animację do "nowej" wartości;

    Tak więc, na przykład, jeśli animowanie zmianę w frame co może być w postępie animacji, możesz zrobić coś takiego:

    CALayer *presentationLayer = animatedView.layer.presentationLayer; 
    CGRect currentFrame = presentationLayer.frame; 
    [animatedView.layer removeAllAnimations]; 
    animatedView.frame = currentFrame; 
    [UIView animateWithDuration:1.0 animations:^{ 
        animatedView.frame = newFrame; 
    }]; 
    

    Ten całkowicie eliminuje wszystkie niezręczności związane z kolejkowaniem w górę "następnej" animacji do uruchomienia po zakończeniu "aktualnej" animacji (i innych animacji w kolejce). W efekcie uzyskujesz również znacznie bardziej responsywny interfejs użytkownika (np. Nie musisz czekać, aż wcześniejsze animacje zakończą się przed rozpoczęciem pożądanej animacji użytkownika).

  2. W iOS 8 proces ten jest nieskończenie łatwiejszy, a kiedy rozpocznie się nowa animacja, często rozpoczyna animację od nie tylko aktualnej wartości animowanej właściwości, ale również określa prędkość na którą zmienia się obecnie ta animowana właściwość, co powoduje płynne przejście między starą animacją i nową animacją.

    Aby uzyskać więcej informacji o tej nowej funkcji systemu iOS 8, sugeruję odnieść się do filmu WWDC 2014 wideo Building Interruptible and Responsive Interactions.

Dla kompletności wywodu, będę poniżej mój oryginalny odpowiedź, ponieważ stara się precyzyjnie zmierzyć się z funkcjonalności opisanej w pytaniu (tylko używa innego mechanizmu zapewniającego głównym kolejka nie jest zablokowany) . Ale naprawdę zasugerowałbym rozważenie zatrzymania aktualnej animacji i uruchomienia nowej w taki sposób, aby rozpoczynała się ona z miejsca, w którym mogłaby zostać przerwana animacja w toku.


Oryginalny odpowiedź:

Nie polecam owijania animację w NSLock (lub semafora, lub innego podobnego mechanizmu), ponieważ może to doprowadzić do zablokowania głównego wątku. Nigdy nie chcesz blokować głównego wątku. Myślę, że twoja intuicja o używaniu kolejki szeregowej do zmiany rozmiaru operacji jest obiecująca. A ty pewnie chcesz „Zmiana rozmiarów” operację:

  • inicjuje animację UIView na głównej kolejki (wszystkie UI aktualizacje musi nastąpić na głównej kolejki); i

  • w bloku zakończenia animacji, zakończ operację (nie kończymy tej operacji do tego czasu, aby upewnić się, że inne operacje kolejkowane nie rozpoczną się, aż do ukończenia tej czynności).

mogę zaproponować zmiany rozmiaru operacji:

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 

Następnie określić kolejkę dla tych operacji:

@property (nonatomic, strong) NSOperationQueue *sizeQueue; 

Upewnij się do wystąpienia tej kolejki (jak kolejce szeregowy):

self.sizeQueue = [[NSOperationQueue alloc] init]; 
self.sizeQueue.maxConcurrentOperationCount = 1; 

a następnie coś, co sprawia, że ​​pogląd w kwestii rośnie, zrobi:

[self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:+50.0 view:self.barView]]; 

i wszystko, co sprawia, że widok skurczy się, zrobiłby:

[self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:-50.0 view:self.barView]]; 

Mam nadzieję, że ilustruje to pomysł. Istnieje wiele możliwych udoskonaleń:

  • zrobiłem animacja bardzo wolno, więc mogę łatwo kolejce całą masę, ale chcesz prawdopodobnie przy użyciu znacznie krótszą wartość;

  • przypadku korzystania układ automatycznego, byłbyś dostosowując szerokość ograniczenie na constant iw bloku animacji chcesz wykonać layoutIfNeeded) zamiast dostosowania ramkę bezpośrednio; i

  • Prawdopodobnie chcesz dodać kontrole, aby nie wykonywać zmiany ramek, jeśli szerokość osiągnęła pewne wartości maksymalne/minimalne.

Ale kluczem jest to, że używanie blokad do kontrolowania animacji zmian interfejsu użytkownika jest niewskazane. Nie chcesz niczego, co mogłoby zablokować główną kolejkę tylko przez kilka milisekund. Bloki animacji są zbyt długie, by kontemplować blokowanie głównej kolejki. Tak więc użyj kolejki operacji szeregowej (a jeśli masz wiele wątków, które wymagają zainicjowania zmian, wszystkie one po prostu dodadzą operację do tej samej kolejki operacji współdzielonej, tym samym automatycznie koordynując zmiany zainicjowane z różnych wątków).

+1

Dziękuję, proszę pana! Doceniam sposób, w jaki demonstrujesz NSOperationQueue w jasny sposób. Zamknięcie operacji wielkości w jej własnej klasie jest raczej eleganckie. Działa idealnie. –

+0

@BlackOrchid FYI, Jestem pewien, że już dawno temu minęło to pytanie, ale zaktualizowałem je o znacznie prostsze podejście. – Rob

+0

Chciałbym móc to zrobić więcej niż raz. :) –

Powiązane problemy