diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js
index cea7b5511ec..5e47dcb54f0 100644
--- a/Examples/UIExplorer/ImageExample.js
+++ b/Examples/UIExplorer/ImageExample.js
@@ -325,6 +325,18 @@ exports.examples = [
);
},
},
+ {
+ title: 'Animated GIF',
+ render: function() {
+ return (
+
+ );
+ },
+ platform: 'ios',
+ },
{
title: 'Cap Insets',
description:
@@ -384,5 +396,9 @@ var styles = StyleSheet.create({
},
horizontal: {
flexDirection: 'row',
- }
+ },
+ gif: {
+ flex: 1,
+ height: 200,
+ },
});
diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m
index 3d85f94e7da..332769e7d31 100644
--- a/Libraries/Image/RCTGIFImageDecoder.m
+++ b/Libraries/Image/RCTGIFImageDecoder.m
@@ -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
diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m
index f110bd628cd..c2aa89f73a4 100644
--- a/Libraries/Image/RCTImageDownloader.m
+++ b/Libraries/Image/RCTImageDownloader.m
@@ -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.");
diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h
index bba88760263..a55df421b12 100644
--- a/Libraries/Image/RCTImageLoader.h
+++ b/Libraries/Image/RCTImageLoader.h
@@ -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
/**
diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m
index 422a189d505..50040c95fb9 100644
--- a/Libraries/Image/RCTImageLoader.m
+++ b/Libraries/Image/RCTImageLoader.m
@@ -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 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 {
diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m
index 01e5b2fbef8..9b5ba8f80d1 100644
--- a/Libraries/Image/RCTImageView.m
+++ b/Libraries/Image/RCTImageView.m
@@ -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;