Changed RCTImageLoader to always return a UIImage

Summary:
GIF images are currently loaded as a CAKeyframeAnimation, however returning this animation directly from RCTImageLoader was dangerous, as any code that expected a UIImage would crash.

This diff changes RCTGIFImageLoader to return a UIImage of the first frame, with the keyframe animation attached as an associated object. This way, code that is not expecting an animation will still work correctly.
This commit is contained in:
Nick Lockwood
2015-09-04 04:35:44 -07:00
parent 3c4adeb2e7
commit e4110456ab
6 changed files with 110 additions and 50 deletions

View File

@@ -325,6 +325,18 @@ exports.examples = [
);
},
},
{
title: 'Animated GIF',
render: function() {
return (
<Image
style={styles.gif}
source={{uri: 'http://38.media.tumblr.com/9e9bd08c6e2d10561dd1fb4197df4c4e/tumblr_mfqekpMktw1rn90umo1_500.gif'}}
/>
);
},
platform: 'ios',
},
{
title: 'Cap Insets',
description:
@@ -384,5 +396,9 @@ var styles = StyleSheet.create({
},
horizontal: {
flexDirection: 'row',
}
},
gif: {
flex: 1,
height: 200,
},
});

View File

@@ -27,60 +27,85 @@ RCT_EXPORT_MODULE()
return !strcmp(header, "GIF87a") || !strcmp(header, "GIF89a");
}
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
{
CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL);
NSUInteger loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
UIImage *image = nil;
size_t imageCount = CGImageSourceGetCount(imageSource);
NSTimeInterval duration = 0;
NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount];
NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount];
for (size_t i = 0; i < imageCount; i++) {
CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL);
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL);
NSDictionary *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary];
if (imageCount > 1) {
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime];
if (delayTime == nil) {
if (i == 0) {
delayTime = @(kDelayTimeIntervalDefault);
} else {
delayTime = delays[i - 1];
NSTimeInterval duration = 0;
NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount];
NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount];
for (size_t i = 0; i < imageCount; i++) {
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, NULL);
if (!image) {
image = [UIImage imageWithCGImage:imageRef];
}
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL);
NSDictionary *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary];
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime];
if (delayTime == nil) {
if (i == 0) {
delayTime = @(kDelayTimeIntervalDefault);
} else {
delayTime = delays[i - 1];
}
}
const NSTimeInterval kDelayTimeIntervalMinimum = 0.02;
if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) {
delayTime = @(kDelayTimeIntervalDefault);
}
duration += delayTime.doubleValue;
delays[i] = delayTime;
images[i] = (__bridge_transfer id)imageRef;
}
CFRelease(imageSource);
NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count];
NSTimeInterval runningDuration = 0;
for (NSNumber *delayNumber in delays) {
[keyTimes addObject:@(runningDuration / duration)];
runningDuration += delayNumber.doubleValue;
}
const NSTimeInterval kDelayTimeIntervalMinimum = 0.02;
if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) {
delayTime = @(kDelayTimeIntervalDefault);
[keyTimes addObject:@1.0];
// Create animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
animation.calculationMode = kCAAnimationDiscrete;
animation.repeatCount = loopCount == 0 ? HUGE_VALF : loopCount;
animation.keyTimes = keyTimes;
animation.values = images;
animation.duration = duration;
image.reactKeyframeAnimation = animation;
} else {
// Don't bother creating an animation
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
if (imageRef) {
image = [UIImage imageWithCGImage:imageRef];
CFRelease(imageRef);
}
duration += delayTime.doubleValue;
delays[i] = delayTime;
images[i] = (__bridge_transfer id)image;
}
CFRelease(imageSource);
NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count];
NSTimeInterval runningDuration = 0;
for (NSNumber *delayNumber in delays) {
[keyTimes addObject:@(runningDuration / duration)];
runningDuration += delayNumber.doubleValue;
CFRelease(imageSource);
}
[keyTimes addObject:@1.0];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
animation.calculationMode = kCAAnimationDiscrete;
animation.repeatCount = loopCount == 0 ? HUGE_VALF : loopCount;
animation.keyTimes = keyTimes;
animation.values = images;
animation.duration = duration;
completionHandler(nil, animation);
return nil;
completionHandler(nil, image);
return ^{};
}
@end

View File

@@ -50,7 +50,7 @@ RCT_EXPORT_MODULE()
*/
- (RCTImageLoaderCancellationBlock)downloadDataForURL:(NSURL *)url
progressHandler:(RCTImageLoaderProgressBlock)progressBlock
completionHandler:(RCTImageLoaderCompletionBlock)completionBlock
completionHandler:(void (^)(NSError *error, NSData *data))completionBlock
{
if (![_bridge respondsToSelector:NSSelectorFromString(@"networking")]) {
RCTLogError(@"You need to import the RCTNetworking library in order to download remote images.");

View File

@@ -15,10 +15,15 @@
@class ALAssetsLibrary;
typedef void (^RCTImageLoaderProgressBlock)(int64_t progress, int64_t total);
typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, id image /* UIImage or CAAnimation */);
typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, id image /* NSData, UIImage, CAAnimation */);
typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, UIImage *image);
typedef void (^RCTImageLoaderCancellationBlock)(void);
@interface UIImage (React)
@property (nonatomic, copy) CAKeyframeAnimation *reactKeyframeAnimation;
@end
@interface RCTImageLoader : NSObject <RCTBridgeModule, RCTURLRequestHandler>
/**

View File

@@ -28,6 +28,20 @@ static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSEr
}
}
@implementation UIImage (React)
- (CAKeyframeAnimation *)reactKeyframeAnimation
{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setReactKeyframeAnimation:(CAKeyframeAnimation *)reactKeyframeAnimation
{
objc_setAssociatedObject(self, @selector(reactKeyframeAnimation), reactKeyframeAnimation, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
@implementation RCTImageLoader
@synthesize bridge = _bridge;
@@ -99,7 +113,7 @@ RCT_EXPORT_MODULE()
progressBlock(progress, total);
});
}
} completionHandler:^(NSError *error, id image) {
} completionHandler:^(NSError *error, UIImage *image) {
RCTDispatchCallbackOnMainQueue(completionBlock, error, image);
}] ?: ^{};
}
@@ -142,7 +156,7 @@ RCT_EXPORT_MODULE()
{
id<RCTImageDecoder> imageDecoder = [self imageDecoderForRequest:data];
if (imageDecoder) {
return [imageDecoder decodeImageData:data size:size scale:scale resizeMode:resizeMode completionHandler:^(NSError *error, id image) {
return [imageDecoder decodeImageData:data size:size scale:scale resizeMode:resizeMode completionHandler:^(NSError *error, UIImage *image) {
RCTDispatchCallbackOnMainQueue(completionBlock, error, image);
}];
} else {

View File

@@ -142,10 +142,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
scale:RCTScreenScale()
resizeMode:self.contentMode
progressBlock:progressHandler
completionBlock:^(NSError *error, id image) {
completionBlock:^(NSError *error, UIImage *image) {
if ([image isKindOfClass:[CAAnimation class]]) {
[self.layer addAnimation:image forKey:@"contents"];
if (image.reactKeyframeAnimation) {
[self.layer addAnimation:image.reactKeyframeAnimation forKey:@"contents"];
} else {
[self.layer removeAnimationForKey:@"contents"];
self.image = image;