/** Copyright (c) 2014-present, Facebook, Inc. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. */ #import "POPAnimator.h" #import "POPAnimatorPrivate.h" #import #import #import #import #import "POPAnimation.h" #import "POPAnimationExtras.h" #import "POPBasicAnimationInternal.h" #import "POPDecayAnimation.h" #import using namespace std; using namespace POP; #define ENABLE_LOGGING_DEBUG 0 #define ENABLE_LOGGING_INFO 0 #if ENABLE_LOGGING_DEBUG #define FBLogAnimDebug NSLog #else #define FBLogAnimDebug(...) #endif #if ENABLE_LOGGING_INFO #define FBLogAnimInfo NSLog #else #define FBLogAnimInfo(...) #endif class POPAnimatorItem { public: id __weak object; NSString *key; POPAnimation *animation; NSInteger refCount; id __unsafe_unretained unretainedObject; POPAnimatorItem(id o, NSString *k, POPAnimation *a) POP_NOTHROW { object = o; key = [k copy]; animation = a; refCount = 1; unretainedObject = o; } ~POPAnimatorItem() { } bool operator==(const POPAnimatorItem& o) const { return unretainedObject == o.unretainedObject && animation == o.animation && [key isEqualToString:o.key]; } }; typedef std::shared_ptr POPAnimatorItemRef; typedef std::shared_ptr POPAnimatorItemConstRef; typedef std::list POPAnimatorItemList; typedef POPAnimatorItemList::iterator POPAnimatorItemListIterator; typedef POPAnimatorItemList::const_iterator POPAnimatorItemListConstIterator; static BOOL _disableBackgroundThread = YES; @interface POPAnimator () { #if TARGET_OS_IPHONE CADisplayLink *_displayLink; #else CVDisplayLinkRef _displayLink; int32_t _enqueuedRender; #endif POPAnimatorItemList _list; CFMutableDictionaryRef _dict; NSMutableArray *_observers; POPAnimatorItemList _pendingList; CFRunLoopObserverRef _pendingListObserver; CFTimeInterval _slowMotionStartTime; CFTimeInterval _slowMotionLastTime; CFTimeInterval _slowMotionAccumulator; CFTimeInterval _beginTime; os_unfair_lock _lock; BOOL _disableDisplayLink; } @end @implementation POPAnimator @synthesize delegate = _delegate; @synthesize disableDisplayLink = _disableDisplayLink; @synthesize beginTime = _beginTime; #if !TARGET_OS_IPHONE static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *context) { if (_disableBackgroundThread) { __unsafe_unretained POPAnimator *pa = (__bridge POPAnimator *)context; int32_t* enqueuedRender = &pa->_enqueuedRender; if (*enqueuedRender == 0) { OSAtomicIncrement32(enqueuedRender); dispatch_async(dispatch_get_main_queue(), ^{ [(__bridge POPAnimator*)context render]; OSAtomicDecrement32(enqueuedRender); }); } } else { [(__bridge POPAnimator*)context render]; } return kCVReturnSuccess; } #endif // call while holding lock static void updateDisplayLink(POPAnimator *self) { BOOL paused = (0 == self->_observers.count && self->_list.empty()) || self->_disableDisplayLink; #if TARGET_OS_IPHONE if (paused != self->_displayLink.paused) { FBLogAnimInfo(paused ? @"pausing display link" : @"unpausing display link"); self->_displayLink.paused = paused; } #else if (paused == CVDisplayLinkIsRunning(self->_displayLink)) { FBLogAnimInfo(paused ? @"pausing display link" : @"unpausing display link"); if (paused) { CVDisplayLinkStop(self->_displayLink); } else { CVDisplayLinkStart(self->_displayLink); } } #endif } static void updateAnimatable(id obj, POPPropertyAnimationState *anim, bool shouldAvoidExtraneousWrite = false) { // handle user-initiated stop or pause; hault animation if (!anim->active || anim->paused) return; if (anim->hasValue()) { pop_animatable_write_block write = anim->property.writeBlock; if (NULL == write) return; // current animation value VectorRef currentVec = anim->currentValue(); if (!anim->additive) { // if avoiding extraneous writes and we have a read block defined if (shouldAvoidExtraneousWrite) { pop_animatable_read_block read = anim->property.readBlock; if (read) { // compare current animation value with object value Vector4r currentValue = currentVec->vector4r(); Vector4r objectValue = read_values(read, obj, anim->valueCount); if (objectValue == currentValue) { return; } } } // update previous values; support animation convergence anim->previous2Vec = anim->previousVec; anim->previousVec = currentVec; // write value write(obj, currentVec->data()); if (anim->tracing) { [anim->tracer writePropertyValue:POPBox(currentVec, anim->valueType, true)]; } } else { pop_animatable_read_block read = anim->property.readBlock; NSCAssert(read, @"additive requires an animatable property readBlock"); if (NULL == read) { return; } // object value Vector4r objectValue = read_values(read, obj, anim->valueCount); // current value Vector4r currentValue = currentVec->vector4r(); // determine animation change if (anim->previousVec) { Vector4r previousValue = anim->previousVec->vector4r(); currentValue -= previousValue; } // avoid writing no change if (shouldAvoidExtraneousWrite && currentValue == Vector4r::Zero()) { return; } // add to object value currentValue += objectValue; // update previous values; support animation convergence anim->previous2Vec = anim->previousVec; anim->previousVec = currentVec; // write value write(obj, currentValue.data()); if (anim->tracing) { [anim->tracer writePropertyValue:POPBox(currentVec, anim->valueType, true)]; } } } } static void applyAnimationTime(id obj, POPAnimationState *state, CFTimeInterval time) { if (!state->advanceTime(time, obj)) { return; } POPPropertyAnimationState *ps = dynamic_cast(state); if (NULL != ps) { updateAnimatable(obj, ps); } state->delegateApply(); } static void applyAnimationToValue(id obj, POPAnimationState *state) { POPPropertyAnimationState *ps = dynamic_cast(state); if (NULL != ps) { // finalize progress ps->finalizeProgress(); // write to value, updating only if needed updateAnimatable(obj, ps, true); } state->delegateApply(); } static POPAnimation *deleteDictEntry(POPAnimator *self, id __unsafe_unretained obj, NSString *key, BOOL cleanup = YES) { POPAnimation *anim = nil; // lock os_unfair_lock_lock(&self->_lock); NSMutableDictionary *keyAnimationsDict = (__bridge id)CFDictionaryGetValue(self->_dict, (__bridge void *)obj); if (keyAnimationsDict) { anim = keyAnimationsDict[key]; if (anim) { // remove key [keyAnimationsDict removeObjectForKey:key]; // cleanup empty dictionaries if (cleanup && 0 == keyAnimationsDict.count) { CFDictionaryRemoveValue(self->_dict, (__bridge void *)obj); } } } // unlock os_unfair_lock_unlock(&self->_lock); return anim; } static void stopAndCleanup(POPAnimator *self, POPAnimatorItemRef item, bool shouldRemove, bool finished) { // remove if (shouldRemove) { deleteDictEntry(self, item->unretainedObject, item->key); } // stop POPAnimationState *state = POPAnimationGetState(item->animation); state->stop(shouldRemove, finished); if (shouldRemove) { // lock os_unfair_lock_lock(&self->_lock); // find item in list // may have already been removed on animationDidStop: POPAnimatorItemListIterator find_iter = find(self->_list.begin(), self->_list.end(), item); BOOL found = find_iter != self->_list.end(); if (found) { self->_list.erase(find_iter); } // unlock os_unfair_lock_unlock(&self->_lock); } } + (id)sharedAnimator { static POPAnimator* _animator = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _animator = [[POPAnimator alloc] init]; }); return _animator; } + (BOOL)disableBackgroundThread { return _disableBackgroundThread; } + (void)setDisableBackgroundThread:(BOOL)flag { _disableBackgroundThread = flag; } #pragma mark - Lifecycle - (id)init { self = [super init]; if (nil == self) return nil; #if TARGET_OS_IPHONE _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render)]; _displayLink.paused = YES; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; #else CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); CVDisplayLinkSetOutputCallback(_displayLink, displayLinkCallback, (__bridge void *)self); #endif _dict = POPDictionaryCreateMutableWeakPointerToStrongObject(5); _lock = OS_UNFAIR_LOCK_INIT; return self; } - (void)dealloc { #if TARGET_OS_IPHONE [_displayLink invalidate]; #else CVDisplayLinkStop(_displayLink); CVDisplayLinkRelease(_displayLink); #endif [self _clearPendingListObserver]; } #pragma mark - Utility - (void)_processPendingList { // rendering pending animations CFTimeInterval time = [self _currentRenderTime]; [self _renderTime:(0 != _beginTime) ? _beginTime : time items:_pendingList]; // lock os_unfair_lock_lock(&_lock); // clear list and observer _pendingList.clear(); [self _clearPendingListObserver]; // unlock os_unfair_lock_unlock(&_lock); } - (void)_clearPendingListObserver { if (_pendingListObserver) { CFRunLoopRemoveObserver(CFRunLoopGetMain(), _pendingListObserver, kCFRunLoopCommonModes); CFRelease(_pendingListObserver); _pendingListObserver = NULL; } } - (void)_scheduleProcessPendingList { // see WebKit for magic numbers, eg http://trac.webkit.org/changeset/166540 static const CFIndex CATransactionCommitRunLoopOrder = 2000000; static const CFIndex POPAnimationApplyRunLoopOrder = CATransactionCommitRunLoopOrder - 1; // lock os_unfair_lock_lock(&_lock); if (!_pendingListObserver) { __weak POPAnimator *weakSelf = self; _pendingListObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopExit, false, POPAnimationApplyRunLoopOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { [weakSelf _processPendingList]; }); if (_pendingListObserver) { CFRunLoopAddObserver(CFRunLoopGetMain(), _pendingListObserver, kCFRunLoopCommonModes); } } // unlock os_unfair_lock_unlock(&_lock); } - (void)_renderTime:(CFTimeInterval)time items:(std::list)items { // begin transaction with actions disabled [CATransaction begin]; [CATransaction setDisableActions:YES]; // notify delegate __strong __typeof__(_delegate) delegate = _delegate; [delegate animatorWillAnimate:self]; // lock os_unfair_lock_lock(&_lock); // count active animations const NSUInteger count = items.size(); if (0 == count) { // unlock os_unfair_lock_unlock(&_lock); } else { // copy list into vector std::vector vector{ std::begin(items), std::end(items) }; // unlock os_unfair_lock_unlock(&_lock); for (auto item : vector) { [self _renderTime:time item:item]; } } // notify observers for (id observer in self.observers) { [observer animatorDidAnimate:(id)self]; } // lock os_unfair_lock_lock(&_lock); // update display link updateDisplayLink(self); // unlock os_unfair_lock_unlock(&_lock); // notify delegate and commit [delegate animatorDidAnimate:self]; [CATransaction commit]; } - (void)_renderTime:(CFTimeInterval)time item:(POPAnimatorItemRef)item { id obj = item->object; POPAnimation *anim = item->animation; POPAnimationState *state = POPAnimationGetState(anim); if (nil == obj) { // object exists not; stop animating NSAssert(item->unretainedObject, @"object should exist"); stopAndCleanup(self, item, true, false); } else { // start if needed state->startIfNeeded(obj, time, _slowMotionAccumulator); // only run active, not paused animations if (state->active && !state->paused) { // object exists; animate applyAnimationTime(obj, state, time); FBLogAnimDebug(@"time:%f running:%@", time, item->animation); if (state->isDone()) { // set end value applyAnimationToValue(obj, state); state->repeatCount--; if (state->repeatForever || state->repeatCount > 0) { if ([anim isKindOfClass:[POPPropertyAnimation class]]) { POPPropertyAnimation *propAnim = (POPPropertyAnimation *)anim; id oldFromValue = propAnim.fromValue; propAnim.fromValue = propAnim.toValue; if (state->autoreverses) { if (state->tracing) { [state->tracer autoreversed]; } if (state->type == kPOPAnimationDecay) { POPDecayAnimation *decayAnimation = (POPDecayAnimation *)propAnim; decayAnimation.velocity = [decayAnimation reversedVelocity]; } else { propAnim.toValue = oldFromValue; } } else { if (state->type == kPOPAnimationDecay) { POPDecayAnimation *decayAnimation = (POPDecayAnimation *)propAnim; id originalVelocity = decayAnimation.originalVelocity; decayAnimation.velocity = originalVelocity; } else { propAnim.fromValue = oldFromValue; } } } state->stop(NO, NO); state->reset(true); state->startIfNeeded(obj, time, _slowMotionAccumulator); } else { stopAndCleanup(self, item, state->removedOnCompletion, YES); } } } } } #pragma mark - API - (NSArray *)observers { // lock os_unfair_lock_lock(&_lock); // get observers NSArray *observers = 0 != _observers.count ? [_observers copy] : nil; // unlock os_unfair_lock_unlock(&_lock); return observers; } - (void)addAnimation:(POPAnimation *)anim forObject:(id)obj key:(NSString *)key { if (!anim || !obj) { return; } // support arbitrarily many nil keys if (!key) { key = [[NSUUID UUID] UUIDString]; } // lock os_unfair_lock_lock(&_lock); // get key, animation dict associated with object NSMutableDictionary *keyAnimationDict = (__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj); // update associated animation state if (nil == keyAnimationDict) { keyAnimationDict = [NSMutableDictionary dictionary]; CFDictionarySetValue(_dict, (__bridge void *)obj, (__bridge void *)keyAnimationDict); } else { // if the animation instance already exists, avoid cancelling only to restart POPAnimation *existingAnim = keyAnimationDict[key]; if (existingAnim) { // unlock os_unfair_lock_unlock(&_lock); if (existingAnim == anim) { return; } [self removeAnimationForObject:obj key:key cleanupDict:NO]; // lock os_unfair_lock_lock(&_lock); } } keyAnimationDict[key] = anim; // create entry after potential removal POPAnimatorItemRef item(new POPAnimatorItem(obj, key, anim)); // add to list and pending list _list.push_back(item); _pendingList.push_back(item); // support animation re-use, reset all animation state POPAnimationGetState(anim)->reset(true); // update display link updateDisplayLink(self); // unlock os_unfair_lock_unlock(&_lock); // schedule runloop processing of pending animations [self _scheduleProcessPendingList]; } - (void)removeAllAnimationsForObject:(id)obj { // lock os_unfair_lock_lock(&_lock); NSArray *animations = [(__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj) allValues]; CFDictionaryRemoveValue(_dict, (__bridge void *)obj); // unlock os_unfair_lock_unlock(&_lock); if (0 == animations.count) { return; } NSHashTable *animationSet = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality capacity:animations.count]; for (id animation in animations) { [animationSet addObject:animation]; } // lock os_unfair_lock_lock(&_lock); POPAnimatorItemRef item; for (auto iter = _list.begin(); iter != _list.end();) { item = *iter; if(![animationSet containsObject:item->animation]) { iter++; } else { iter = _list.erase(iter); } } // unlock os_unfair_lock_unlock(&_lock); for (POPAnimation *anim in animations) { POPAnimationState *state = POPAnimationGetState(anim); state->stop(true, !state->active); } } - (void)removeAnimationForObject:(id)obj key:(NSString *)key cleanupDict:(BOOL)cleanupDict { POPAnimation *anim = deleteDictEntry(self, obj, key, cleanupDict); if (nil == anim) { return; } // lock os_unfair_lock_lock(&_lock); // remove from list POPAnimatorItemRef item; for (auto iter = _list.begin(); iter != _list.end();) { item = *iter; if(anim == item->animation) { _list.erase(iter); break; } else { iter++; } } // remove from pending list for (auto iter = _pendingList.begin(); iter != _pendingList.end();) { item = *iter; if(anim == item->animation) { _pendingList.erase(iter); break; } else { iter++; } } // unlock os_unfair_lock_unlock(&_lock); // stop animation and callout POPAnimationState *state = POPAnimationGetState(anim); state->stop(true, (!state->active && !state->paused)); } - (void)removeAnimationForObject:(id)obj key:(NSString *)key { [self removeAnimationForObject:obj key:key cleanupDict:YES]; } - (NSArray *)animationKeysForObject:(id)obj { // lock os_unfair_lock_lock(&_lock); // get keys NSArray *keys = [(__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj) allKeys]; // unlock os_unfair_lock_unlock(&_lock); return keys; } - (id)animationForObject:(id)obj key:(NSString *)key { // lock os_unfair_lock_lock(&_lock); // lookup animation NSDictionary *keyAnimationsDict = (__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj); POPAnimation *animation = keyAnimationsDict[key]; // unlock os_unfair_lock_unlock(&_lock); return animation; } - (CFTimeInterval)_currentRenderTime { CFTimeInterval time = CACurrentMediaTime(); #if TARGET_IPHONE_SIMULATOR // support slow-motion animations time += _slowMotionAccumulator; float f = POPAnimationDragCoefficient(); if (f > 1.0) { if (!_slowMotionStartTime) { _slowMotionStartTime = time; } else { time = (time - _slowMotionStartTime) / f + _slowMotionStartTime; _slowMotionLastTime = time; } } else if (_slowMotionStartTime) { CFTimeInterval dt = (_slowMotionLastTime - time); time += dt; _slowMotionAccumulator += dt; _slowMotionStartTime = 0; } #endif return time; } - (void)render { CFTimeInterval time = [self _currentRenderTime]; [self renderTime:time]; } - (void)renderTime:(CFTimeInterval)time { [self _renderTime:time items:_list]; } - (void)addObserver:(id)observer { NSAssert(nil != observer, @"attempting to add nil %@ observer", self); if (nil == observer) { return; } // lock os_unfair_lock_lock(&_lock); if (!_observers) { // use ordered collection for deterministic callout _observers = [[NSMutableArray alloc] initWithCapacity:1]; } [_observers addObject:observer]; updateDisplayLink(self); // unlock os_unfair_lock_unlock(&_lock); } - (void)removeObserver:(id)observer { NSAssert(nil != observer, @"attempting to remove nil %@ observer", self); if (nil == observer) { return; } // lock os_unfair_lock_lock(&_lock); [_observers removeObject:observer]; updateDisplayLink(self); // unlock os_unfair_lock_unlock(&_lock); } @end