From f136ae136278c374e24989c693a5b3a0a101178c Mon Sep 17 00:00:00 2001 From: "Andrew Chen (Eng)" Date: Tue, 20 Mar 2018 00:58:16 -0700 Subject: [PATCH] Add test for FabricText Reviewed By: mdvacca Differential Revision: D7326562 fbshipit-source-id: e1229f84496e9181475979d757066e3796a24a3f --- Libraries/Text/FabricText.js | 242 ++++++++++++++++++ Libraries/Text/TestFabricText.js | 24 ++ .../com/facebook/react/common/ArrayUtils.java | 3 +- .../react/fabric/FabricReconciler.java | 18 +- 4 files changed, 279 insertions(+), 8 deletions(-) create mode 100644 Libraries/Text/FabricText.js create mode 100644 Libraries/Text/TestFabricText.js diff --git a/Libraries/Text/FabricText.js b/Libraries/Text/FabricText.js new file mode 100644 index 000000000..1eedd8b18 --- /dev/null +++ b/Libraries/Text/FabricText.js @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule FabricText + * @flow + * @format + */ +'use strict'; + +const React = require('React'); +const ReactNative = require('ReactNative'); +const ReactNativeViewAttributes = require('ReactNativeViewAttributes'); +const TextPropTypes = require('TextPropTypes'); +const Touchable = require('Touchable'); +const UIManager = require('UIManager'); + +const {createReactNativeComponentClass} = require('ReactFabricInternals'); +const mergeFast = require('mergeFast'); +const processColor = require('processColor'); +const {ViewContextTypes} = require('ViewContext'); + +import type {PressEvent} from 'CoreEventTypes'; +import type {TextProps} from 'TextProps'; +import type {ViewChildContext} from 'ViewContext'; + +type State = { + isHighlighted: boolean, +}; + +type RectOffset = { + top: number, + left: number, + right: number, + bottom: number, +}; + +const PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; + +const viewConfig = { + validAttributes: mergeFast(ReactNativeViewAttributes.UIView, { + isHighlighted: true, + numberOfLines: true, + ellipsizeMode: true, + allowFontScaling: true, + disabled: true, + selectable: true, + selectionColor: true, + adjustsFontSizeToFit: true, + minimumFontScale: true, + textBreakStrategy: true, + }), + uiViewClassName: 'RCTText', +}; + +/** + * A React component for displaying text. + * + * See https://facebook.github.io/react-native/docs/text.html + */ +class Text extends ReactNative.NativeComponent { + static propTypes = TextPropTypes; + static childContextTypes = ViewContextTypes; + static contextTypes = ViewContextTypes; + + static defaultProps = { + accessible: true, + allowFontScaling: true, + ellipsizeMode: 'tail', + }; + + state = mergeFast(Touchable.Mixin.touchableGetInitialState(), { + isHighlighted: false, + }); + + viewConfig = viewConfig; + + getChildContext(): ViewChildContext { + return { + isInAParentText: true, + }; + } + + _handlers: ?Object; + + _hasPressHandler(): boolean { + return !!this.props.onPress || !!this.props.onLongPress; + } + /** + * These are assigned lazily the first time the responder is set to make plain + * text nodes as cheap as possible. + */ + touchableHandleActivePressIn: ?Function; + touchableHandleActivePressOut: ?Function; + touchableHandlePress: ?Function; + touchableHandleLongPress: ?Function; + touchableHandleResponderGrant: ?Function; + touchableHandleResponderMove: ?Function; + touchableHandleResponderRelease: ?Function; + touchableHandleResponderTerminate: ?Function; + touchableHandleResponderTerminationRequest: ?Function; + touchableGetPressRectOffset: ?Function; + + render(): React.Element { + let newProps = this.props; + if (this.props.onStartShouldSetResponder || this._hasPressHandler()) { + if (!this._handlers) { + this._handlers = { + onStartShouldSetResponder: (): boolean => { + const shouldSetFromProps = + this.props.onStartShouldSetResponder && + this.props.onStartShouldSetResponder(); + const setResponder = shouldSetFromProps || this._hasPressHandler(); + if (setResponder && !this.touchableHandleActivePressIn) { + // Attach and bind all the other handlers only the first time a touch + // actually happens. + for (const key in Touchable.Mixin) { + if (typeof Touchable.Mixin[key] === 'function') { + (this: any)[key] = Touchable.Mixin[key].bind(this); + } + } + this.touchableHandleActivePressIn = () => { + if ( + this.props.suppressHighlighting || + !this._hasPressHandler() + ) { + return; + } + this.setState({ + isHighlighted: true, + }); + }; + + this.touchableHandleActivePressOut = () => { + if ( + this.props.suppressHighlighting || + !this._hasPressHandler() + ) { + return; + } + this.setState({ + isHighlighted: false, + }); + }; + + this.touchableHandlePress = (e: PressEvent) => { + this.props.onPress && this.props.onPress(e); + }; + + this.touchableHandleLongPress = (e: PressEvent) => { + this.props.onLongPress && this.props.onLongPress(e); + }; + + this.touchableGetPressRectOffset = function(): RectOffset { + return this.props.pressRetentionOffset || PRESS_RECT_OFFSET; + }; + } + return setResponder; + }, + onResponderGrant: function(e: SyntheticEvent<>, dispatchID: string) { + // $FlowFixMe TouchableMixin handlers couldn't actually be null + this.touchableHandleResponderGrant(e, dispatchID); + this.props.onResponderGrant && + this.props.onResponderGrant.apply(this, arguments); + }.bind(this), + onResponderMove: function(e: SyntheticEvent<>) { + // $FlowFixMe TouchableMixin handlers couldn't actually be null + this.touchableHandleResponderMove(e); + this.props.onResponderMove && + this.props.onResponderMove.apply(this, arguments); + }.bind(this), + onResponderRelease: function(e: SyntheticEvent<>) { + // $FlowFixMe TouchableMixin handlers couldn't actually be null + this.touchableHandleResponderRelease(e); + this.props.onResponderRelease && + this.props.onResponderRelease.apply(this, arguments); + }.bind(this), + onResponderTerminate: function(e: SyntheticEvent<>) { + // $FlowFixMe TouchableMixin handlers couldn't actually be null + this.touchableHandleResponderTerminate(e); + this.props.onResponderTerminate && + this.props.onResponderTerminate.apply(this, arguments); + }.bind(this), + onResponderTerminationRequest: function(): boolean { + // Allow touchable or props.onResponderTerminationRequest to deny + // the request + // $FlowFixMe TouchableMixin handlers couldn't actually be null + var allowTermination = this.touchableHandleResponderTerminationRequest(); + if (allowTermination && this.props.onResponderTerminationRequest) { + allowTermination = this.props.onResponderTerminationRequest.apply( + this, + arguments, + ); + } + return allowTermination; + }.bind(this), + }; + } + newProps = { + ...this.props, + ...this._handlers, + isHighlighted: this.state.isHighlighted, + }; + } + if (newProps.selectionColor != null) { + newProps = { + ...newProps, + selectionColor: processColor(newProps.selectionColor), + }; + } + if (Touchable.TOUCH_TARGET_DEBUG && newProps.onPress) { + newProps = { + ...newProps, + style: [this.props.style, {color: 'magenta'}], + }; + } + if (this.context.isInAParentText) { + return ; + } else { + return ; + } + } +} + +var RCTText = createReactNativeComponentClass( + viewConfig.uiViewClassName, + () => viewConfig, +); +var RCTVirtualText = RCTText; + +if (UIManager.RCTVirtualText) { + RCTVirtualText = createReactNativeComponentClass('RCTVirtualText', () => ({ + validAttributes: mergeFast(ReactNativeViewAttributes.UIView, { + isHighlighted: true, + }), + uiViewClassName: 'RCTVirtualText', + })); +} + +module.exports = Text; diff --git a/Libraries/Text/TestFabricText.js b/Libraries/Text/TestFabricText.js new file mode 100644 index 000000000..cdedcc317 --- /dev/null +++ b/Libraries/Text/TestFabricText.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule TestFabricText + * @flow + * @format + */ +'use strict'; + +/** + * This is a switch on the correct Text to use for Fabric testing purposes + */ +let TestFabricText; +const FabricTestModule = require('NativeModules').FabricTestModule; +if (FabricTestModule && FabricTestModule.IS_FABRIC_ENABLED) { + TestFabricText = require('FabricText'); +} else { + TestFabricText = require('Text'); +} + +module.exports = TestFabricText; diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/ArrayUtils.java b/ReactAndroid/src/main/java/com/facebook/react/common/ArrayUtils.java index 41b62c19f..5b2f32023 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/ArrayUtils.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/ArrayUtils.java @@ -11,10 +11,9 @@ public class ArrayUtils { public static int[] copyListToArray(List list) { int[] array = new int[list.size()]; - for (int t = 0 ; t < list.size() ; t++) { + for (int t = 0; t < list.size(); t++) { array[t] = list.get(t); } return array; } - } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricReconciler.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricReconciler.java index 4ea5d143b..3d7571e49 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricReconciler.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricReconciler.java @@ -8,6 +8,7 @@ package com.facebook.react.fabric; import android.util.Log; +import android.util.SparseArray; import com.facebook.react.common.ArrayUtils; import com.facebook.react.uimanager.ReactShadowNode; import com.facebook.react.uimanager.UIViewOperationQueue; @@ -74,15 +75,15 @@ public class FabricReconciler { // It is more efficient to reorder removing and adding all the views in the right order, instead // of calculating the minimum amount of reorder operations. Set addedTags = new HashSet<>(); - ViewAtIndex[] viewsToAdd = new ViewAtIndex[newList.size() - firstRemovedOrAddedViewIndex]; - int viewsToAddIndex = 0; + List viewsToAdd = new LinkedList<>(); for (int k = firstRemovedOrAddedViewIndex; k < newList.size(); k++) { ReactShadowNode newNode = newList.get(k); + if (newNode.isVirtual()) continue; if (newNode.getNewProps() != null) { uiViewOperationQueue.enqueueUpdateProperties( newNode.getReactTag(), newNode.getViewClass(), newNode.getNewProps()); } - viewsToAdd[viewsToAddIndex++] = new ViewAtIndex(newNode.getReactTag(), k); + viewsToAdd.add(new ViewAtIndex(newNode.getReactTag(), k)); List previousChildrenList = newNode.getOriginalReactShadowNode() == null ? null : newNode.getOriginalReactShadowNode().getChildrenList(); manageChildren(newNode, previousChildrenList, newNode.getChildrenList()); newNode.setOriginalReactShadowNode(newNode); @@ -100,6 +101,7 @@ public class FabricReconciler { int indicesToRemoveIndex = 0; for (int j = firstRemovedOrAddedViewIndex; j < prevList.size(); j++) { ReactShadowNode nodeToRemove = prevList.get(j); + if (nodeToRemove.isVirtual()) continue; indicesToRemove[indicesToRemoveIndex++] = j; if (!addedTags.contains(nodeToRemove.getReactTag())) { tagsToDelete.add(nodeToRemove.getReactTag()); @@ -110,16 +112,20 @@ public class FabricReconciler { } int[] tagsToDeleteArray = ArrayUtils.copyListToArray(tagsToDelete); + ViewAtIndex[] viewsToAddArray = viewsToAdd.toArray(new ViewAtIndex[viewsToAdd.size()]); if (DEBUG) { Log.d( TAG, "manageChildren.enqueueManageChildren parent: " + parent.getReactTag() + "\n\tIndices2Remove: " + Arrays.toString(indicesToRemove) + - "\n\tViews2Add: " + Arrays.toString(viewsToAdd) + + "\n\tViews2Add: " + Arrays.toString(viewsToAddArray) + "\n\tTags2Delete: " + Arrays.toString(tagsToDeleteArray)); } - uiViewOperationQueue.enqueueManageChildren( - parent.getReactTag(), indicesToRemove, viewsToAdd, tagsToDeleteArray); + // TODO (t27180994): Mutate views synchronously on main thread + if (indicesToRemove.length > 0 || viewsToAddArray.length > 0 || tagsToDeleteArray.length > 0) { + uiViewOperationQueue.enqueueManageChildren( + parent.getReactTag(), indicesToRemove, viewsToAddArray, tagsToDeleteArray); + } } }