mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-02-07 22:42:13 +08:00
Summary: public UIImplementationProvider allows plugging in an alternative UIImplementation. A follow up diff adds a toggle under FB Dev Settings and uses this class to control an implementation. This allows us experimenting with other ways of generating UI hierarchy from JavaScript components. Reviewed By: astreet Differential Revision: D2554774 fb-gh-sync-id: 6574a893020e3519bd2ab00b9620a6dbdfaed595
428 lines
16 KiB
Java
428 lines
16 KiB
Java
/**
|
|
* 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.uimanager;
|
|
|
|
import javax.annotation.Nullable;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import android.util.DisplayMetrics;
|
|
|
|
import com.facebook.csslayout.CSSLayoutContext;
|
|
import com.facebook.infer.annotation.Assertions;
|
|
import com.facebook.react.animation.Animation;
|
|
import com.facebook.react.bridge.Arguments;
|
|
import com.facebook.react.bridge.Callback;
|
|
import com.facebook.react.bridge.LifecycleEventListener;
|
|
import com.facebook.react.bridge.OnBatchCompleteListener;
|
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
import com.facebook.react.bridge.ReactMethod;
|
|
import com.facebook.react.bridge.ReadableArray;
|
|
import com.facebook.react.bridge.ReadableMap;
|
|
import com.facebook.react.bridge.WritableArray;
|
|
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener;
|
|
import com.facebook.react.uimanager.events.EventDispatcher;
|
|
import com.facebook.systrace.Systrace;
|
|
import com.facebook.systrace.SystraceMessage;
|
|
|
|
/**
|
|
* <p>Native module to allow JS to create and update native Views.</p>
|
|
*
|
|
* <p>
|
|
* <h2>== Transactional Requirement ==</h2>
|
|
* A requirement of this class is to make sure that transactional UI updates occur all at, meaning
|
|
* that no intermediate state is ever rendered to the screen. For example, if a JS application
|
|
* update changes the background of View A to blue and the width of View B to 100, both need to
|
|
* appear at once. Practically, this means that all UI update code related to a single transaction
|
|
* must be executed as a single code block on the UI thread. Executing as multiple code blocks
|
|
* could allow the platform UI system to interrupt and render a partial UI state.
|
|
* </p>
|
|
*
|
|
* <p>To facilitate this, this module enqueues operations that are then applied to native view
|
|
* hierarchy through {@link NativeViewHierarchyManager} at the end of each transaction.
|
|
*
|
|
* <p>
|
|
* <h2>== CSSNodes ==</h2>
|
|
* In order to allow layout and measurement to occur on a non-UI thread, this module also
|
|
* operates on intermediate CSSNode objects that correspond to a native view. These CSSNode are able
|
|
* to calculate layout according to their styling rules, and then the resulting x/y/width/height of
|
|
* that layout is scheduled as an operation that will be applied to native view hierarchy at the end
|
|
* of current batch.
|
|
* </p>
|
|
*
|
|
* TODO(5241856): Investigate memory usage of creating many small objects in UIManageModule and
|
|
* consider implementing a pool
|
|
* TODO(5483063): Don't dispatch the view hierarchy at the end of a batch if no UI changes occurred
|
|
*/
|
|
public class UIManagerModule extends ReactContextBaseJavaModule implements
|
|
OnBatchCompleteListener, LifecycleEventListener {
|
|
|
|
// Keep in sync with ReactIOSTagHandles JS module - see that file for an explanation on why the
|
|
// increment here is 10
|
|
private static final int ROOT_VIEW_TAG_INCREMENT = 10;
|
|
|
|
private final EventDispatcher mEventDispatcher;
|
|
private final Map<String, Object> mModuleConstants;
|
|
private final UIImplementation mUIImplementation;
|
|
|
|
private int mNextRootViewTag = 1;
|
|
private int mBatchId = 0;
|
|
|
|
public UIManagerModule(
|
|
ReactApplicationContext reactContext,
|
|
List<ViewManager> viewManagerList,
|
|
UIImplementation uiImplementation) {
|
|
super(reactContext);
|
|
mEventDispatcher = new EventDispatcher(reactContext);
|
|
DisplayMetrics displayMetrics = reactContext.getResources().getDisplayMetrics();
|
|
DisplayMetricsHolder.setDisplayMetrics(displayMetrics);
|
|
mModuleConstants = createConstants(displayMetrics, viewManagerList);
|
|
mUIImplementation = uiImplementation;
|
|
|
|
reactContext.addLifecycleEventListener(this);
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
return "RKUIManager";
|
|
}
|
|
|
|
@Override
|
|
public Map<String, Object> getConstants() {
|
|
return mModuleConstants;
|
|
}
|
|
|
|
@Override
|
|
public void onHostResume() {
|
|
mUIImplementation.onHostResume();
|
|
}
|
|
|
|
@Override
|
|
public void onHostPause() {
|
|
mUIImplementation.onHostPause();
|
|
}
|
|
|
|
@Override
|
|
public void onHostDestroy() {
|
|
mUIImplementation.onHostDestroy();
|
|
}
|
|
|
|
@Override
|
|
public void onCatalystInstanceDestroy() {
|
|
super.onCatalystInstanceDestroy();
|
|
mEventDispatcher.onCatalystInstanceDestroyed();
|
|
}
|
|
|
|
private static Map<String, Object> createConstants(
|
|
DisplayMetrics displayMetrics,
|
|
List<ViewManager> viewManagerList) {
|
|
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "CreateUIManagerConstants");
|
|
try {
|
|
return UIManagerModuleConstantsHelper.createConstants(
|
|
displayMetrics,
|
|
viewManagerList);
|
|
} finally {
|
|
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers a new root view. JS can use the returned tag with manageChildren to add/remove
|
|
* children to this view.
|
|
*
|
|
* Note that this must be called after getWidth()/getHeight() actually return something. See
|
|
* CatalystApplicationFragment as an example.
|
|
*
|
|
* TODO(6242243): Make addMeasuredRootView thread safe
|
|
* NB: this method is horribly not-thread-safe, the only reason it works right now is because
|
|
* it's called exactly once and is called before any JS calls are made. As soon as that fact no
|
|
* longer holds, this method will need to be fixed.
|
|
*/
|
|
public int addMeasuredRootView(final SizeMonitoringFrameLayout rootView) {
|
|
final int tag = mNextRootViewTag;
|
|
mNextRootViewTag += ROOT_VIEW_TAG_INCREMENT;
|
|
|
|
final int width;
|
|
final int height;
|
|
// If LayoutParams sets size explicitly, we can use that. Otherwise get the size from the view.
|
|
if (rootView.getLayoutParams() != null &&
|
|
rootView.getLayoutParams().width > 0 &&
|
|
rootView.getLayoutParams().height > 0) {
|
|
width = rootView.getLayoutParams().width;
|
|
height = rootView.getLayoutParams().height;
|
|
} else {
|
|
width = rootView.getWidth();
|
|
height = rootView.getHeight();
|
|
}
|
|
|
|
final ThemedReactContext themedRootContext =
|
|
new ThemedReactContext(getReactApplicationContext(), rootView.getContext());
|
|
|
|
mUIImplementation.registerRootView(rootView, tag, width, height, themedRootContext);
|
|
|
|
rootView.setOnSizeChangedListener(
|
|
new SizeMonitoringFrameLayout.OnSizeChangedListener() {
|
|
@Override
|
|
public void onSizeChanged(final int width, final int height, int oldW, int oldH) {
|
|
getReactApplicationContext().runOnNativeModulesQueueThread(
|
|
new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
updateRootNodeSize(tag, width, height);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return tag;
|
|
}
|
|
|
|
@ReactMethod
|
|
public void removeRootView(int rootViewTag) {
|
|
mUIImplementation.removeRootView(rootViewTag);
|
|
}
|
|
|
|
private void updateRootNodeSize(int rootViewTag, int newWidth, int newHeight) {
|
|
getReactApplicationContext().assertOnNativeModulesQueueThread();
|
|
|
|
mUIImplementation.updateRootNodeSize(rootViewTag, newWidth, newHeight, mEventDispatcher);
|
|
}
|
|
|
|
@ReactMethod
|
|
public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
|
|
mUIImplementation.createView(tag, className, rootViewTag, props);
|
|
}
|
|
|
|
@ReactMethod
|
|
public void updateView(int tag, String className, ReadableMap props) {
|
|
mUIImplementation.updateView(tag, className, props);
|
|
}
|
|
|
|
/**
|
|
* Interface for adding/removing/moving views within a parent view from JS.
|
|
*
|
|
* @param viewTag the view tag of the parent view
|
|
* @param moveFrom a list of indices in the parent view to move views from
|
|
* @param moveTo parallel to moveFrom, a list of indices in the parent view to move views to
|
|
* @param addChildTags a list of tags of views to add to the parent
|
|
* @param addAtIndices parallel to addChildTags, a list of indices to insert those children at
|
|
* @param removeFrom a list of indices of views to permanently remove. The memory for the
|
|
* corresponding views and data structures should be reclaimed.
|
|
*/
|
|
@ReactMethod
|
|
public void manageChildren(
|
|
int viewTag,
|
|
@Nullable ReadableArray moveFrom,
|
|
@Nullable ReadableArray moveTo,
|
|
@Nullable ReadableArray addChildTags,
|
|
@Nullable ReadableArray addAtIndices,
|
|
@Nullable ReadableArray removeFrom) {
|
|
mUIImplementation.manageChildren(
|
|
viewTag,
|
|
moveFrom,
|
|
moveTo,
|
|
addChildTags,
|
|
addAtIndices,
|
|
removeFrom);
|
|
}
|
|
|
|
/**
|
|
* Replaces the View specified by oldTag with the View specified by newTag within oldTag's parent.
|
|
* This resolves to a simple {@link #manageChildren} call, but React doesn't have enough info in
|
|
* JS to formulate it itself.
|
|
*/
|
|
@ReactMethod
|
|
public void replaceExistingNonRootView(int oldTag, int newTag) {
|
|
mUIImplementation.replaceExistingNonRootView(oldTag, newTag);
|
|
}
|
|
|
|
/**
|
|
* Method which takes a container tag and then releases all subviews for that container upon
|
|
* receipt.
|
|
* TODO: The method name is incorrect and will be renamed, #6033872
|
|
* @param containerTag the tag of the container for which the subviews must be removed
|
|
*/
|
|
@ReactMethod
|
|
public void removeSubviewsFromContainerWithID(int containerTag) {
|
|
mUIImplementation.removeSubviewsFromContainerWithID(containerTag);
|
|
}
|
|
|
|
/**
|
|
* Determines the location on screen, width, and height of the given view and returns the values
|
|
* via an async callback.
|
|
*/
|
|
@ReactMethod
|
|
public void measure(int reactTag, Callback callback) {
|
|
mUIImplementation.measure(reactTag, callback);
|
|
}
|
|
|
|
/**
|
|
* Measures the view specified by tag relative to the given ancestorTag. This means that the
|
|
* returned x, y are relative to the origin x, y of the ancestor view. Results are stored in the
|
|
* given outputBuffer. We allow ancestor view and measured view to be the same, in which case
|
|
* the position always will be (0, 0) and method will only measure the view dimensions.
|
|
*
|
|
* NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
|
|
* window which can cause unexpected results when measuring relative to things like ScrollViews
|
|
* that can have offset content on the screen.
|
|
*/
|
|
@ReactMethod
|
|
public void measureLayout(
|
|
int tag,
|
|
int ancestorTag,
|
|
Callback errorCallback,
|
|
Callback successCallback) {
|
|
mUIImplementation.measureLayout(tag, ancestorTag, errorCallback, successCallback);
|
|
}
|
|
|
|
/**
|
|
* Like {@link #measure} and {@link #measureLayout} but measures relative to the immediate parent.
|
|
*
|
|
* NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
|
|
* window which can cause unexpected results when measuring relative to things like ScrollViews
|
|
* that can have offset content on the screen.
|
|
*/
|
|
@ReactMethod
|
|
public void measureLayoutRelativeToParent(
|
|
int tag,
|
|
Callback errorCallback,
|
|
Callback successCallback) {
|
|
mUIImplementation.measureLayoutRelativeToParent(tag, errorCallback, successCallback);
|
|
}
|
|
|
|
/**
|
|
* Find the touch target child native view in the supplied root view hierarchy, given a react
|
|
* target location.
|
|
*
|
|
* This method is currently used only by Element Inspector DevTool.
|
|
*
|
|
* @param reactTag the tag of the root view to traverse
|
|
* @param point an array containing both X and Y target location
|
|
* @param callback will be called if with the identified child view react ID, and measurement
|
|
* info. If no view was found, callback will be invoked with no data.
|
|
*/
|
|
@ReactMethod
|
|
public void findSubviewIn(
|
|
final int reactTag,
|
|
final ReadableArray point,
|
|
final Callback callback) {
|
|
mUIImplementation.findSubviewIn(
|
|
reactTag,
|
|
Math.round(PixelUtil.toPixelFromDIP(point.getDouble(0))),
|
|
Math.round(PixelUtil.toPixelFromDIP(point.getDouble(1))),
|
|
callback);
|
|
}
|
|
|
|
/**
|
|
* Registers a new Animation that can then be added to a View using {@link #addAnimation}.
|
|
*/
|
|
public void registerAnimation(Animation animation) {
|
|
mUIImplementation.registerAnimation(animation);
|
|
}
|
|
|
|
/**
|
|
* Adds an Animation previously registered with {@link #registerAnimation} to a View and starts it
|
|
*/
|
|
public void addAnimation(int reactTag, int animationID, Callback onSuccess) {
|
|
mUIImplementation.addAnimation(reactTag, animationID, onSuccess);
|
|
}
|
|
|
|
/**
|
|
* Removes an existing Animation, canceling it if it was in progress.
|
|
*/
|
|
public void removeAnimation(int reactTag, int animationID) {
|
|
mUIImplementation.removeAnimation(reactTag, animationID);
|
|
}
|
|
|
|
@ReactMethod
|
|
public void setJSResponder(int reactTag, boolean blockNativeResponder) {
|
|
mUIImplementation.setJSResponder(reactTag, blockNativeResponder);
|
|
}
|
|
|
|
@ReactMethod
|
|
public void clearJSResponder() {
|
|
mUIImplementation.clearJSResponder();
|
|
}
|
|
|
|
@ReactMethod
|
|
public void dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs) {
|
|
mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs);
|
|
}
|
|
|
|
/**
|
|
* Show a PopupMenu.
|
|
*
|
|
* @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this
|
|
* needs to be the tag of a native view (shadow views can not be anchors)
|
|
* @param items the menu items as an array of strings
|
|
* @param error will be called if there is an error displaying the menu
|
|
* @param success will be called with the position of the selected item as the first argument, or
|
|
* no arguments if the menu is dismissed
|
|
*/
|
|
@ReactMethod
|
|
public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Callback success) {
|
|
mUIImplementation.showPopupMenu(reactTag, items, error, success);
|
|
}
|
|
|
|
@ReactMethod
|
|
public void configureNextLayoutAnimation(
|
|
ReadableMap config,
|
|
Callback successCallback,
|
|
Callback errorCallback) {
|
|
// TODO(6588266): Implement if required
|
|
}
|
|
|
|
/**
|
|
* To implement the transactional requirement mentioned in the class javadoc, we only commit
|
|
* UI changes to the actual view hierarchy once a batch of JS->Java calls have been completed.
|
|
* We know this is safe because all JS->Java calls that are triggered by a Java->JS call (e.g.
|
|
* the delivery of a touch event or execution of 'renderApplication') end up in a single
|
|
* JS->Java transaction.
|
|
*
|
|
* A better way to do this would be to have JS explicitly signal to this module when a UI
|
|
* transaction is done. Right now, though, this is how iOS does it, and we should probably
|
|
* update the JS and native code and make this change at the same time.
|
|
*
|
|
* TODO(5279396): Make JS UI library explicitly notify the native UI module of the end of a UI
|
|
* transaction using a standard native call
|
|
*/
|
|
@Override
|
|
public void onBatchComplete() {
|
|
int batchId = mBatchId;
|
|
mBatchId++;
|
|
|
|
SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchCompleteUI")
|
|
.arg("BatchId", batchId)
|
|
.flush();
|
|
try {
|
|
mUIImplementation.dispatchViewUpdates(mEventDispatcher, batchId);
|
|
} finally {
|
|
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
|
|
}
|
|
}
|
|
|
|
public void setViewHierarchyUpdateDebugListener(
|
|
@Nullable NotThreadSafeViewHierarchyUpdateDebugListener listener) {
|
|
mUIImplementation.setViewHierarchyUpdateDebugListener(listener);
|
|
}
|
|
|
|
public EventDispatcher getEventDispatcher() {
|
|
return mEventDispatcher;
|
|
}
|
|
|
|
@ReactMethod
|
|
public void sendAccessibilityEvent(int tag, int eventType) {
|
|
mUIImplementation.sendAccessibilityEvent(tag, eventType);
|
|
}
|
|
}
|