From de858716546d199545ecf795ca980730e06aefad Mon Sep 17 00:00:00 2001 From: wwwcg Date: Tue, 2 Jul 2024 22:13:25 +0800 Subject: [PATCH] fix(ios): improve synchronization between animationSets --- .../module/animation2/HippyNextAnimation.h | 6 ++ .../animation2/HippyNextAnimationGroup.m | 54 +++++-------- .../animation2/HippyNextAnimationModule.m | 80 +++++++++++++++++++ ios/sdk/module/animation2/pop/HPOPAnimator.h | 9 ++- ios/sdk/module/animation2/pop/HPOPAnimator.mm | 70 ++++++++++++++++ .../animation2/pop/HPOPAnimatorPrivate.h | 1 + 6 files changed, 182 insertions(+), 38 deletions(-) diff --git a/ios/sdk/module/animation2/HippyNextAnimation.h b/ios/sdk/module/animation2/HippyNextAnimation.h index d1faff93490..6398d0200fc 100644 --- a/ios/sdk/module/animation2/HippyNextAnimation.h +++ b/ios/sdk/module/animation2/HippyNextAnimation.h @@ -48,6 +48,12 @@ typedef NS_ENUM(NSInteger, HippyNextAnimationValueType) { /// request for updating layout - (void)requestUpdateUILayout:(HippyNextAnimation *)anim withNextFrameProp:(nullable NSDictionary *)nextFrameProp; +/// Add animation to pending start list +/// +/// To ensure that the animation in AnimationGroup starts simultaneously. +/// - Parameter anim: HippyNextAnimation instance +- (void)addAnimInGroupToPendingStartList:(HippyNextAnimation *)anim; + @end diff --git a/ios/sdk/module/animation2/HippyNextAnimationGroup.m b/ios/sdk/module/animation2/HippyNextAnimationGroup.m index 166255eaccb..a0cb6d0e103 100644 --- a/ios/sdk/module/animation2/HippyNextAnimationGroup.m +++ b/ios/sdk/module/animation2/HippyNextAnimationGroup.m @@ -33,6 +33,16 @@ - (BOOL)isFollow { return [objc_getAssociatedObject(self, _cmd) boolValue]; } +- (void)startAnimationInGroupForFirstFire:(BOOL)isFirstFire { + if (isFirstFire) { + [self startAnimation]; + } else { + // In order to ensure the time synchronization between different animation groups, + // We need to make sure the animations are executed at the same time. + [self.controlDelegate addAnimInGroupToPendingStartList:self]; + } +} + @end #pragma mark - @@ -51,26 +61,13 @@ @implementation HippyNextAnimationGroup { NSInteger _currentRepeatCount; BOOL _isGroupPausedCausedReturn; - - // Member variables used to correct the animation time. - CFTimeInterval _totalDuration; - CFTimeInterval _lastStartTime; - CFTimeInterval _cumulativeFrameDelay; } - (BOOL)prepareForTarget:(id)target withType:(NSString *)type { - CFTimeInterval totalDuration = 0.0; - HippyNextAnimation *previousAnimation; - for (HippyNextAnimation *anim in self.animations) { if (![anim prepareForTarget:target withType:type]) { return NO; } - if (!previousAnimation || (previousAnimation && anim.isFollow)) { - totalDuration += anim.duration; - } - previousAnimation = anim; - _totalDuration = totalDuration; } return YES; } @@ -97,38 +94,23 @@ - (void)startAnimationWithRepeatCount:(NSUInteger)repeatCount { _isGroupPausedCausedReturn = YES; return; } - __block HippyNextAnimation *previousAnimation; + HippyNextAnimation *previousAnimation; for (HippyNextAnimation *animation in self.animations) { if (animation.isFollow && previousAnimation) { [previousAnimation setCompletionBlock:^(HPOPAnimation *anim, BOOL finished) { if (finished) { - [animation startAnimation]; + [animation startAnimationInGroupForFirstFire:NO]; } }]; } else { - // Record the time when the animation group started, - // and correct the time offset if needed. if (!previousAnimation) { - if (_lastStartTime > DBL_EPSILON) { - // Since CADisplayLink's callback is used to execute the animation group, - // there is a frame time interval between each animation. - // In order to ensure the time synchronization between different animation groups, - // we need to continuously correct possible time deviations to avoid the accumulation of time differences. - CFTimeInterval refreshPeriod = HPOPAnimator.sharedAnimator.refreshPeriod; - if (refreshPeriod > DBL_EPSILON) { - if (_cumulativeFrameDelay <= DBL_EPSILON) { - for (HippyNextAnimation *animation in self.animations) { - _cumulativeFrameDelay += ceil(animation.duration / refreshPeriod) * refreshPeriod - animation.duration; - } - } - - CFTimeInterval timeOffset = (CACurrentMediaTime() - _lastStartTime) - (_totalDuration + _cumulativeFrameDelay); - animation.beginTime = timeOffset; - } - } - _lastStartTime = CACurrentMediaTime(); + // Use repeatCount to determine whether the AnimationSet is executed for the first time. + // If it is not the first time, use the synchronization mechanism + // to ensure that the progress of different animation groups started at the same time is synchronized. + [animation startAnimationInGroupForFirstFire:(repeatCount == self.repeatCount)]; + } else { + [animation startAnimationInGroupForFirstFire:NO]; } - [animation startAnimation]; } previousAnimation = animation; } diff --git a/ios/sdk/module/animation2/HippyNextAnimationModule.m b/ios/sdk/module/animation2/HippyNextAnimationModule.m index 453c667299b..f3eba7865a3 100644 --- a/ios/sdk/module/animation2/HippyNextAnimationModule.m +++ b/ios/sdk/module/animation2/HippyNextAnimationModule.m @@ -26,6 +26,7 @@ #import "HippyNextAnimation.h" #import "HippyNextAnimationGroup.h" #import "HippyShadowView.h" +#import "HPOPAnimatorPrivate.h" @interface HippyNextAnimationModule () @@ -40,6 +41,15 @@ @interface HippyNextAnimationModule () *> *pendingStartGroupAnimations; +/// AnimationGroup synchronization - states of all queues +/// Key: hash of queue, Value: should sync state +@property (nonatomic, strong) NSMutableDictionary *shouldFlushPendingAnimsInNextSync; + @end @@ -70,6 +80,9 @@ - (instancetype)init { _paramsByHippyTag = [NSMutableDictionary dictionary]; _paramsByAnimationId = [NSMutableDictionary dictionary]; _updatedPropsForNextFrameDict = [NSMutableDictionary dictionary]; + _groupAnimSyncLock = [[NSLock alloc] init]; + _pendingStartGroupAnimations = [NSMutableDictionary dictionary]; + _shouldFlushPendingAnimsInNextSync = [NSMutableDictionary dictionary]; [HPOPAnimator.sharedAnimator addAnimatorDelegate:self]; } return self; @@ -372,6 +385,31 @@ - (void)requestUpdateUILayout:(HippyNextAnimation *)anim withNextFrameProp:(NSDi } } +- (void)addAnimInGroupToPendingStartList:(HippyNextAnimation *)anim { + // run in mainQueue or anim.customRunningQueue + NSNumber *queueKey = @([anim.customRunningQueue?:dispatch_get_main_queue() hash]); + + // lock + [self.groupAnimSyncLock lock]; + + // get pending animations array and state for current queue + BOOL shouldFlush = [[self.shouldFlushPendingAnimsInNextSync objectForKey:queueKey] boolValue]; + NSMutableArray *pendings = [self.pendingStartGroupAnimations objectForKey:queueKey]; + if (!pendings) { + pendings = [NSMutableArray arrayWithObject:anim]; + self.pendingStartGroupAnimations[queueKey] = pendings; + } else { + [pendings addObject:anim]; + } + + // update state + if (!shouldFlush) { + self.shouldFlushPendingAnimsInNextSync[queueKey] = @(YES); + } + + // unlock + [self.groupAnimSyncLock unlock]; +} #pragma mark - HPOPAnimatorDelegate @@ -385,6 +423,9 @@ - (void)animatorDidAnimate:(HPOPAnimator *)animator { __weak __typeof(self)weakSelf = self; [self.bridge.uiManager executeBlockOnUIManagerQueue:^{ __strong __typeof(weakSelf)strongSelf = weakSelf; + if (!strongSelf) { + return; + } [strongSelf->_updatedPropsForNextFrameDict enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) { @@ -398,6 +439,45 @@ - (void)animatorDidAnimate:(HPOPAnimator *)animator { } } +- (void)animatorDidAnimate:(HPOPAnimator *)animator inCustomQueue:(dispatch_queue_t)queue { + // call from main and custom queue + NSNumber *queueKey = @(queue.hash); + + // lock + [self.groupAnimSyncLock lock]; + + // get sync state for current queue + BOOL shouldFlush = [[self.shouldFlushPendingAnimsInNextSync objectForKey:queueKey] boolValue]; + if (shouldFlush) { + [self.shouldFlushPendingAnimsInNextSync removeObjectForKey:queueKey]; + + // flush pending animations + __weak __typeof(self)weakSelf = self; + dispatch_async(queue ?: dispatch_get_main_queue(), ^{ + __strong __typeof(weakSelf)strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + // flush pending animations + [strongSelf.groupAnimSyncLock lock]; + NSMutableArray *pendingAnims = [strongSelf.pendingStartGroupAnimations objectForKey:queueKey]; + [strongSelf.groupAnimSyncLock unlock]; + + NSMutableArray *targetObjects = [NSMutableArray arrayWithCapacity:pendingAnims.count]; + for (HippyNextAnimation *anim in pendingAnims) { + [targetObjects addObject:anim.targetObject]; + } + + [[HPOPAnimator sharedAnimator] addAnimations:pendingAnims forObjects:targetObjects andKeys:nil]; + [pendingAnims removeAllObjects]; + }); + } + + // unlock + [self.groupAnimSyncLock unlock]; +} + #pragma mark - HPOPAnimationDelegate diff --git a/ios/sdk/module/animation2/pop/HPOPAnimator.h b/ios/sdk/module/animation2/pop/HPOPAnimator.h index bcb8b31f671..6480c82f7b8 100644 --- a/ios/sdk/module/animation2/pop/HPOPAnimator.h +++ b/ios/sdk/module/animation2/pop/HPOPAnimator.h @@ -79,13 +79,18 @@ @protocol HPOPAnimatorDelegate /** - @abstract Called on each frame before animation application. + @abstract Called every frame before the animation is executed, only on the main thread. */ - (void)animatorWillAnimate:(HPOPAnimator *)animator; /** - @abstract Called on each frame after animation application. + @abstract Called every frame after the animation is executed, only on the main thread. */ - (void)animatorDidAnimate:(HPOPAnimator *)animator; +/** + @abstract Called every frame after the animation is executed, along with queue information + */ +- (void)animatorDidAnimate:(HPOPAnimator *)animator inCustomQueue:(dispatch_queue_t)queue; + @end diff --git a/ios/sdk/module/animation2/pop/HPOPAnimator.mm b/ios/sdk/module/animation2/pop/HPOPAnimator.mm index e4f59412ffb..f7a0e063f7a 100644 --- a/ios/sdk/module/animation2/pop/HPOPAnimator.mm +++ b/ios/sdk/module/animation2/pop/HPOPAnimator.mm @@ -593,12 +593,21 @@ - (void)_renderTime:(CFTimeInterval)time items:(std::list &) for (auto& item : itemList) { [strongSelf _renderTime:time item:item]; } + // notify delegate for custom queue anims + for (id delegate in allDelegates) { + [delegate animatorDidAnimate:self inCustomQueue:queue]; + } } }); } else { for (auto& item : itemList) { [self _renderTime:time item:item]; } + // notify delegate for main queue anims + queue = dispatch_get_main_queue(); + for (id delegate in allDelegates) { + [delegate animatorDidAnimate:self inCustomQueue:queue]; + } } } } @@ -768,6 +777,67 @@ - (void)addAnimation:(HPOPAnimation *)anim forObject:(id)obj key:(NSString *)key [self _scheduleProcessPendingList]; } +- (void)addAnimations:(NSArray *)anims + forObjects:(NSArray *)objs + andKeys:(NSArray *)keys +{ + if (!anims.count || (anims.count != objs.count)) { + return; + } + + if (keys.count > 0 && (objs.count != keys.count)) { + NSAssert(NO, @"keys number should match objs"); + return; + } + + // lock + pthread_mutex_lock(&_lock); + + for (NSUInteger index = 0; index < anims.count; index++) { + HPOPAnimation *anim = anims[index]; + id obj = objs[index]; + NSString *key = keys ? keys[index] : [[NSUUID UUID] UUIDString]; + + // 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 + HPOPAnimation *existingAnim = keyAnimationDict[key]; + if (existingAnim) { + if (existingAnim == anim) { + continue; + } + [self removeAnimationForObject:obj key:key cleanupDict:NO]; + } + } + 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 + pthread_mutex_unlock(&_lock); + + // schedule runloop processing of pending animations + [self _scheduleProcessPendingList]; +} + - (void)removeAllAnimationsForObject:(id)obj { // lock diff --git a/ios/sdk/module/animation2/pop/HPOPAnimatorPrivate.h b/ios/sdk/module/animation2/pop/HPOPAnimatorPrivate.h index aa48d60061d..eaf9a13b913 100644 --- a/ios/sdk/module/animation2/pop/HPOPAnimatorPrivate.h +++ b/ios/sdk/module/animation2/pop/HPOPAnimatorPrivate.h @@ -78,6 +78,7 @@ Funnel methods for category additions. */ - (void)addAnimation:(HPOPAnimation *)anim forObject:(id)obj key:(NSString *)key; +- (void)addAnimations:(NSArray *)anims forObjects:(NSArray *)objs andKeys:(NSArray *)keys; - (void)removeAllAnimationsForObject:(id)obj; - (void)removeAnimationForObject:(id)obj key:(NSString *)key; - (NSArray *)animationKeysForObject:(id)obj;