diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 5b9e04288f..d401200419 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */; }; 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; }; 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; }; - 144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTClipRectTests.m */; }; + 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */; }; 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; 1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */; }; 1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */; }; @@ -191,7 +191,7 @@ 143BC5951B21E3E100462512 /* UIExplorerIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 143BC5981B21E3E100462512 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerSnapshotTests.m; sourceTree = ""; }; - 144D21231B2204C5006DB32B /* RCTClipRectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClipRectTests.m; sourceTree = ""; }; + 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtilTests.m; sourceTree = ""; }; 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAllocationTests.m; sourceTree = ""; }; 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeTests.m; sourceTree = ""; }; 1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutorTests.m; sourceTree = ""; }; @@ -357,12 +357,12 @@ 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */, 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */, 138D6A151B53CD440074A87E /* RCTCacheTests.m */, - 144D21231B2204C5006DB32B /* RCTClipRectTests.m */, 1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */, 1497CFA71B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m */, 1497CFA81B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m */, 1497CFA91B21F5E400C1F8F2 /* RCTEventDispatcherTests.m */, 1300627E1B59179B0043FE5A /* RCTGzipTests.m */, + 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */, 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */, 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */, 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */, @@ -793,7 +793,7 @@ buildActionMask = 2147483647; files = ( 1497CFB01B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m in Sources */, - 144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */, + 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */, 1393D0381B68CD1300E1B601 /* RCTModuleMethodTests.m in Sources */, 1497CFB21B21F5E400C1F8F2 /* RCTSparseArrayTests.m in Sources */, 1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */, diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m similarity index 65% rename from Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m rename to Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m index 0041a1b46c..f1effc8d64 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m @@ -33,11 +33,11 @@ RCTAssertEqualPoints(a.origin, b.origin); \ RCTAssertEqualSizes(a.size, b.size); \ } -@interface RCTClipRectTests : XCTestCase +@interface RCTImageUtilTests : XCTestCase @end -@implementation RCTClipRectTests +@implementation RCTImageUtilTests - (void)testLandscapeSourceLandscapeTarget { @@ -46,19 +46,19 @@ RCTAssertEqualSizes(a.size, b.size); \ { CGRect expected = {CGPointZero, {100, 20}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); RCTAssertEqualRects(expected, result); } { CGRect expected = {CGPointZero, {100, 10}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); RCTAssertEqualRects(expected, result); } { CGRect expected = {{-50, 0}, {200, 20}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); RCTAssertEqualRects(expected, result); } } @@ -69,20 +69,20 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {100, 20}; { - CGRect expected = {CGPointZero, {10, 20}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + CGRect expected = {CGPointZero, {100, 20}}; + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); RCTAssertEqualRects(expected, result); } { CGRect expected = {CGPointZero, {2, 20}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); RCTAssertEqualRects(expected, result); } { - CGRect expected = {{0, -49}, {10, 100}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + CGRect expected = {{0, -490}, {100, 1000}}; + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); RCTAssertEqualRects(expected, result); } } @@ -93,20 +93,20 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {20, 50}; { - CGRect expected = {CGPointZero, {10, 50}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + CGRect expected = {CGPointZero, {20, 50}}; + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); RCTAssertEqualRects(expected, result); } { CGRect expected = {CGPointZero, {5, 50}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); RCTAssertEqualRects(expected, result); } { - CGRect expected = {{0, -37.5}, {10, 100}}; - CGRect result = RCTClipRect(content, 2, target, 2, UIViewContentModeScaleAspectFill); + CGRect expected = {{0, -75}, {20, 200}}; + CGRect result = RCTTargetRect(content, target, 2, UIViewContentModeScaleAspectFill); RCTAssertEqualRects(expected, result); } } @@ -117,8 +117,8 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {20, 50}; { - CGRect expected = {{0, -38}, {10, 100}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + CGRect expected = {{0, -75}, {20, 200}}; + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); RCTAssertEqualRects(expected, result); } } @@ -129,7 +129,7 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {3, 3}; CGRect expected = {CGPointZero, {3, 3}}; - CGRect result = RCTClipRect(content, 2, target, 1, UIViewContentModeScaleToFill); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); RCTAssertEqualRects(expected, result); } diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index 8bae601125..6213431e29 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -15,9 +15,6 @@ #import "RCTNetworking.h" #import "RCTUtils.h" -CGSize RCTTargetSizeForClipRect(CGRect); -CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode); - @implementation RCTImageDownloader { NSURLCache *_cache; @@ -131,14 +128,14 @@ RCT_EXPORT_MODULE() UIImage *image = [UIImage imageWithData:data scale:scale]; if (image && !CGSizeEqualToSize(size, CGSizeZero)) { - // Get scale and size - CGRect imageRect = RCTClipRect(image.size, scale, size, scale, resizeMode); - CGSize destSize = RCTTargetSizeForClipRect(imageRect); + // Get destination size + CGSize targetSize = RCTTargetSize(image.size, image.scale, + size, scale, resizeMode, NO); // Decompress image at required size BOOL opaque = !RCTImageHasAlpha(image.CGImage); - UIGraphicsBeginImageContextWithOptions(destSize, opaque, scale); - [image drawInRect:imageRect]; + UIGraphicsBeginImageContextWithOptions(targetSize, opaque, scale); + [image drawInRect:(CGRect){CGPointZero, targetSize}]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); } diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 44ad8d6b1d..dc0fbf640b 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -85,8 +85,8 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation, } CGSize sourceSize = representation.dimensions; - CGRect targetRect = RCTClipRect(sourceSize, representation.scale, size, scale, resizeMode); - CGSize targetSize = targetRect.size; + CGSize targetSize = RCTTargetSize(sourceSize, representation.scale, + size, scale, resizeMode, NO); NSDictionary *options = @{ (id)kCGImageSourceShouldAllowFloat: @YES, diff --git a/Libraries/Image/RCTImageUtils.h b/Libraries/Image/RCTImageUtils.h index cbb38cda89..1bca232416 100644 --- a/Libraries/Image/RCTImageUtils.h +++ b/Libraries/Image/RCTImageUtils.h @@ -13,20 +13,24 @@ #import "RCTDefines.h" /** - * Returns the optimal context size for an image drawn using the clip rect - * returned by RCTClipRect. + * This function takes an input content size (typically from an image), a target + * size and scale that it will be drawn at (typically in a CGContext) and then + * calculates the rectangle to draw the image into so that it will be sized and + * positioned correctly if drawn using the specified content mode. */ -RCT_EXTERN CGSize RCTTargetSizeForClipRect(CGRect clipRect); +RCT_EXTERN CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, + CGFloat destScale, UIViewContentMode resizeMode); /** * This function takes an input content size & scale (typically from an image), - * a target size & scale that it will be drawn into (typically a CGContext) and - * then calculates the optimal rectangle to draw the image into so that it will - * be sized and positioned correctly if drawn using the specified content mode. + * a target size & scale at which it will be displayed (typically in a + * UIImageView) and then calculates the optimal size at which to redraw the + * image so that it will be displayed correctly with the specified content mode. */ -RCT_EXTERN CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, - CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode); +RCT_EXTERN CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode, + BOOL allowUpscaling); /** * This function takes an input content size & scale (typically from an image), diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index 82a97a778b..78008dcadf 100644 --- a/Libraries/Image/RCTImageUtils.m +++ b/Libraries/Image/RCTImageUtils.m @@ -29,28 +29,14 @@ static CGSize RCTCeilSize(CGSize size, CGFloat scale) }; } -CGSize RCTTargetSizeForClipRect(CGRect clipRect) -{ - return (CGSize){ - clipRect.size.width + clipRect.origin.x * 2, - clipRect.size.height + clipRect.origin.y * 2 - }; -} - -CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, - CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode) +CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, + CGFloat destScale, UIViewContentMode resizeMode) { if (CGSizeEqualToSize(destSize, CGSizeZero)) { // Assume we require the largest size available return (CGRect){CGPointZero, sourceSize}; } - // Precompensate for scale - CGFloat scale = sourceScale / destScale; - sourceSize.width *= scale; - sourceSize.height *= scale; - CGFloat aspect = sourceSize.width / sourceSize.height; // If only one dimension in destSize is non-zero (for example, an Image // with `flex: 1` whose height is indeterminate), calculate the unknown @@ -61,7 +47,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, if (destSize.height == 0) { destSize.height = destSize.width / aspect; } - + // Calculate target aspect ratio if needed (don't bother if resizeMode == stretch) CGFloat targetAspect = 0.0; if (resizeMode != UIViewContentModeScaleToFill) { @@ -74,20 +60,18 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, switch (resizeMode) { case UIViewContentModeScaleToFill: // stretch - sourceSize.width = MIN(destSize.width, sourceSize.width); - sourceSize.height = MIN(destSize.height, sourceSize.height); - return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)}; + return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)}; case UIViewContentModeScaleAspectFit: // contain if (targetAspect <= aspect) { // target is taller than content - sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); + sourceSize.width = destSize.width = destSize.width; sourceSize.height = sourceSize.width / aspect; } else { // target is wider than content - sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); + sourceSize.height = destSize.height = destSize.height; sourceSize.width = sourceSize.height * aspect; } return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)}; @@ -96,7 +80,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, if (targetAspect <= aspect) { // target is taller than content - sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); + sourceSize.height = destSize.height = destSize.height; sourceSize.width = sourceSize.height * aspect; destSize.width = destSize.height * targetAspect; return (CGRect){ @@ -106,7 +90,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, } else { // target is wider than content - sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); + sourceSize.width = destSize.width = destSize.width; sourceSize.height = sourceSize.width / aspect; destSize.height = destSize.width / targetAspect; return (CGRect){ @@ -122,9 +106,39 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, } } -RCT_EXTERN BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, - CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode) +CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode, + BOOL allowUpscaling) +{ + switch (resizeMode) { + case UIViewContentModeScaleToFill: // stretch + + if (!allowUpscaling) { + CGFloat scale = sourceScale / destScale; + destSize.width = MIN(sourceSize.width * scale, destSize.width); + destSize.height = MIN(sourceSize.height * scale, destSize.height); + } + return RCTCeilSize(destSize, destScale); + + default: { + + // Get target size + CGSize size = RCTTargetRect(sourceSize, destSize, destScale, resizeMode).size; + if (!allowUpscaling) { + // return sourceSize if target size is larger + if (sourceSize.width * sourceScale < size.width * destScale) { + return sourceSize; + } + } + return size; + } + } +} + +BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode) { if (CGSizeEqualToSize(destSize, CGSizeZero)) { // Assume we require the largest size available diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 56c37b21a2..f56b3d1839 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -14,6 +14,7 @@ #import "RCTEventDispatcher.h" #import "RCTGIFImage.h" #import "RCTImageLoader.h" +#import "RCTImageUtils.h" #import "RCTUtils.h" #import "UIView+React.h" @@ -43,7 +44,7 @@ RCT_NOT_IMPLEMENTED(-init) -- (void)_updateImage +- (void)updateImage { UIImage *image = self.image; if (!image) { @@ -72,7 +73,7 @@ RCT_NOT_IMPLEMENTED(-init) image = image ?: _defaultImage; if (image != super.image) { super.image = image; - [self _updateImage]; + [self updateImage]; } } @@ -80,7 +81,7 @@ RCT_NOT_IMPLEMENTED(-init) { if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, capInsets)) { _capInsets = capInsets; - [self _updateImage]; + [self updateImage]; } } @@ -88,7 +89,7 @@ RCT_NOT_IMPLEMENTED(-init) { if (_renderingMode != renderingMode) { _renderingMode = renderingMode; - [self _updateImage]; + [self updateImage]; } } @@ -100,6 +101,16 @@ RCT_NOT_IMPLEMENTED(-init) } } +- (void)setContentMode:(UIViewContentMode)contentMode +{ + if (self.contentMode != contentMode) { + super.contentMode = contentMode; + if ([RCTImageLoader isAssetLibraryImage:_src] || [RCTImageLoader isRemoteImage:_src]) { + [self reloadImage]; + } + } +} + - (void)reloadImage { if (_src && !CGSizeEqualToSize(self.frame.size, CGSizeZero)) { @@ -165,12 +176,15 @@ RCT_NOT_IMPLEMENTED(-init) if (self.image == nil) { [self reloadImage]; } else if ([RCTImageLoader isAssetLibraryImage:_src] || [RCTImageLoader isRemoteImage:_src]) { - CGSize imageSize = { - self.image.size.width / RCTScreenScale(), - self.image.size.height / RCTScreenScale() - }; - CGFloat widthChangeFraction = imageSize.width ? ABS(imageSize.width - frame.size.width) / imageSize.width : 1; - CGFloat heightChangeFraction = imageSize.height ? ABS(imageSize.height - frame.size.height) / imageSize.height : 1; + + // Get optimal image size + CGSize currentSize = self.image.size; + CGSize idealSize = RCTTargetSize(self.image.size, self.image.scale, frame.size, + RCTScreenScale(), self.contentMode, YES); + + CGFloat widthChangeFraction = ABS(currentSize.width - idealSize.width) / currentSize.width; + CGFloat heightChangeFraction = ABS(currentSize.height - idealSize.height) / currentSize.height; + // If the combined change is more than 20%, reload the asset in case there is a better size. if (widthChangeFraction + heightChangeFraction > 0.2) { [self reloadImage];