mirror of
https://github.com/status-im/react-native.git
synced 2025-01-19 05:51:01 +00:00
5793f5c4c4
Summary: [CATransaction Class Reference](https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/CATransaction_class/index.html) In Tasks > Getting and Setting Completion Block Objects > Discussion: The completion block object that is guaranteed to be called (on the main thread) as soon as all animations subsequently added by this transaction group have completed (or have been removed.) If no animations are added before the current transaction group is committed (or the completion block is set to a different value,) the block will be invoked immediately. Closes https://github.com/facebook/react-native/pull/1400 Github Author: Shuangzuan <shuangzuan.he@icloud.com> Test Plan: Imported from GitHub, without a `Test Plan:` line.
295 lines
9.2 KiB
Objective-C
295 lines
9.2 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-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 "RCTAnimationExperimentalManager.h"
|
|
|
|
#import <UIKit/UIKit.h>
|
|
|
|
#import "RCTSparseArray.h"
|
|
#import "RCTUIManager.h"
|
|
#import "RCTUtils.h"
|
|
|
|
#if CGFLOAT_IS_DOUBLE
|
|
#define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_D
|
|
#else
|
|
#define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_F
|
|
#endif
|
|
|
|
@implementation RCTAnimationExperimentalManager
|
|
{
|
|
RCTSparseArray *_animationRegistry; // Main thread only; animation tag -> view tag
|
|
RCTSparseArray *_callbackRegistry; // Main thread only; animation tag -> callback
|
|
NSDictionary *_keypathMapping;
|
|
}
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
_animationRegistry = [[RCTSparseArray alloc] init];
|
|
_callbackRegistry = [[RCTSparseArray alloc] init];
|
|
_keypathMapping = @{
|
|
@"opacity": @{
|
|
@"keypath": @"opacity",
|
|
@"type": @"NSNumber",
|
|
},
|
|
@"position": @{
|
|
@"keypath": @"position",
|
|
@"type": @"CGPoint",
|
|
},
|
|
@"positionX": @{
|
|
@"keypath": @"position.x",
|
|
@"type": @"NSNumber",
|
|
},
|
|
@"positionY": @{
|
|
@"keypath": @"position.y",
|
|
@"type": @"NSNumber",
|
|
},
|
|
@"rotation": @{
|
|
@"keypath": @"transform.rotation.z",
|
|
@"type": @"NSNumber",
|
|
},
|
|
@"scaleXY": @{
|
|
@"keypath": @"transform.scale",
|
|
@"type": @"CGPoint",
|
|
},
|
|
};
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
return _bridge.uiManager.methodQueue;
|
|
}
|
|
|
|
- (id (^)(CGFloat))interpolateFrom:(CGFloat[])fromArray to:(CGFloat[])toArray count:(NSUInteger)count typeName:(const char *)typeName
|
|
{
|
|
if (count == 1) {
|
|
CGFloat from = *fromArray, to = *toArray, delta = to - from;
|
|
return ^(CGFloat t) {
|
|
return @(from + t * delta);
|
|
};
|
|
}
|
|
|
|
CG_APPEND(vDSP_vsub,,D)(fromArray, 1, toArray, 1, toArray, 1, count);
|
|
|
|
const size_t size = count * sizeof(CGFloat);
|
|
NSData *deltaData = [NSData dataWithBytes:toArray length:size];
|
|
NSData *fromData = [NSData dataWithBytes:fromArray length:size];
|
|
|
|
return ^(CGFloat t) {
|
|
const CGFloat *delta = deltaData.bytes;
|
|
const CGFloat *_fromArray = fromData.bytes;
|
|
|
|
CGFloat value[count];
|
|
CG_APPEND(vDSP_vma,,D)(delta, 1, &t, 0, _fromArray, 1, value, 1, count);
|
|
return [NSValue valueWithBytes:value objCType:typeName];
|
|
};
|
|
}
|
|
|
|
static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NSString *key, id value)
|
|
{
|
|
RCTResponseSenderBlock callback = callbacks[tag];
|
|
RCTLogError(@"Invalid animation property `%@ = %@`", key, value);
|
|
if (callback) {
|
|
callback(@[@NO]);
|
|
callbacks[tag] = nil;
|
|
}
|
|
[CATransaction commit];
|
|
return;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(startAnimation:(NSNumber *)reactTag
|
|
animationTag:(NSNumber *)animationTag
|
|
duration:(NSTimeInterval)duration
|
|
delay:(NSTimeInterval)delay
|
|
easingSample:(NSNumberArray *)easingSample
|
|
properties:(NSDictionary *)properties
|
|
callback:(RCTResponseSenderBlock)callback)
|
|
{
|
|
__weak RCTAnimationExperimentalManager *weakSelf = self;
|
|
[_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
|
RCTAnimationExperimentalManager *strongSelf = weakSelf;
|
|
|
|
UIView *view = viewRegistry[reactTag];
|
|
if (!view) {
|
|
RCTLogWarn(@"React tag #%@ is not registered with the view registry", reactTag);
|
|
return;
|
|
}
|
|
__block BOOL completionBlockSet = NO;
|
|
[CATransaction begin];
|
|
for (NSString *prop in properties) {
|
|
NSString *keypath = _keypathMapping[prop][@"keypath"];
|
|
id obj = properties[prop][@"to"];
|
|
if (!keypath) {
|
|
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, obj);
|
|
}
|
|
NSValue *toValue = nil;
|
|
if ([keypath isEqualToString:@"transform.scale"]) {
|
|
CGPoint point = [RCTConvert CGPoint:obj];
|
|
if (point.x != point.y) {
|
|
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, obj);
|
|
}
|
|
toValue = @(point.x);
|
|
} else if ([obj respondsToSelector:@selector(count)]) {
|
|
switch ([obj count]) {
|
|
case 2:
|
|
if (![obj respondsToSelector:@selector(objectForKeyedSubscript:)] || obj[@"x"]) {
|
|
toValue = [NSValue valueWithCGPoint:[RCTConvert CGPoint:obj]];
|
|
} else {
|
|
toValue = [NSValue valueWithCGSize:[RCTConvert CGSize:obj]];
|
|
}
|
|
break;
|
|
case 4:
|
|
toValue = [NSValue valueWithCGRect:[RCTConvert CGRect:obj]];
|
|
break;
|
|
case 16:
|
|
toValue = [NSValue valueWithCGAffineTransform:[RCTConvert CGAffineTransform:obj]];
|
|
break;
|
|
default:
|
|
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, obj);
|
|
}
|
|
} else if (![obj respondsToSelector:@selector(objCType)]) {
|
|
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, obj);
|
|
}
|
|
if (!toValue) {
|
|
toValue = obj;
|
|
}
|
|
const char *typeName = toValue.objCType;
|
|
|
|
size_t count;
|
|
switch (typeName[0]) {
|
|
case 'i':
|
|
case 'I':
|
|
case 's':
|
|
case 'S':
|
|
case 'l':
|
|
case 'L':
|
|
case 'q':
|
|
case 'Q':
|
|
count = 1;
|
|
break;
|
|
|
|
default: {
|
|
NSUInteger size;
|
|
NSGetSizeAndAlignment(typeName, &size, NULL);
|
|
count = size / sizeof(CGFloat);
|
|
break;
|
|
}
|
|
}
|
|
|
|
CGFloat toFields[count];
|
|
|
|
switch (typeName[0]) {
|
|
#define CASE(encoding, type) \
|
|
case encoding: { \
|
|
type value; \
|
|
[toValue getValue:&value]; \
|
|
toFields[0] = value; \
|
|
break; \
|
|
}
|
|
|
|
CASE('i', int)
|
|
CASE('I', unsigned int)
|
|
CASE('s', short)
|
|
CASE('S', unsigned short)
|
|
CASE('l', long)
|
|
CASE('L', unsigned long)
|
|
CASE('q', long long)
|
|
CASE('Q', unsigned long long)
|
|
|
|
#undef CASE
|
|
|
|
default:
|
|
[toValue getValue:toFields];
|
|
break;
|
|
}
|
|
|
|
NSValue *fromValue = [view.layer.presentationLayer valueForKeyPath:keypath];
|
|
CGFloat fromFields[count];
|
|
[fromValue getValue:fromFields];
|
|
|
|
id (^interpolationBlock)(CGFloat t) = [strongSelf interpolateFrom:fromFields to:toFields count:count typeName:typeName];
|
|
|
|
NSMutableArray *sampledValues = [NSMutableArray arrayWithCapacity:easingSample.count];
|
|
for (NSNumber *sample in easingSample) {
|
|
CGFloat t = sample.CG_APPEND(, floatValue, doubleValue);
|
|
[sampledValues addObject:interpolationBlock(t)];
|
|
}
|
|
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:keypath];
|
|
animation.beginTime = CACurrentMediaTime() + delay;
|
|
animation.duration = duration;
|
|
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
|
|
animation.values = sampledValues;
|
|
@try {
|
|
[view.layer setValue:toValue forKey:keypath];
|
|
NSString *animationKey = [@"RCT" stringByAppendingString:RCTJSONStringify(@{@"tag": animationTag, @"key": keypath}, nil)];
|
|
if (!completionBlockSet) {
|
|
strongSelf->_callbackRegistry[animationTag] = callback;
|
|
[CATransaction setCompletionBlock:^{
|
|
RCTResponseSenderBlock cb = strongSelf->_callbackRegistry[animationTag];
|
|
if (cb) {
|
|
cb(@[@YES]);
|
|
strongSelf->_callbackRegistry[animationTag] = nil;
|
|
}
|
|
}];
|
|
completionBlockSet = YES;
|
|
}
|
|
[view.layer addAnimation:animation forKey:animationKey];
|
|
}
|
|
@catch (NSException *exception) {
|
|
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, toValue);
|
|
}
|
|
}
|
|
[CATransaction commit];
|
|
strongSelf->_animationRegistry[animationTag] = reactTag;
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(stopAnimation:(NSNumber *)animationTag)
|
|
{
|
|
__weak RCTAnimationExperimentalManager *weakSelf = self;
|
|
[_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
|
RCTAnimationExperimentalManager *strongSelf = weakSelf;
|
|
|
|
NSNumber *reactTag = strongSelf->_animationRegistry[animationTag];
|
|
if (!reactTag) {
|
|
return;
|
|
}
|
|
|
|
UIView *view = viewRegistry[reactTag];
|
|
for (NSString *animationKey in view.layer.animationKeys) {
|
|
if ([animationKey hasPrefix:@"RCT{"]) {
|
|
NSDictionary *data = RCTJSONParse([animationKey substringFromIndex:3], nil);
|
|
if (animationTag.integerValue == [data[@"tag"] integerValue]) {
|
|
[view.layer removeAnimationForKey:animationKey];
|
|
}
|
|
}
|
|
}
|
|
RCTResponseSenderBlock cb = strongSelf->_callbackRegistry[animationTag];
|
|
if (cb) {
|
|
cb(@[@NO]);
|
|
strongSelf->_callbackRegistry[animationTag] = nil;
|
|
}
|
|
strongSelf->_animationRegistry[animationTag] = nil;
|
|
}];
|
|
}
|
|
|
|
- (NSDictionary *)constantsToExport
|
|
{
|
|
return @{@"Properties": [_keypathMapping allKeys] };
|
|
}
|
|
|
|
@end
|