From 91e6c98ecdf5c9116bd894475ce3f3ed601fc5bc Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 8 Oct 2015 11:32:11 -0700 Subject: [PATCH] Implemented inline image support for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: @​public This diff implements inline image support for nodes. Images are specified using tags, however all properties of the image are currently ignored apart from the source (including width/height styles). Images are loaded asyncronously, and will trigger a text re-layout when they have loaded. Reviewed By: @javache Differential Revision: D2507725 fb-gh-sync-id: 59d0696d00a1bc531915cc35242a16b2dec96e85 --- Examples/UIExplorer/TextExample.ios.js | 12 +++++ Libraries/Image/Image.ios.js | 31 +++++++----- Libraries/Image/RCTGIFImageDecoder.m | 4 +- .../Image/RCTImage.xcodeproj/project.pbxproj | 12 +++++ Libraries/Image/RCTImageDownloader.m | 2 +- Libraries/Image/RCTImageLoader.m | 3 +- Libraries/Image/RCTImageView.h | 3 +- Libraries/Image/RCTImageView.m | 3 +- Libraries/Image/RCTShadowVirtualImage.h | 25 ++++++++++ Libraries/Image/RCTShadowVirtualImage.m | 48 +++++++++++++++++++ Libraries/Image/RCTVirtualImageManager.h | 14 ++++++ Libraries/Image/RCTVirtualImageManager.m | 24 ++++++++++ Libraries/Text/RCTShadowText.m | 12 ++++- React/Modules/RCTAccessibilityManager.m | 1 - React/React.xcodeproj/project.pbxproj | 2 + React/Views/RCTImageComponent.h | 19 ++++++++ 16 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 Libraries/Image/RCTShadowVirtualImage.h create mode 100644 Libraries/Image/RCTShadowVirtualImage.m create mode 100644 Libraries/Image/RCTVirtualImageManager.h create mode 100644 Libraries/Image/RCTVirtualImageManager.m create mode 100644 React/Views/RCTImageComponent.h diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index cceca74e6..0d6e20c10 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -17,6 +17,7 @@ var React = require('react-native'); var { + Image, StyleSheet, Text, View, @@ -397,6 +398,17 @@ exports.examples = [ ); }, +}, { + title: 'Inline images', + render: function() { + return ( + + + This text contains an inline image . Neat, huh? + + + ); + }, }]; var styles = StyleSheet.create({ diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 59084721e..4da2d53e5 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -153,6 +153,10 @@ var Image = React.createClass({ validAttributes: ReactNativeViewAttributes.UIView }, + contextTypes: { + isInAParentText: React.PropTypes.bool + }, + render: function() { for (var prop in cfg.nativeOnly) { if (this.props[prop] !== undefined) { @@ -182,16 +186,20 @@ var Image = React.createClass({ RawImage = RCTImageView; } - return ( - - ); + if (this.context.isInAParentText) { + return ; + } else { + return ( + + ); + } } }); @@ -210,6 +218,7 @@ var cfg = { }, }; var RCTImageView = requireNativeComponent('RCTImageView', Image, cfg); -var RCTNetworkImageView = (NativeModules.NetworkImageViewManager) ? requireNativeComponent('RCTNetworkImageView', Image, cfg) : RCTImageView; +var RCTNetworkImageView = NativeModules.NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image, cfg) : RCTImageView; +var RCTVirtualImage = requireNativeComponent('RCTVirtualImage', Image); module.exports = Image; diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index 332769e7d..899a266e8 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -48,7 +48,7 @@ RCT_EXPORT_MODULE() CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); if (!image) { - image = [UIImage imageWithCGImage:imageRef]; + image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; } NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL); @@ -98,7 +98,7 @@ RCT_EXPORT_MODULE() // Don't bother creating an animation CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); if (imageRef) { - image = [UIImage imageWithCGImage:imageRef]; + image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; CFRelease(imageRef); } CFRelease(imageSource); diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 183cb850e..a6412bcc3 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 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 */; }; + 13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */; settings = {ASSET_TAGS = (); }; }; + 13EF7F0C1BC42D4E003F47DD /* RCTVirtualImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */; settings = {ASSET_TAGS = (); }; }; 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */; }; 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 354631671B69857700AA0B86 /* RCTImageEditingManager.m */; }; @@ -39,6 +41,10 @@ 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 = ""; }; + 13EF7F071BC42D4E003F47DD /* RCTShadowVirtualImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowVirtualImage.h; sourceTree = ""; }; + 13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowVirtualImage.m; sourceTree = ""; }; + 13EF7F091BC42D4E003F47DD /* RCTVirtualImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVirtualImageManager.h; sourceTree = ""; }; + 13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTVirtualImageManager.m; sourceTree = ""; }; 143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = ""; }; 143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = ""; }; 35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageStoreManager.h; sourceTree = ""; }; @@ -84,6 +90,10 @@ 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */, 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */, 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */, + 13EF7F071BC42D4E003F47DD /* RCTShadowVirtualImage.h */, + 13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */, + 13EF7F091BC42D4E003F47DD /* RCTVirtualImageManager.h */, + 13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */, 58B5115E1A9E6B3D00147676 /* Products */, ); indentWidth = 2; @@ -154,6 +164,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 13EF7F0C1BC42D4E003F47DD /* RCTVirtualImageManager.m in Sources */, 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */, 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */, @@ -161,6 +172,7 @@ 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */, 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */, + 13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */, 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */, 83DDA1571B8DCA5800892A1C /* RCTAssetBundleImageLoader.m in Sources */, ); diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index e299b1328..5b8befb8b 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -135,7 +135,7 @@ RCT_EXPORT_MODULE() // Normally -dataWithContentsOfURL: would be bad but this is a data URL. NSData *data = [NSData dataWithContentsOfURL:imageURL]; - UIImage *image = [UIImage imageWithData:data]; + UIImage *image = [UIImage imageWithData:data scale:scale]; if (image) { if (progressHandler) { progressHandler(1, 1); diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 7a3c235a2..d91096243 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -14,6 +14,7 @@ #import "RCTConvert.h" #import "RCTDefines.h" #import "RCTImageDownloader.h" +#import "RCTImageUtils.h" #import "RCTLog.h" #import "RCTUtils.h" @@ -165,7 +166,7 @@ RCT_EXPORT_MODULE() }]; } else { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - UIImage *image = [UIImage imageWithData:data]; + UIImage *image = [UIImage imageWithData:data scale:scale]; if (image) { RCTDispatchCallbackOnMainQueue(completionBlock, nil, image); } else { diff --git a/Libraries/Image/RCTImageView.h b/Libraries/Image/RCTImageView.h index fff7c96a0..a561c0f7f 100644 --- a/Libraries/Image/RCTImageView.h +++ b/Libraries/Image/RCTImageView.h @@ -8,10 +8,11 @@ */ #import +#import "RCTImageComponent.h" @class RCTBridge; -@interface RCTImageView : UIImageView +@interface RCTImageView : UIImageView - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 104308131..5926f6331 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -22,7 +22,8 @@ * Determines whether an image of `currentSize` should be reloaded for display * at `idealSize`. */ -static BOOL RCTShouldReloadImageForSizeChange(CGSize currentSize, CGSize idealSize) { +static BOOL RCTShouldReloadImageForSizeChange(CGSize currentSize, CGSize idealSize) +{ static const CGFloat upscaleThreshold = 1.2; static const CGFloat downscaleThreshold = 0.5; diff --git a/Libraries/Image/RCTShadowVirtualImage.h b/Libraries/Image/RCTShadowVirtualImage.h new file mode 100644 index 000000000..841c215e2 --- /dev/null +++ b/Libraries/Image/RCTShadowVirtualImage.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTShadowView.h" +#import "RCTImageComponent.h" + +@class RCTBridge; + +/** + * Shadow image component, used for embedding images in non-view contexts such + * as text. This is NOT used for ordinary views. + */ +@interface RCTShadowVirtualImage : RCTShadowView + +- (instancetype)initWithBridge:(RCTBridge *)bridge; + +@property (nonatomic, copy) NSDictionary *source; + +@end diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m new file mode 100644 index 000000000..57dbef04b --- /dev/null +++ b/Libraries/Image/RCTShadowVirtualImage.m @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTShadowVirtualImage.h" +#import "RCTImageLoader.h" +#import "RCTBridge.h" +#import "RCTConvert.h" + +@implementation RCTShadowVirtualImage +{ + RCTBridge *_bridge; +} + +@synthesize image = _image; + +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + if ((self = [super init])) { + _bridge = bridge; + } + return self; +} + +RCT_NOT_IMPLEMENTED(-(instancetype)init) + +- (void)setSource:(NSDictionary *)source +{ + if (![source isEqual:_source]) { + _source = [source copy]; + NSString *imageTag = [RCTConvert NSString:_source[@"uri"]]; + CGFloat scale = [RCTConvert CGFloat:_source[@"scale"]] ?: 1; + + __weak RCTShadowVirtualImage *weakSelf = self; + [_bridge.imageLoader loadImageWithTag:imageTag size:CGSizeZero scale:scale resizeMode:UIViewContentModeScaleToFill progressBlock:nil completionBlock:^(NSError *error, UIImage *image) { + RCTShadowVirtualImage *strongSelf = weakSelf; + strongSelf->_image = image; + [strongSelf dirtyText]; + }]; + } +} + +@end diff --git a/Libraries/Image/RCTVirtualImageManager.h b/Libraries/Image/RCTVirtualImageManager.h new file mode 100644 index 000000000..b92896235 --- /dev/null +++ b/Libraries/Image/RCTVirtualImageManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTViewManager.h" + +@interface RCTVirtualImageManager : RCTViewManager + +@end diff --git a/Libraries/Image/RCTVirtualImageManager.m b/Libraries/Image/RCTVirtualImageManager.m new file mode 100644 index 000000000..2800a92b0 --- /dev/null +++ b/Libraries/Image/RCTVirtualImageManager.m @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTVirtualImageManager.h" +#import "RCTShadowVirtualImage.h" + +@implementation RCTVirtualImageManager + +RCT_EXPORT_MODULE() + +- (RCTShadowView *)shadowView +{ + return [[RCTShadowVirtualImage alloc] initWithBridge:self.bridge]; +} + +RCT_EXPORT_SHADOW_PROPERTY(source, NSDictionary) + +@end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index b9c1ff2e7..357c3784c 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -13,6 +13,7 @@ #import "RCTUIManager.h" #import "RCTBridge.h" #import "RCTConvert.h" +#import "RCTImageComponent.h" #import "RCTLog.h" #import "RCTShadowRawText.h" #import "RCTSparseArray.h" @@ -191,8 +192,17 @@ static css_dim_t RCTMeasure(void *context, float width) } else if ([child isKindOfClass:[RCTShadowRawText class]]) { RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]]; + } else if ([child conformsToProtocol:@protocol(RCTImageComponent)]) { + UIImage *image = ((id)child).image; + if (image) { + NSTextAttachment *imageAttachment = [NSTextAttachment new]; + imageAttachment.image = image; + [attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]]; + } else { + //TODO: add placeholder image? + } } else { - RCTLogError(@" can't have any children except or raw strings"); + RCTLogError(@" can't have any children except , or raw strings"); } [child setTextComputed]; diff --git a/React/Modules/RCTAccessibilityManager.m b/React/Modules/RCTAccessibilityManager.m index 4ee7b811c..f7bfe8feb 100644 --- a/React/Modules/RCTAccessibilityManager.m +++ b/React/Modules/RCTAccessibilityManager.m @@ -26,7 +26,6 @@ NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification = @"RCTAc @synthesize bridge = _bridge; @synthesize multipliers = _multipliers; -@synthesize isVoiceOverEnabled = _isVoiceOverEnabled; RCT_EXPORT_MODULE() diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 49959ea17..be9efcb83 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -189,6 +189,7 @@ 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; + 13EF7F441BC69646003F47DD /* RCTImageComponent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageComponent.h; sourceTree = ""; }; 13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = ""; }; 13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = ""; }; 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPerfStats.h; sourceTree = ""; }; @@ -341,6 +342,7 @@ 13C325281AA63B6A0048765F /* RCTComponent.h */, 13AB90BF1B6FA36700713B4F /* RCTComponentData.h */, 13AB90C01B6FA36700713B4F /* RCTComponentData.m */, + 13EF7F441BC69646003F47DD /* RCTImageComponent.h */, 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */, diff --git a/React/Views/RCTImageComponent.h b/React/Views/RCTImageComponent.h new file mode 100644 index 000000000..a6916c9aa --- /dev/null +++ b/React/Views/RCTImageComponent.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/** + * Generic interface for components that contain an image. + */ +@protocol RCTImageComponent + +@property (nonatomic, strong, readonly) UIImage *image; + +@end