Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ios): improve synchronization between animationSets #3937

Merged
merged 1 commit into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading