Skip to content

Commit

Permalink
fix(ios): improve synchronization between animationSets
Browse files Browse the repository at this point in the history
  • Loading branch information
wwwcg committed Jul 11, 2024
1 parent 9b77b8f commit 8dacfb0
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 38 deletions.
6 changes: 6 additions & 0 deletions ios/sdk/module/animation2/HippyNextAnimation.h
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
54 changes: 18 additions & 36 deletions ios/sdk/module/animation2/HippyNextAnimationGroup.m
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
80 changes: 80 additions & 0 deletions ios/sdk/module/animation2/HippyNextAnimationModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#import "HippyNextAnimation.h"
#import "HippyNextAnimationGroup.h"
#import "HippyShadowView.h"
#import "HPOPAnimatorPrivate.h"


@interface HippyNextAnimationModule () <HPOPAnimationDelegate, HPOPAnimatorDelegate, HippyNextAnimationControlDelegate>
Expand All @@ -40,6 +41,15 @@ @interface HippyNextAnimationModule () <HPOPAnimationDelegate, HPOPAnimatorDeleg
/// whether should relayout on next frame
@property (atomic, assign) BOOL shouldCallUIManagerToUpdateLayout;

/// AnimationGroup synchronization - lock
@property (nonatomic, strong) NSLock *groupAnimSyncLock;
/// AnimationGroup synchronization - pending animations dictionary in AnimationGroup
/// Key: hash of queue, Value: HippyNextAnimation array
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSMutableArray<HippyNextAnimation *> *> *pendingStartGroupAnimations;
/// AnimationGroup synchronization - states of all queues
/// Key: hash of queue, Value: should sync state
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSNumber * > *shouldFlushPendingAnimsInNextSync;

@end


Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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

Expand All @@ -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) {
Expand All @@ -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<HippyNextAnimation *> *pendingAnims = [strongSelf.pendingStartGroupAnimations objectForKey:queueKey];
[strongSelf.groupAnimSyncLock unlock];

NSMutableArray<id> *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

Expand Down
9 changes: 7 additions & 2 deletions ios/sdk/module/animation2/pop/HPOPAnimator.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,18 @@
@protocol HPOPAnimatorDelegate <NSObject>

/**
@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
70 changes: 70 additions & 0 deletions ios/sdk/module/animation2/pop/HPOPAnimator.mm
Original file line number Diff line number Diff line change
Expand Up @@ -593,12 +593,21 @@ - (void)_renderTime:(CFTimeInterval)time items:(std::list<POPAnimatorItemRef> &)
for (auto& item : itemList) {
[strongSelf _renderTime:time item:item];
}
// notify delegate for custom queue anims
for (id<HPOPAnimatorDelegate> 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<HPOPAnimatorDelegate> delegate in allDelegates) {
[delegate animatorDidAnimate:self inCustomQueue:queue];
}
}
}
}
Expand Down Expand Up @@ -768,6 +777,67 @@ - (void)addAnimation:(HPOPAnimation *)anim forObject:(id)obj key:(NSString *)key
[self _scheduleProcessPendingList];
}

- (void)addAnimations:(NSArray<HPOPAnimation *> *)anims
forObjects:(NSArray<id> *)objs
andKeys:(NSArray<NSString *> *)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
Expand Down
1 change: 1 addition & 0 deletions ios/sdk/module/animation2/pop/HPOPAnimatorPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
Funnel methods for category additions.
*/
- (void)addAnimation:(HPOPAnimation *)anim forObject:(id)obj key:(NSString *)key;
- (void)addAnimations:(NSArray<HPOPAnimation *> *)anims forObjects:(NSArray<id> *)objs andKeys:(NSArray<NSString *> *)keys;
- (void)removeAllAnimationsForObject:(id)obj;
- (void)removeAnimationForObject:(id)obj key:(NSString *)key;
- (NSArray *)animationKeysForObject:(id)obj;
Expand Down

0 comments on commit 8dacfb0

Please sign in to comment.