From 238fd4ad190e294068cab341d57ef8800fa70e55 Mon Sep 17 00:00:00 2001 From: "Andrew Y. Chen" Date: Fri, 17 Mar 2017 16:30:27 -0700 Subject: [PATCH 01/18] Move idle detection classes to its own directory Reviewed By: AaaChiuuu Differential Revision: D4676282 fbshipit-source-id: 1b07e66ba638d4293eec65cb4e336f21dfb78218 --- .../java/com/facebook/react/testing/BUCK | 6 +- .../ReactAppInstrumentationTestCase.java | 2 +- .../react/testing/ReactAppTestActivity.java | 2 + .../react/testing/ReactIdleDetectionUtil.java | 2 +- .../testing/ReactIntegrationTestCase.java | 2 + .../testing/SingleTouchGestureGenerator.java | 2 + .../facebook/react/testing/idledetection/BUCK | 14 ++ .../{ => idledetection}/IdleWaiter.java | 2 +- .../ReactBridgeIdleSignaler.java | 2 +- .../idledetection/ReactIdleDetectionUtil.java | 125 ++++++++++++++++++ .../java/com/facebook/react/tests/BUCK | 1 + 11 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/BUCK rename ReactAndroid/src/androidTest/java/com/facebook/react/testing/{ => idledetection}/IdleWaiter.java (90%) rename ReactAndroid/src/androidTest/java/com/facebook/react/testing/{ => idledetection}/ReactBridgeIdleSignaler.java (97%) create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/ReactIdleDetectionUtil.java diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK index 5cf6affe9a6fab..6814e3aa316138 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK @@ -2,7 +2,10 @@ include_defs("//ReactAndroid/DEFS") android_library( name = "testing", - srcs = glob(["**/*.java"]), + srcs = glob( + ["**/*.java"], + excludes = ["idledetection/**/*.java"], + ), visibility = [ "PUBLIC", ], @@ -25,5 +28,6 @@ android_library( react_native_target("java/com/facebook/react/modules/debug:interfaces"), react_native_target("java/com/facebook/react/shell:shell"), react_native_target("java/com/facebook/react/uimanager:uimanager"), + react_native_integration_tests_target("java/com/facebook/react/testing/idledetection:idledetection"), ], ) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactAppInstrumentationTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactAppInstrumentationTestCase.java index aaba651def949c..eeb8c02e06715d 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactAppInstrumentationTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactAppInstrumentationTestCase.java @@ -20,6 +20,7 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; +import com.facebook.react.testing.idledetection.IdleWaiter; /** * Base class for instrumentation tests that runs React based react application in UI mode @@ -123,7 +124,6 @@ public void run() { } }; - getActivity().runOnUiThread(getScreenshotRunnable); try { if (!latch.await(5000, TimeUnit.MILLISECONDS)) { diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactAppTestActivity.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactAppTestActivity.java index 022e7826c1fe83..11edda17a429b0 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactAppTestActivity.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactAppTestActivity.java @@ -28,6 +28,8 @@ import com.facebook.react.common.LifecycleState; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.shell.MainReactPackage; +import com.facebook.react.testing.idledetection.ReactBridgeIdleSignaler; +import com.facebook.react.testing.idledetection.ReactIdleDetectionUtil; import com.facebook.react.uimanager.UIImplementationProvider; public class ReactAppTestActivity extends FragmentActivity implements diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIdleDetectionUtil.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIdleDetectionUtil.java index 49e2219327b96f..af6ca2ebb82a55 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIdleDetectionUtil.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIdleDetectionUtil.java @@ -6,7 +6,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -package com.facebook.react.testing; +package com.facebook.react.testing.idledetection; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIntegrationTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIntegrationTestCase.java index 752c89ea618161..77f5492c36cafd 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIntegrationTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIntegrationTestCase.java @@ -32,6 +32,8 @@ import com.facebook.react.common.futures.SimpleSettableFuture; import com.facebook.react.devsupport.interfaces.DevSupportManager; import com.facebook.react.modules.core.Timing; +import com.facebook.react.testing.idledetection.ReactBridgeIdleSignaler; +import com.facebook.react.testing.idledetection.ReactIdleDetectionUtil; import com.facebook.soloader.SoLoader; import static org.mockito.Mockito.mock; diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/SingleTouchGestureGenerator.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/SingleTouchGestureGenerator.java index 278facbcb280b4..f0ae877a953d42 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/SingleTouchGestureGenerator.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/SingleTouchGestureGenerator.java @@ -13,6 +13,8 @@ import android.view.View; import android.view.ViewConfiguration; +import com.facebook.react.testing.idledetection.IdleWaiter; + /** * Provides methods for generating touch events and dispatching them directly to a given view. * Events scenarios are based on {@link android.test.TouchUtils} but they get gets dispatched diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/BUCK new file mode 100644 index 00000000000000..f6b33535737d3f --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/BUCK @@ -0,0 +1,14 @@ +include_defs("//ReactAndroid/DEFS") + +android_library( + name = "idledetection", + srcs = glob(["**/*.java"]), + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_dep("third-party/java/testing-support-lib:runner"), + react_native_target("java/com/facebook/react/bridge:bridge"), + react_native_target("java/com/facebook/react/modules/core:core"), + ], +) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/IdleWaiter.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/IdleWaiter.java similarity index 90% rename from ReactAndroid/src/androidTest/java/com/facebook/react/testing/IdleWaiter.java rename to ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/IdleWaiter.java index 98884c1033a375..1b94b7c1fc88db 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/IdleWaiter.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/IdleWaiter.java @@ -6,7 +6,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -package com.facebook.react.testing; +package com.facebook.react.testing.idledetection; /** * Interface for something that knows how to wait for bridge and UI idle. diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactBridgeIdleSignaler.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/ReactBridgeIdleSignaler.java similarity index 97% rename from ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactBridgeIdleSignaler.java rename to ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/ReactBridgeIdleSignaler.java index ffd941f9a228e6..4aaa451e43ab24 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactBridgeIdleSignaler.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/ReactBridgeIdleSignaler.java @@ -6,7 +6,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -package com.facebook.react.testing; +package com.facebook.react.testing.idledetection; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/ReactIdleDetectionUtil.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/ReactIdleDetectionUtil.java new file mode 100644 index 00000000000000..af6ca2ebb82a55 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/idledetection/ReactIdleDetectionUtil.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2014-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. + */ + +package com.facebook.react.testing.idledetection; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import android.app.Instrumentation; +import android.os.SystemClock; +import android.support.test.InstrumentationRegistry; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.modules.core.ChoreographerCompat; + +public class ReactIdleDetectionUtil { + + /** + * Waits for both the UI thread and bridge to be idle. It determines this by waiting for the + * bridge to become idle, then waiting for the UI thread to become idle, then checking if the + * bridge is idle again (if the bridge was idle before and is still idle after running the UI + * thread to idle, then there are no more events to process in either place). + *

