如何使用GCD进行“连续”动画?

时间:2012-05-30 13:47:15

标签: iphone ios core-animation grand-central-dispatch

我正在尝试在远程通知到来时在屏幕上显示自定义UIView 5秒。

这样的代码:

//customView.alpha = 1.0 here
[UIView animateWithDuration:1 animations:^{
                                  customView.alpha = 0.3;
                              } 
                              completion:^(BOOL finished){
                                  // remove customView from super view.
                              }];

问题和我需要的东西

但是有些情况可能会在很短的时间间隔内发出一些通知,其中有几个customView可能会同时制作动画,而另一个可能会覆盖其他人。

我希望这些动画一个接一个地执行,这样它们就不会发生冲突。

假设但失败

//(dispatch_queue_t)queue was created in other parts of the code
dispatch_sync(queue, ^{
    [UIView animationWithDuration:animations:...];
});

在GCD队列中制作动画后,我得到的结果与我使用的原始代码相同,后者没有使用GCD。动画仍然存在冲突。

BTW ,我听说 动画或涉及UI 的任务应始终在主线程上运行,但在我的第二段代码中动画似乎做得很顺利。为什么呢?

5 个答案:

答案 0 :(得分:4)

如果它是每次运行的相同动画,那么您可以只存储动画应该运行的次数(与动画的重复计数属性不同)。

当您收到远程通知时,您会递增计数器并调用计数器的方法,如果计数器正好是一个。然后在方法中,你会在完成块中递归调用自己,同时每次都减少计数器。它看起来像这样(使用伪代码方法名称):

- (void)methodThatIsRunWhenTheNotificationIsReceived {
    // Do other stuff here I assume...
    self.numberOfTimesToRunAnimation = self.numberOfTimesToRunAnimation + 1;
    if ([self.numberOfTimesToRunAnimation == 1]) {
        [self methodThatAnimates];
    }
}

- (void)methodThatAnimates {
    if (self.numberOfTimesToRunAnimation > 0) {
        // Animation preparations ...
        [UIView animateWithDuration:1 
                         animations:^{
                                  customView.alpha = 0.3;
                         } 
                         completion:^(BOOL finished){
                                  // Animation clean up ...
                                  self.numberOfTimesToRunAnimation = self.numberOfTimesToRunAnimation - 1;
                                  [self methodThatAnimates];
                         }];
    }
}

答案 1 :(得分:4)

使用队列按顺序提交动画将不起作用,因为开始动画的方法会立即返回,并且动画将添加到动画树中以便稍后执行。队列中的每个条目都会在很短的时间内完成。

如果您的每个动画都在同一个视图上运行,那么默认情况下系统应该让每个动画在开始下一个动画之前完成运行。

引用UIViewAnimationOptionBeginFromCurrentState选项值的文档:

  

UIViewAnimationOptionBeginFromCurrentState

     

从中开始动画   与已在飞行中的动画相关联的当前设置。   如果此键不存在,则允许在新动画开始之前完成任何正在进行的动画。如果另一个动画是   不在飞行中,此键无效。

如果你想链接一系列动画,我会做的就是:

创建一个可变的动画块数组。 (代码块是对象,可以添加到数组中。)编写一个方法,将顶部动画块从数组中拉出(并从数组中删除)并使用animateWithDuration提交它:动画:完成,其中完成方法只是再次调用该方法。在将项目从数组中拉出之前,使代码断言锁定,并在删除项目后释放锁定。

然后,您可以编写响应传入通知的代码,方法是断言动画数组锁定,向锁定添加动画块,然后释放锁定。

答案 2 :(得分:1)

您可以使用(非)并发NSOperationQueue来“逐步”执行动画

  

NSOperationQueue类调节一组NSOperation对象的执行。添加到队列后,操作将保留在该队列中,直到明确取消或完成其任务为止。队列内的操作(​​但尚未执行)本身根据优先级和操作间对象依赖性进行组织,并相应地执行。应用程序可以创建多个操作队列并向其中任何一个提交操作。

     

操作间依赖关系提供绝对执行顺序   操作,即使这些操作位于不同的位置   操作队列。操作对象不被认为是准备好的   执行直到其所有相关操作都已完成执行。   对于准备执行的操作,始终是操作队列   执行相对于另一个具有最高优先级的那个就绪   操作。

答案 3 :(得分:0)

我建议在完成块中向触发动画的任何对象发送消息。然后,您可以让该对象对通知本身进行排队,并在每次收到消息时启动下一个通知。

答案 4 :(得分:0)

ProcedureKit(基于NSOperation)是现成解决方案的一个示例,但仅将其用于动画时非常重要。

我的Operation子类,用于对动画弹出窗口和其他内容进行排队:

class SerialAsyncOperation: Operation {

    private var _started = false

    private var _finished = false {
        willSet {
            guard _started, newValue != _finished else {
                return
            }
            willChangeValue(forKey: "isFinished")
        }
        didSet {
            guard _started, oldValue != _finished else {
                return
            }
            didChangeValue(forKey: "isFinished")
        }
    }

    private var _executing = false {
        willSet {
            guard newValue != _executing else {
                return
            }
            willChangeValue(forKey: "isExecuting")
        }
        didSet {
            guard oldValue != _executing else {
                return
            }
            didChangeValue(forKey: "isExecuting")
        }
    }

    override var isAsynchronous: Bool {
        return true
    }

    override var isFinished: Bool {
        return _finished
    }

    override var isExecuting: Bool {
        return _executing
    }

    override func start() {
        guard !isCancelled else {
            return
        }
        _executing = true
        _started = true
        main()
    }

    func finish() {
        _executing = false
        _finished = true
    }

    override func cancel() {
        _executing = false
        _finished = true
        super.cancel()
    }
}

使用示例:

// Setup a serial queue
private lazy var serialQueue: OperationQueue = {
    let queue = OperationQueue()
    queue.maxConcurrentOperationCount = 1
    queue.name = String(describing: type(of: self))
    return queue
}()

// subclass SerialAsyncOperation
private class MessageOperation: SerialAsyncOperation {

    // ...

    override func main() {
        DispatchQueue.main.async { [weak self] in
            // do UI stuff

            self?.present(completion: {
                self?.finish()  
            })
        }
    }

    func present(completion: @escaping () -> Void) {
        // do async animated presentation, calling completion() in its completion
    }

    func dismiss(completion: @escaping () -> Void) {
        // do async animated dismissal, calling completion() in its completion
    }

    // animated cancellation support
    override func cancel() {
        if isExecuting {
            dismiss(completion: {
                super.cancel()
            })
        } else {
            super.cancel()
        }
    }
}

基本上,只需将此操作添加到串行队列中,并记得在完成异步操作后调用finish()。此外,您可以通过一次呼叫取消串行队列上的所有操作,这些操作将被正常解除。