2013-08-24 48 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

希望我可以不止一次地对此赞赏。 :) –