diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index c7e038c9c..8dea9bc79 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 1341802C1AA9178B003F314A /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1341802B1AA91779003F314A /* libRCTNetwork.a */; }; 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1344545A1AAFCAAE003F0779 /* libRCTAdSupport.a */; }; 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 134A8A251AACED6A00945AAE /* libRCTGeolocation.a */; }; + 1353F5461B0E64F9009B4FAC /* ClippingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1353F5451B0E64F9009B4FAC /* ClippingTests.m */; }; 139FDEDB1B0651FB00C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDED91B0651EA00C62182 /* libRCTWebSocket.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; @@ -129,6 +130,7 @@ 134180261AA91779003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAdSupport.xcodeproj; path = ../../Libraries/AdSupport/RCTAdSupport.xcodeproj; sourceTree = ""; }; 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../../Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; + 1353F5451B0E64F9009B4FAC /* ClippingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ClippingTests.m; sourceTree = ""; }; 139FDECA1B0651EA00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../../Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* UIExplorer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIExplorer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = UIExplorer/AppDelegate.h; sourceTree = ""; }; @@ -179,6 +181,7 @@ isa = PBXGroup; children = ( 004D28A21AAF61C70097A701 /* UIExplorerTests.m */, + 1353F5451B0E64F9009B4FAC /* ClippingTests.m */, 004D28A01AAF61C70097A701 /* Supporting Files */, ); path = UIExplorerTests; @@ -575,6 +578,7 @@ buildActionMask = 2147483647; files = ( 004D28A31AAF61C70097A701 /* UIExplorerTests.m in Sources */, + 1353F5461B0E64F9009B4FAC /* ClippingTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Examples/UIExplorer/UIExplorerTests/ClippingTests.m b/Examples/UIExplorer/UIExplorerTests/ClippingTests.m new file mode 100644 index 000000000..1f94a80c1 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerTests/ClippingTests.m @@ -0,0 +1,127 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import +#import +#import + +extern CGRect RCTClipRect(CGSize contentSize, CGFloat contentScale, + CGSize targetSize, CGFloat targetScale, + UIViewContentMode resizeMode); + +#define RCTAssertEqualPoints(a, b) { \ +XCTAssertEqual(a.x, b.x); \ +XCTAssertEqual(a.y, b.y); \ +} + +#define RCTAssertEqualSizes(a, b) { \ +XCTAssertEqual(a.width, b.width); \ +XCTAssertEqual(a.height, b.height); \ +} + +#define RCTAssertEqualRects(a, b) { \ +RCTAssertEqualPoints(a.origin, b.origin); \ +RCTAssertEqualSizes(a.size, b.size); \ +} + +@interface ClippingTests : XCTestCase + +@end + +@implementation ClippingTests + +- (void)testLandscapeSourceLandscapeTarget +{ + CGSize content = {1000, 100}; + CGSize target = {100, 20}; + + { + CGRect expected = {CGPointZero, {100, 20}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + RCTAssertEqualRects(expected, result); + } + + { + CGRect expected = {CGPointZero, {100, 10}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + RCTAssertEqualRects(expected, result); + } + + { + CGRect expected = {{-50, 0}, {200, 20}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + RCTAssertEqualRects(expected, result); + } +} + +- (void)testPortraitSourceLandscapeTarget +{ + CGSize content = {10, 100}; + CGSize target = {100, 20}; + + { + CGRect expected = {CGPointZero, {10, 20}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + RCTAssertEqualRects(expected, result); + } + + { + CGRect expected = {CGPointZero, {2, 20}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + RCTAssertEqualRects(expected, result); + } + + { + CGRect expected = {{0, -49}, {10, 100}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + RCTAssertEqualRects(expected, result); + } +} + +- (void)testPortraitSourcePortraitTarget +{ + CGSize content = {10, 100}; + CGSize target = {20, 50}; + + { + CGRect expected = {CGPointZero, {10, 50}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + RCTAssertEqualRects(expected, result); + } + + { + CGRect expected = {CGPointZero, {5, 50}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + RCTAssertEqualRects(expected, result); + } + + { + CGRect expected = {{0, -37.5}, {10, 100}}; + CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + RCTAssertEqualRects(expected, result); + } +} + +- (void)testScaling +{ + CGSize content = {2, 2}; + CGSize target = {3, 3}; + + CGRect expected = {CGPointZero, {3, 3}}; + CGRect result = RCTClipRect(content, 2, target, 1, UIViewContentModeScaleToFill); + RCTAssertEqualRects(expected, result); +} + +@end diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 569351745..b951d7582 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -1298,6 +1298,14 @@ var Navigator = React.createClass({ if (i !== this.state.presentedIndex) { disabledSceneStyle = styles.disabledScene; } + var originalRef = child.ref; + if (originalRef != null && typeof originalRef !== 'function') { + console.warn( + 'String refs are not supported for navigator scenes. Use a callback ' + + 'ref instead. Ignoring ref: ' + originalRef + ); + originalRef = null; + } return ( {React.cloneElement(child, { - ref: this._handleItemRef.bind(null, this.state.idStack[i], route), + ref: component => { + this._handleItemRef(this.state.idStack[i], route, component); + if (originalRef) { + originalRef(component); + } + } })} ); diff --git a/Libraries/Image/RCTImageDownloader.h b/Libraries/Image/RCTImageDownloader.h index 178bc734c..89a77975f 100644 --- a/Libraries/Image/RCTImageDownloader.h +++ b/Libraries/Image/RCTImageDownloader.h @@ -33,6 +33,8 @@ typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error); - (id)downloadImageForURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + backgroundColor:(UIColor *)backgroundColor block:(RCTImageDownloadBlock)block; /** diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index 9577ce4e5..aa524ef56 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -10,6 +10,7 @@ #import "RCTImageDownloader.h" #import "RCTCache.h" +#import "RCTLog.h" #import "RCTUtils.h" typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *error); @@ -121,34 +122,134 @@ static NSString *RCTCacheKeyForURL(NSURL *url) }]; } -- (id)downloadImageForURL:(NSURL *)url size:(CGSize)size - scale:(CGFloat)scale block:(RCTImageDownloadBlock)block +/** + * Returns the optimal context size for an image drawn using the clip rect + * returned by RCTClipRect. + */ +CGSize RCTTargetSizeForClipRect(CGRect); +CGSize RCTTargetSizeForClipRect(CGRect clipRect) +{ + return (CGSize){ + clipRect.size.width + clipRect.origin.x * 2, + clipRect.size.height + clipRect.origin.y * 2 + }; +} + +/** + * 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. + */ +CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode); +CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode) +{ + // Precompensate for scale + CGFloat scale = sourceScale / destScale; + sourceSize.width *= scale; + sourceSize.height *= scale; + + // Calculate aspect ratios if needed (don't bother is resizeMode == stretch) + CGFloat aspect = 0.0, targetAspect = 0.0; + if (resizeMode != UIViewContentModeScaleToFill) { + aspect = sourceSize.width / sourceSize.height; + targetAspect = destSize.width / destSize.height; + if (aspect == targetAspect) { + resizeMode = UIViewContentModeScaleToFill; + } + } + + switch (resizeMode) { + case UIViewContentModeScaleToFill: // stretch + + sourceSize.width = MIN(destSize.width, sourceSize.width); + sourceSize.height = MIN(destSize.height, sourceSize.height); + return (CGRect){CGPointZero, sourceSize}; + + case UIViewContentModeScaleAspectFit: // contain + + if (targetAspect <= aspect) { // target is taller than content + sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); + sourceSize.height = sourceSize.width / aspect; + } else { // target is wider than content + sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); + sourceSize.width = sourceSize.height * aspect; + } + return (CGRect){CGPointZero, sourceSize}; + + case UIViewContentModeScaleAspectFill: // cover + + if (targetAspect <= aspect) { // target is taller than content + + sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); + sourceSize.width = sourceSize.height * aspect; + destSize.width = destSize.height * targetAspect; + return (CGRect){{(destSize.width - sourceSize.width) / 2, 0}, sourceSize}; + + } else { // target is wider than content + + sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); + sourceSize.height = sourceSize.width / aspect; + destSize.height = destSize.width / targetAspect; + return (CGRect){{0, (destSize.height - sourceSize.height) / 2}, sourceSize}; + } + + default: + + RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode); + return (CGRect){CGPointZero, destSize}; + } +} + +- (id)downloadImageForURL:(NSURL *)url + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + backgroundColor:(UIColor *)backgroundColor + block:(RCTImageDownloadBlock)block { return [self downloadDataForURL:url block:^(NSData *data, NSError *error) { + if (!data || error) { + block(nil, error); + return; + } + + if (CGSizeEqualToSize(size, CGSizeZero)) { + // Target size wasn't available yet, so abort image drawing + block(nil, nil); + return; + } + UIImage *image = [UIImage imageWithData:data scale:scale]; if (image) { - // Resize (TODO: should we take aspect ratio into account?) - CGSize imageSize = size; - if (CGSizeEqualToSize(imageSize, CGSizeZero)) { - imageSize = image.size; - } else { - imageSize = (CGSize){ - MIN(size.width, image.size.width), - MIN(size.height, image.size.height) - }; - } + // Get scale and size + CGFloat destScale = scale ?: RCTScreenScale(); + CGRect imageRect = RCTClipRect(image.size, image.scale, size, destScale, resizeMode); + CGSize destSize = RCTTargetSizeForClipRect(imageRect); - // Rescale image if required size is smaller - CGFloat imageScale = scale; - if (imageScale == 0 || imageScale < image.scale) { - imageScale = image.scale; + // Opacity optimizations + UIColor *blendColor = nil; + BOOL opaque = !RCTImageHasAlpha(image.CGImage); + if (!opaque && backgroundColor) { + CGFloat alpha; + [backgroundColor getRed:NULL green:NULL blue:NULL alpha:&alpha]; + if (alpha > 0.999) { // no benefit to blending if background is translucent + opaque = YES; + blendColor = backgroundColor; + } } // Decompress image at required size - UIGraphicsBeginImageContextWithOptions(imageSize, NO, imageScale); - [image drawInRect:(CGRect){{0, 0}, imageSize}]; + UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale); + if (blendColor) { + [blendColor setFill]; + UIRectFill((CGRect){CGPointZero, destSize}); + } + [image drawInRect:imageRect]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); } diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 948503cce..7525b37d0 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -26,8 +26,6 @@ static dispatch_queue_t RCTImageLoaderQueue(void) static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ queue = dispatch_queue_create("com.facebook.rctImageLoader", DISPATCH_QUEUE_SERIAL); - dispatch_set_target_queue(queue, - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); }); return queue; diff --git a/Libraries/Image/RCTNetworkImageView.m b/Libraries/Image/RCTNetworkImageView.m index 6ad6a4c80..89cddef59 100644 --- a/Libraries/Image/RCTNetworkImageView.m +++ b/Libraries/Image/RCTNetworkImageView.m @@ -13,6 +13,7 @@ #import "RCTGIFImage.h" #import "RCTImageDownloader.h" #import "RCTUtils.h" +#import "UIView+React.h" @implementation RCTNetworkImageView { @@ -26,8 +27,7 @@ - (instancetype)initWithFrame:(CGRect)frame imageDownloader:(RCTImageDownloader *)imageDownloader { - self = [super initWithFrame:frame]; - if (self) { + if ((self = [super initWithFrame:frame])) { _deferSentinel = 0; _imageDownloader = imageDownloader; self.userInteractionEnabled = NO; @@ -37,20 +37,44 @@ - (NSURL *)imageURL { - // We clear our backing layer's imageURL when we are not in a window for a while, + // We clear our imageURL when we are not in a window for a while, // to make sure we don't consume network resources while offscreen. // However we don't want to expose this hackery externally. return _deferred ? _deferredImageURL : _imageURL; } +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + super.backgroundColor = backgroundColor; + [self _updateImage]; +} + +- (void)reactSetFrame:(CGRect)frame +{ + [super reactSetFrame:frame]; + [self _updateImage]; +} + +- (void)_updateImage +{ + [self setImageURL:_imageURL resetToDefaultImageWhileLoading:NO]; +} + - (void)setImageURL:(NSURL *)imageURL resetToDefaultImageWhileLoading:(BOOL)reset { + if (![_imageURL isEqual:imageURL] && _downloadToken) { + [_imageDownloader cancelDownload:_downloadToken]; + _downloadToken = nil; + } + + _imageURL = imageURL; + if (_deferred) { _deferredImageURL = imageURL; } else { - if (_downloadToken) { - [_imageDownloader cancelDownload:_downloadToken]; - _downloadToken = nil; + if (!imageURL) { + self.layer.contents = nil; + return; } if (reset) { self.layer.contentsScale = _defaultImage.scale; @@ -62,25 +86,35 @@ _downloadToken = [_imageDownloader downloadDataForURL:imageURL block:^(NSData *data, NSError *error) { if (data) { dispatch_async(dispatch_get_main_queue(), ^{ + if (imageURL != self.imageURL) { + // Image has changed + return; + } CAKeyframeAnimation *animation = RCTGIFImageWithData(data); self.layer.contentsScale = 1.0; self.layer.minificationFilter = kCAFilterLinear; self.layer.magnificationFilter = kCAFilterLinear; [self.layer addAnimation:animation forKey:@"contents"]; }); + } else if (error) { + RCTLogWarn(@"Unable to download image data. Error: %@", error); } - // TODO: handle errors }]; } else { - _downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() block:^(UIImage *image, NSError *error) { + _downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() resizeMode:self.contentMode backgroundColor:self.backgroundColor block:^(UIImage *image, NSError *error) { if (image) { dispatch_async(dispatch_get_main_queue(), ^{ + if (imageURL != self.imageURL) { + // Image has changed + return; + } [self.layer removeAnimationForKey:@"contents"]; self.layer.contentsScale = image.scale; self.layer.contents = (__bridge id)image.CGImage; }); + } else if (error) { + RCTLogWarn(@"Unable to download image. Error: %@", error); } - // TODO: handle errors }]; } } diff --git a/Libraries/Interaction/InteractionManager.js b/Libraries/Interaction/InteractionManager.js index 93c384d23..098dfcee9 100644 --- a/Libraries/Interaction/InteractionManager.js +++ b/Libraries/Interaction/InteractionManager.js @@ -19,6 +19,13 @@ var invariant = require('invariant'); var keyMirror = require('keyMirror'); var setImmediate = require('setImmediate'); +type Handle = number; + +/** + * Maximum time a handle can be open before warning in DEV. + */ +var DEV_TIMEOUT = 2000; + var _emitter = new EventEmitter(); var _interactionSet = new Set(); var _addInteractionSet = new Set(); @@ -83,17 +90,25 @@ var InteractionManager = { /** * Notify manager that an interaction has started. */ - createInteractionHandle(): number { + createInteractionHandle(): Handle { scheduleUpdate(); var handle = ++_inc; _addInteractionSet.add(handle); + if (__DEV__) { + // Capture the stack trace of what created the handle. + var error = new Error( + 'InteractionManager: interaction handle not cleared within ' + + DEV_TIMEOUT + ' ms.' + ); + setDevTimeoutHandle(handle, error, DEV_TIMEOUT); + } return handle; }, /** * Notify manager that an interaction has completed. */ - clearInteractionHandle(handle: number) { + clearInteractionHandle(handle: Handle) { invariant( !!handle, 'Must provide a handle to clear.' @@ -151,4 +166,19 @@ function processUpdate() { _deleteInteractionSet.clear(); } +/** + * Wait until `timeout` has passed and warn if the handle has not been cleared. + */ +function setDevTimeoutHandle( + handle: Handle, + error: Error, + timeout: number +): void { + setTimeout(() => { + if (_interactionSet.has(handle)) { + console.warn(error.message + '\n' + error.stack); + } + }, timeout); +} + module.exports = InteractionManager; diff --git a/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js b/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js index 09b25d32f..6434541bb 100644 --- a/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js +++ b/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js @@ -43,7 +43,6 @@ var JSTimers = { var newID = JSTimersExecution.GUID++; var freeIndex = JSTimers._getFreeIndex(); JSTimersExecution.timerIDs[freeIndex] = newID; - JSTimersExecution.callbacks[freeIndex] = func; JSTimersExecution.callbacks[freeIndex] = function() { return func.apply(undefined, args); }; @@ -60,12 +59,15 @@ var JSTimers = { var newID = JSTimersExecution.GUID++; var freeIndex = JSTimers._getFreeIndex(); JSTimersExecution.timerIDs[freeIndex] = newID; - JSTimersExecution.callbacks[freeIndex] = func; JSTimersExecution.callbacks[freeIndex] = function() { - return func.apply(undefined, args); + var startTime = Date.now(); + var ret = func.apply(undefined, args); + var endTime = Date.now(); + RCTTiming.createTimer(newID, Math.max(0, duration - (endTime - startTime)), endTime, false); + return ret; }; JSTimersExecution.types[freeIndex] = JSTimersExecution.Type.setInterval; - RCTTiming.createTimer(newID, duration, Date.now(), /** recurring */ true); + RCTTiming.createTimer(newID, duration, Date.now(), /** recurring */ false); return newID; }, @@ -77,7 +79,6 @@ var JSTimers = { var newID = JSTimersExecution.GUID++; var freeIndex = JSTimers._getFreeIndex(); JSTimersExecution.timerIDs[freeIndex] = newID; - JSTimersExecution.callbacks[freeIndex] = func; JSTimersExecution.callbacks[freeIndex] = function() { return func.apply(undefined, args); }; diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 5e1aa7f27..523333aac 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -22,6 +22,7 @@ #import "RCTJavaScriptLoader.h" #import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTPerfStats.h" #import "RCTProfile.h" #import "RCTRedBox.h" #import "RCTRootView.h" @@ -930,6 +931,11 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin _queuesByID = [[RCTSparseArray alloc] init]; _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + if (RCT_DEV) { + _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; + [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + /** * Initialize executor to allow enqueueing calls */ @@ -1560,6 +1566,8 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin } RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); + + [self.perfStats.jsGraph tick:displayLink.timestamp]; } - (void)_mainThreadUpdate:(CADisplayLink *)displayLink @@ -1567,6 +1575,8 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin RCTAssertMainThread(); RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); + + [self.perfStats.uiGraph tick:displayLink.timestamp]; } - (void)startProfiling @@ -1578,10 +1588,6 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin return; } - [_mainDisplayLink invalidate]; - _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; - [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - RCTProfileInit(); } @@ -1589,8 +1595,6 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin { RCTAssertMainThread(); - [_mainDisplayLink invalidate]; - NSString *log = RCTProfileEnd(); NSURL *bundleURL = _parentBridge.bundleURL; NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index bb80ac208..b260fca4a 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -35,6 +35,11 @@ */ @property (nonatomic, assign) BOOL liveReloadEnabled; +/** + * Shows the FPS monitor for the JS and Main threads + */ +@property (nonatomic, assign) BOOL showFPS; + /** * Manually show the dev menu (can be called from JS). */ diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 4e05f0e54..421380e5b 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -13,6 +13,7 @@ #import "RCTDefines.h" #import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTPerfStats.h" #import "RCTProfile.h" #import "RCTRootView.h" #import "RCTSourceCode.h" @@ -145,6 +146,7 @@ RCT_EXPORT_MODULE() self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; + self.showFPS = [_settings[@"showFPS"] ?: @NO boolValue]; self.executorClass = NSClassFromString(_settings[@"executorClass"]); } @@ -230,13 +232,14 @@ RCT_EXPORT_METHOD(show) NSString *debugTitleChrome = _executorClass && _executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Debug in Chrome"; NSString *debugTitleSafari = _executorClass && _executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Debug in Safari"; + NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor"; UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil - otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, nil]; + otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, fpsMonitor, nil]; if (_liveReloadURL) { @@ -293,10 +296,14 @@ RCT_EXPORT_METHOD(reload) break; } case 3: { - self.liveReloadEnabled = !_liveReloadEnabled; + self.showFPS = !_showFPS; break; } case 4: { + self.liveReloadEnabled = !_liveReloadEnabled; + break; + } + case 5: { self.profilingEnabled = !_profilingEnabled; break; } @@ -368,6 +375,21 @@ RCT_EXPORT_METHOD(reload) } } +- (void)setShowFPS:(BOOL)showFPS +{ + if (_showFPS != showFPS) { + _showFPS = showFPS; + + if (showFPS) { + [_bridge.perfStats show]; + } else { + [_bridge.perfStats hide]; + } + + [self updateSetting:@"showFPS" value:@(showFPS)]; + } +} + - (void)checkForUpdates { if (!_jsLoaded || !_liveReloadEnabled || !_liveReloadURL) { diff --git a/React/Base/RCTFPSGraph.h b/React/Base/RCTFPSGraph.h new file mode 100644 index 000000000..905829aba --- /dev/null +++ b/React/Base/RCTFPSGraph.h @@ -0,0 +1,23 @@ +/** + * 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 + +typedef NS_ENUM(NSUInteger, RCTFPSGraphPosition) { + RCTFPSGraphPositionLeft = 1, + RCTFPSGraphPositionRight = 2 +}; + +@interface RCTFPSGraph : UIView + +- (instancetype)initWithFrame:(CGRect)frame graphPosition:(RCTFPSGraphPosition)position name:(NSString *)name color:(UIColor *)color NS_DESIGNATED_INITIALIZER; + +- (void)tick:(NSTimeInterval)timestamp; + +@end diff --git a/React/Base/RCTFPSGraph.m b/React/Base/RCTFPSGraph.m new file mode 100644 index 000000000..461f17e41 --- /dev/null +++ b/React/Base/RCTFPSGraph.m @@ -0,0 +1,132 @@ +/** + * 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 "RCTFPSGraph.h" + +#import "RCTDefines.h" + +#if RCT_DEV + +@implementation RCTFPSGraph +{ + CAShapeLayer *_graph; + NSString *_name; + NSTimeInterval _prevTime; + RCTFPSGraphPosition _position; + UILabel *_label; + + float *_frames; + int _frameCount; + int _maxFPS; + int _minFPS; + int _length; + int _margin; + int _height; +} + +- (instancetype)initWithFrame:(CGRect)frame graphPosition:(RCTFPSGraphPosition)position name:(NSString *)name color:(UIColor *)color +{ + if (self = [super initWithFrame:frame]) { + _margin = 2; + _prevTime = -1; + _maxFPS = 0; + _minFPS = 60; + _length = (frame.size.width - 2 * _margin) / 2; + _height = frame.size.height - 2 * _margin; + _frames = malloc(sizeof(float) * _length); + memset(_frames, 0, sizeof(float) * _length); + + _name = name; + _position = position; + _graph = [self createGraph:color]; + _label = [self createLabel:color]; + + [self addSubview:_label]; + [self.layer addSublayer:_graph]; + } + return self; +} + +- (void)dealloc +{ + free(_frames); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; +} + +- (CAShapeLayer *)createGraph:(UIColor *)color +{ + CGFloat left = _position & RCTFPSGraphPositionLeft ? 0 : _length; + CAShapeLayer *graph = [[CAShapeLayer alloc] init]; + graph.frame = CGRectMake(left, 0, 2 * _margin + _length, self.frame.size.height); + graph.backgroundColor = [[color colorWithAlphaComponent:.2] CGColor]; + graph.fillColor = [color CGColor]; + return graph; +} + +- (UILabel *)createLabel:(UIColor *)color +{ + CGFloat left = _position & RCTFPSGraphPositionLeft ? 2 * _margin + _length : 0; + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(left, 0, _length, self.frame.size.height)]; + label.textColor = color; + label.font = [UIFont systemFontOfSize:9]; + label.minimumScaleFactor = .5; + label.adjustsFontSizeToFitWidth = YES; + label.numberOfLines = 3; + label.lineBreakMode = NSLineBreakByWordWrapping; + label.textAlignment = NSTextAlignmentCenter; + return label; +} + +- (void)tick:(NSTimeInterval)timestamp +{ + _frameCount++; + if (_prevTime == -1) { + _prevTime = timestamp; + } else if (timestamp - _prevTime > 1) { + float fps = round(_frameCount / (timestamp - _prevTime)); + _minFPS = MIN(_minFPS, fps); + _maxFPS = MAX(_maxFPS, fps); + + _label.text = [NSString stringWithFormat:@"%@\n%d FPS\n(%d - %d)", _name, (int)fps, _minFPS, _maxFPS]; + + float scale = 60.0 / _height; + for (int i = 0; i < _length - 1; i++) { + _frames[i] = _frames[i + 1]; + } + _frames[_length - 1] = fps / scale; + + CGMutablePathRef path = CGPathCreateMutable(); + if (_position & RCTFPSGraphPositionLeft) { + CGPathMoveToPoint(path, NULL, _margin, _margin + _height); + for (int i = 0; i < _length; i++) { + CGPathAddLineToPoint(path, NULL, _margin + i, _margin + _height - _frames[i]); + } + CGPathAddLineToPoint(path, NULL, _margin + _length - 1, _margin + _height); + } else { + CGPathMoveToPoint(path, NULL, _margin + _length - 1, _margin + _height); + for (int i = 0; i < _length; i++) { + CGPathAddLineToPoint(path, NULL, _margin + _length - i - 1, _margin + _height - _frames[i]); + } + CGPathAddLineToPoint(path, NULL, _margin, _margin + _height); + } + _graph.path = path; + CGPathRelease(path); + + _prevTime = timestamp; + _frameCount = 0; + } +} + +@end + +#endif diff --git a/React/Base/RCTPerfStats.h b/React/Base/RCTPerfStats.h new file mode 100644 index 000000000..18c13cad6 --- /dev/null +++ b/React/Base/RCTPerfStats.h @@ -0,0 +1,27 @@ +/** + * 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 "RCTBridge.h" +#import "RCTFPSGraph.h" + +@interface RCTPerfStats : NSObject + +@property (nonatomic, strong) RCTFPSGraph *jsGraph; +@property (nonatomic, strong) RCTFPSGraph *uiGraph; + +- (void)show; +- (void)hide; + +@end + +@interface RCTBridge (RCTPerfStats) + +@property (nonatomic, strong, readonly) RCTPerfStats *perfStats; + +@end diff --git a/React/Base/RCTPerfStats.m b/React/Base/RCTPerfStats.m new file mode 100644 index 000000000..2e5ec8d14 --- /dev/null +++ b/React/Base/RCTPerfStats.m @@ -0,0 +1,133 @@ +/** + * 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 "RCTPerfStats.h" + +#import "RCTDefines.h" + +#if RCT_DEV + +@interface RCTPerfStats() + +@end + +@implementation RCTPerfStats +{ + UIView *_container; +} + +RCT_EXPORT_MODULE() + +- (void)dealloc +{ + [self hide]; +} + +- (UIView *)container +{ + if (!_container) { + _container = [[UIView alloc] init]; + _container.backgroundColor = [UIColor colorWithRed:0 green:0 blue:34/255.0 alpha:1]; + _container.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; + } + return _container; +} + +- (RCTFPSGraph *)jsGraph +{ + if (!_jsGraph) { + UIColor *jsColor = [UIColor colorWithRed:0 green:1 blue:0 alpha:1]; + _jsGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(2, 2, 124, 34) + graphPosition:RCTFPSGraphPositionRight + name:@"[ JS ]" + color:jsColor]; + _jsGraph.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + } + return _jsGraph; +} + +- (RCTFPSGraph *)uiGraph +{ + if (!_uiGraph) { + UIColor *uiColor = [UIColor colorWithRed:0 green:1 blue:1 alpha:1]; + _uiGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(2, 2, 124, 34) + graphPosition:RCTFPSGraphPositionLeft + name:@"[ UI ]" + color:uiColor]; + } + return _uiGraph; +} + +- (void)show +{ + UIView *targetView = [[[[[UIApplication sharedApplication] delegate] window] rootViewController] view]; + + targetView.frame = (CGRect){ + targetView.frame.origin, + { + targetView.frame.size.width, + targetView.frame.size.height - 38, + } + }; + + self.container.frame = (CGRect){{0, targetView.frame.size.height}, {targetView.frame.size.width, 38}}; + self.jsGraph.frame = (CGRect){ + { + targetView.frame.size.width - self.uiGraph.frame.size.width - self.uiGraph.frame.origin.x, + self.uiGraph.frame.origin.x, + }, + self.uiGraph.frame.size, + }; + + [self.container addSubview:self.jsGraph]; + [self.container addSubview:self.uiGraph]; + [targetView addSubview:self.container]; +} + +- (void)hide +{ + UIView *targetView = _container.superview; + + targetView.frame = (CGRect){ + targetView.frame.origin, + { + targetView.frame.size.width, + targetView.frame.size.height + _container.frame.size.height + } + }; + + [_container removeFromSuperview]; +} + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +@end + +#else + +@implementation RCTPerfStats + +- (void)show {} +- (void)hide {} + +@end + +#endif + +@implementation RCTBridge (RCTPerfStats) + +- (RCTPerfStats *)perfStats +{ + return self.modules[RCTBridgeModuleNameForClass([RCTPerfStats class])]; +} + +@end diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 6c4b91464..5c34d0e0a 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -52,3 +52,6 @@ RCT_EXTERN NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, N // Returns YES if React is running in a test environment RCT_EXTERN BOOL RCTRunningInTestEnvironment(void); + +// Return YES if image has an alpha component +RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index ff11b5764..712e9724e 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -261,3 +261,15 @@ BOOL RCTRunningInTestEnvironment(void) }); return _isTestEnvironment; } + +BOOL RCTImageHasAlpha(CGImageRef image) +{ + switch (CGImageGetAlphaInfo(image)) { + case kCGImageAlphaNone: + case kCGImageAlphaNoneSkipLast: + case kCGImageAlphaNoneSkipFirst: + return NO; + default: + return YES; + } +} diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 42954d36e..cf05f4e39 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -44,9 +44,11 @@ 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; }; 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+React.m */; }; + 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */ = {isa = PBXBuildFile; fileRef = 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */; }; 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */; }; 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE21AAC4AE100FC20F4 /* RCTMap.m */; }; 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */; }; + 146459261B06C49500B389AA /* RCTFPSGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = 146459251B06C49500B389AA /* RCTFPSGraph.m */; }; 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F362081AABD06A001CE568 /* RCTSwitch.m */; }; 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */; }; 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */; }; @@ -163,6 +165,8 @@ 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 = ""; }; + 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPerfStats.h; sourceTree = ""; }; + 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerfStats.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptLoader.m; sourceTree = ""; }; 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTFrameUpdate.h; sourceTree = ""; }; @@ -170,6 +174,8 @@ 14435CE21AAC4AE100FC20F4 /* RCTMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMap.m; sourceTree = ""; }; 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapManager.h; sourceTree = ""; }; 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapManager.m; sourceTree = ""; }; + 146459241B06C49500B389AA /* RCTFPSGraph.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTFPSGraph.h; sourceTree = ""; }; + 146459251B06C49500B389AA /* RCTFPSGraph.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFPSGraph.m; sourceTree = ""; }; 14F362071AABD06A001CE568 /* RCTSwitch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSwitch.h; sourceTree = ""; }; 14F362081AABD06A001CE568 /* RCTSwitch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSwitch.m; sourceTree = ""; }; 14F362091AABD06A001CE568 /* RCTSwitchManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSwitchManager.h; sourceTree = ""; }; @@ -415,6 +421,10 @@ 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */, 14F4D3891AE1B7E40049C042 /* RCTProfile.h */, 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */, + 146459241B06C49500B389AA /* RCTFPSGraph.h */, + 146459251B06C49500B389AA /* RCTFPSGraph.m */, + 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */, + 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */, ); path = Base; sourceTree = ""; @@ -514,6 +524,7 @@ 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */, 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, + 146459261B06C49500B389AA /* RCTFPSGraph.m in Sources */, 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */, 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */, 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */, @@ -549,6 +560,7 @@ 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */, + 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */, 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */, 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */, 13B0801B1A69489C00A75B9A /* RCTNavigatorManager.m in Sources */, diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index d6394f2c6..61f8240e8 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -127,7 +127,6 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) _borderBottomRightRadius = -1; _backgroundColor = [super backgroundColor]; - [super setBackgroundColor:[UIColor clearColor]]; } return self; @@ -443,6 +442,8 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) - (UIImage *)generateBorderImage:(out CGRect *)contentsCenter { + static const CGFloat threshold = 0.001; + const CGFloat maxRadius = MIN(self.bounds.size.height, self.bounds.size.width); const CGFloat radius = MAX(0, _borderRadius); const CGFloat topLeftRadius = MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius); @@ -456,6 +457,17 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) const CGFloat bottomWidth = _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth; const CGFloat leftWidth = _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth; + if (topLeftRadius < threshold && + topRightRadius < threshold && + bottomLeftRadius < threshold && + bottomRightRadius < threshold && + topWidth < threshold && + rightWidth < threshold && + bottomWidth < threshold && + leftWidth < threshold) { + return nil; + } + const CGFloat innerTopLeftRadiusX = MAX(0, topLeftRadius - leftWidth); const CGFloat innerTopLeftRadiusY = MAX(0, topLeftRadius - topWidth); @@ -657,22 +669,21 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) - (void)displayLayer:(CALayer *)layer { - CGRect contentsCenter; + CGRect contentsCenter = (CGRect){CGPointZero, {1, 1}}; UIImage *image = [self generateBorderImage:&contentsCenter]; - if (RCTRunningInTestEnvironment()) { + if (image && RCTRunningInTestEnvironment()) { const CGSize size = self.bounds.size; UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); [image drawInRect:(CGRect){CGPointZero, size}]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - - contentsCenter = CGRectMake(0, 0, 1, 1); } + layer.backgroundColor = [image ? [UIColor clearColor] : _backgroundColor CGColor]; layer.contents = (id)image.CGImage; layer.contentsCenter = contentsCenter; - layer.contentsScale = image.scale; + layer.contentsScale = image.scale ?: 1.0; layer.magnificationFilter = kCAFilterNearest; } diff --git a/package.json b/package.json index 6c4fa493d..a2c7a38b4 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "stacktrace-parser": "git://github.com/frantic/stacktrace-parser.git#493c5e5638", "uglify-js": "~2.4.16", "underscore": "1.7.0", - "worker-farm": "^1.3.0", + "worker-farm": "^1.3.1", "ws": "0.4.31", "yargs": "1.3.2" }, diff --git a/packager/parseCommandLine.js b/packager/parseCommandLine.js index 65061e3f2..36b8ab119 100644 --- a/packager/parseCommandLine.js +++ b/packager/parseCommandLine.js @@ -24,12 +24,21 @@ function parseCommandLine(config) { // optimist default API requires you to write the command name three time // This is a small wrapper to accept an object instead for (var i = 0; i < config.length; ++i) { + if (config[i].type === 'string') { + optimist.string(config[i].command); + } else { + optimist.boolean(config[i].command); + } + optimist - .boolean(config[i].command) .default(config[i].command, config[i].default) .describe(config[i].command, config[i].description); + + if (config[i].required) { + optimist.demand(config[i].command); + } } - var argv = optimist.argv; + var argv = optimist.parse(process.argv); // optimist doesn't have support for --dev=false, instead it returns 'false' for (var i = 0; i < config.length; ++i) { @@ -43,6 +52,15 @@ function parseCommandLine(config) { if (argv[command] === 'false') { argv[command] = false; } + if (config[i].type === 'string') { + // According to https://github.com/substack/node-optimist#numbers, + // every argument that looks like a number should be converted to one. + var strValue = argv[command]; + var numValue = strValue ? Number(strValue) : undefined; + if (typeof numValue === 'number' && !isNaN(numValue)) { + argv[command] = numValue; + } + } } // Show --help diff --git a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js index 43f223453..c6acc6a84 100644 --- a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js +++ b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -47,6 +47,32 @@ describe('AssetServer', function() { }); }); + pit('should work for the simple case with jpg', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png', 'jpg'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b.png': 'png image', + 'b.jpg': 'jpeg image', + } + } + }); + + return Promise.all([ + server.get('imgs/b.jpg'), + server.get('imgs/b.png'), + ]).then(function(data) { + expect(data).toEqual([ + 'jpeg image', + 'png image', + ]); + }); + }); + pit('should pick the bigger one', function() { var server = new AssetServer({ projectRoots: ['/root'], @@ -136,5 +162,45 @@ describe('AssetServer', function() { }); }); }); + + pit('should get assetData for non-png images', function() { + var hash = { + update: jest.genMockFn(), + digest: jest.genMockFn(), + }; + + hash.digest.mockImpl(function() { + return 'wow such hash'; + }); + crypto.createHash.mockImpl(function() { + return hash; + }); + + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png', 'jpeg'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b@1x.jpg': 'b1 image', + 'b@2x.jpg': 'b2 image', + 'b@4x.jpg': 'b4 image', + 'b@4.5x.jpg': 'b4.5 image', + } + } + }); + + return server.getAssetData('imgs/b.jpg').then(function(data) { + expect(hash.update.mock.calls.length).toBe(4); + expect(data).toEqual({ + type: 'jpg', + name: 'b', + scales: [1, 2, 4, 4.5], + hash: 'wow such hash', + }); + }); + }); }); }); diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index 6f07dd01d..9bae26823 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -28,7 +28,7 @@ var validateOpts = declareOpts({ }, assetExts: { type: 'array', - default: ['png'], + required: true, }, }); @@ -90,7 +90,7 @@ AssetServer.prototype.getAssetData = function(assetPath) { var nameData = getAssetDataFromName(assetPath); var data = { name: nameData.name, - type: 'png', + type: nameData.type, }; return this._getAssetRecord(assetPath).then(function(record) { diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index f75445d09..c247e59d3 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -55,7 +55,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -91,7 +92,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -121,7 +123,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -161,6 +164,7 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], assetRoots_DEPRECATED: ['/root/imgs'], }); return dgraph.load().then(function() { @@ -199,6 +203,7 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -246,6 +251,7 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -308,6 +314,7 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], assetRoots_DEPRECATED: ['/root/imgs'], }); return dgraph.load().then(function() { @@ -358,7 +365,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -391,7 +399,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -421,7 +430,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -455,7 +465,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -489,7 +500,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -519,7 +531,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -552,7 +565,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -595,7 +609,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/somedir/somefile.js')) @@ -641,7 +656,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -674,7 +690,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -712,7 +729,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -755,7 +773,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -798,7 +817,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -847,7 +867,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -888,7 +909,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -931,7 +953,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -974,7 +997,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -1027,7 +1051,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -1092,7 +1117,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) @@ -1158,7 +1184,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { filesystem.root['index.js'] = @@ -1209,7 +1236,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { filesystem.root['index.js'] = @@ -1260,7 +1288,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { delete filesystem.root.foo; @@ -1310,7 +1339,8 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { filesystem.root['bar.js'] = [ @@ -1367,7 +1397,7 @@ describe('DependencyGraph', function() { roots: [root], assetRoots_DEPRECATED: [root], assetExts: ['png'], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, }); return dgraph.load().then(function() { @@ -1419,7 +1449,7 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], assetExts: ['png'], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, }); return dgraph.load().then(function() { @@ -1482,6 +1512,7 @@ describe('DependencyGraph', function() { var dgraph = new DependencyGraph({ roots: [root], fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], ignoreFilePath: function(filePath) { if (filePath === '/root/bar.js') { return true; @@ -1550,7 +1581,8 @@ describe('DependencyGraph', function() { }); var dgraph = new DependencyGraph({ roots: [root], - fileWatcher: fileWatcher + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], }); return dgraph.load().then(function() { triggerFileChange('change', 'aPackage', '/root', { diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index 08a4b513b..0881e5dc7 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -44,7 +44,7 @@ var validateOpts = declareOpts({ }, assetExts: { type: 'array', - default: ['png'], + required: true, } }); diff --git a/packager/react-packager/src/DependencyResolver/haste/index.js b/packager/react-packager/src/DependencyResolver/haste/index.js index 2583ac8fd..da68785ea 100644 --- a/packager/react-packager/src/DependencyResolver/haste/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/index.js @@ -54,6 +54,10 @@ var validateOpts = declareOpts({ type: 'object', required: true, }, + assetExts: { + type: 'array', + required: true, + } }); function HasteDependencyResolver(options) { @@ -62,6 +66,7 @@ function HasteDependencyResolver(options) { this._depGraph = new DependencyGraph({ roots: opts.projectRoots, assetRoots_DEPRECATED: opts.assetRoots, + assetExts: opts.assetExts, ignoreFilePath: function(filepath) { return filepath.indexOf('__tests__') !== -1 || (opts.blacklistRE && opts.blacklistRE.test(filepath)); diff --git a/packager/react-packager/src/Packager/Package.js b/packager/react-packager/src/Packager/Package.js index eca709d50..70f57c4ab 100644 --- a/packager/react-packager/src/Packager/Package.js +++ b/packager/react-packager/src/Packager/Package.js @@ -249,6 +249,15 @@ Package.prototype._getMappings = function() { return mappings; }; +Package.prototype.getJSModulePaths = function() { + return this._modules.filter(function(module) { + // Filter out non-js files. Like images etc. + return !module.virtual; + }).map(function(module) { + return module.sourcePath; + }); +}; + Package.prototype.getDebugInfo = function() { return [ '

Main Module:

' + this._mainModuleId + '
', diff --git a/packager/react-packager/src/Packager/__tests__/Package-test.js b/packager/react-packager/src/Packager/__tests__/Package-test.js index 0aaa3971c..1d8c3dd12 100644 --- a/packager/react-packager/src/Packager/__tests__/Package-test.js +++ b/packager/react-packager/src/Packager/__tests__/Package-test.js @@ -204,8 +204,28 @@ describe('Package', function() { expect(p.getAssets()).toEqual([asset1, asset2]); }); }); + + describe('getJSModulePaths()', function() { + it('should return module paths', function() { + var p = new Package('test_url'); + p.addModule(new ModuleTransport({ + code: 'transformed foo;\n', + sourceCode: 'source foo', + sourcePath: 'foo path' + })); + p.addModule(new ModuleTransport({ + code: 'image module;\nimage module;', + virtual: true, + sourceCode: 'image module;\nimage module;', + sourcePath: 'image.png', + })); + + expect(p.getJSModulePaths()).toEqual(['foo path']); + }); + }); }); + function genSourceMap(modules) { var sourceMapGen = new SourceMapGenerator({file: 'bundle.js', version: 3}); var packageLineNo = 0; diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index e5afefe76..a85281d2a 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -86,6 +86,7 @@ function Packager(options) { moduleFormat: opts.moduleFormat, assetRoots: opts.assetRoots, fileWatcher: opts.fileWatcher, + assetExts: opts.assetExts, }); this._transformer = new Transformer({ diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index d2c157177..d4da5ae10 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -60,7 +60,7 @@ var validateOpts = declareOpts({ }, assetExts: { type: 'array', - default: ['png'], + default: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp'], }, });