+ * Also waits for any Choreographer callbacks to run after the initial sync since things like UI + * events are initiated from Choreographer callbacks. + */ + public static void waitForBridgeAndUIIdle( + ReactBridgeIdleSignaler idleSignaler, + final ReactContext reactContext, + long timeoutMs) { + UiThreadUtil.assertNotOnUiThread(); + + long startTime = SystemClock.uptimeMillis(); + waitInner(idleSignaler, timeoutMs); + + long timeToWait = Math.max(1, timeoutMs - (SystemClock.uptimeMillis() - startTime)); + waitForChoreographer(timeToWait); + waitForJSIdle(reactContext); + + timeToWait = Math.max(1, timeoutMs - (SystemClock.uptimeMillis() - startTime)); + waitInner(idleSignaler, timeToWait); + timeToWait = Math.max(1, timeoutMs - (SystemClock.uptimeMillis() - startTime)); + waitForChoreographer(timeToWait); + } + + private static void waitForChoreographer(long timeToWait) { + final int waitFrameCount = 2; + final CountDownLatch latch = new CountDownLatch(1); + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + ChoreographerCompat.getInstance().postFrameCallback( + new ChoreographerCompat.FrameCallback() { + + private int frameCount = 0; + + @Override + public void doFrame(long frameTimeNanos) { + frameCount++; + if (frameCount == waitFrameCount) { + latch.countDown(); + } else { + ChoreographerCompat.getInstance().postFrameCallback(this); + } + } + }); + } + }); + try { + if (!latch.await(timeToWait, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Timed out waiting for Choreographer"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void waitForJSIdle(ReactContext reactContext) { + if (!reactContext.hasActiveCatalystInstance()) { + return; + } + final CountDownLatch latch = new CountDownLatch(1); + + reactContext.runOnJSQueueThread( + new Runnable() { + @Override + public void run() { + latch.countDown(); + } + }); + + try { + if (!latch.await(5000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Timed out waiting for JS thread"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void waitInner(ReactBridgeIdleSignaler idleSignaler, long timeToWait) { + // TODO gets broken in gradle, do we need it? + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + long startTime = SystemClock.uptimeMillis(); + boolean bridgeWasIdle = false; + while (SystemClock.uptimeMillis() - startTime < timeToWait) { + boolean bridgeIsIdle = idleSignaler.isBridgeIdle(); + if (bridgeIsIdle && bridgeWasIdle) { + return; + } + bridgeWasIdle = bridgeIsIdle; + long newTimeToWait = Math.max(1, timeToWait - (SystemClock.uptimeMillis() - startTime)); + idleSignaler.waitForIdle(newTimeToWait); + instrumentation.waitForIdleSync(); + } + throw new RuntimeException("Timed out waiting for bridge and UI idle!"); + } +} diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK index 8188c87c6175c0..6e07e5fd24ae1f 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK @@ -5,6 +5,7 @@ deps = [ react_native_dep("third-party/java/jsr-305:jsr-305"), react_native_dep("third-party/java/junit:junit"), react_native_integration_tests_target("java/com/facebook/react/testing:testing"), + react_native_integration_tests_target("java/com/facebook/react/testing/idledetection:idledetection"), react_native_target("java/com/facebook/react:react"), react_native_target("java/com/facebook/react/bridge:bridge"), react_native_target("java/com/facebook/react/common:common"), From 95c192619372e6b3dfdc55aeafa8f44faf013ecf Mon Sep 17 00:00:00 2001 From: Ashwin Bharambe Date: Fri, 17 Mar 2017 16:47:51 -0700 Subject: [PATCH 02/18] Introduce `DeviceInfo` as a new native module Summary: The `UIManager` already has a lot of responsibilities and is deeply tied with React Native's view architecture. This diff separates out a `DeviceInfo` native module to provide information about screen dimensions and font scale, etc. Reviewed By: fkgozali Differential Revision: D4713834 fbshipit-source-id: f2ee93acf876a4221c29a8c731f5abeffbb97974 --- Libraries/Utilities/DeviceInfo.js | 20 +++ Libraries/Utilities/Dimensions.js | 4 +- .../react-native-implementation.js | 1 + React/Modules/RCTDeviceInfo.h | 17 +++ React/Modules/RCTDeviceInfo.m | 116 ++++++++++++++++++ React/Modules/RCTUIManager.m | 57 --------- React/React.xcodeproj/project.pbxproj | 12 ++ React/ReactCxx.xcodeproj/project.pbxproj | 12 ++ .../java/com/facebook/react/tests/BUCK | 1 + ...alystNativeJSToJavaParametersTestCase.java | 2 + ...talystNativeJavaToJSArgumentsTestCase.java | 2 + ...ystNativeJavaToJSReturnValuesTestCase.java | 2 + .../tests/CatalystUIManagerTestCase.java | 2 + .../facebook/react/tests/JSLocaleTest.java | 2 + .../react/tests/ProgressBarTestCase.java | 2 + .../react/tests/ViewRenderingTestCase.java | 2 + .../src/main/java/com/facebook/react/BUCK | 1 + .../facebook/react/CoreModulesPackage.java | 9 ++ .../com/facebook/react/ReactRootView.java | 4 +- .../facebook/react/modules/deviceinfo/BUCK | 17 +++ .../modules/deviceinfo/DeviceInfoModule.java | 105 ++++++++++++++++ .../react/uimanager/UIManagerModule.java | 26 +--- .../uimanager/UIManagerModuleConstants.java | 31 +---- .../UIManagerModuleConstantsHelper.java | 5 +- jest/setup.js | 18 +-- 25 files changed, 345 insertions(+), 125 deletions(-) create mode 100644 Libraries/Utilities/DeviceInfo.js create mode 100644 React/Modules/RCTDeviceInfo.h create mode 100644 React/Modules/RCTDeviceInfo.m create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java diff --git a/Libraries/Utilities/DeviceInfo.js b/Libraries/Utilities/DeviceInfo.js new file mode 100644 index 00000000000000..d398f808312459 --- /dev/null +++ b/Libraries/Utilities/DeviceInfo.js @@ -0,0 +1,20 @@ +/** + * 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. + * + * @providesModule DeviceInfo + * @flow + */ +'use strict'; + +const DeviceInfo = require('NativeModules').DeviceInfo; + +const invariant = require('invariant'); + +invariant(DeviceInfo, 'DeviceInfo native module is not installed correctly'); + +module.exports = DeviceInfo; diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js index 2204ba4fcc331a..76b4098b8589cd 100644 --- a/Libraries/Utilities/Dimensions.js +++ b/Libraries/Utilities/Dimensions.js @@ -11,9 +11,9 @@ */ 'use strict'; +var DeviceInfo = require('DeviceInfo'); var EventEmitter = require('EventEmitter'); var Platform = require('Platform'); -var UIManager = require('UIManager'); var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var invariant = require('fbjs/lib/invariant'); @@ -128,7 +128,7 @@ class Dimensions { } } -Dimensions.set(UIManager.Dimensions); +Dimensions.set(DeviceInfo.Dimensions); RCTDeviceEventEmitter.addListener('didUpdateDimensions', function(update) { Dimensions.set(update); }); diff --git a/Libraries/react-native/react-native-implementation.js b/Libraries/react-native/react-native-implementation.js index d11a202215d013..3eff5dedd60ebe 100644 --- a/Libraries/react-native/react-native-implementation.js +++ b/Libraries/react-native/react-native-implementation.js @@ -85,6 +85,7 @@ const ReactNative = { get CameraRoll() { return require('CameraRoll'); }, get Clipboard() { return require('Clipboard'); }, get DatePickerAndroid() { return require('DatePickerAndroid'); }, + get DeviceInfo() { return require('DeviceInfo'); }, get Dimensions() { return require('Dimensions'); }, get Easing() { return require('Easing'); }, get I18nManager() { return require('I18nManager'); }, diff --git a/React/Modules/RCTDeviceInfo.h b/React/Modules/RCTDeviceInfo.h new file mode 100644 index 00000000000000..2ff2ad965d5a08 --- /dev/null +++ b/React/Modules/RCTDeviceInfo.h @@ -0,0 +1,17 @@ +/** + * 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 + +#import +#import + +@interface RCTDeviceInfo : NSObject + +@end diff --git a/React/Modules/RCTDeviceInfo.m b/React/Modules/RCTDeviceInfo.m new file mode 100644 index 00000000000000..ef4b71a79ee411 --- /dev/null +++ b/React/Modules/RCTDeviceInfo.m @@ -0,0 +1,116 @@ +/** + * 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 "RCTDeviceInfo.h" + +#import "RCTAccessibilityManager.h" +#import "RCTAssert.h" +#import "RCTEventDispatcher.h" +#import "RCTUtils.h" + +@implementation RCTDeviceInfo { +#if !TARGET_OS_TV + UIInterfaceOrientation _currentInterfaceOrientation; +#endif +} + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveNewContentSizeMultiplier) + name:RCTAccessibilityManagerDidUpdateMultiplierNotification + object:_bridge.accessibilityManager]; +#if !TARGET_OS_TV + _currentInterfaceOrientation = [RCTSharedApplication() statusBarOrientation]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(interfaceOrientationDidChange) + name:UIApplicationDidChangeStatusBarOrientationNotification + object:nil]; +#endif +} + +static NSDictionary *RCTExportedDimensions(RCTBridge *bridge) +{ + RCTAssertMainQueue(); + + // Don't use RCTScreenSize since it the interface orientation doesn't apply to it + CGRect screenSize = [[UIScreen mainScreen] bounds]; + NSDictionary *dims = @{ + @"width": @(screenSize.size.width), + @"height": @(screenSize.size.height), + @"scale": @(RCTScreenScale()), + @"fontScale": @(bridge.accessibilityManager.multiplier) + }; + return @{ + @"window": dims, + @"screen": dims + }; +} + +- (void)invalidate +{ + dispatch_async(dispatch_get_main_queue(), ^{ + self->_bridge = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + }); +} + +- (NSDictionary *)constantsToExport +{ + NSMutableDictionary *constants = [NSMutableDictionary new]; + constants[@"Dimensions"] = RCTExportedDimensions(_bridge); + return constants; +} + +- (void)didReceiveNewContentSizeMultiplier +{ + // Report the event across the bridge. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateDimensions" + body:RCTExportedDimensions(_bridge)]; +#pragma clang diagnostic pop +} + + +- (void)interfaceOrientationDidChange +{ +#if !TARGET_OS_TV + UIInterfaceOrientation nextOrientation = [RCTSharedApplication() statusBarOrientation]; + + // Update when we go from portrait to landscape, or landscape to portrait + if ((UIInterfaceOrientationIsPortrait(_currentInterfaceOrientation) && + !UIInterfaceOrientationIsPortrait(nextOrientation)) || + (UIInterfaceOrientationIsLandscape(_currentInterfaceOrientation) && + !UIInterfaceOrientationIsLandscape(nextOrientation))) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateDimensions" + body:RCTExportedDimensions(_bridge)]; +#pragma clang diagnostic pop + } + + _currentInterfaceOrientation = nextOrientation; +#endif +} + + +@end diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index b672cafbe1046b..6a1576236f01d9 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -225,9 +225,6 @@ @implementation RCTUIManager NSDictionary *_componentDataByName; NSMutableSet> *_bridgeTransactionListeners; -#if !TARGET_OS_TV - UIInterfaceOrientation _currentInterfaceOrientation; -#endif } @synthesize bridge = _bridge; @@ -239,8 +236,6 @@ - (void)didReceiveNewContentSizeMultiplier // Report the event across the bridge. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateDimensions" - body:RCTExportedDimensions(_bridge)]; [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateContentSizeMultiplier" body:@([_bridge.accessibilityManager multiplier])]; #pragma clang diagnostic pop @@ -252,28 +247,6 @@ - (void)didReceiveNewContentSizeMultiplier }); } -- (void)interfaceOrientationDidChange -{ -#if !TARGET_OS_TV - UIInterfaceOrientation nextOrientation = - [RCTSharedApplication() statusBarOrientation]; - - // Update when we go from portrait to landscape, or landscape to portrait - if ((UIInterfaceOrientationIsPortrait(_currentInterfaceOrientation) && - !UIInterfaceOrientationIsPortrait(nextOrientation)) || - (UIInterfaceOrientationIsLandscape(_currentInterfaceOrientation) && - !UIInterfaceOrientationIsLandscape(nextOrientation))) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateDimensions" - body:RCTExportedDimensions(_bridge)]; -#pragma clang diagnostic pop - } - - _currentInterfaceOrientation = nextOrientation; -#endif -} - - (void)invalidate { /** @@ -351,13 +324,6 @@ - (void)setBridge:(RCTBridge *)bridge selector:@selector(didReceiveNewContentSizeMultiplier) name:RCTAccessibilityManagerDidUpdateMultiplierNotification object:_bridge.accessibilityManager]; -#if !TARGET_OS_TV - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(interfaceOrientationDidChange) - name:UIApplicationDidChangeStatusBarOrientationNotification - object:nil]; -#endif - [RCTAnimation initializeStatics]; } @@ -1544,35 +1510,12 @@ static void RCTMeasureLayout(RCTShadowView *view, constants[name] = moduleConstants; }]; -#if !TARGET_OS_TV - _currentInterfaceOrientation = [RCTSharedApplication() statusBarOrientation]; -#endif - constants[@"customBubblingEventTypes"] = bubblingEvents; constants[@"customDirectEventTypes"] = directEvents; - constants[@"Dimensions"] = RCTExportedDimensions(_bridge); return constants; } -static NSDictionary *RCTExportedDimensions(RCTBridge *bridge) -{ - RCTAssertMainQueue(); - - // Don't use RCTScreenSize since it the interface orientation doesn't apply to it - CGRect screenSize = [[UIScreen mainScreen] bounds]; - NSDictionary *dims = @{ - @"width": @(screenSize.size.width), - @"height": @(screenSize.size.height), - @"scale": @(RCTScreenScale()), - @"fontScale": @(bridge.accessibilityManager.multiplier) - }; - return @{ - @"window": dims, - @"screen": dims - }; -} - RCT_EXPORT_METHOD(configureNextLayoutAnimation:(NSDictionary *)config withCallback:(RCTResponseSenderBlock)callback errorCallback:(__unused RCTResponseSenderBlock)errorCallback) diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 1e2d1be93f3a55..a79946fbfa4b40 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -738,6 +738,10 @@ B50558421E43E14000F71A00 /* RCTDevSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = B505583D1E43DFB900F71A00 /* RCTDevSettings.mm */; }; B50558431E43E64600F71A00 /* RCTDevSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = B505583C1E43DFB900F71A00 /* RCTDevSettings.h */; }; B95154321D1B34B200FE7B80 /* RCTActivityIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = B95154311D1B34B200FE7B80 /* RCTActivityIndicatorView.m */; }; + CF85BC321E79EC6B00F1EF3B /* RCTDeviceInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = CF85BC301E79EC6B00F1EF3B /* RCTDeviceInfo.h */; }; + CF85BC331E79EC6B00F1EF3B /* RCTDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = CF85BC311E79EC6B00F1EF3B /* RCTDeviceInfo.m */; }; + CF85BC341E79EC7A00F1EF3B /* RCTDeviceInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = CF85BC301E79EC6B00F1EF3B /* RCTDeviceInfo.h */; }; + CF85BC351E79EC7D00F1EF3B /* RCTDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = CF85BC311E79EC6B00F1EF3B /* RCTDeviceInfo.m */; }; E9B20B7B1B500126007A2DA7 /* RCTAccessibilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */; }; /* End PBXBuildFile section */ @@ -1381,6 +1385,8 @@ B505583D1E43DFB900F71A00 /* RCTDevSettings.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTDevSettings.mm; sourceTree = ""; }; B95154301D1B34B200FE7B80 /* RCTActivityIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorView.h; sourceTree = ""; }; B95154311D1B34B200FE7B80 /* RCTActivityIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorView.m; sourceTree = ""; }; + CF85BC301E79EC6B00F1EF3B /* RCTDeviceInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDeviceInfo.h; sourceTree = ""; }; + CF85BC311E79EC6B00F1EF3B /* RCTDeviceInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDeviceInfo.m; sourceTree = ""; }; E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = ""; }; E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAccessibilityManager.h; sourceTree = ""; }; E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAccessibilityManager.m; sourceTree = ""; }; @@ -1456,6 +1462,8 @@ B505583B1E43DFB900F71A00 /* RCTDevMenu.m */, B505583C1E43DFB900F71A00 /* RCTDevSettings.h */, B505583D1E43DFB900F71A00 /* RCTDevSettings.mm */, + CF85BC301E79EC6B00F1EF3B /* RCTDeviceInfo.h */, + CF85BC311E79EC6B00F1EF3B /* RCTDeviceInfo.m */, 13D9FEE91CDCCECF00158BD7 /* RCTEventEmitter.h */, 13D9FEEA1CDCCECF00158BD7 /* RCTEventEmitter.m */, 13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */, @@ -1869,6 +1877,7 @@ 3D302F3E1DF828F800D6DDAE /* RCTKeyCommands.h in Headers */, 3D302F3F1DF828F800D6DDAE /* RCTLog.h in Headers */, 3D302F401DF828F800D6DDAE /* RCTModuleData.h in Headers */, + CF85BC341E79EC7A00F1EF3B /* RCTDeviceInfo.h in Headers */, 3D302F411DF828F800D6DDAE /* RCTModuleMethod.h in Headers */, 3D302F421DF828F800D6DDAE /* RCTMultipartDataTask.h in Headers */, 3D302F431DF828F800D6DDAE /* RCTMultipartStreamReader.h in Headers */, @@ -2055,6 +2064,7 @@ A12E9E2A1E5DEB860029001B /* RCTReconnectingWebSocket.h in Headers */, 3D80DA311DF820620028D040 /* RCTJavaScriptLoader.h in Headers */, 3D80DA321DF820620028D040 /* RCTJSStackFrame.h in Headers */, + CF85BC321E79EC6B00F1EF3B /* RCTDeviceInfo.h in Headers */, 3D80DA331DF820620028D040 /* RCTKeyCommands.h in Headers */, 3D80DA341DF820620028D040 /* RCTLog.h in Headers */, 3D80DA351DF820620028D040 /* RCTModuleData.h in Headers */, @@ -2465,6 +2475,7 @@ 2D3B5EC21D9B093B00451313 /* RCTProfile.m in Sources */, 2D3B5ECB1D9B096200451313 /* RCTConvert+CoreLocation.m in Sources */, A12E9E261E5DEB510029001B /* RCTPackagerClientResponder.m in Sources */, + CF85BC351E79EC7D00F1EF3B /* RCTDeviceInfo.m in Sources */, 2D3B5EEE1D9B09DA00451313 /* RCTView.m in Sources */, 594AD5D01E46D87500B07237 /* RCTScrollContentShadowView.m in Sources */, 2D3B5E981D9B089500451313 /* RCTConvert.m in Sources */, @@ -2675,6 +2686,7 @@ 945929C41DD62ADD00653A7D /* RCTConvert+Transform.m in Sources */, 13AB90C11B6FA36700713B4F /* RCTComponentData.m in Sources */, 13B0801B1A69489C00A75B9A /* RCTNavigatorManager.m in Sources */, + CF85BC331E79EC6B00F1EF3B /* RCTDeviceInfo.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/React/ReactCxx.xcodeproj/project.pbxproj b/React/ReactCxx.xcodeproj/project.pbxproj index 1d9ac80584e181..cef7f0d506bb2b 100644 --- a/React/ReactCxx.xcodeproj/project.pbxproj +++ b/React/ReactCxx.xcodeproj/project.pbxproj @@ -980,6 +980,10 @@ AC70D2E91DE489E4002E6351 /* RCTJavaScriptLoader.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC70D2E81DE489E4002E6351 /* RCTJavaScriptLoader.mm */; }; B233E6EA1D2D845D00BC68BA /* RCTI18nManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B233E6E91D2D845D00BC68BA /* RCTI18nManager.m */; }; B95154321D1B34B200FE7B80 /* RCTActivityIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = B95154311D1B34B200FE7B80 /* RCTActivityIndicatorView.m */; }; + CF2731C01E7B8DE40044CA4F /* RCTDeviceInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = CF2731BE1E7B8DE40044CA4F /* RCTDeviceInfo.h */; }; + CF2731C11E7B8DE40044CA4F /* RCTDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = CF2731BF1E7B8DE40044CA4F /* RCTDeviceInfo.m */; }; + CF2731C21E7B8DEF0044CA4F /* RCTDeviceInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = CF2731BE1E7B8DE40044CA4F /* RCTDeviceInfo.h */; }; + CF2731C31E7B8DF30044CA4F /* RCTDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = CF2731BF1E7B8DE40044CA4F /* RCTDeviceInfo.m */; }; E9B20B7B1B500126007A2DA7 /* RCTAccessibilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */; }; /* End PBXBuildFile section */ @@ -1828,6 +1832,8 @@ B233E6E91D2D845D00BC68BA /* RCTI18nManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTI18nManager.m; sourceTree = ""; }; B95154301D1B34B200FE7B80 /* RCTActivityIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorView.h; sourceTree = ""; }; B95154311D1B34B200FE7B80 /* RCTActivityIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorView.m; sourceTree = ""; }; + CF2731BE1E7B8DE40044CA4F /* RCTDeviceInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDeviceInfo.h; sourceTree = ""; }; + CF2731BF1E7B8DE40044CA4F /* RCTDeviceInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDeviceInfo.m; sourceTree = ""; }; E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = ""; }; E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAccessibilityManager.h; sourceTree = ""; }; E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAccessibilityManager.m; sourceTree = ""; }; @@ -2034,6 +2040,8 @@ 13B07FE01A69315300A75B9A /* Modules */ = { isa = PBXGroup; children = ( + CF2731BE1E7B8DE40044CA4F /* RCTDeviceInfo.h */, + CF2731BF1E7B8DE40044CA4F /* RCTDeviceInfo.m */, 130E3D861E6A082100ACE484 /* RCTDevSettings.h */, 130E3D871E6A082100ACE484 /* RCTDevSettings.mm */, 369123DF1DDC75850095B341 /* JSCSamplingProfiler.h */, @@ -2595,6 +2603,7 @@ 3D302F881DF828F800D6DDAE /* RCTRootShadowView.h in Headers */, 3D302F891DF828F800D6DDAE /* RCTScrollableProtocol.h in Headers */, 3D302F8A1DF828F800D6DDAE /* RCTScrollView.h in Headers */, + CF2731C21E7B8DEF0044CA4F /* RCTDeviceInfo.h in Headers */, 3D302F8B1DF828F800D6DDAE /* RCTScrollViewManager.h in Headers */, 3D302F8C1DF828F800D6DDAE /* RCTSegmentedControl.h in Headers */, 3D302F8D1DF828F800D6DDAE /* RCTSegmentedControlManager.h in Headers */, @@ -2837,6 +2846,7 @@ 3D80DA5E1DF820620028D040 /* RCTProfile.h in Headers */, 3D80DA5F1DF820620028D040 /* RCTActivityIndicatorView.h in Headers */, 3D80DA601DF820620028D040 /* RCTActivityIndicatorViewManager.h in Headers */, + CF2731C01E7B8DE40044CA4F /* RCTDeviceInfo.h in Headers */, 3D80DA611DF820620028D040 /* RCTAnimationType.h in Headers */, 3D80DA621DF820620028D040 /* RCTAutoInsetsProtocol.h in Headers */, 3D80DA631DF820620028D040 /* RCTBorderDrawing.h in Headers */, @@ -3314,6 +3324,7 @@ 2D3B5EA51D9B08C700451313 /* RCTRootView.m in Sources */, 2D3B5EAC1D9B08EF00451313 /* RCTJSCExecutor.mm in Sources */, 13134C871E296B2A00B9F3CB /* RCTCxxBridge.mm in Sources */, + CF2731C31E7B8DF30044CA4F /* RCTDeviceInfo.m in Sources */, 2D3B5EB11D9B090100451313 /* RCTAppState.m in Sources */, 2D3B5EC21D9B093B00451313 /* RCTProfile.m in Sources */, 13134C931E296B2A00B9F3CB /* RCTNativeModule.mm in Sources */, @@ -3524,6 +3535,7 @@ 14C2CA781B3ACB0400E6CBB2 /* RCTBatchedBridge.m in Sources */, 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */, 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */, + CF2731C11E7B8DE40044CA4F /* RCTDeviceInfo.m in Sources */, 3D7AA9C41E548CD5001955CF /* NSDataBigString.mm in Sources */, 13D033631C1837FE0021DC29 /* RCTClipboard.m in Sources */, 14C2CA741B3AC64300E6CBB2 /* RCTModuleData.mm in Sources */, diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK index 6e07e5fd24ae1f..992351f62026aa 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK @@ -15,6 +15,7 @@ deps = [ react_native_target("java/com/facebook/react/modules/datepicker:datepicker"), react_native_target("java/com/facebook/react/modules/share:share"), react_native_target("java/com/facebook/react/modules/systeminfo:systeminfo"), + react_native_target("java/com/facebook/react/modules/deviceinfo:deviceinfo"), react_native_target("java/com/facebook/react/modules/timepicker:timepicker"), react_native_target("java/com/facebook/react/touch:touch"), react_native_target("java/com/facebook/react/uimanager:uimanager"), diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java index ec2dcd5a82d876..7c2da8c8803fe1 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java @@ -31,6 +31,7 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.appstate.AppStateModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.modules.systeminfo.AndroidInfoModule; import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; @@ -101,6 +102,7 @@ public void run() { mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mRecordingTestModule) .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) .addNativeModule(mUIManager) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java index 8d21e7f4291ed0..60b951ca5dcd90 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java @@ -20,6 +20,7 @@ import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.appstate.AppStateModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.testing.AssertModule; import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; @@ -78,6 +79,7 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mAssertModule) + .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) .addJSModule(TestJavaToJSArgumentsModule.class) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSReturnValuesTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSReturnValuesTestCase.java index 236a51a0f3fc37..8dcc2d0d69fb17 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSReturnValuesTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSReturnValuesTestCase.java @@ -19,6 +19,7 @@ import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.appstate.AppStateModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.testing.AssertModule; import com.facebook.react.testing.FakeWebSocketModule; @@ -119,6 +120,7 @@ protected void setUp() throws Exception { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mAssertModule) + .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) .addJSModule(TestJavaToJSReturnValuesModule.class) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java index 6f09e69af41c88..3bcecc6eca748b 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystUIManagerTestCase.java @@ -22,6 +22,7 @@ import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.modules.appstate.AppStateModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.modules.systeminfo.AndroidInfoModule; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.UIImplementation; @@ -96,6 +97,7 @@ public void run() { jsModule = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(uiManager) .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) .addJSModule(UIManagerTestModule.class) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java index 67ccb8997d35da..4c524e020de001 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java @@ -19,6 +19,7 @@ import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.modules.appstate.AppStateModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.uimanager.UIImplementationProvider; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.ViewManager; @@ -62,6 +63,7 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mStringRecordingModule) .addNativeModule(mUIManager) + .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) .addJSModule(TestJSLocaleModule.class) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java index 9865ce343e0db5..40983432aedfd3 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java @@ -25,6 +25,7 @@ import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.modules.appstate.AppStateModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.modules.systeminfo.AndroidInfoModule; import com.facebook.react.uimanager.UIImplementation; import com.facebook.react.uimanager.UIImplementationProvider; @@ -87,6 +88,7 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mUIManager) .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) .addJSModule(ProgressBarTestModule.class) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java index 4c84c71816cc6e..956269d9683771 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java @@ -20,6 +20,7 @@ import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.modules.appstate.AppStateModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.modules.systeminfo.AndroidInfoModule; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.UIImplementation; @@ -68,6 +69,7 @@ public void run() { mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(uiManager) .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new DeviceInfoModule(getContext())) .addNativeModule(new AppStateModule(getContext())) .addNativeModule(new FakeWebSocketModule()) .addJSModule(ViewRenderingTestModule.class) diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK index 7d1d9342225aba..2982e603c15b76 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -19,6 +19,7 @@ DEPS = [ react_native_target("java/com/facebook/react/modules/core:core"), react_native_target("java/com/facebook/react/modules/debug:debug"), react_native_target("java/com/facebook/react/modules/debug:interfaces"), + react_native_target("java/com/facebook/react/modules/deviceinfo:deviceinfo"), react_native_target("java/com/facebook/react/modules/systeminfo:systeminfo"), react_native_target("java/com/facebook/react/modules/toast:toast"), react_native_target("java/com/facebook/react/uimanager:uimanager"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 2827c47203bb8c..88e53489065cda 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -36,6 +36,7 @@ import com.facebook.react.modules.core.Timing; import com.facebook.react.modules.debug.AnimationsDebugModule; import com.facebook.react.modules.debug.SourceCodeModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.modules.systeminfo.AndroidInfoModule; import com.facebook.react.modules.appregistry.AppRegistry; import com.facebook.react.uimanager.UIImplementationProvider; @@ -65,6 +66,7 @@ SourceCodeModule.class, Timing.class, UIManagerModule.class, + DeviceInfoModule.class, // Debug only DebugComponentOwnershipModule.class, JSCHeapCapture.class, @@ -151,6 +153,13 @@ public NativeModule get() { return createUIManager(reactContext); } })); + moduleSpecList.add( + new ModuleSpec(DeviceInfoModule.class, new Provider() { + @Override + public NativeModule get() { + return new DeviceInfoModule(reactContext); + } + })); if (ReactBuildConfig.DEBUG) { moduleSpecList.add( diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 46ad7c6c47f658..8babff13d1d269 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -15,7 +15,6 @@ import android.graphics.Rect; import android.os.Bundle; import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.Surface; import android.view.View; @@ -32,6 +31,7 @@ import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.JSTouchDispatcher; import com.facebook.react.uimanager.PixelUtil; @@ -396,7 +396,7 @@ private void emitOrientationChanged(final int newRotation) { private void emitUpdateDimensionsEvent() { mReactInstanceManager .getCurrentReactContext() - .getNativeModule(UIManagerModule.class) + .getNativeModule(DeviceInfoModule.class) .emitUpdateDimensionsEvent(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/BUCK new file mode 100644 index 00000000000000..14f601585c2cb7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/BUCK @@ -0,0 +1,17 @@ +include_defs("//ReactAndroid/DEFS") + +android_library( + name = "deviceinfo", + srcs = glob(["**/*.java"]), + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_dep("third-party/java/jsr-305:jsr-305"), + react_native_target("java/com/facebook/react/bridge:bridge"), + react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/module/annotations:annotations"), + react_native_target("java/com/facebook/react/modules/core:core"), + react_native_target("java/com/facebook/react/uimanager:uimanager"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java new file mode 100644 index 00000000000000..c7b5324145225b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java @@ -0,0 +1,105 @@ +/** + * 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. + */ + +package com.facebook.react.modules.deviceinfo; + +import javax.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import android.util.DisplayMetrics; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.DisplayMetricsHolder; + +/** + * Module that exposes Android Constants to JS. + */ +@ReactModule(name = "DeviceInfo") +public class DeviceInfoModule extends ReactContextBaseJavaModule implements + LifecycleEventListener { + + private float mFontScale; + + public DeviceInfoModule( + ReactApplicationContext reactContext) { + super(reactContext); + + mFontScale = getReactApplicationContext().getResources().getConfiguration().fontScale; + } + + @Override + public String getName() { + return "DeviceInfo"; + } + + @Override + public @Nullable Map getConstants() { + HashMap constants = new HashMap<>(); + constants.put( + "Dimensions", + getDimensionsConstants()); + return constants; + } + + @Override + public void onHostResume() { + float fontScale = getReactApplicationContext().getResources().getConfiguration().fontScale; + if (mFontScale != fontScale) { + mFontScale = fontScale; + emitUpdateDimensionsEvent(); + } + } + + @Override + public void onHostPause() { + } + + @Override + public void onHostDestroy() { + } + + public void emitUpdateDimensionsEvent() { + getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("didUpdateDimensions", getDimensionsConstants()); + } + + private WritableMap getDimensionsConstants() { + DisplayMetrics windowDisplayMetrics = DisplayMetricsHolder.getWindowDisplayMetrics(); + DisplayMetrics screenDisplayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics(); + + WritableMap windowDisplayMetricsMap = Arguments.createMap(); + windowDisplayMetricsMap.putInt("width", windowDisplayMetrics.widthPixels); + windowDisplayMetricsMap.putInt("height", windowDisplayMetrics.heightPixels); + windowDisplayMetricsMap.putDouble("scale", windowDisplayMetrics.density); + windowDisplayMetricsMap.putDouble("fontScale", mFontScale); + windowDisplayMetricsMap.putDouble("densityDpi", windowDisplayMetrics.densityDpi); + + WritableMap screenDisplayMetricsMap = Arguments.createMap(); + screenDisplayMetricsMap.putInt("width", screenDisplayMetrics.widthPixels); + screenDisplayMetricsMap.putInt("height", screenDisplayMetrics.heightPixels); + screenDisplayMetricsMap.putDouble("scale", screenDisplayMetrics.density); + screenDisplayMetricsMap.putDouble("fontScale", mFontScale); + screenDisplayMetricsMap.putDouble("densityDpi", screenDisplayMetrics.densityDpi); + + WritableMap dimensionsMap = Arguments.createMap(); + dimensionsMap.putMap("windowPhysicalPixels", windowDisplayMetricsMap); + dimensionsMap.putMap("screenPhysicalPixels", screenDisplayMetricsMap); + + return dimensionsMap; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index 725554596559a1..bde687d5216930 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -87,7 +87,6 @@ public class UIManagerModule extends ReactContextBaseJavaModule implements private final Map mModuleConstants; private final UIImplementation mUIImplementation; private final MemoryTrimCallback mMemoryTrimCallback = new MemoryTrimCallback(); - private float mFontScale; private int mNextRootViewTag = 1; private int mBatchId = 0; @@ -100,8 +99,7 @@ public UIManagerModule( super(reactContext); DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext); mEventDispatcher = new EventDispatcher(reactContext); - mFontScale = getReactApplicationContext().getResources().getConfiguration().fontScale; - mModuleConstants = createConstants(viewManagerList, lazyViewManagersEnabled, mFontScale); + mModuleConstants = createConstants(viewManagerList, lazyViewManagersEnabled); mUIImplementation = uiImplementationProvider .createUIImplementation(reactContext, viewManagerList, mEventDispatcher); @@ -134,12 +132,6 @@ public void initialize() { @Override public void onHostResume() { mUIImplementation.onHostResume(); - - float fontScale = getReactApplicationContext().getResources().getConfiguration().fontScale; - if (mFontScale != fontScale) { - mFontScale = fontScale; - emitUpdateDimensionsEvent(); - } } @Override @@ -163,15 +155,13 @@ public void onCatalystInstanceDestroy() { private static Map createConstants( List viewManagerList, - boolean lazyViewManagersEnabled, - float fontScale) { + boolean lazyViewManagersEnabled) { ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_CONSTANTS_START); Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "CreateUIManagerConstants"); try { return UIManagerModuleConstantsHelper.createConstants( viewManagerList, - lazyViewManagersEnabled, - fontScale); + lazyViewManagersEnabled); } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_CONSTANTS_END); @@ -550,16 +540,6 @@ public void sendAccessibilityEvent(int tag, int eventType) { mUIImplementation.sendAccessibilityEvent(tag, eventType); } - public void emitUpdateDimensionsEvent() { - sendEvent("didUpdateDimensions", UIManagerModuleConstants.getDimensionsConstants(mFontScale)); - } - - private void sendEvent(String eventName, @Nullable WritableMap params) { - getReactApplicationContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(eventName, params); - } - /** * Schedule a block to be executed on the UI thread. Useful if you need to execute * view logic after all currently queued view updates have completed. diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java index 7dc5602ae4b7c7..9dd93ac82f1e2f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java @@ -83,7 +83,7 @@ .build(); } - public static Map getConstants(float fontScale) { + public static Map getConstants() { HashMap constants = new HashMap(); constants.put( "UIView", @@ -97,10 +97,6 @@ public static Map getConstants(float fontScale) { "ScaleAspectCenter", ImageView.ScaleType.CENTER_INSIDE.ordinal()))); - constants.put( - "Dimensions", - getDimensionsConstants(fontScale)); - constants.put( "StyleConstants", MapBuilder.of( @@ -133,29 +129,4 @@ public static Map getConstants(float fontScale) { return constants; } - - public static WritableMap getDimensionsConstants(float fontScale) { - DisplayMetrics windowDisplayMetrics = DisplayMetricsHolder.getWindowDisplayMetrics(); - DisplayMetrics screenDisplayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics(); - - WritableMap windowDisplayMetricsMap = Arguments.createMap(); - windowDisplayMetricsMap.putInt("width", windowDisplayMetrics.widthPixels); - windowDisplayMetricsMap.putInt("height", windowDisplayMetrics.heightPixels); - windowDisplayMetricsMap.putDouble("scale", windowDisplayMetrics.density); - windowDisplayMetricsMap.putDouble("fontScale", fontScale); - windowDisplayMetricsMap.putDouble("densityDpi", windowDisplayMetrics.densityDpi); - - WritableMap screenDisplayMetricsMap = Arguments.createMap(); - screenDisplayMetricsMap.putInt("width", screenDisplayMetrics.widthPixels); - screenDisplayMetricsMap.putInt("height", screenDisplayMetrics.heightPixels); - screenDisplayMetricsMap.putDouble("scale", screenDisplayMetrics.density); - screenDisplayMetricsMap.putDouble("fontScale", fontScale); - screenDisplayMetricsMap.putDouble("densityDpi", screenDisplayMetrics.densityDpi); - - WritableMap dimensionsMap = Arguments.createMap(); - dimensionsMap.putMap("windowPhysicalPixels", windowDisplayMetricsMap); - dimensionsMap.putMap("screenPhysicalPixels", screenDisplayMetricsMap); - - return dimensionsMap; - } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java index dbbd283d0e3943..825f81dfe3220e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java @@ -43,9 +43,8 @@ */ /* package */ static Map createConstants( List viewManagers, - boolean lazyViewManagersEnabled, - float fontScale) { - Map constants = UIManagerModuleConstants.getConstants(fontScale); + boolean lazyViewManagersEnabled) { + Map constants = UIManagerModuleConstants.getConstants(); Map bubblingEventTypesConstants = UIManagerModuleConstants.getBubblingEventTypeConstants(); Map directEventTypesConstants = UIManagerModuleConstants.getDirectEventTypeConstants(); diff --git a/jest/setup.js b/jest/setup.js index ee7ae562ae22a9..413d3b91a9260f 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -87,6 +87,16 @@ const mockNativeModules = { DataManager: { queryData: jest.fn(), }, + DeviceInfo: { + Dimensions: { + window: { + fontScale: 2, + height: 1334, + scale: 2, + width: 750, + }, + }, + }, FacebookSDK: { login: jest.fn(), logout: jest.fn(), @@ -150,14 +160,6 @@ const mockNativeModules = { replaceExistingNonRootView: jest.fn(), customBubblingEventTypes: {}, customDirectEventTypes: {}, - Dimensions: { - window: { - fontScale: 2, - height: 1334, - scale: 2, - width: 750, - }, - }, ModalFullscreenView: { Constants: {}, }, From 11814a5a81a1850a6c28c78252afa68be8348815 Mon Sep 17 00:00:00 2001 From: Ashwin Bharambe Date: Fri, 17 Mar 2017 16:47:56 -0700 Subject: [PATCH 03/18] Ensure `ResourceDrawableIdHelper` is thread-safe Reviewed By: jaegs Differential Revision: D4696625 fbshipit-source-id: e0aa7870ba02d8e6542c436d7f775bb251cf91ae --- .../imagehelper/ResourceDrawableIdHelper.java | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/imagehelper/ResourceDrawableIdHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/imagehelper/ResourceDrawableIdHelper.java index fc0c041f02cf64..dd2465156add5b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/imagehelper/ResourceDrawableIdHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/imagehelper/ResourceDrawableIdHelper.java @@ -10,6 +10,7 @@ package com.facebook.react.views.imagehelper; import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; import java.util.HashMap; import java.util.Map; @@ -21,12 +22,13 @@ /** * Helper class for obtaining information about local images. */ +@ThreadSafe public class ResourceDrawableIdHelper { private Map mResourceDrawableIdMap; private static final String LOCAL_RESOURCE_SCHEME = "res"; - private static ResourceDrawableIdHelper sResourceDrawableIdHelper; + private static volatile ResourceDrawableIdHelper sResourceDrawableIdHelper; private ResourceDrawableIdHelper() { mResourceDrawableIdMap = new HashMap(); @@ -34,12 +36,16 @@ private ResourceDrawableIdHelper() { public static ResourceDrawableIdHelper getInstance() { if (sResourceDrawableIdHelper == null) { - sResourceDrawableIdHelper = new ResourceDrawableIdHelper(); + synchronized (ResourceDrawableIdHelper.class) { + if (sResourceDrawableIdHelper == null) { + sResourceDrawableIdHelper = new ResourceDrawableIdHelper(); + } + } } return sResourceDrawableIdHelper; } - public void clear() { + public synchronized void clear() { mResourceDrawableIdMap.clear(); } @@ -48,15 +54,18 @@ public int getResourceDrawableId(Context context, @Nullable String name) { return 0; } name = name.toLowerCase().replace("-", "_"); - if (mResourceDrawableIdMap.containsKey(name)) { - return mResourceDrawableIdMap.get(name); - } - int id = context.getResources().getIdentifier( + + synchronized (this) { + if (mResourceDrawableIdMap.containsKey(name)) { + return mResourceDrawableIdMap.get(name); + } + int id = context.getResources().getIdentifier( name, "drawable", context.getPackageName()); - mResourceDrawableIdMap.put(name, id); - return id; + mResourceDrawableIdMap.put(name, id); + return id; + } } public @Nullable Drawable getResourceDrawable(Context context, @Nullable String name) { From f48b54bf6211dbbdd32839b0de68b1d0a451e486 Mon Sep 17 00:00:00 2001 From: Ashwin Bharambe Date: Fri, 17 Mar 2017 16:47:58 -0700 Subject: [PATCH 04/18] Expose RCTImageLocalAssetURL as a utility Reviewed By: javache Differential Revision: D4696627 fbshipit-source-id: 56d3e59983f524dfd5021835734b9b34203e20f2 --- Libraries/Image/RCTLocalAssetImageLoader.m | 51 +------------------- React/Base/RCTUtils.h | 4 ++ React/Base/RCTUtils.m | 54 ++++++++++++++++++++++ 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/Libraries/Image/RCTLocalAssetImageLoader.m b/Libraries/Image/RCTLocalAssetImageLoader.m index b1bf78c2ddaaaa..17acd15b88c29c 100644 --- a/Libraries/Image/RCTLocalAssetImageLoader.m +++ b/Libraries/Image/RCTLocalAssetImageLoader.m @@ -36,36 +36,6 @@ - (BOOL)shouldCacheLoadedImages return NO; } -static NSString *bundleName(NSBundle *bundle) -{ - NSString *name = bundle.infoDictionary[@"CFBundleName"]; - if (!name) { - name = [[bundle.bundlePath lastPathComponent] stringByDeletingPathExtension]; - } - return name; -} - -static NSBundle *bundleForPath(NSString *key) -{ - static NSMutableDictionary *bundleCache; - if (!bundleCache) { - bundleCache = [NSMutableDictionary new]; - bundleCache[@"main"] = [NSBundle mainBundle]; - - // Initialize every bundle in the array - for (NSString *path in [[NSBundle mainBundle] pathsForResourcesOfType:@"bundle" inDirectory:nil]) { - [NSBundle bundleWithPath:path]; - } - - // The bundles initialized above will now also be in `allBundles` - for (NSBundle *bundle in [NSBundle allBundles]) { - bundleCache[bundleName(bundle)] = bundle; - } - } - - return bundleCache[key]; -} - - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale @@ -80,31 +50,14 @@ - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL return; } - NSString *imageName = RCTBundlePathForURL(imageURL); - - NSBundle *bundle; - NSArray *imagePathComponents = [imageName pathComponents]; - if ([imagePathComponents count] > 1 && - [[[imagePathComponents firstObject] pathExtension] isEqualToString:@"bundle"]) { - NSString *bundlePath = [imagePathComponents firstObject]; - bundle = bundleForPath([bundlePath stringByDeletingPathExtension]); - imageName = [imageName substringFromIndex:(bundlePath.length + 1)]; - } - - UIImage *image; - if (bundle) { - image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil]; - } else { - image = [UIImage imageNamed:imageName]; - } - + UIImage *image = RCTImageFromLocalAssetURL(imageURL); if (image) { if (progressHandler) { progressHandler(1, 1); } completionHandler(nil, image); } else { - NSString *message = [NSString stringWithFormat:@"Could not find image named %@", imageName]; + NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL]; RCTLogWarn(@"%@", message); completionHandler(RCTErrorWithMessage(message), nil); } diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index a277120e243acb..14e169ba22a9e3 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -114,6 +114,10 @@ RCT_EXTERN NSString *__nullable RCTBundlePathForURL(NSURL *__nullable URL); // Determines if a given image URL refers to a local image RCT_EXTERN BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL); +// Returns an UIImage for a local image asset. Returns nil if the URL +// does not correspond to a local asset. +RCT_EXTERN UIImage *RCTImageFromLocalAssetURL(NSURL *imageURL); + // Creates a new, unique temporary file path with the specified extension RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *__nullable extension, NSError **error); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 9617d0a3afb9db..80ff69ed538d49 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -599,6 +599,60 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL) return [extension isEqualToString:@"png"] || [extension isEqualToString:@"jpg"]; } +static NSString *bundleName(NSBundle *bundle) +{ + NSString *name = bundle.infoDictionary[@"CFBundleName"]; + if (!name) { + name = [[bundle.bundlePath lastPathComponent] stringByDeletingPathExtension]; + } + return name; +} + +static NSBundle *bundleForPath(NSString *key) +{ + static NSMutableDictionary *bundleCache; + if (!bundleCache) { + bundleCache = [NSMutableDictionary new]; + bundleCache[@"main"] = [NSBundle mainBundle]; + + // Initialize every bundle in the array + for (NSString *path in [[NSBundle mainBundle] pathsForResourcesOfType:@"bundle" inDirectory:nil]) { + [NSBundle bundleWithPath:path]; + } + + // The bundles initialized above will now also be in `allBundles` + for (NSBundle *bundle in [NSBundle allBundles]) { + bundleCache[bundleName(bundle)] = bundle; + } + } + + return bundleCache[key]; +} + +UIImage *RCTImageFromLocalAssetURL(NSURL *imageURL) +{ + if (!RCTIsLocalAssetURL(imageURL)) { + return nil; + } + + NSString *imageName = RCTBundlePathForURL(imageURL); + + NSBundle *bundle; + NSArray *imagePathComponents = [imageName pathComponents]; + if ([imagePathComponents count] > 1 && + [[[imagePathComponents firstObject] pathExtension] isEqualToString:@"bundle"]) { + NSString *bundlePath = [imagePathComponents firstObject]; + bundle = bundleForPath([bundlePath stringByDeletingPathExtension]); + imageName = [imageName substringFromIndex:(bundlePath.length + 1)]; + } + + if (bundle) { + return [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil]; + } else { + return [UIImage imageNamed:imageName]; + } +} + RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *extension, NSError **error) { static NSError *setupError = nil; From 9c3e6ae9f0f246e5fd8cdc3599801c37d1c11690 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Fri, 17 Mar 2017 22:02:04 -0700 Subject: [PATCH 05/18] Fix Animated.event attach/detach on component re-render Summary: Need to make sure `detach` happens on the old `scrollableNode` before it's unmounted and that `attach` happens on the new `scrollableNode` after it's mounted. This should also be more performant because the detach step no longer requires iterating through all the props, most of which are not animated, and we filter out unneeded updates if props or ref haven't changed. = Test Plan = Hook up native onscroll events in `FlatListExample` and toggle "debug" - before, the events would stop working because they would try to attach to the old unmounted node, but with this diff it keeps working as expected. Reviewed By: fkgozali Differential Revision: D4687186 fbshipit-source-id: 313a7964d4614190308490a51fc4f56abb6690f8 --- .../Animated/src/AnimatedImplementation.js | 74 +++++++++---------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 769b3b0164a64e..09c3c2129452a6 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -1510,9 +1510,9 @@ class AnimatedStyle extends AnimatedWithChildren { // Recursively get values for nested styles (like iOS's shadowOffset) __walkStyleAndGetValues(style) { - let updatedStyle = {}; - for (let key in style) { - let value = style[key]; + const updatedStyle = {}; + for (const key in style) { + const value = style[key]; if (value instanceof Animated) { if (!value.__isNative) { // We cannot use value of natively driven nodes this way as the value we have access from @@ -1535,9 +1535,9 @@ class AnimatedStyle extends AnimatedWithChildren { // Recursively get animated values for nested styles (like iOS's shadowOffset) __walkStyleAndGetAnimatedValues(style) { - let updatedStyle = {}; - for (let key in style) { - let value = style[key]; + const updatedStyle = {}; + for (const key in style) { + const value = style[key]; if (value instanceof Animated) { updatedStyle[key] = value.__getAnimatedValue(); } else if (value && !Array.isArray(value) && typeof value === 'object') { @@ -1690,7 +1690,9 @@ class AnimatedProps extends Animated { } setNativeView(animatedView: any): void { - invariant(this._animatedView === undefined, 'Animated view already set.'); + if (this._animatedView === animatedView) { + return; + } this._animatedView = animatedView; if (this.__isNative) { this.__connectAnimatedView(); @@ -1729,7 +1731,9 @@ class AnimatedProps extends Animated { function createAnimatedComponent(Component: any): any { class AnimatedComponent extends React.Component { _component: any; + _prevComponent: any; _propsAnimated: AnimatedProps; + _eventDetachers: Array = []; _setComponentRef: Function; constructor(props: Object) { @@ -1739,7 +1743,7 @@ function createAnimatedComponent(Component: any): any { componentWillUnmount() { this._propsAnimated && this._propsAnimated.__detach(); - this._detachNativeEvents(this.props); + this._detachNativeEvents(); } setNativeProps(props) { @@ -1752,42 +1756,28 @@ function createAnimatedComponent(Component: any): any { componentDidMount() { this._propsAnimated.setNativeView(this._component); - - this._attachNativeEvents(this.props); + this._attachNativeEvents(); } - _attachNativeEvents(newProps) { - if (newProps !== this.props) { - this._detachNativeEvents(this.props); - } - + _attachNativeEvents() { // Make sure to get the scrollable node for components that implement // `ScrollResponder.Mixin`. - const ref = this._component.getScrollableNode ? + const scrollableNode = this._component.getScrollableNode ? this._component.getScrollableNode() : this._component; - for (const key in newProps) { - const prop = newProps[key]; + for (const key in this.props) { + const prop = this.props[key]; if (prop instanceof AnimatedEvent && prop.__isNative) { - prop.__attach(ref, key); + prop.__attach(scrollableNode, key); + this._eventDetachers.push(() => prop.__detach(scrollableNode, key)); } } } - _detachNativeEvents(props) { - // Make sure to get the scrollable node for components that implement - // `ScrollResponder.Mixin`. - const ref = this._component.getScrollableNode ? - this._component.getScrollableNode() : - this._component; - - for (const key in props) { - const prop = props[key]; - if (prop instanceof AnimatedEvent && prop.__isNative) { - prop.__detach(ref, key); - } - } + _detachNativeEvents() { + this._eventDetachers.forEach(remove => remove()); + this._eventDetachers = []; } _attachProps(nextProps) { @@ -1820,10 +1810,6 @@ function createAnimatedComponent(Component: any): any { callback, ); - if (this._component) { - this._propsAnimated.setNativeView(this._component); - } - // When you call detach, it removes the element from the parent list // of children. If it goes to 0, then the parent also detaches itself // and so on. @@ -1835,9 +1821,18 @@ function createAnimatedComponent(Component: any): any { oldPropsAnimated && oldPropsAnimated.__detach(); } - componentWillReceiveProps(nextProps) { - this._attachProps(nextProps); - this._attachNativeEvents(nextProps); + componentWillReceiveProps(newProps) { + this._attachProps(newProps); + } + + componentDidUpdate(prevProps) { + if (this._component !== this._prevComponent) { + this._propsAnimated.setNativeView(this._component); + } + if (this._component !== this._prevComponent || prevProps !== this.props) { + this._detachNativeEvents(); + this._attachNativeEvents(); + } } render() { @@ -1850,6 +1845,7 @@ function createAnimatedComponent(Component: any): any { } _setComponentRef(c) { + this._prevComponent = this._component; this._component = c; } From 09b8ef41e1ab937b660d513fe7728334272b3f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eloy=20Dur=C3=A1n?= Date: Sat, 18 Mar 2017 12:21:50 -0700 Subject: [PATCH 06/18] Fix issue with whitespace in path to source and stabilise CI. Summary: * Fixes issue where headers could not be found due to whitespace in an unquoted header search path. https://github.com/facebook/react-native/issues/11781#issuecomment-287176373 * Stabilises CI but not needing to download the source for Yoga, but use the existing cloned repo instead. /cc mkonicek Closes https://github.com/facebook/react-native/pull/13007 Differential Revision: D4735347 fbshipit-source-id: 933aefcb0e65537d2e759d25f4e3b81cdf4b8cb5 --- React.podspec | 4 ++-- ReactCommon/yoga/Yoga.podspec | 2 +- scripts/process-podspecs.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/React.podspec b/React.podspec index e6352b3fd821d2..dcc10543f45e61 100644 --- a/React.podspec +++ b/React.podspec @@ -62,7 +62,7 @@ Pod::Spec.new do |s| s.subspec "jschelpers" do |ss| ss.source_files = "ReactCommon/jschelpers/{JavaScriptCore,JSCWrapper}.{cpp,h}", "ReactCommon/jschelpers/systemJSCWrapper.cpp" ss.private_header_files = "ReactCommon/jschelpers/{JavaScriptCore,JSCWrapper}.h" - ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/ReactCommon" } + ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\"" } ss.framework = "JavaScriptCore" end @@ -70,7 +70,7 @@ Pod::Spec.new do |s| ss.dependency "React/jschelpers" ss.source_files = "ReactCommon/cxxreact/{JSBundleType,oss-compat-util}.{cpp,h}" ss.private_header_files = "ReactCommon/cxxreact/{JSBundleType,oss-compat-util}.h" - ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/ReactCommon" } + ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\"" } end s.subspec "ART" do |ss| diff --git a/ReactCommon/yoga/Yoga.podspec b/ReactCommon/yoga/Yoga.podspec index 1e9c75ff7721ed..6d053b34eee654 100644 --- a/ReactCommon/yoga/Yoga.podspec +++ b/ReactCommon/yoga/Yoga.podspec @@ -1,7 +1,7 @@ package = JSON.parse(File.read(File.expand_path('../../package.json', __dir__))) version = package['version'] -source = { :git => 'https://github.com/facebook/react-native.git' } +source = { :git => ENV['INSTALL_YOGA_FROM_LOCATION'] || 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. source[:commit] = `git rev-parse HEAD`.strip diff --git a/scripts/process-podspecs.sh b/scripts/process-podspecs.sh index d87fa4f719131b..e39f8c78b421c1 100755 --- a/scripts/process-podspecs.sh +++ b/scripts/process-podspecs.sh @@ -44,7 +44,7 @@ push() { local SPEC_DIR="$SPEC_REPO_DIR/$POD_NAME/$(version $SPEC_NAME)" local SPEC_PATH="$SPEC_DIR/$SPEC_NAME.json" mkdir -p $SPEC_DIR - env INSTALL_YOGA_WITHOUT_PATH_OPTION=1 pod ipc spec $SPEC_NAME > $SPEC_PATH + env INSTALL_YOGA_WITHOUT_PATH_OPTION=1 INSTALL_YOGA_FROM_LOCATION="$ROOT" pod ipc spec $SPEC_NAME > $SPEC_PATH } # Perform linting and publishing of podspec in cwd. From 06dd08316f449d63ccb67a788f4bc845c1e5c060 Mon Sep 17 00:00:00 2001 From: Kevin Cooper Date: Sat, 18 Mar 2017 12:48:19 -0700 Subject: [PATCH 07/18] Fix suggestion to "npm start -- --reset-cache" Summary: As discussed in https://github.com/facebook/react-native/pull/11983. The double dash is necessary to pass through the argument to node. Based on the comments [here](https://github.com/facebook/react-native/issues/1924#issuecomment-249861004), it looks like most people use the double dash; it's unclear whether it would do anything at all if the dashes were omitted. If anyone else has better insight, let me know! Closes https://github.com/facebook/react-native/pull/13003 Differential Revision: D4731566 Pulled By: hramos fbshipit-source-id: 62562536db7589a03a511762117cbf0e36d3aafb --- packager/src/node-haste/DependencyGraph/ResolutionRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packager/src/node-haste/DependencyGraph/ResolutionRequest.js b/packager/src/node-haste/DependencyGraph/ResolutionRequest.js index 46583d9643b529..0ffa7446373b32 100644 --- a/packager/src/node-haste/DependencyGraph/ResolutionRequest.js +++ b/packager/src/node-haste/DependencyGraph/ResolutionRequest.js @@ -391,7 +391,7 @@ class ResolutionRequest { `To resolve try the following:\n` + ` 1. Clear watchman watches: \`watchman watch-del-all\`.\n` + ` 2. Delete the \`node_modules\` folder: \`rm -rf node_modules && npm install\`.\n` + - ' 3. Reset packager cache: `rm -fr $TMPDIR/react-*` or `npm start --reset-cache`.' + ' 3. Reset packager cache: `rm -fr $TMPDIR/react-*` or `npm start -- --reset-cache`.' ); }); }); From 22da6f2f3f7bab852f05921d32d8425123e70b55 Mon Sep 17 00:00:00 2001 From: Arman Dezfuli-Arjomandi Date: Sat, 18 Mar 2017 12:56:24 -0700 Subject: [PATCH 08/18] Fix typo in FlatList docs Summary: Thanks for submitting a PR! Please read these instructions carefully: - [x] Explain the **motivation** for making this change. - [x] Provide a **test plan** demonstrating that the code is solid. - [x] Match the **code formatting** of the rest of the codebase. - [x] Target the `master` branch, NOT a "stable" branch. There was a typo in the FlatList docs for numColumns affecting the formatting of its description. N/A Sign the [CLA][2], if you haven't already. Small pull requests are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. Make sure all **tests pass** on both [Travis][3] and [Circle CI][4]. PRs that break tests are unlikely to be merged. For more info, see the ["Pull Requests"][5] section of our "Contributing" guidelines. [1]: https://medium.com/martinkonicek/what-is-a-test-plan-8bfc840ec171#.y9lcuqqi9 [2]: https://code.facebook.com/cla Closes https://github.com/facebook/react-native/pull/13002 Differential Revision: D4735343 fbshipit-source-id: f781c50c892d64e69f61aec980614e948a48f48b --- Libraries/CustomComponents/Lists/FlatList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/CustomComponents/Lists/FlatList.js b/Libraries/CustomComponents/Lists/FlatList.js index 5fedff5d612446..daf99d33c2fd51 100644 --- a/Libraries/CustomComponents/Lists/FlatList.js +++ b/Libraries/CustomComponents/Lists/FlatList.js @@ -103,7 +103,7 @@ type OptionalProps = { */ keyExtractor: (item: ItemT, index: number) => string, /** - * Multiple columns can only be rendered with `horizontal={false}`` and will zig-zag like a + * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a * `flexWrap` layout. Items should all be the same height - masonry layouts are not supported. */ numColumns: number, From cbd46aaa7c95a42be682736f4be9f7b11201b3c5 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 18 Mar 2017 13:01:54 -0700 Subject: [PATCH 09/18] more accurate product-name regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Some projects define multiple targets, including app extensions, which are built with a “.appex” extension. This fix prevents the buildProject method from selecting any app extension (e.g. a Today.appex today-widget extension) as the product name. Thanks for submitting a PR! Please read these instructions carefully: - [X] Explain the **motivation** for making this change. - [X] Provide a **test plan** demonstrating that the code is solid. - [X] Match the **code formatting** of the rest of the codebase. - [X] Target the `master` branch, NOT a "stable" branch. When building our workspace, ReactNative was failing to install the app to the simulator because it calculated an incorrect path to the app itself. It was attempting to install "Today.app" when it should have been installing "Remitly.app". I discovered that ReactNative parses the build output to identify the generated app name, and that this was broken when the build also generated an app extension. The f Closes https://github.com/facebook/react-native/pull/13001 Differential Revision: D4735360 fbshipit-source-id: afeeb2073ccd65c95916b153fcde574b5343af8c --- local-cli/runIOS/runIOS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-cli/runIOS/runIOS.js b/local-cli/runIOS/runIOS.js index ea605530c8a30a..e890d643b1317c 100644 --- a/local-cli/runIOS/runIOS.js +++ b/local-cli/runIOS/runIOS.js @@ -157,7 +157,7 @@ function buildProject(xcodeProject, udid, scheme, configuration = 'Debug', launc }); buildProcess.on('close', function(code) { //FULL_PRODUCT_NAME is the actual file name of the app, which actually comes from the Product Name in the build config, which does not necessary match a scheme name, example output line: export FULL_PRODUCT_NAME="Super App Dev.app" - let productNameMatch = /export FULL_PRODUCT_NAME="?(.+).app/.exec(buildOutput); + let productNameMatch = /export FULL_PRODUCT_NAME="?(.+).app"?$/.exec(buildOutput); if (productNameMatch && productNameMatch.length && productNameMatch.length > 1) { return resolve(productNameMatch[1]);//0 is the full match, 1 is the app name } From 5328d952a8dd0945005d9f7305aba913f438ffa7 Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Sun, 19 Mar 2017 13:16:04 -0700 Subject: [PATCH 10/18] kill bridge only after cleaning up NativeModules Reviewed By: javache Differential Revision: D4734634 fbshipit-source-id: c2d425485679454397d18b1a0c389714c0e3c484 --- .../com/facebook/react/cxxbridge/CatalystInstanceImpl.java | 2 +- .../facebook/react/uimanager/events/EventDispatcher.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java index 80be1fbe51a559..6133b2576765dc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java @@ -292,11 +292,11 @@ public void destroy() { // TODO: tell all APIs to shut down mDestroyed = true; - mHybridData.resetNative(); mReactQueueConfiguration.getNativeModulesQueueThread().runOnQueue(new Runnable() { @Override public void run() { mJavaRegistry.notifyJSInstanceDestroy(); + mHybridData.resetNative(); } }); boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java index d00d14c38e6ccb..540714b2feefb0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java @@ -171,7 +171,12 @@ public void onHostDestroy() { } public void onCatalystInstanceDestroyed() { - stopFrameCallback(); + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + stopFrameCallback(); + } + }); } private void stopFrameCallback() { From 47d2cfeac5addb24f8fcfbeeb2cb8ff11a604e7a Mon Sep 17 00:00:00 2001 From: Adam Ernst Date: Sun, 19 Mar 2017 13:59:41 -0700 Subject: [PATCH 11/18] Whitelist some stuff in React Reviewed By: mzlee Differential Revision: D4731968 fbshipit-source-id: 1d1ae12a50beef4cd024a467427ec3b6cd446f4c --- ReactCommon/cxxreact/BUCK | 1 + ReactCommon/microprofiler/BUCK | 1 + 2 files changed, 2 insertions(+) diff --git a/ReactCommon/cxxreact/BUCK b/ReactCommon/cxxreact/BUCK index 5b58dbd2dc2c63..8e94880e254625 100644 --- a/ReactCommon/cxxreact/BUCK +++ b/ReactCommon/cxxreact/BUCK @@ -93,6 +93,7 @@ cxx_library( ], force_static = True, header_namespace = "cxxreact", + labels = ["accounts_for_platform_and_build_mode_flags"], visibility = [ "PUBLIC", ], diff --git a/ReactCommon/microprofiler/BUCK b/ReactCommon/microprofiler/BUCK index adbc87ba12024f..d65e3f927d73e6 100644 --- a/ReactCommon/microprofiler/BUCK +++ b/ReactCommon/microprofiler/BUCK @@ -14,6 +14,7 @@ cxx_library( ], force_static = True, header_namespace = "microprofiler", + labels = ["accounts_for_platform_and_build_mode_flags"], visibility = [ "PUBLIC", ], From 23c2a6cf1ae297182a62d1a8143b55b186f5e1b7 Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Sun, 19 Mar 2017 21:43:14 -0700 Subject: [PATCH 12/18] Removed harmful optimization in ReactNativeEventEmitter Reviewed By: spicyj Differential Revision: D4729779 fbshipit-source-id: 2dd5ec10d42df7f24804796c4100eca107edeedb --- .../Renderer/src/renderers/native/ReactNativeEventEmitter.js | 5 ----- .../src/renderers/shared/shared/event/EventPropagators.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Libraries/Renderer/src/renderers/native/ReactNativeEventEmitter.js b/Libraries/Renderer/src/renderers/native/ReactNativeEventEmitter.js index 1e1c97e308f6e7..b7b18f9ff0177b 100644 --- a/Libraries/Renderer/src/renderers/native/ReactNativeEventEmitter.js +++ b/Libraries/Renderer/src/renderers/native/ReactNativeEventEmitter.js @@ -103,11 +103,6 @@ var ReactNativeEventEmitter = { ) { var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT; var inst = ReactNativeComponentTree.getInstanceFromNode(rootNodeID); - if (!inst) { - // If the original instance is already gone, we don't have to dispatch - // any events. - return; - } ReactGenericBatching.batchedUpdates(function() { ReactNativeEventEmitter.handleTopLevel( topLevelType, diff --git a/Libraries/Renderer/src/renderers/shared/shared/event/EventPropagators.js b/Libraries/Renderer/src/renderers/shared/shared/event/EventPropagators.js index 159d2e6c1992b0..c97f880939d16d 100644 --- a/Libraries/Renderer/src/renderers/shared/shared/event/EventPropagators.js +++ b/Libraries/Renderer/src/renderers/shared/shared/event/EventPropagators.js @@ -93,7 +93,7 @@ function accumulateTwoPhaseDispatchesSingleSkipTarget(event) { * requiring that the `dispatchMarker` be the same as the dispatched ID. */ function accumulateDispatches(inst, ignoredDirection, event) { - if (event && event.dispatchConfig.registrationName) { + if (inst && event && event.dispatchConfig.registrationName) { var registrationName = event.dispatchConfig.registrationName; var listener = getListener(inst, registrationName); if (listener) { From 64c327ae684690ae9cabaff412467b54095839ea Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Sun, 19 Mar 2017 21:48:22 -0700 Subject: [PATCH 13/18] Fixed issue where setting `zero scale` transfrom matrix to UIView brokes `hitTest` mechanism Summary: The Math Strikes Back Several related things: * When we specify `scale: 0;` style for some view it ends up with calling `CATransform3DScale` with zero scale parameter. * In this case `CATransform3DScale` returns transform matrix full of zeros. It actually depends on representation and matrix-type (2d or 3d) but in UIView debugger it appears as [0, 0, 0, 0, ...]. And probably it is correct result. * By default, for hit-testing, UIKit uses specially optimized logic based on GPU/CALayer infrastructure under the hood. And the transform matrix full of zeros breaks this algorithm. I guess, it happens because zero-matrix doesn't quite make sense. So, `scale: 0;` is a weird edge case, and in this diff, we are trying to illuminate it by replacing with epsilon value. Related SO issues: http://stackoverflow.com/questions/25964224/cgaffinetransformscale-not-working-with-zero-scale http://stackoverflow.com/questions/7937369/animate-uiview-scale-to-zero Reviewed By: blairvanderhoof Differential Revision: D4734475 fbshipit-source-id: 7241cdffa86c05a6552860a25789e2281588ba23 --- React/Views/RCTConvert+Transform.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/React/Views/RCTConvert+Transform.m b/React/Views/RCTConvert+Transform.m index 11639105626725..32a1abfb57c7e2 100644 --- a/React/Views/RCTConvert+Transform.m +++ b/React/Views/RCTConvert+Transform.m @@ -90,15 +90,15 @@ + (CATransform3D)CATransform3D:(id)json transform = CATransform3DRotate(transform, rotate, 0, 0, 1); } else if ([property isEqualToString:@"scale"]) { - CGFloat scale = [value floatValue]; + CGFloat scale = MAX([value floatValue], FLT_EPSILON); transform = CATransform3DScale(transform, scale, scale, 1); } else if ([property isEqualToString:@"scaleX"]) { - CGFloat scale = [value floatValue]; + CGFloat scale = MAX([value floatValue], FLT_EPSILON); transform = CATransform3DScale(transform, scale, 1, 1); } else if ([property isEqualToString:@"scaleY"]) { - CGFloat scale = [value floatValue]; + CGFloat scale = MAX([value floatValue], FLT_EPSILON); transform = CATransform3DScale(transform, 1, scale, 1); } else if ([property isEqualToString:@"translate"]) { From d2723341864b92e7ad44e39aad2436cb1c9a3394 Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Sun, 19 Mar 2017 21:49:26 -0700 Subject: [PATCH 14/18] RCTRootView is now has empty autoresizing mask by default Summary: `autoresizingMask` is supposed to be set outside self class, this is UIKit convention. Reviewed By: mmmulani Differential Revision: D4697098 fbshipit-source-id: 7e0aa5d3032184de980b3cecafebbc4ce8ef9ada --- React/Base/RCTRootView.m | 1 - 1 file changed, 1 deletion(-) diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index db1ed02343c415..9870ebfaeee436 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -69,7 +69,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _loadingViewFadeDelay = 0.25; _loadingViewFadeDuration = 0.25; _sizeFlexibility = RCTRootViewSizeFlexibilityNone; - self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(bridgeDidReload) From 3acafd1f3dd42cc27a592cdee0530e0dbf1c8afd Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Mon, 20 Mar 2017 00:00:17 -0700 Subject: [PATCH 15/18] Better TextInput: Removed redundant UIScrollView from RCTTextView Reviewed By: mmmulani Differential Revision: D4640207 fbshipit-source-id: 01fc65b0212ad6baef500625679dab5e99da9db5 --- Libraries/Text/RCTTextView.m | 65 ++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 83cde446a19355..b640d7a3712133 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -58,6 +58,13 @@ - (void)didMoveToWindow } } +- (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated +{ + // Turning off scroll animation. + // This fixes the problem also known as "flaky scrolling". + [super setContentOffset:contentOffset animated:NO]; +} + @end @implementation RCTTextView @@ -69,7 +76,6 @@ @implementation RCTTextView UITextView *_textView; RCTText *_richTextView; NSAttributedString *_pendingAttributedText; - UIScrollView *_scrollView; UITextRange *_previousSelectionRange; NSUInteger _previousTextLength; @@ -100,17 +106,10 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher #if !TARGET_OS_TV _textView.scrollsToTop = NO; #endif - _textView.scrollEnabled = NO; + _textView.scrollEnabled = YES; _textView.delegate = self; - _scrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; -#if !TARGET_OS_TV - _scrollView.scrollsToTop = NO; -#endif - _scrollView.delegate = self; - [_scrollView addSubview:_textView]; - - [self addSubview:_scrollView]; + [self addSubview:_textView]; } return self; } @@ -118,9 +117,12 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) +#pragma mark - RCTComponent + - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index { [super insertReactSubview:subview atIndex:index]; + if ([subview isKindOfClass:[RCTText class]]) { if (_richTextView) { RCTLogError(@"Tried to insert a second into - there can only be one."); @@ -143,11 +145,6 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index } } -- (void)dealloc -{ - _scrollView.delegate = nil; -} - - (void)removeReactSubview:(UIView *)subview { [super removeReactSubview:subview]; @@ -159,9 +156,11 @@ - (void)removeReactSubview:(UIView *)subview - (void)didUpdateReactSubviews { - // Do nothing, as we don't allow non-text subviews + // Do nothing, as we don't allow non-text subviews. } +#pragma mark - Routine + - (void)setMostRecentEventCount:(NSInteger)mostRecentEventCount { _mostRecentEventCount = mostRecentEventCount; @@ -262,7 +261,6 @@ - (void)updateFrames CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset); _textView.frame = frame; _placeholderView.frame = frame; - _scrollView.frame = frame; [self updateContentSize]; _textView.textContainerInset = adjustedTextContainerInset; @@ -271,10 +269,8 @@ - (void)updateFrames - (void)updateContentSize { - CGSize size = (CGSize){_scrollView.frame.size.width, INFINITY}; - size.height = [_textView sizeThatFits:size].height; - _scrollView.contentSize = size; - _textView.frame = (CGRect){CGPointZero, size}; + CGSize size = _textView.frame.size; + size.height = [_textView sizeThatFits:CGSizeMake(size.width, INFINITY)].height; if (_viewDidCompleteInitialLayout && _onContentSizeChange && !CGSizeEqualToSize(_previousContentSize, size)) { _previousContentSize = size; @@ -725,26 +721,31 @@ - (UIColor *)defaultPlaceholderTextColor - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (_onScroll) { + CGPoint contentOffset = scrollView.contentOffset; + CGSize contentSize = scrollView.contentSize; + CGSize size = scrollView.bounds.size; + UIEdgeInsets contentInset = scrollView.contentInset; + _onScroll(@{ @"contentOffset": @{ - @"x": @(scrollView.contentOffset.x), - @"y": @(scrollView.contentOffset.y) + @"x": @(contentOffset.x), + @"y": @(contentOffset.y) }, @"contentInset": @{ - @"top": @(_scrollView.contentInset.top), - @"left": @(_scrollView.contentInset.left), - @"bottom": @(_scrollView.contentInset.bottom), - @"right": @(_scrollView.contentInset.right) + @"top": @(contentInset.top), + @"left": @(contentInset.left), + @"bottom": @(contentInset.bottom), + @"right": @(contentInset.right) }, @"contentSize": @{ - @"width": @(_scrollView.contentSize.width), - @"height": @(_scrollView.contentSize.height) + @"width": @(contentSize.width), + @"height": @(contentSize.height) }, @"layoutMeasurement": @{ - @"width": @(_scrollView.frame.size.width), - @"height": @(_scrollView.frame.size.height) + @"width": @(size.width), + @"height": @(size.height) }, - @"zoomScale": @(_scrollView.zoomScale ?: 1), + @"zoomScale": @(scrollView.zoomScale ?: 1), }); } } From 1b013cd30ce201cafb376782bdf427f601940220 Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Mon, 20 Mar 2017 00:00:18 -0700 Subject: [PATCH 16/18] Better TextInput: Fixing multiline insets and prepare for auto-expanding feature Summary: Several things: * The mess with insets was fixed. Previously we tried to compensate the insets difference with `UITextField` by adjusting `textContainerInset` property, moreover we delegated negative part of this compensation to the view inset. That was terrible because it breaks `contentSize` computation, complicates whole insets consept, complicates everything; it just was not right. Now we are fixing the top and left inset differences in different places. We disable left and right 5pt margin by setting `_textView.textContainer.lineFragmentPadding = 0` and we introduce top 5px inset as a DEFAULT value for top inset for common multiline (this value can be easilly overwritten in Javascript). * Internal layout and contentSize computations were unified and simplified. * Now we report `intrinsicContentSize` value to Yoga, one step before auto-expandable TextInput. Depends on D4640207. Reviewed By: mmmulani Differential Revision: D4645921 fbshipit-source-id: da5988ebac50be967caecd71e780c014f6eb257a --- Libraries/Components/TextInput/TextInput.js | 7 + Libraries/Text/RCTTextView.h | 5 +- Libraries/Text/RCTTextView.m | 136 +++++++++++--------- Libraries/Text/RCTTextViewManager.m | 2 +- React/Modules/RCTUIManager.h | 3 +- 5 files changed, 88 insertions(+), 65 deletions(-) diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index d1c3e6fd27f723..c3c9eff2b4ae9b 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -678,6 +678,7 @@ const TextInput = React.createClass({ if (props.inputView) { children = [children, props.inputView]; } + props.style.unshift(styles.multilineInput); textContainer = #import -@class RCTEventDispatcher; +@class RCTBridge; @interface RCTTextView : RCTView @@ -28,6 +28,7 @@ @property (nonatomic, strong) UIFont *font; @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, strong) NSNumber *maxLength; +@property (nonatomic, assign, readonly) CGSize contentSize; @property (nonatomic, copy) RCTDirectEventBlock onChange; @property (nonatomic, copy) RCTDirectEventBlock onContentSizeChange; @@ -35,7 +36,7 @@ @property (nonatomic, copy) RCTDirectEventBlock onTextInput; @property (nonatomic, copy) RCTDirectEventBlock onScroll; -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; - (void)performTextUpdate; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index b640d7a3712133..b88f34bff0aad5 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -11,6 +11,7 @@ #import #import +#import #import #import @@ -69,6 +70,7 @@ - (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated @implementation RCTTextView { + RCTBridge *_bridge; RCTEventDispatcher *_eventDispatcher; NSString *_placeholder; @@ -87,22 +89,25 @@ @implementation RCTTextView NSInteger _nativeEventCount; CGSize _previousContentSize; - BOOL _viewDidCompleteInitialLayout; } -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithBridge:(RCTBridge *)bridge { - RCTAssertParam(eventDispatcher); + RCTAssertParam(bridge); - if ((self = [super initWithFrame:CGRectZero])) { + if (self = [super initWithFrame:CGRectZero]) { _contentInset = UIEdgeInsetsZero; - _eventDispatcher = eventDispatcher; + _bridge = bridge; + _eventDispatcher = bridge.eventDispatcher; _placeholderTextColor = [self defaultPlaceholderTextColor]; _blurOnSubmit = NO; - _textView = [[RCTUITextView alloc] initWithFrame:CGRectZero]; + _textView = [[RCTUITextView alloc] initWithFrame:self.bounds]; + _textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _textView.backgroundColor = [UIColor clearColor]; _textView.textColor = [UIColor blackColor]; + // This line actually removes 5pt (default value) left and right padding in UITextView. + _textView.textContainer.lineFragmentPadding = 0; #if !TARGET_OS_TV _textView.scrollsToTop = NO; #endif @@ -132,7 +137,7 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index // If this is in rich text editing mode, and the child node providing rich text // styling has a backgroundColor, then the attributedText produced by the child node will have an // NSBackgroundColor attribute. We need to forward this attribute to the text view manually because the text view - // always has a clear background color in -initWithEventDispatcher:. + // always has a clear background color in `initWithBridge:`. // // TODO: This should be removed when the related hack in -performPendingTextUpdate is removed. if (subview.backgroundColor) { @@ -237,60 +242,20 @@ - (void)performPendingTextUpdate [_textView layoutIfNeeded]; [self updatePlaceholderVisibility]; - [self updateContentSize]; + [self invalidateContentSize]; _blockTextShouldChange = NO; } -- (void)updateFrames -{ - // Adjust the insets so that they are as close as possible to single-line - // RCTTextField defaults, using the system defaults of font size 17 and a - // height of 31 points. - // - // We apply the left inset to the frame since a negative left text-container - // inset mysteriously causes the text to be hidden until the text view is - // first focused. - UIEdgeInsets adjustedFrameInset = UIEdgeInsetsZero; - adjustedFrameInset.left = _contentInset.left - 5; - - UIEdgeInsets adjustedTextContainerInset = _contentInset; - adjustedTextContainerInset.top += 5; - adjustedTextContainerInset.left = 0; - - CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset); - _textView.frame = frame; - _placeholderView.frame = frame; - [self updateContentSize]; - - _textView.textContainerInset = adjustedTextContainerInset; - _placeholderView.textContainerInset = adjustedTextContainerInset; -} - -- (void)updateContentSize -{ - CGSize size = _textView.frame.size; - size.height = [_textView sizeThatFits:CGSizeMake(size.width, INFINITY)].height; - - if (_viewDidCompleteInitialLayout && _onContentSizeChange && !CGSizeEqualToSize(_previousContentSize, size)) { - _previousContentSize = size; - _onContentSizeChange(@{ - @"contentSize": @{ - @"height": @(size.height), - @"width": @(size.width), - }, - @"target": self.reactTag, - }); - } -} - - (void)updatePlaceholder { [_placeholderView removeFromSuperview]; _placeholderView = nil; if (_placeholder) { - _placeholderView = [[UITextView alloc] initWithFrame:self.bounds]; + _placeholderView = [[UITextView alloc] initWithFrame:_textView.frame]; + _placeholderView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _placeholderView.textContainer.lineFragmentPadding = 0; _placeholderView.userInteractionEnabled = NO; _placeholderView.backgroundColor = [UIColor clearColor]; _placeholderView.scrollEnabled = NO; @@ -340,7 +305,9 @@ - (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor - (void)setContentInset:(UIEdgeInsets)contentInset { _contentInset = contentInset; - [self updateFrames]; + _textView.textContainerInset = contentInset; + _placeholderView.textContainerInset = contentInset; + [self setNeedsLayout]; } #pragma mark - UITextViewDelegate @@ -503,8 +470,7 @@ - (void)setText:(NSString *)text } [self updatePlaceholderVisibility]; - [self updateContentSize]; //keep the text wrapping when the length of - //the textline has been extended longer than the length of textinputView + [self invalidateContentSize]; } else if (eventLag > RCTTextUpdateLagWarningThreshold) { RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); } @@ -595,7 +561,7 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, - (void)textViewDidChange:(UITextView *)textView { [self updatePlaceholderVisibility]; - [self updateContentSize]; + [self invalidateContentSize]; // Detect when textView updates happend that didn't invoke `shouldChangeTextInRange` // (e.g. typing simplified chinese in pinyin will insert and remove spaces without @@ -664,6 +630,8 @@ - (void)textViewDidEndEditing:(UITextView *)textView eventCount:_nativeEventCount]; } +#pragma mark - UIResponder + - (BOOL)isFirstResponder { return [_textView isFirstResponder]; @@ -695,17 +663,63 @@ - (BOOL)resignFirstResponder return [_textView resignFirstResponder]; } -- (void)layoutSubviews +#pragma mark - Content Size + +- (CGSize)contentSize { - [super layoutSubviews]; + // Returning value does NOT include insets. + CGSize contentSize = self.intrinsicContentSize; + contentSize.width -= _contentInset.left + _contentInset.right; + contentSize.height -= _contentInset.top + _contentInset.bottom; + return contentSize; +} + +- (void)invalidateContentSize +{ + CGSize contentSize = self.contentSize; + + if (CGSizeEqualToSize(_previousContentSize, contentSize)) { + return; + } + _previousContentSize = contentSize; + + [_bridge.uiManager setIntrinsicContentSize:contentSize forView:self]; + + if (_onContentSizeChange) { + _onContentSizeChange(@{ + @"contentSize": @{ + @"height": @(contentSize.height), + @"width": @(contentSize.width), + }, + @"target": self.reactTag, + }); + } +} + +#pragma mark - Layout + +- (CGSize)intrinsicContentSize +{ + // Calling `sizeThatFits:` is probably more expensive method to compute + // content size compare to direct access `_textView.contentSize` property, + // but seems `sizeThatFits:` returns more reliable and consistent result. + // Returning value DOES include insets. + return [self sizeThatFits:CGSizeMake(self.bounds.size.width, INFINITY)]; +} - // Start sending content size updates only after the view has been laid out - // otherwise we send multiple events with bad dimensions on initial render. - _viewDidCompleteInitialLayout = YES; +- (CGSize)sizeThatFits:(CGSize)size +{ + return [_textView sizeThatFits:size]; +} - [self updateFrames]; +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self invalidateContentSize]; } +#pragma mark - Default values + - (UIFont *)defaultPlaceholderFont { return [UIFont systemFontOfSize:17]; diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m index 7b7f5d0473d422..ab26d1b8ec8858 100644 --- a/Libraries/Text/RCTTextViewManager.m +++ b/Libraries/Text/RCTTextViewManager.m @@ -29,7 +29,7 @@ - (RCTShadowView *)shadowView - (UIView *)view { - return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; + return [[RCTTextView alloc] initWithBridge:self.bridge]; } RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType) diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index 14f866f754b69c..f1aa37b954aba4 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -87,7 +87,8 @@ RCT_EXTERN NSString *const RCTUIManagerRootViewKey; /** * Set the natural size of a view, which is used when no explicit size is set. - * Use UIViewNoIntrinsicMetric to ignore a dimension. + * Use `UIViewNoIntrinsicMetric` to ignore a dimension. + * The `size` must NOT include padding and border. */ - (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view; From 26e2c08544f3beac9926652f1b8f38ca7647df6e Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Mon, 20 Mar 2017 00:00:21 -0700 Subject: [PATCH 17/18] Better TextInput: Native auto-expandable is here (iOS only) Reviewed By: mmmulani Differential Revision: D4646962 fbshipit-source-id: bc054d9c68c385b13222e7c9fb8728d21f987a48 --- .../UIExplorer/js/TextInputExample.ios.js | 39 +++++-------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/Examples/UIExplorer/js/TextInputExample.ios.js b/Examples/UIExplorer/js/TextInputExample.ios.js index f6499160448063..960066872b270c 100644 --- a/Examples/UIExplorer/js/TextInputExample.ios.js +++ b/Examples/UIExplorer/js/TextInputExample.ios.js @@ -103,34 +103,6 @@ class TextEventsExample extends React.Component { } } -class AutoExpandingTextInput extends React.Component { - state: any; - - constructor(props) { - super(props); - this.state = { - text: 'React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and React. The focus of React Native is on developer efficiency across all the platforms you care about — learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native.', - height: 0, - }; - } - render() { - return ( - { - this.setState({text}); - }} - onContentSizeChange={(event) => { - this.setState({height: event.nativeEvent.contentSize.height}); - }} - style={[styles.default, {height: Math.max(35, this.state.height)}]} - value={this.state.text} - /> - ); - } -} - class RewriteExample extends React.Component { state: any; @@ -403,6 +375,10 @@ var styles = StyleSheet.create({ padding: 4, marginBottom: 4, }, + multilineExpandable: { + height: 'auto', + maxHeight: 100, + }, multilineWithFontStyles: { color: 'blue', fontWeight: 'bold', @@ -801,10 +777,13 @@ exports.examples = [ render: function() { return ( - ); From b53d76efb7c5e7239061267a28d69b51fb068dfe Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Mon, 20 Mar 2017 00:00:23 -0700 Subject: [PATCH 18/18] Better TextInput: RCTUITextView was decoupled in separate file and now handles placeholder feature Reviewed By: mmmulani Differential Revision: D4663151 fbshipit-source-id: ce57ca4bebf4676df2ae5e586a1b175ec2aac760 --- .../UIExplorer/js/TextInputExample.ios.js | 2 +- .../Text/RCTText.xcodeproj/project.pbxproj | 8 + Libraries/Text/RCTTextView.h | 1 + Libraries/Text/RCTTextView.m | 283 ++++++------------ Libraries/Text/RCTUITextView.h | 28 ++ Libraries/Text/RCTUITextView.m | 189 ++++++++++++ React/Base/RCTBatchedBridge.m | 1 + 7 files changed, 319 insertions(+), 193 deletions(-) create mode 100644 Libraries/Text/RCTUITextView.h create mode 100644 Libraries/Text/RCTUITextView.m diff --git a/Examples/UIExplorer/js/TextInputExample.ios.js b/Examples/UIExplorer/js/TextInputExample.ios.js index 960066872b270c..89f8ccaff2ec2b 100644 --- a/Examples/UIExplorer/js/TextInputExample.ios.js +++ b/Examples/UIExplorer/js/TextInputExample.ios.js @@ -779,7 +779,7 @@ exports.examples = [ RCTTextUpdateLagWarningThreshold) { + RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); + } +} + +- (NSString *)text +{ + return _textView.text; +} + +- (void)setText:(NSString *)text +{ + NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; + if (eventLag == 0 && ![text isEqualToString:_textView.text]) { + UITextRange *selection = _textView.selectedTextRange; + NSInteger oldTextLength = _textView.text.length; + + _predictedText = text; + _textView.text = text; + + if (selection.empty) { + // maintain cursor position relative to the end of the old text + NSInteger start = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:selection.start]; + NSInteger offsetFromEnd = oldTextLength - start; + NSInteger newOffset = text.length - offsetFromEnd; + UITextPosition *position = [_textView positionFromPosition:_textView.beginningOfDocument offset:newOffset]; + _textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position]; + } + + [self invalidateContentSize]; + } else if (eventLag > RCTTextUpdateLagWarningThreshold) { + RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); + } +} + +- (NSString *)placeholder +{ + return _textView.placeholderText; } - (void)setPlaceholder:(NSString *)placeholder { - _placeholder = placeholder; - [self updatePlaceholder]; + _textView.placeholderText = placeholder; +} + +- (UIColor *)placeholderTextColor +{ + return _textView.placeholderTextColor; } - (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor { - if (placeholderTextColor) { - _placeholderTextColor = placeholderTextColor; - } else { - _placeholderTextColor = [self defaultPlaceholderTextColor]; - } - [self updatePlaceholder]; + _textView.placeholderTextColor = placeholderTextColor; } -- (void)setContentInset:(UIEdgeInsets)contentInset +- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType { - _contentInset = contentInset; - _textView.textContainerInset = contentInset; - _placeholderView.textContainerInset = contentInset; - [self setNeedsLayout]; + _textView.autocorrectionType = autocorrectionType; +} + +- (UITextAutocorrectionType)autocorrectionType +{ + return _textView.autocorrectionType; +} + +- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType +{ + _textView.spellCheckingType = spellCheckingType; +} + +- (UITextSpellCheckingType)spellCheckingType +{ + return _textView.spellCheckingType; } #pragma mark - UITextViewDelegate - (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { - if (textView.textWasPasted) { - textView.textWasPasted = NO; - } else { + if (!textView.textWasPasted) { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress reactTag:self.reactTag text:nil @@ -425,86 +417,6 @@ - (void)textViewDidChangeSelection:(RCTUITextView *)textView } } -- (NSString *)text -{ - return _textView.text; -} - -- (void)setSelection:(RCTTextSelection *)selection -{ - if (!selection) { - return; - } - - UITextRange *currentSelection = _textView.selectedTextRange; - UITextPosition *start = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.start]; - UITextPosition *end = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.end]; - UITextRange *selectedTextRange = [_textView textRangeFromPosition:start toPosition:end]; - - NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; - if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) { - _previousSelectionRange = selectedTextRange; - _textView.selectedTextRange = selectedTextRange; - } else if (eventLag > RCTTextUpdateLagWarningThreshold) { - RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); - } -} - -- (void)setText:(NSString *)text -{ - NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; - if (eventLag == 0 && ![text isEqualToString:_textView.text]) { - UITextRange *selection = _textView.selectedTextRange; - NSInteger oldTextLength = _textView.text.length; - - _predictedText = text; - _textView.text = text; - - if (selection.empty) { - // maintain cursor position relative to the end of the old text - NSInteger start = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:selection.start]; - NSInteger offsetFromEnd = oldTextLength - start; - NSInteger newOffset = text.length - offsetFromEnd; - UITextPosition *position = [_textView positionFromPosition:_textView.beginningOfDocument offset:newOffset]; - _textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position]; - } - - [self updatePlaceholderVisibility]; - [self invalidateContentSize]; - } else if (eventLag > RCTTextUpdateLagWarningThreshold) { - RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); - } -} - -- (void)updatePlaceholderVisibility -{ - if (_textView.text.length > 0) { - [_placeholderView setHidden:YES]; - } else { - [_placeholderView setHidden:NO]; - } -} - -- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType -{ - _textView.autocorrectionType = autocorrectionType; -} - -- (UITextAutocorrectionType)autocorrectionType -{ - return _textView.autocorrectionType; -} - -- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType -{ - _textView.spellCheckingType = spellCheckingType; -} - -- (UITextSpellCheckingType)spellCheckingType -{ - return _textView.spellCheckingType; -} - - (BOOL)textViewShouldBeginEditing:(UITextView *)textView { if (_selectTextOnFocus) { @@ -519,7 +431,6 @@ - (void)textViewDidBeginEditing:(UITextView *)textView { if (_clearTextOnFocus) { _textView.text = @""; - [self updatePlaceholderVisibility]; } [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus @@ -560,7 +471,6 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, - (void)textViewDidChange:(UITextView *)textView { - [self updatePlaceholderVisibility]; [self invalidateContentSize]; // Detect when textView updates happend that didn't invoke `shouldChangeTextInRange` @@ -580,6 +490,7 @@ - (void)textViewDidChange:(UITextView *)textView _nativeUpdatesInFlight = NO; _nativeEventCount++; + // TODO: t16435709 This part will be removed soon. if (!self.reactTag || !_onChange) { return; } @@ -718,18 +629,6 @@ - (void)layoutSubviews [self invalidateContentSize]; } -#pragma mark - Default values - -- (UIFont *)defaultPlaceholderFont -{ - return [UIFont systemFontOfSize:17]; -} - -- (UIColor *)defaultPlaceholderTextColor -{ - return [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22]; -} - #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView diff --git a/Libraries/Text/RCTUITextView.h b/Libraries/Text/RCTUITextView.h new file mode 100644 index 00000000000000..762c1c2ad70cdb --- /dev/null +++ b/Libraries/Text/RCTUITextView.h @@ -0,0 +1,28 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/* + * Just regular UITextView... but much better! + */ +@interface RCTUITextView : UITextView + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_UNAVAILABLE; +- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; + +@property (nonatomic, assign, readonly) BOOL textWasPasted; +@property (nonatomic, copy, nullable) NSString *placeholderText; +@property (nonatomic, assign, nullable) UIColor *placeholderTextColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/Text/RCTUITextView.m b/Libraries/Text/RCTUITextView.m new file mode 100644 index 00000000000000..24302f823ed89a --- /dev/null +++ b/Libraries/Text/RCTUITextView.m @@ -0,0 +1,189 @@ +/** + * 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 "RCTUITextView.h" + +@implementation RCTUITextView +{ + BOOL _jsRequestingFirstResponder; + UILabel *_placeholderView; + UITextView *_detachedTextView; +} + +static UIFont *defaultPlaceholderFont() +{ + return [UIFont systemFontOfSize:17]; +} + +static UIColor *defaultPlaceholderTextColor() +{ + // Default placeholder color from UITextField. + return [UIColor colorWithRed:0 green:0 blue:0.0980392 alpha:0.22]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(textDidChange) + name:UITextViewTextDidChangeNotification + object:self]; + + _placeholderView = [[UILabel alloc] initWithFrame:self.bounds]; + _placeholderView.hidden = YES; + _placeholderView.isAccessibilityElement = NO; + _placeholderView.numberOfLines = 0; + [self addSubview:_placeholderView]; + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Properties + +- (void)setPlaceholderText:(NSString *)placeholderText +{ + _placeholderText = placeholderText; + [self invalidatePlaceholder]; +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ + _placeholderTextColor = placeholderTextColor; + [self invalidatePlaceholder]; +} + + +- (void)textDidChange +{ + _textWasPasted = NO; + [self invalidatePlaceholder]; +} + +#pragma mark - UIResponder + +- (void)reactWillMakeFirstResponder +{ + _jsRequestingFirstResponder = YES; +} + +- (BOOL)canBecomeFirstResponder +{ + return _jsRequestingFirstResponder; +} + +- (void)reactDidMakeFirstResponder +{ + _jsRequestingFirstResponder = NO; +} + +- (void)didMoveToWindow +{ + if (_jsRequestingFirstResponder) { + [self becomeFirstResponder]; + [self reactDidMakeFirstResponder]; + } +} + +#pragma mark - Overrides + +- (void)setFont:(UIFont *)font +{ + [super setFont:font]; + [self invalidatePlaceholder]; +} + +- (void)setText:(NSString *)text +{ + [super setText:text]; + [self textDidChange]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + [super setAttributedText:attributedText]; + [self textDidChange]; +} + +- (void)paste:(id)sender +{ + [super paste:sender]; + _textWasPasted = YES; +} + +- (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated +{ + // Turning off scroll animation. + // This fixes the problem also known as "flaky scrolling". + [super setContentOffset:contentOffset animated:NO]; +} + +#pragma mark - Layout + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, self.textContainerInset); + CGFloat placeholderHeight = [_placeholderView sizeThatFits:textFrame.size].height; + textFrame.size.height = MIN(placeholderHeight, textFrame.size.height); + _placeholderView.frame = textFrame; +} + +- (CGSize)sizeThatFits:(CGSize)size +{ + // UITextView on iOS 8 has a bug that automatically scrolls to the top + // when calling `sizeThatFits:`. Use a copy so that self is not screwed up. + static BOOL useCustomImplementation = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + useCustomImplementation = ![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,0,0}]; + }); + + if (!useCustomImplementation) { + return [super sizeThatFits:size]; + } + + if (!_detachedTextView) { + _detachedTextView = [UITextView new]; + } + + _detachedTextView.attributedText = self.attributedText; + _detachedTextView.font = self.font; + _detachedTextView.textContainerInset = self.textContainerInset; + + return [_detachedTextView sizeThatFits:size]; +} + +#pragma mark - Placeholder + +- (void)invalidatePlaceholder +{ + BOOL wasVisible = !_placeholderView.isHidden; + BOOL isVisible = _placeholderText.length != 0 && self.text.length == 0; + + if (wasVisible != isVisible) { + _placeholderView.hidden = !isVisible; + } + + if (isVisible) { + _placeholderView.font = self.font ?: defaultPlaceholderFont(); + _placeholderView.textColor = _placeholderTextColor ?: defaultPlaceholderTextColor(); + _placeholderView.textAlignment = self.textAlignment; + _placeholderView.text = _placeholderText; + [self setNeedsLayout]; + } +} + +@end diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 4c065d0d612f61..480df01a953518 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -10,6 +10,7 @@ #import #import "RCTAssert.h" + #import "RCTBridge+Private.h" #import "RCTBridge.h" #import "RCTBridgeMethod.h"