2013-08-24 46 views
5

我正在實施一個通過用戶輸入進行動畫的健康欄。如何執行等待動畫完成的鎖定?

這些動畫使它上下一定量(比如50個單位),並且是按下按鈕的結果。有兩個按鈕。增加和減少。

我想在健康欄上執行鎖定,以便一次只有一個線程可以更改它。問題是我陷入了僵局。

我猜測是因爲一個單獨的線程運行由另一個線程持有的鎖。但是當動畫完成時,該鎖定會讓路。你如何實現在[UIView AnimateWithDuration]完成時結束的鎖定?

我在想如果NSConditionLock是要走的路,但我想盡可能使用NSLocks以避免不必要的複雜性。你有什麼建議?

(最後我想有動畫「排隊」,同時讓用戶輸入繼續,但現在我只是想獲得鎖的工作,即使它阻止輸入第一。)

(HMM來想象一下,同一個UIView只有一個[UIView AnimateWithDuration]運行,第二個調用會中斷第一個,導致完成處理程序立即運行,也許第二個鎖在第一個運行之前有一個機會在這種情況下處理鎖定的最好辦法是什麼?或許我應該重訪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 ..

tryTheLock beforeLock .. 免費。

tryTheLock afterLock .. LOCKED。 BEGIN animationBlock - VAL:250 END animationBlock - VAL:250 - 完成:YES

tryTheLock beforeUnlock .. 鎖定。

tryTheLock afterUnlock .. 免費。

結論:這按預期工作。

-

輸出:

重現的說明:輕按 「降低」 快速兩次(中斷初始動畫)..

touchThreadButton1 ..

tryTheLock beforeLock .. 免費。

tryTheLock afterLock .. LOCKED。 BEGIN animationBlock - VAL:250 touchThreadButton1 ..

tryTheLock beforeLock .. 鎖定。 * - [NS鎖定鎖定]:死鎖('(null)') *中斷_NSLockError()以進行調試。

結論:死鎖錯誤。用戶輸入被凍結。

回答

8

最後,在我的原始答案中,我描述了一種實現所需功能的方法(如果在先前的動畫仍在進行時啓動動畫,則將此後續動畫排隊以僅在當前動畫之後啓動完成)。

雖然我爲了歷史目的而保留這一點,但我可能想提出一種完全不同的方法。具體來說,如果您點擊一個應該導致動畫的按鈕,但以前的動畫仍在進行中,我會建議您刪除舊動畫並立即啓動新動畫,但要這樣做,以使新動畫從當前位置離開的任何地方拾取。

  1. 在之前的iOS 8的iOS版本,面臨的挑戰是,如果你開始一個新的動畫,而另一個是在進步,OS笨拙立即跳轉到當前動畫將結束和開始新的動畫那裏。

    在IOS之前的版本8中的典型的解決方案是:

    • 搶動畫視圖的presentationLayer(這是UIViewCALayer的當前狀態...如果您在動畫進行過程中查看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; 
    }]; 
    

    這樣就完全消除了所有的尷尬與「當前」動畫(和其他排隊動畫)完成後排隊「下一個」動畫相關聯。最後,您的UI也會更加靈敏(例如,您不必等待之前的動畫在用戶所需的動畫開始之前完成)。

  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),而不是直接調整幀;和

  • 如果寬度已達到某些最大/最小值,您可能希望添加檢查以不執行幀更改。

但關鍵是使用鎖來控制UI變化的動畫是不可取的。你不需要任何可以阻塞主隊列的任何東西,只需要幾毫秒。動畫塊太長而無法阻止主隊列。因此,使用一個串行操作隊列(如果你有多個線程需要發起更改,它們都只是將操作添加到同一個共享操作隊列中,從而自動協調從各種不同線程發起的更改)。

+1

謝謝先生!我很欣賞你如何以清晰的方式展示NSOperationQueue。將大小操作封裝在自己的類中相當優雅。它完美的作品。 –

+0

@BlackOrchid僅供參考,我相信你很久以前就已經過去了這個問題,但我用一種更簡單的方法對它進行了更新。 – Rob

+0

希望我可以不止一次地對此讚賞。 :) –