diff --git a/Libraries/Image/RCTAnimatedImage.h b/Libraries/Image/RCTAnimatedImage.h new file mode 100644 index 00000000000..689effb2b1d --- /dev/null +++ b/Libraries/Image/RCTAnimatedImage.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@protocol RCTAnimatedImage +@property (nonatomic, assign, readonly) NSUInteger animatedImageFrameCount; +@property (nonatomic, assign, readonly) NSUInteger animatedImageLoopCount; + +- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index; +- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index; + +@end + +@interface RCTAnimatedImage : UIImage + +@end diff --git a/Libraries/Image/RCTAnimatedImage.m b/Libraries/Image/RCTAnimatedImage.m new file mode 100644 index 00000000000..bfb4ca5a4d6 --- /dev/null +++ b/Libraries/Image/RCTAnimatedImage.m @@ -0,0 +1,165 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTAnimatedImage.h" + +@interface RCTGIFCoderFrame : NSObject + +@property (nonatomic, assign) NSUInteger index; +@property (nonatomic, assign) NSTimeInterval duration; + +@end + +@implementation RCTGIFCoderFrame +@end + +@implementation RCTAnimatedImage { + CGImageSourceRef _imageSource; + CGFloat _scale; + NSUInteger _loopCount; + NSUInteger _frameCount; + NSArray *_frames; +} + +- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale +{ + if (self = [super init]) { + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); + if (!imageSource) { + return nil; + } + + BOOL framesValid = [self scanAndCheckFramesValidWithSource:imageSource]; + if (!framesValid) { + CFRelease(imageSource); + return nil; + } + + _imageSource = imageSource; + + // grab image at the first index + UIImage *image = [self animatedImageFrameAtIndex:0]; + if (!image) { + return nil; + } + self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + } + + return self; +} + +- (BOOL)scanAndCheckFramesValidWithSource:(CGImageSourceRef)imageSource +{ + if (!imageSource) { + return NO; + } + NSUInteger frameCount = CGImageSourceGetCount(imageSource); + NSUInteger loopCount = [self imageLoopCountWithSource:imageSource]; + NSMutableArray *frames = [NSMutableArray array]; + + for (size_t i = 0; i < frameCount; i++) { + RCTGIFCoderFrame *frame = [[RCTGIFCoderFrame alloc] init]; + frame.index = i; + frame.duration = [self frameDurationAtIndex:i source:imageSource]; + [frames addObject:frame]; + } + + _frameCount = frameCount; + _loopCount = loopCount; + _frames = [frames copy]; + + return YES; +} + +- (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source +{ + NSUInteger loopCount = 1; + NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil); + NSDictionary *gifProperties = imageProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary]; + if (gifProperties) { + NSNumber *gifLoopCount = gifProperties[(__bridge NSString *)kCGImagePropertyGIFLoopCount]; + if (gifLoopCount != nil) { + loopCount = gifLoopCount.unsignedIntegerValue; + } + } + return loopCount; +} + +- (float)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source +{ + float frameDuration = 0.1f; + CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil); + if (!cfFrameProperties) { + return frameDuration; + } + NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties; + NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary]; + + NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime]; + if (delayTimeUnclampedProp != nil) { + frameDuration = [delayTimeUnclampedProp floatValue]; + } else { + NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime]; + if (delayTimeProp != nil) { + frameDuration = [delayTimeProp floatValue]; + } + } + + CFRelease(cfFrameProperties); + return frameDuration; +} + +- (NSUInteger)animatedImageLoopCount +{ + return _loopCount; +} + +- (NSUInteger)animatedImageFrameCount +{ + return _frameCount; +} + +- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index +{ + if (index >= _frameCount) { + return 0; + } + return _frames[index].duration; +} + +- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index +{ + CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL); + if (!imageRef) { + return nil; + } + UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:UIImageOrientationUp]; + CGImageRelease(imageRef); + return image; +} + +- (void)didReceiveMemoryWarning:(NSNotification *)notification +{ + if (_imageSource) { + for (size_t i = 0; i < _frameCount; i++) { + CGImageSourceRemoveCacheAtIndex(_imageSource, i); + } + } +} + +- (void)dealloc +{ + if (_imageSource) { + CFRelease(_imageSource); + _imageSource = NULL; + } + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +} + +@end diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index a0afd43f4da..a5f13a66681 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -11,6 +11,7 @@ #import #import +#import "RCTAnimatedImage.h" @implementation RCTGIFImageDecoder @@ -30,96 +31,13 @@ RCT_EXPORT_MODULE() resizeMode:(RCTResizeMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { - CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); - if (!imageSource) { + RCTAnimatedImage *image = [[RCTAnimatedImage alloc] initWithData:imageData scale:scale]; + + if (!image) { completionHandler(nil, nil); return ^{}; } - NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL); - CGFloat loopCount = 0; - if ([[properties[(id)kCGImagePropertyGIFDictionary] allKeys] containsObject:(id)kCGImagePropertyGIFLoopCount]) { - loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; - if (loopCount == 0) { - // A loop count of 0 means infinite - loopCount = HUGE_VALF; - } else { - // A loop count of 1 means it should repeat twice, 2 means, thrice, etc. - loopCount += 1; - } - } - - UIImage *image = nil; - size_t imageCount = CGImageSourceGetCount(imageSource); - if (imageCount > 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 (!imageRef) { - continue; - } - if (!image) { - image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; - } - - 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 (delays.count == 0) { - delayTime = @(kDelayTimeIntervalDefault); - } else { - delayTime = delays.lastObject; - } - } - - const NSTimeInterval kDelayTimeIntervalMinimum = 0.02; - if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) { - delayTime = @(kDelayTimeIntervalDefault); - } - - duration += delayTime.doubleValue; - [delays addObject:delayTime]; - [images addObject:(__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; - } - - [keyTimes addObject:@1.0]; - - // Create animation - CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"]; - animation.calculationMode = kCAAnimationDiscrete; - animation.repeatCount = loopCount; - animation.keyTimes = keyTimes; - animation.values = images; - animation.duration = duration; - animation.removedOnCompletion = NO; - animation.fillMode = kCAFillModeForwards; - image.reactKeyframeAnimation = animation; - - } else { - - // Don't bother creating an animation - CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); - if (imageRef) { - image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; - CFRelease(imageRef); - } - CFRelease(imageSource); - } - + completionHandler(nil, image); return ^{}; } diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index bbf4d710422..30fbcdf83b8 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */; }; 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */; }; - 1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */; }; 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */; }; 139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 139A38831C4D587C00862840 /* RCTResizeMode.m */; }; 13EF7F7F1BC825B1003F47DD /* RCTLocalAssetImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */; }; @@ -18,7 +17,6 @@ 2D3B5F1B1D9B0D0700451313 /* RCTImageBlurUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */; }; 2D3B5F1C1D9B0D1300451313 /* RCTResizeMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 139A38831C4D587C00862840 /* RCTResizeMode.m */; }; 2D3B5F1D1D9B0D1300451313 /* RCTLocalAssetImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */; }; - 2D3B5F1E1D9B0D1300451313 /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */; }; 2D3B5F1F1D9B0D1300451313 /* RCTImageEditingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 354631671B69857700AA0B86 /* RCTImageEditingManager.m */; }; 2D3B5F201D9B0D1300451313 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; 2D3B5F211D9B0D1300451313 /* RCTImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */; }; @@ -31,7 +29,6 @@ 3D302F211DF8269200D6DDAE /* RCTImageUtils.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */; }; 3DA05A5A1EE0312600805843 /* RCTImageShadowView.h in Headers */ = {isa = PBXBuildFile; fileRef = 59AB09281EDE5DD1009F97B5 /* RCTImageShadowView.h */; }; 3DA05A5B1EE0312900805843 /* RCTImageShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59AB09291EDE5DD1009F97B5 /* RCTImageShadowView.m */; }; - 3DED3A8A1DE6F79800336DD7 /* RCTGIFImageDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */; }; 3DED3A8B1DE6F79800336DD7 /* RCTImageBlurUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */; }; 3DED3A8C1DE6F79800336DD7 /* RCTImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CCD34C251D4B8FE900268922 /* RCTImageCache.h */; }; 3DED3A8D1DE6F79800336DD7 /* RCTImageEditingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 354631661B69857700AA0B86 /* RCTImageEditingManager.h */; }; @@ -42,7 +39,6 @@ 3DED3A921DE6F79800336DD7 /* RCTImageViewManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */; }; 3DED3A931DE6F79800336DD7 /* RCTLocalAssetImageLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 13EF7F7D1BC825B1003F47DD /* RCTLocalAssetImageLoader.h */; }; 3DED3A941DE6F79800336DD7 /* RCTResizeMode.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D5FA63E1DE4B44A0058FD77 /* RCTResizeMode.h */; }; - 3DED3A961DE6F7A400336DD7 /* RCTGIFImageDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */; }; 3DED3A971DE6F7A400336DD7 /* RCTImageBlurUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */; }; 3DED3A981DE6F7A400336DD7 /* RCTImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CCD34C251D4B8FE900268922 /* RCTImageCache.h */; }; 3DED3A991DE6F7A400336DD7 /* RCTImageEditingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 354631661B69857700AA0B86 /* RCTImageEditingManager.h */; }; @@ -55,6 +51,14 @@ 3DED3AA01DE6F7A400336DD7 /* RCTResizeMode.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D5FA63E1DE4B44A0058FD77 /* RCTResizeMode.h */; }; 59AB092A1EDE5DD1009F97B5 /* RCTImageShadowView.h in Headers */ = {isa = PBXBuildFile; fileRef = 59AB09281EDE5DD1009F97B5 /* RCTImageShadowView.h */; }; 59AB092B1EDE5DD1009F97B5 /* RCTImageShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59AB09291EDE5DD1009F97B5 /* RCTImageShadowView.m */; }; + 681D33F822880795003CD9D1 /* RCTAnimatedImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 681D33F622880794003CD9D1 /* RCTAnimatedImage.m */; }; + 681D33F922880795003CD9D1 /* RCTAnimatedImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 681D33F722880795003CD9D1 /* RCTAnimatedImage.h */; }; + 681D33FC228807A1003CD9D1 /* RCTUIImageViewAnimated.h in Headers */ = {isa = PBXBuildFile; fileRef = 681D33FA228807A1003CD9D1 /* RCTUIImageViewAnimated.h */; }; + 681D33FD228807A1003CD9D1 /* RCTUIImageViewAnimated.m in Sources */ = {isa = PBXBuildFile; fileRef = 681D33FB228807A1003CD9D1 /* RCTUIImageViewAnimated.m */; }; + 681D340422880832003CD9D1 /* RCTGIFImageDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 681D340222880831003CD9D1 /* RCTGIFImageDecoder.h */; }; + 681D340522880832003CD9D1 /* RCTGIFImageDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 681D340222880831003CD9D1 /* RCTGIFImageDecoder.h */; }; + 681D340622880832003CD9D1 /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 681D340322880832003CD9D1 /* RCTGIFImageDecoder.m */; }; + 681D340722880832003CD9D1 /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 681D340322880832003CD9D1 /* RCTGIFImageDecoder.m */; }; CCD34C271D4B8FE900268922 /* RCTImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = CCD34C261D4B8FE900268922 /* RCTImageCache.m */; }; EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */; }; /* End PBXBuildFile section */ @@ -89,8 +93,6 @@ 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageView.m; sourceTree = ""; }; 1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageViewManager.h; sourceTree = ""; }; 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageViewManager.m; sourceTree = ""; }; - 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTGIFImageDecoder.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; - 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImageDecoder.m; sourceTree = ""; }; 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageUtils.h; sourceTree = ""; }; 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtils.m; sourceTree = ""; }; 139A38831C4D587C00862840 /* RCTResizeMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTResizeMode.m; sourceTree = ""; }; @@ -108,6 +110,12 @@ 58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; }; 59AB09281EDE5DD1009F97B5 /* RCTImageShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageShadowView.h; sourceTree = ""; }; 59AB09291EDE5DD1009F97B5 /* RCTImageShadowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageShadowView.m; sourceTree = ""; }; + 681D33F622880794003CD9D1 /* RCTAnimatedImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimatedImage.m; sourceTree = ""; }; + 681D33F722880795003CD9D1 /* RCTAnimatedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimatedImage.h; sourceTree = ""; }; + 681D33FA228807A1003CD9D1 /* RCTUIImageViewAnimated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIImageViewAnimated.h; sourceTree = ""; }; + 681D33FB228807A1003CD9D1 /* RCTUIImageViewAnimated.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIImageViewAnimated.m; sourceTree = ""; }; + 681D340222880831003CD9D1 /* RCTGIFImageDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTGIFImageDecoder.h; sourceTree = ""; }; + 681D340322880832003CD9D1 /* RCTGIFImageDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImageDecoder.m; sourceTree = ""; }; CCD34C251D4B8FE900268922 /* RCTImageCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTImageCache.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; CCD34C261D4B8FE900268922 /* RCTImageCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageCache.m; sourceTree = ""; }; EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTImageBlurUtils.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; @@ -128,8 +136,6 @@ children = ( 3D5FA68B1DE4BA290058FD77 /* Frameworks */, 58B5115E1A9E6B3D00147676 /* Products */, - 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */, - 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */, EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */, EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */, CCD34C251D4B8FE900268922 /* RCTImageCache.h */, @@ -152,6 +158,12 @@ 13EF7F7E1BC825B1003F47DD /* RCTLocalAssetImageLoader.m */, 3D5FA63E1DE4B44A0058FD77 /* RCTResizeMode.h */, 139A38831C4D587C00862840 /* RCTResizeMode.m */, + 681D33F722880795003CD9D1 /* RCTAnimatedImage.h */, + 681D33F622880794003CD9D1 /* RCTAnimatedImage.m */, + 681D33FA228807A1003CD9D1 /* RCTUIImageViewAnimated.h */, + 681D33FB228807A1003CD9D1 /* RCTUIImageViewAnimated.m */, + 681D340222880831003CD9D1 /* RCTGIFImageDecoder.h */, + 681D340322880832003CD9D1 /* RCTGIFImageDecoder.m */, ); indentWidth = 2; sourceTree = ""; @@ -174,11 +186,13 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 3DED3A8A1DE6F79800336DD7 /* RCTGIFImageDecoder.h in Headers */, 3DED3A901DE6F79800336DD7 /* RCTImageUtils.h in Headers */, + 681D33F922880795003CD9D1 /* RCTAnimatedImage.h in Headers */, 3DED3A8B1DE6F79800336DD7 /* RCTImageBlurUtils.h in Headers */, 3DED3A8C1DE6F79800336DD7 /* RCTImageCache.h in Headers */, + 681D340422880832003CD9D1 /* RCTGIFImageDecoder.h in Headers */, 3DED3A8D1DE6F79800336DD7 /* RCTImageEditingManager.h in Headers */, + 681D33FC228807A1003CD9D1 /* RCTUIImageViewAnimated.h in Headers */, 3DED3A8E1DE6F79800336DD7 /* RCTImageLoader.h in Headers */, 3DED3A8F1DE6F79800336DD7 /* RCTImageStoreManager.h in Headers */, 59AB092A1EDE5DD1009F97B5 /* RCTImageShadowView.h in Headers */, @@ -193,9 +207,9 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 3DED3A961DE6F7A400336DD7 /* RCTGIFImageDecoder.h in Headers */, 3DED3A971DE6F7A400336DD7 /* RCTImageBlurUtils.h in Headers */, 3DED3A981DE6F7A400336DD7 /* RCTImageCache.h in Headers */, + 681D340522880832003CD9D1 /* RCTGIFImageDecoder.h in Headers */, 3DED3A991DE6F7A400336DD7 /* RCTImageEditingManager.h in Headers */, 3DED3A9A1DE6F7A400336DD7 /* RCTImageLoader.h in Headers */, 3DED3A9B1DE6F7A400336DD7 /* RCTImageStoreManager.h in Headers */, @@ -268,6 +282,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 58B511541A9E6B3D00147676; @@ -289,8 +304,8 @@ 2D3B5F231D9B0D1300451313 /* RCTImageStoreManager.m in Sources */, 2D3B5F1A1D9B0D0400451313 /* RCTImageCache.m in Sources */, 2D3B5F1D1D9B0D1300451313 /* RCTLocalAssetImageLoader.m in Sources */, + 681D340722880832003CD9D1 /* RCTGIFImageDecoder.m in Sources */, 2D3B5F1F1D9B0D1300451313 /* RCTImageEditingManager.m in Sources */, - 2D3B5F1E1D9B0D1300451313 /* RCTGIFImageDecoder.m in Sources */, 2D3B5F1C1D9B0D1300451313 /* RCTResizeMode.m in Sources */, 2D3B5F221D9B0D1300451313 /* RCTImageViewManager.m in Sources */, 2D3B5F211D9B0D1300451313 /* RCTImageView.m in Sources */, @@ -306,13 +321,15 @@ buildActionMask = 2147483647; files = ( 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */, + 681D340622880832003CD9D1 /* RCTGIFImageDecoder.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */, - 1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */, + 681D33F822880795003CD9D1 /* RCTAnimatedImage.m in Sources */, 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */, 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */, 139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */, EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */, + 681D33FD228807A1003CD9D1 /* RCTUIImageViewAnimated.m in Sources */, 59AB092B1EDE5DD1009F97B5 /* RCTImageShadowView.m in Sources */, 13EF7F7F1BC825B1003F47DD /* RCTLocalAssetImageLoader.m in Sources */, 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */, diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index e2f221ba85a..7cea4625f1b 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -14,6 +14,7 @@ #import #import +#import "RCTUIImageViewAnimated.h" #import "RCTImageBlurUtils.h" #import "RCTImageLoader.h" #import "RCTImageUtils.h" @@ -79,7 +80,7 @@ static NSDictionary *onLoadParamsForSource(RCTImageSource *source) // Whether the latest change of props requires the image to be reloaded BOOL _needsReload; - UIImageView *_imageView; + RCTUIImageViewAnimated *_imageView; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -96,7 +97,7 @@ static NSDictionary *onLoadParamsForSource(RCTImageSource *source) selector:@selector(clearImageIfDetached) name:UIApplicationDidEnterBackgroundNotification object:nil]; - _imageView = [[UIImageView alloc] init]; + _imageView = [[RCTUIImageViewAnimated alloc] init]; _imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self addSubview:_imageView]; } diff --git a/Libraries/Image/RCTUIImageViewAnimated.h b/Libraries/Image/RCTUIImageViewAnimated.h new file mode 100644 index 00000000000..4afe90a64eb --- /dev/null +++ b/Libraries/Image/RCTUIImageViewAnimated.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTAnimatedImage.h" + +@interface RCTUIImageViewAnimated : UIImageView + +@end diff --git a/Libraries/Image/RCTUIImageViewAnimated.m b/Libraries/Image/RCTUIImageViewAnimated.m new file mode 100644 index 00000000000..b47db503d73 --- /dev/null +++ b/Libraries/Image/RCTUIImageViewAnimated.m @@ -0,0 +1,323 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTUIImageViewAnimated.h" + +#import +#import + +static NSUInteger RCTDeviceTotalMemory() { + return (NSUInteger)[[NSProcessInfo processInfo] physicalMemory]; +} + +static NSUInteger RCTDeviceFreeMemory() { + mach_port_t host_port = mach_host_self(); + mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t); + vm_size_t page_size; + vm_statistics_data_t vm_stat; + kern_return_t kern; + + kern = host_page_size(host_port, &page_size); + if (kern != KERN_SUCCESS) return 0; + kern = host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size); + if (kern != KERN_SUCCESS) return 0; + return vm_stat.free_count * page_size; +} + +@interface RCTUIImageViewAnimated () + +@property (nonatomic, assign) NSUInteger maxBufferSize; +@property (nonatomic, strong, readwrite) UIImage *currentFrame; +@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; +@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount; +@property (nonatomic, assign) NSUInteger totalFrameCount; +@property (nonatomic, assign) NSUInteger totalLoopCount; +@property (nonatomic, strong) UIImage *animatedImage; +@property (nonatomic, strong) NSMutableDictionary *frameBuffer; +@property (nonatomic, assign) NSTimeInterval currentTime; +@property (nonatomic, assign) BOOL bufferMiss; +@property (nonatomic, assign) NSUInteger maxBufferCount; +@property (nonatomic, strong) NSOperationQueue *fetchQueue; +@property (nonatomic, strong) dispatch_semaphore_t lock; +@property (nonatomic, assign) CGFloat animatedImageScale; +@property (nonatomic, strong) CADisplayLink *displayLink; + +@end + +@implementation RCTUIImageViewAnimated + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.lock = dispatch_semaphore_create(1); + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + + } + return self; +} + +- (void)resetAnimatedImage +{ + self.animatedImage = nil; + self.totalFrameCount = 0; + self.totalLoopCount = 0; + self.currentFrame = nil; + self.currentFrameIndex = 0; + self.currentLoopCount = 0; + self.currentTime = 0; + self.bufferMiss = NO; + self.maxBufferCount = 0; + self.animatedImageScale = 1; + [_fetchQueue cancelAllOperations]; + _fetchQueue = nil; + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + [_frameBuffer removeAllObjects]; + _frameBuffer = nil; + dispatch_semaphore_signal(self.lock); +} + +- (void)setImage:(UIImage *)image +{ + if (self.image == image) { + return; + } + + if ([image respondsToSelector:@selector(animatedImageFrameAtIndex:)]) { + [self stop]; + [self resetAnimatedImage]; + + NSUInteger animatedImageFrameCount = ((UIImage *)image).animatedImageFrameCount; + + // Check the frame count + if (animatedImageFrameCount <= 1) { + return; + } + + self.animatedImage = (UIImage *)image; + self.totalFrameCount = animatedImageFrameCount; + + // Get the current frame and loop count. + self.totalLoopCount = self.animatedImage.animatedImageLoopCount; + + self.animatedImageScale = image.scale; + + self.currentFrame = image; + + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame; + dispatch_semaphore_signal(self.lock); + + // Calculate max buffer size + [self calculateMaxBufferCount]; + + if ([self paused]) { + [self start]; + } + + [self.layer setNeedsDisplay]; + } else { + super.image = image; + } +} + +#pragma mark - Private + +- (NSOperationQueue *)fetchQueue +{ + if (!_fetchQueue) { + _fetchQueue = [[NSOperationQueue alloc] init]; + _fetchQueue.maxConcurrentOperationCount = 1; + } + return _fetchQueue; +} + +- (NSMutableDictionary *)frameBuffer +{ + if (!_frameBuffer) { + _frameBuffer = [NSMutableDictionary dictionary]; + } + return _frameBuffer; +} + +- (CADisplayLink *)displayLink +{ + if (!_displayLink) { + __weak __typeof(self) weakSelf = self; + _displayLink = [CADisplayLink displayLinkWithTarget:weakSelf selector:@selector(displayDidRefresh:)]; + NSString *runLoopMode = [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode]; + } + return _displayLink; +} + +#pragma mark - Animation + +- (void)start +{ + self.displayLink.paused = NO; +} + +- (void)stop +{ + self.displayLink.paused = YES; +} + +- (BOOL)paused +{ + return self.displayLink.isPaused; +} + +- (void)displayDidRefresh:(CADisplayLink *)displayLink +{ + NSTimeInterval duration = displayLink.duration * displayLink.frameInterval; + NSUInteger totalFrameCount = self.totalFrameCount; + NSUInteger currentFrameIndex = self.currentFrameIndex; + NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount; + + // Check if we have the frame buffer firstly to improve performance + if (!self.bufferMiss) { + // Then check if timestamp is reached + self.currentTime += duration; + NSTimeInterval currentDuration = [self.animatedImage animatedImageDurationAtIndex:currentFrameIndex]; + if (self.currentTime < currentDuration) { + // Current frame timestamp not reached, return + return; + } + self.currentTime -= currentDuration; + NSTimeInterval nextDuration = [self.animatedImage animatedImageDurationAtIndex:nextFrameIndex]; + if (self.currentTime > nextDuration) { + // Do not skip frame + self.currentTime = nextDuration; + } + } + + // Update the current frame + UIImage *currentFrame; + UIImage *fetchFrame; + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + currentFrame = self.frameBuffer[@(currentFrameIndex)]; + fetchFrame = currentFrame ? self.frameBuffer[@(nextFrameIndex)] : nil; + dispatch_semaphore_signal(self.lock); + BOOL bufferFull = NO; + if (currentFrame) { + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + // Remove the frame buffer if need + if (self.frameBuffer.count > self.maxBufferCount) { + self.frameBuffer[@(currentFrameIndex)] = nil; + } + // Check whether we can stop fetch + if (self.frameBuffer.count == totalFrameCount) { + bufferFull = YES; + } + dispatch_semaphore_signal(self.lock); + self.currentFrame = currentFrame; + self.currentFrameIndex = nextFrameIndex; + self.bufferMiss = NO; + [self.layer setNeedsDisplay]; + } else { + self.bufferMiss = YES; + } + + // Update the loop count when last frame rendered + if (nextFrameIndex == 0 && !self.bufferMiss) { + // Update the loop count + self.currentLoopCount++; + // if reached the max loop count, stop animating, 0 means loop indefinitely + NSUInteger maxLoopCount = self.totalLoopCount; + if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) { + [self stop]; + return; + } + } + + // Check if we should prefetch next frame or current frame + NSUInteger fetchFrameIndex; + if (self.bufferMiss) { + // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame + fetchFrameIndex = currentFrameIndex; + } else { + // Or, most cases, the decode speed is faster than render speed, we fetch next frame + fetchFrameIndex = nextFrameIndex; + } + + if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) { + // Prefetch next frame in background queue + UIImage *animatedImage = self.animatedImage; + NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + UIImage *frame = [animatedImage animatedImageFrameAtIndex:fetchFrameIndex]; + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + self.frameBuffer[@(fetchFrameIndex)] = frame; + dispatch_semaphore_signal(self.lock); + }]; + [self.fetchQueue addOperation:operation]; + } +} + +#pragma mark - CALayerDelegate + +- (void)displayLayer:(CALayer *)layer +{ + if (_currentFrame) { + layer.contentsScale = self.animatedImageScale; + layer.contents = (__bridge id)_currentFrame.CGImage; + } +} + +#pragma mark - Util + +- (void)calculateMaxBufferCount +{ + NSUInteger bytes = CGImageGetBytesPerRow(self.currentFrame.CGImage) * CGImageGetHeight(self.currentFrame.CGImage); + if (bytes == 0) bytes = 1024; + + NSUInteger max = 0; + if (self.maxBufferSize > 0) { + max = self.maxBufferSize; + } else { + // Calculate based on current memory, these factors are by experience + NSUInteger total = RCTDeviceTotalMemory(); + NSUInteger free = RCTDeviceFreeMemory(); + max = MIN(total * 0.2, free * 0.6); + } + + NSUInteger maxBufferCount = (double)max / (double)bytes; + if (!maxBufferCount) { + // At least 1 frame + maxBufferCount = 1; + } + + self.maxBufferCount = maxBufferCount; +} + +#pragma mark - Lifecycle + +- (void)dealloc +{ + // Removes the display link from all run loop modes. + [_displayLink invalidate]; + _displayLink = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +} + +- (void)didReceiveMemoryWarning:(NSNotification *)notification +{ + [_fetchQueue cancelAllOperations]; + [_fetchQueue addOperationWithBlock:^{ + NSNumber *currentFrameIndex = @(self.currentFrameIndex); + dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER); + NSArray *keys = self.frameBuffer.allKeys; + // only keep the next frame for later rendering + for (NSNumber * key in keys) { + if (![key isEqualToNumber:currentFrameIndex]) { + [self.frameBuffer removeObjectForKey:key]; + } + } + dispatch_semaphore_signal(self.lock); + }]; +} + +@end