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;