7

我正在实现一个通过用户输入动画的健康栏。

这些动画使其上升或下降一定量(比如 50 个单位),并且是按下按钮的结果。有两个按钮。增加和减少。

我想在健康栏上执行锁定,以便一次只有一个线程可以更改它。问题是我遇到了僵局。

我猜是因为一个单独的线程运行一个由另一个线程持有的锁。但是当动画完成时,那个锁就会让位。如何实现在 [UIView AnimateWithDuration] 完成时结束的锁?

我想知道是否NSConditionLock可行,但如果可能的话,我想使用 NSLocks 以避免不必要的复杂性。你有什么建议吗?

(最终我想让动画“排队”,同时让用户输入继续,但现在我只想让锁工作,即使它首先阻止输入。)

(嗯,想想看,同一个 UIView 一次只有一个 [UIView AnimateWithDuration] 运行。第二个调用将中断第一个调用,导致完成处理程序立即为第一个运行。也许第二个锁在首先有机会解锁。在这种情况下处理锁定的最佳方法是什么?也许我应该重新访问 Grand Central Dispatch 但我想看看是否有更简单的方法。)

在 ViewController.h 我声明:

NSLock *_lock;

在 ViewController.m 我有:

在加载视图中:

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

输出:

复制说明:点击“减少”一次

触摸线程按钮1..

tryTheLock beforeLock .. 免费。

tryTheLock afterLock.. 锁定。开始动画块 - 值:250 结束动画块 - 值:250 - 完成:是

tryTheLock beforeUnlock .. 锁定。

tryTheLock afterUnlock .. 免费。

结论:这按预期工作。

--

输出:

重现说明:快速点击“减少”两次(中断初始动画)..

触摸线程按钮1..

tryTheLock beforeLock .. 免费。

tryTheLock afterLock.. 锁定。BEGIN animationBlock - val: 250 touchThreadButton1..

tryTheLock beforeLock.. 锁定。 * -[NSLock lock]: deadlock ( '(null)') * Break on _NSLockError() 进行调试。

结论。死锁错误。用户输入被冻结。

4

1 回答 1

11

在底部,在我的原始答案中,我描述了一种实现所请求功能的方法(如果您在先前的动画仍在进行中时启动动画,则将这个后续动画排队以仅在当前动画完成后开始)。

虽然出于历史目的我会保留它,但我可能想提出一种完全不同的方法。具体来说,如果您点击一个应该产生动画的按钮,但之前的动画仍在进行中,我建议您删除旧动画并立即开始新动画,但这样做的方式是让新动画动画从当前动画停止的地方开始。

  1. 在 iOS 8 之前的 iOS 版本中,挑战在于,如果您在另一个动画正在进行时开始一个新动画,操作系统会笨拙地立即跳转到当前动画将结束的位置并从那里开始新动画。

    8 之前的 iOS 版本中的典型解决方案是:

    • 抓取动画视图的状态(这是动画的presentationLayer当前状态...如果您查看动画正在进行的时间,您将看到最终值,我们需要抓取当前状态);CALayerUIViewUIView

    • 从中获取动画属性值的当前值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 更新必须在主队列上进行);和

  • 在动画的完成块中,完成操作(直到那时我们才完成操作,以确保其他排队的操作在完成之前不会启动)。

我可能会建议调整大小操作:

大小操作.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并在动画块中执行 a layoutIfNeeded) 而不是直接调整框架;和

  • 如果宽度达到某些最大值/最小值,您可能希望添加检查以不执行帧更改。

但关键是使用锁来控制 UI 变化的动画是不可取的。除了几毫秒之外,您不想要任何可能阻塞主队列的东西。动画块太长,无法考虑阻塞主队列。所以使用串行操作队列(如果您有多个线程需要启动更改,它们都会将一个操作添加到同一个共享操作队列中,从而自动协调从各种不同线程启动的更改)。

于 2013-08-25T03:15:20.193 回答