From 3cae6fa950eed54bccccf61a28c0e85d5a004c6c Mon Sep 17 00:00:00 2001 From: Eli White Date: Mon, 24 Jun 2019 18:42:16 -0700 Subject: [PATCH] RN Android: Support View Manager Commands that are strings Summary: Right now JS triggers a view manager command with the following code: ``` UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), UIManager.getViewManagerConfig('RCTView').Commands.hotspotUpdate, [destX || 0, destY || 0], ); ``` As we want to get rid of calls to UIManager, we need to stop looking for the integer defined in native from JavaScript. We will be changing methods like this to be: ``` UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), 'hotspotUpdate', [destX || 0, destY || 0], ); ``` We need to support ints and Strings to be backwards compatible, but ints will be deprecated. Reviewed By: shergin Differential Revision: D15955444 fbshipit-source-id: d1c488975ae03404f8f851a7035b58a90ed34163 --- .../com/facebook/react/bridge/UIManager.java | 12 +++++ .../react/fabric/FabricJSIModuleProvider.java | 2 + .../react/fabric/FabricUIManager.java | 9 ++++ .../fabric/mounting/MountingManager.java | 14 +++++ .../DispatchStringCommandMountItem.java | 36 +++++++++++++ .../uimanager/NativeViewHierarchyManager.java | 15 ++++++ .../react/uimanager/UIImplementation.java | 5 ++ .../react/uimanager/UIManagerModule.java | 21 ++++++-- .../react/uimanager/UIViewOperationQueue.java | 24 +++++++++ .../facebook/react/uimanager/ViewManager.java | 12 +++++ .../react/views/view/ReactViewManager.java | 54 ++++++++++++++----- 11 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchStringCommandMountItem.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java index f34ac2b2591..883c039dc9b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java @@ -32,12 +32,24 @@ public interface UIManager extends JSIModule, PerformanceCounter { * Dispatches the commandId received by parameter to the view associated with the reactTag. * The command will be processed in the UIThread. * + * Receiving commands as ints is deprecated and will be removed in a future release. + * * @param reactTag {@link int} that identifies the view that will receive this command * @param commandId {@link int} command id * @param commandArgs {@link ReadableArray} parameters associated with the command */ void dispatchCommand(int reactTag, int commandId, @Nullable ReadableArray commandArgs); + /** + * Dispatches the commandId received by parameter to the view associated with the reactTag. + * The command will be processed in the UIThread. + * + * @param reactTag {@link int} that identifies the view that will receive this command + * @param commandId {@link String} command id + * @param commandArgs {@link ReadableArray} parameters associated with the command + */ + void dispatchCommand(int reactTag, String commandId, @Nullable ReadableArray commandArgs); + void setJSResponder(int reactTag, boolean blockNativeResponder); void clearJSResponder(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricJSIModuleProvider.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricJSIModuleProvider.java index 1fde5b7c35c..1ae2f49caf8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricJSIModuleProvider.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricJSIModuleProvider.java @@ -18,6 +18,7 @@ import com.facebook.react.fabric.mounting.ViewPool; import com.facebook.react.fabric.mounting.mountitems.BatchMountItem; import com.facebook.react.fabric.mounting.mountitems.DeleteMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; +import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.InsertMountItem; import com.facebook.react.fabric.mounting.mountitems.MountItem; import com.facebook.react.fabric.mounting.mountitems.PreAllocateViewMountItem; @@ -102,6 +103,7 @@ public class FabricJSIModuleProvider implements JSIModuleProvider { BatchMountItem.class.getClass(); DeleteMountItem.class.getClass(); DispatchCommandMountItem.class.getClass(); + DispatchStringCommandMountItem.class.getClass(); InsertMountItem.class.getClass(); MountItem.class.getClass(); RemoveMountItem.class.getClass(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 82e0e22730e..362903d1deb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -44,6 +44,7 @@ import com.facebook.react.fabric.mounting.mountitems.BatchMountItem; import com.facebook.react.fabric.mounting.mountitems.CreateMountItem; import com.facebook.react.fabric.mounting.mountitems.DeleteMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; +import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.InsertMountItem; import com.facebook.react.fabric.mounting.mountitems.MountItem; import com.facebook.react.fabric.mounting.mountitems.PreAllocateViewMountItem; @@ -486,6 +487,14 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { } } + @Override + public void dispatchCommand( + final int reactTag, final String commandId, @Nullable final ReadableArray commandArgs) { + synchronized (mMountItemsLock) { + mMountItems.add(new DispatchStringCommandMountItem(reactTag, commandId, commandArgs)); + } + } + @Override public void setJSResponder(int reactTag, boolean blockNativeResponder) { // do nothing for now. diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java index a8cc5266e3b..4e671aa1c51 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java @@ -148,6 +148,20 @@ public class MountingManager { viewState.mViewManager.receiveCommand(viewState.mView, commandId, commandArgs); } + public void receiveCommand(int reactTag, String commandId, @Nullable ReadableArray commandArgs) { + ViewState viewState = getViewState(reactTag); + + if (viewState.mViewManager == null) { + throw new IllegalStateException("Unable to find viewState manager for tag " + reactTag); + } + + if (viewState.mView == null) { + throw new IllegalStateException("Unable to find viewState view for tag " + reactTag); + } + + viewState.mViewManager.receiveCommand(viewState.mView, commandId, commandArgs); + } + @SuppressWarnings("unchecked") // prevents unchecked conversion warn of the type private static ViewGroupManager getViewGroupManager(ViewState viewState) { if (viewState.mViewManager == null) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchStringCommandMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchStringCommandMountItem.java new file mode 100644 index 00000000000..32668becaea --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DispatchStringCommandMountItem.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2014-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. + */ +package com.facebook.react.fabric.mounting.mountitems; + +import androidx.annotation.Nullable; +import com.facebook.react.fabric.mounting.MountingManager; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.UiThreadUtil; + +public class DispatchStringCommandMountItem implements MountItem { + + private final int mReactTag; + private final String mCommandId; + private final @Nullable ReadableArray mCommandArgs; + + public DispatchStringCommandMountItem( + int reactTag, String commandId, @Nullable ReadableArray commandArgs) { + mReactTag = reactTag; + mCommandId = commandId; + mCommandArgs = commandArgs; + } + + @Override + public void execute(MountingManager mountingManager) { + mountingManager.receiveCommand(mReactTag, mCommandId, mCommandArgs); + } + + @Override + public String toString() { + return "DispatchStringCommandMountItem [" + mReactTag + "] " + mCommandId; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index f54998b3cc1..02b481c1618 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -770,6 +770,21 @@ public class NativeViewHierarchyManager { viewManager.receiveCommand(view, commandId, args); } + public synchronized void dispatchCommand( + int reactTag, + String commandId, + @Nullable ReadableArray args) { + UiThreadUtil.assertOnUiThread(); + View view = mTagsToViews.get(reactTag); + if (view == null) { + throw new IllegalViewOperationException("Trying to send command to a non-existing view " + + "with tag " + reactTag); + } + + ViewManager viewManager = resolveViewManager(reactTag); + viewManager.receiveCommand(view, commandId, args); + } + /** * Show a {@link PopupMenu}. * diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index dfa310b76d4..2afbfc30851 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -748,6 +748,11 @@ public class UIImplementation { mOperationsQueue.enqueueDispatchCommand(reactTag, commandId, commandArgs); } + public void dispatchViewManagerCommand(int reactTag, String commandId, @Nullable ReadableArray commandArgs) { + assertViewExists(reactTag, "dispatchViewManagerCommand"); + mOperationsQueue.enqueueDispatchCommand(reactTag, commandId, commandArgs); + } + /** * Show a PopupMenu. * 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 60668e652b3..8f3d64d0f8b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -22,6 +22,7 @@ import com.facebook.debug.holder.PrinterHolder; import com.facebook.debug.tags.ReactDebugOverlayTags; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.GuardedRunnable; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.OnBatchCompleteListener; @@ -31,6 +32,7 @@ import com.facebook.react.bridge.ReactMarker; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.UIManager; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; @@ -648,11 +650,19 @@ public class UIManagerModule extends ReactContextBaseJavaModule @ReactMethod public void dispatchViewManagerCommand( - int reactTag, int commandId, @Nullable ReadableArray commandArgs) { + int reactTag, Dynamic commandId, @Nullable ReadableArray commandArgs) { // TODO: this is a temporary approach to support ViewManagerCommands in Fabric until // the dispatchViewManagerCommand() method is supported by Fabric JS API. - UIManagerHelper.getUIManager(getReactApplicationContext(), ViewUtil.getUIManagerType(reactTag)) - .dispatchCommand(reactTag, commandId, commandArgs); + if(commandId.getType() == ReadableType.Number) { + final int commandIdNum = commandId.asInt(); + UIManagerHelper.getUIManager(getReactApplicationContext(), ViewUtil.getUIManagerType(reactTag)) + .dispatchCommand(reactTag, commandIdNum, commandArgs); + } else if (commandId.getType() == ReadableType.String) { + final String commandIdStr = commandId.asString(); + UIManagerHelper.getUIManager(getReactApplicationContext(), ViewUtil.getUIManagerType(reactTag)) + .dispatchCommand(reactTag, commandIdStr, commandArgs); + } + } @Override @@ -660,6 +670,11 @@ public class UIManagerModule extends ReactContextBaseJavaModule mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs); } + @Override + public void dispatchCommand(int reactTag, String commandId, @Nullable ReadableArray commandArgs) { + mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs); + } + @ReactMethod public void playTouchSound() { AudioManager audioManager = diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index cf66d2a5c50..80efe29ffdc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -312,6 +312,23 @@ public class UIViewOperationQueue { } } + private final class DispatchStringCommandOperation extends ViewOperation { + + private final String mCommand; + private final @Nullable ReadableArray mArgs; + + public DispatchStringCommandOperation(int tag, String command, @Nullable ReadableArray args) { + super(tag); + mCommand = command; + mArgs = args; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs); + } + } + private final class ShowPopupMenuOperation extends ViewOperation { private final ReadableArray mItems; @@ -659,6 +676,13 @@ public class UIViewOperationQueue { mOperations.add(new DispatchCommandOperation(reactTag, commandId, commandArgs)); } + public void enqueueDispatchCommand( + int reactTag, + String commandId, + @Nullable ReadableArray commandArgs) { + mOperations.add(new DispatchStringCommandOperation(reactTag, commandId, commandArgs)); + } + public void enqueueUpdateExtraData(int reactTag, Object extraData) { mOperations.add(new UpdateViewExtraData(reactTag, extraData)); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java index a0f7c9ba51c..e589954f69f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java @@ -172,6 +172,18 @@ public abstract class ViewManager public void receiveCommand(@Nonnull T root, int commandId, @Nullable ReadableArray args) { } + /** + * Subclasses may use this method to receive events/commands directly from JS through the + * {@link UIManager}. Good example of such a command would be {@code scrollTo} request with + * coordinates for a {@link ScrollView} instance. + * + * @param root View instance that should receive the command + * @param commandId code of the command + * @param args optional arguments for the command + */ + public void receiveCommand(@Nonnull T root, String commandId, @Nullable ReadableArray args) { + } + /** * Subclasses of {@link ViewManager} that expect to receive commands through * {@link UIManagerModule#dispatchViewManagerCommand} should override this method returning the diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index b21cfa99b16..75e95947622 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -289,28 +289,54 @@ public class ReactViewManager extends ViewGroupManager { public void receiveCommand(ReactViewGroup root, int commandId, @Nullable ReadableArray args) { switch (commandId) { case CMD_HOTSPOT_UPDATE: { - if (args == null || args.size() != 2) { - throw new JSApplicationIllegalArgumentException( - "Illegal number of arguments for 'updateHotspot' command"); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - float x = PixelUtil.toPixelFromDIP(args.getDouble(0)); - float y = PixelUtil.toPixelFromDIP(args.getDouble(1)); - root.drawableHotspotChanged(x, y); - } + handleHotspotUpdate(root, args); break; } case CMD_SET_PRESSED: { - if (args == null || args.size() != 1) { - throw new JSApplicationIllegalArgumentException( - "Illegal number of arguments for 'setPressed' command"); - } - root.setPressed(args.getBoolean(0)); + handleSetPressed(root, args); break; } } } + @Override + public void receiveCommand(ReactViewGroup root, String commandId, @Nullable ReadableArray args) { + switch (commandId) { + case "hotspotUpdate": { + handleHotspotUpdate(root, args); + break; + } + case "setPressed": { + handleSetPressed(root, args); + break; + } + } + } + + private void handleSetPressed( + ReactViewGroup root, + @Nullable ReadableArray args) { + if (args == null || args.size() != 1) { + throw new JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'setPressed' command"); + } + root.setPressed(args.getBoolean(0)); + } + + private void handleHotspotUpdate( + ReactViewGroup root, + @Nullable ReadableArray args) { + if (args == null || args.size() != 2) { + throw new JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'updateHotspot' command"); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + float x = PixelUtil.toPixelFromDIP(args.getDouble(0)); + float y = PixelUtil.toPixelFromDIP(args.getDouble(1)); + root.drawableHotspotChanged(x, y); + } + } + @Override public void addView(ReactViewGroup parent, View child, int index) { boolean removeClippedSubviews = parent.getRemoveClippedSubviews();