mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
a65cd683ae
Summary: Ship responsibility for most View creation logic to ViewManager, where it already largely lies, and simplify code in Fabric and non-Fabric mounting layers. Notably, some of this work was *already* being duplicated so we can expect an extremely tiny perf gain here. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D26742711 fbshipit-source-id: 4213766d4cd366bc69cd47d4654f7b269bb9e7f4
951 lines
35 KiB
Java
951 lines
35 KiB
Java
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* 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.uimanager;
|
|
|
|
import android.graphics.Matrix;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.util.SparseArray;
|
|
import android.util.SparseBooleanArray;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewParent;
|
|
import android.widget.PopupMenu;
|
|
import androidx.annotation.Nullable;
|
|
import com.facebook.common.logging.FLog;
|
|
import com.facebook.react.R;
|
|
import com.facebook.react.bridge.Callback;
|
|
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
|
import com.facebook.react.bridge.ReactContext;
|
|
import com.facebook.react.bridge.ReadableArray;
|
|
import com.facebook.react.bridge.ReadableMap;
|
|
import com.facebook.react.bridge.RetryableMountingLayerException;
|
|
import com.facebook.react.bridge.SoftAssertions;
|
|
import com.facebook.react.bridge.UiThreadUtil;
|
|
import com.facebook.react.common.build.ReactBuildConfig;
|
|
import com.facebook.react.config.ReactFeatureFlags;
|
|
import com.facebook.react.touch.JSResponderHandler;
|
|
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController;
|
|
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationListener;
|
|
import com.facebook.systrace.Systrace;
|
|
import com.facebook.systrace.SystraceMessage;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
import javax.annotation.concurrent.NotThreadSafe;
|
|
|
|
/**
|
|
* Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between
|
|
* native view names used in JS and corresponding instances of {@link ViewManager}. The {@link
|
|
* UIManagerModule} communicates with this class by it's public interface methods:
|
|
*
|
|
* <ul>
|
|
* <li>{@link #updateProperties}
|
|
* <li>{@link #updateLayout}
|
|
* <li>{@link #createView}
|
|
* <li>{@link #manageChildren}
|
|
* </ul>
|
|
*
|
|
* executing all the scheduled UI operations at the end of JS batch.
|
|
*
|
|
* <p>NB: All native view management methods listed above must be called from the UI thread.
|
|
*
|
|
* <p>The {@link ReactContext} instance that is passed to views that this manager creates differs
|
|
* from the one that we pass as a constructor. Instead we wrap the provided instance of {@link
|
|
* ReactContext} in an instance of {@link ThemedReactContext} that additionally provide a correct
|
|
* theme based on the root view for a view tree that we attach newly created view to. Therefore this
|
|
* view manager will create a copy of {@link ThemedReactContext} that wraps the instance of {@link
|
|
* ReactContext} for each root view added to the manager (see {@link #addRootView}).
|
|
*
|
|
* <p>TODO(5483031): Only dispatch updates when shadow views have changed
|
|
*/
|
|
@NotThreadSafe
|
|
public class NativeViewHierarchyManager {
|
|
|
|
private static final String TAG = NativeViewHierarchyManager.class.getSimpleName();
|
|
private final boolean DEBUG_MODE = ReactBuildConfig.DEBUG && false;
|
|
|
|
private final SparseArray<View> mTagsToViews;
|
|
private final SparseArray<ViewManager> mTagsToViewManagers;
|
|
private final SparseBooleanArray mRootTags;
|
|
private final ViewManagerRegistry mViewManagers;
|
|
private final JSResponderHandler mJSResponderHandler = new JSResponderHandler();
|
|
private final RootViewManager mRootViewManager;
|
|
private final LayoutAnimationController mLayoutAnimator = new LayoutAnimationController();
|
|
private final RectF mBoundingBox = new RectF();
|
|
|
|
private boolean mLayoutAnimationEnabled;
|
|
private PopupMenu mPopupMenu;
|
|
private HashMap<Integer, Set<Integer>> mPendingDeletionsForTag;
|
|
|
|
public NativeViewHierarchyManager(ViewManagerRegistry viewManagers) {
|
|
this(viewManagers, new RootViewManager());
|
|
}
|
|
|
|
public NativeViewHierarchyManager(ViewManagerRegistry viewManagers, RootViewManager manager) {
|
|
mViewManagers = viewManagers;
|
|
mTagsToViews = new SparseArray<>();
|
|
mTagsToViewManagers = new SparseArray<>();
|
|
mRootTags = new SparseBooleanArray();
|
|
mRootViewManager = manager;
|
|
}
|
|
|
|
public final synchronized View resolveView(int tag) {
|
|
View view = mTagsToViews.get(tag);
|
|
if (view == null) {
|
|
throw new IllegalViewOperationException(
|
|
"Trying to resolve view with tag " + tag + " which doesn't exist");
|
|
}
|
|
return view;
|
|
}
|
|
|
|
public final synchronized ViewManager resolveViewManager(int tag) {
|
|
ViewManager viewManager = mTagsToViewManagers.get(tag);
|
|
if (viewManager == null) {
|
|
throw new IllegalViewOperationException(
|
|
"ViewManager for tag " + tag + " could not be found.\n");
|
|
}
|
|
return viewManager;
|
|
}
|
|
|
|
public void setLayoutAnimationEnabled(boolean enabled) {
|
|
mLayoutAnimationEnabled = enabled;
|
|
}
|
|
|
|
public synchronized void updateInstanceHandle(int tag, long instanceHandle) {
|
|
UiThreadUtil.assertOnUiThread();
|
|
|
|
try {
|
|
updateInstanceHandle(resolveView(tag), instanceHandle);
|
|
} catch (IllegalViewOperationException e) {
|
|
FLog.e(TAG, "Unable to update properties for view tag " + tag, e);
|
|
}
|
|
}
|
|
|
|
public synchronized void updateProperties(int tag, ReactStylesDiffMap props) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "updateProperties[%d]: %s", tag, props.toString());
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
|
|
try {
|
|
ViewManager viewManager = resolveViewManager(tag);
|
|
View viewToUpdate = resolveView(tag);
|
|
|
|
if (props != null) {
|
|
viewManager.updateProperties(viewToUpdate, props);
|
|
}
|
|
} catch (IllegalViewOperationException e) {
|
|
FLog.e(TAG, "Unable to update properties for view tag " + tag, e);
|
|
}
|
|
}
|
|
|
|
public synchronized void updateViewExtraData(int tag, Object extraData) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "updateViewExtraData[%d]: %s", tag, extraData.toString());
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
|
|
ViewManager viewManager = resolveViewManager(tag);
|
|
View viewToUpdate = resolveView(tag);
|
|
viewManager.updateExtraData(viewToUpdate, extraData);
|
|
}
|
|
|
|
public synchronized void updateLayout(
|
|
int parentTag, int tag, int x, int y, int width, int height) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "updateLayout[%d]->[%d]: %d %d %d %d", tag, parentTag, x, y, width, height);
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
SystraceMessage.beginSection(
|
|
Systrace.TRACE_TAG_REACT_VIEW, "NativeViewHierarchyManager_updateLayout")
|
|
.arg("parentTag", parentTag)
|
|
.arg("tag", tag)
|
|
.flush();
|
|
try {
|
|
View viewToUpdate = resolveView(tag);
|
|
|
|
// Even though we have exact dimensions, we still call measure because some platform views
|
|
// (e.g.
|
|
// Switch) assume that method will always be called before onLayout and onDraw. They use it to
|
|
// calculate and cache information used in the draw pass. For most views, onMeasure can be
|
|
// stubbed out to only call setMeasuredDimensions. For ViewGroups, onLayout should be stubbed
|
|
// out to not recursively call layout on its children: React Native already handles doing
|
|
// that.
|
|
//
|
|
// Also, note measure and layout need to be called *after* all View properties have been
|
|
// updated
|
|
// because of caching and calculation that may occur in onMeasure and onLayout. Layout
|
|
// operations should also follow the native view hierarchy and go top to bottom for
|
|
// consistency
|
|
// with standard layout passes (some views may depend on this).
|
|
|
|
viewToUpdate.measure(
|
|
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
|
|
|
|
// We update the layout of the ReactRootView when there is a change in the layout of its
|
|
// child.
|
|
// This is required to re-measure the size of the native View container (usually a
|
|
// FrameLayout) that is configured with layout_height = WRAP_CONTENT or layout_width =
|
|
// WRAP_CONTENT
|
|
//
|
|
// This code is going to be executed ONLY when there is a change in the size of the Root
|
|
// View defined in the js side. Changes in the layout of inner views will not trigger an
|
|
// update
|
|
// on the layout of the Root View.
|
|
ViewParent parent = viewToUpdate.getParent();
|
|
if (parent instanceof RootView) {
|
|
parent.requestLayout();
|
|
}
|
|
|
|
// Check if the parent of the view has to layout the view, or the child has to lay itself out.
|
|
if (!mRootTags.get(parentTag)) {
|
|
ViewManager parentViewManager = mTagsToViewManagers.get(parentTag);
|
|
IViewManagerWithChildren parentViewManagerWithChildren;
|
|
if (parentViewManager instanceof IViewManagerWithChildren) {
|
|
parentViewManagerWithChildren = (IViewManagerWithChildren) parentViewManager;
|
|
} else {
|
|
throw new IllegalViewOperationException(
|
|
"Trying to use view with tag "
|
|
+ parentTag
|
|
+ " as a parent, but its Manager doesn't implement IViewManagerWithChildren");
|
|
}
|
|
if (parentViewManagerWithChildren != null
|
|
&& !parentViewManagerWithChildren.needsCustomLayoutForChildren()) {
|
|
updateLayout(viewToUpdate, x, y, width, height);
|
|
}
|
|
} else {
|
|
updateLayout(viewToUpdate, x, y, width, height);
|
|
}
|
|
} finally {
|
|
Systrace.endSection(Systrace.TRACE_TAG_REACT_VIEW);
|
|
}
|
|
}
|
|
|
|
private void updateInstanceHandle(View viewToUpdate, long instanceHandle) {
|
|
UiThreadUtil.assertOnUiThread();
|
|
viewToUpdate.setTag(R.id.view_tag_instance_handle, instanceHandle);
|
|
}
|
|
|
|
@Nullable
|
|
public long getInstanceHandle(int reactTag) {
|
|
View view = mTagsToViews.get(reactTag);
|
|
if (view == null) {
|
|
throw new IllegalViewOperationException("Unable to find view for tag: " + reactTag);
|
|
}
|
|
Long instanceHandle = (Long) view.getTag(R.id.view_tag_instance_handle);
|
|
if (instanceHandle == null) {
|
|
throw new IllegalViewOperationException("Unable to find instanceHandle for tag: " + reactTag);
|
|
}
|
|
return instanceHandle;
|
|
}
|
|
|
|
private void updateLayout(View viewToUpdate, int x, int y, int width, int height) {
|
|
if (mLayoutAnimationEnabled && mLayoutAnimator.shouldAnimateLayout(viewToUpdate)) {
|
|
mLayoutAnimator.applyLayoutUpdate(viewToUpdate, x, y, width, height);
|
|
} else {
|
|
viewToUpdate.layout(x, y, x + width, y + height);
|
|
}
|
|
}
|
|
|
|
public synchronized void createView(
|
|
ThemedReactContext themedContext,
|
|
int tag,
|
|
String className,
|
|
@Nullable ReactStylesDiffMap initialProps) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(
|
|
TAG,
|
|
"createView[%d]: %s %s",
|
|
tag,
|
|
className,
|
|
(initialProps != null ? initialProps.toString() : "<null>"));
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
SystraceMessage.beginSection(
|
|
Systrace.TRACE_TAG_REACT_VIEW, "NativeViewHierarchyManager_createView")
|
|
.arg("tag", tag)
|
|
.arg("className", className)
|
|
.flush();
|
|
try {
|
|
ViewManager viewManager = mViewManagers.get(className);
|
|
|
|
View view =
|
|
viewManager.createView(tag, themedContext, initialProps, null, mJSResponderHandler);
|
|
mTagsToViews.put(tag, view);
|
|
mTagsToViewManagers.put(tag, viewManager);
|
|
} finally {
|
|
Systrace.endSection(Systrace.TRACE_TAG_REACT_VIEW);
|
|
}
|
|
}
|
|
|
|
private static String constructManageChildrenErrorMessage(
|
|
ViewGroup viewToManage,
|
|
ViewGroupManager viewManager,
|
|
@Nullable int[] indicesToRemove,
|
|
@Nullable ViewAtIndex[] viewsToAdd,
|
|
@Nullable int[] tagsToDelete) {
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
|
|
if (null != viewToManage) {
|
|
stringBuilder.append(
|
|
"View tag:"
|
|
+ viewToManage.getId()
|
|
+ " View Type:"
|
|
+ viewToManage.getClass().toString()
|
|
+ "\n");
|
|
stringBuilder.append(" children(" + viewManager.getChildCount(viewToManage) + "): [\n");
|
|
for (int index = 0; viewManager.getChildAt(viewToManage, index) != null; index += 16) {
|
|
for (int innerOffset = 0;
|
|
viewManager.getChildAt(viewToManage, index + innerOffset) != null && innerOffset < 16;
|
|
innerOffset++) {
|
|
stringBuilder.append(
|
|
viewManager.getChildAt(viewToManage, index + innerOffset).getId() + ",");
|
|
}
|
|
stringBuilder.append("\n");
|
|
}
|
|
stringBuilder.append(" ],\n");
|
|
}
|
|
|
|
if (indicesToRemove != null) {
|
|
stringBuilder.append(" indicesToRemove(" + indicesToRemove.length + "): [\n");
|
|
for (int index = 0; index < indicesToRemove.length; index += 16) {
|
|
for (int innerOffset = 0;
|
|
((index + innerOffset) < indicesToRemove.length) && innerOffset < 16;
|
|
innerOffset++) {
|
|
stringBuilder.append(indicesToRemove[index + innerOffset] + ",");
|
|
}
|
|
stringBuilder.append("\n");
|
|
}
|
|
stringBuilder.append(" ],\n");
|
|
}
|
|
if (viewsToAdd != null) {
|
|
stringBuilder.append(" viewsToAdd(" + viewsToAdd.length + "): [\n");
|
|
for (int index = 0; index < viewsToAdd.length; index += 16) {
|
|
for (int innerOffset = 0;
|
|
((index + innerOffset) < viewsToAdd.length) && innerOffset < 16;
|
|
innerOffset++) {
|
|
stringBuilder.append(
|
|
"["
|
|
+ viewsToAdd[index + innerOffset].mIndex
|
|
+ ","
|
|
+ viewsToAdd[index + innerOffset].mTag
|
|
+ "],");
|
|
}
|
|
stringBuilder.append("\n");
|
|
}
|
|
stringBuilder.append(" ],\n");
|
|
}
|
|
if (tagsToDelete != null) {
|
|
stringBuilder.append(" tagsToDelete(" + tagsToDelete.length + "): [\n");
|
|
for (int index = 0; index < tagsToDelete.length; index += 16) {
|
|
for (int innerOffset = 0;
|
|
((index + innerOffset) < tagsToDelete.length) && innerOffset < 16;
|
|
innerOffset++) {
|
|
stringBuilder.append(tagsToDelete[index + innerOffset] + ",");
|
|
}
|
|
stringBuilder.append("\n");
|
|
}
|
|
stringBuilder.append(" ]\n");
|
|
}
|
|
|
|
return stringBuilder.toString();
|
|
}
|
|
|
|
private Set<Integer> getPendingDeletionsForTag(int tag) {
|
|
if (mPendingDeletionsForTag == null) {
|
|
mPendingDeletionsForTag = new HashMap<>();
|
|
}
|
|
|
|
if (!mPendingDeletionsForTag.containsKey(tag)) {
|
|
mPendingDeletionsForTag.put(tag, new HashSet<Integer>());
|
|
}
|
|
|
|
return mPendingDeletionsForTag.get(tag);
|
|
}
|
|
|
|
/**
|
|
* @param tag react tag of the node we want to manage
|
|
* @param indicesToRemove ordered (asc) list of indicies at which view should be removed
|
|
* @param viewsToAdd ordered (asc based on mIndex property) list of tag-index pairs that represent
|
|
* a view which should be added at the specified index
|
|
* @param tagsToDelete list of tags corresponding to views that should be removed
|
|
*/
|
|
public synchronized void manageChildren(
|
|
final int tag,
|
|
@Nullable int[] indicesToRemove,
|
|
@Nullable ViewAtIndex[] viewsToAdd,
|
|
@Nullable int[] tagsToDelete) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(
|
|
TAG,
|
|
"createView[%d]: %s %s %s",
|
|
tag,
|
|
(indicesToRemove != null ? indicesToRemove.toString() : "<null>"),
|
|
(viewsToAdd != null ? viewsToAdd.toString() : "<null>"),
|
|
(tagsToDelete != null ? tagsToDelete.toString() : "<null>"));
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
|
|
final Set<Integer> pendingDeletionTags = getPendingDeletionsForTag(tag);
|
|
final ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag);
|
|
final ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag);
|
|
if (viewToManage == null) {
|
|
throw new IllegalViewOperationException(
|
|
"Trying to manageChildren view with tag "
|
|
+ tag
|
|
+ " which doesn't exist\n detail: "
|
|
+ constructManageChildrenErrorMessage(
|
|
viewToManage, viewManager, indicesToRemove, viewsToAdd, tagsToDelete));
|
|
}
|
|
|
|
int lastIndexToRemove = viewManager.getChildCount(viewToManage);
|
|
|
|
if (indicesToRemove != null) {
|
|
for (int i = indicesToRemove.length - 1; i >= 0; i--) {
|
|
int indexToRemove = indicesToRemove[i];
|
|
if (indexToRemove < 0) {
|
|
throw new IllegalViewOperationException(
|
|
"Trying to remove a negative view index:"
|
|
+ indexToRemove
|
|
+ " view tag: "
|
|
+ tag
|
|
+ "\n detail: "
|
|
+ constructManageChildrenErrorMessage(
|
|
viewToManage, viewManager, indicesToRemove, viewsToAdd, tagsToDelete));
|
|
}
|
|
if (viewManager.getChildAt(viewToManage, indexToRemove) == null) {
|
|
if (mRootTags.get(tag) && viewManager.getChildCount(viewToManage) == 0) {
|
|
// This root node has already been removed (likely due to a threading issue caused by
|
|
// async js execution). Ignore this root removal.
|
|
return;
|
|
}
|
|
throw new IllegalViewOperationException(
|
|
"Trying to remove a view index above child "
|
|
+ "count "
|
|
+ indexToRemove
|
|
+ " view tag: "
|
|
+ tag
|
|
+ "\n detail: "
|
|
+ constructManageChildrenErrorMessage(
|
|
viewToManage, viewManager, indicesToRemove, viewsToAdd, tagsToDelete));
|
|
}
|
|
if (indexToRemove >= lastIndexToRemove) {
|
|
throw new IllegalViewOperationException(
|
|
"Trying to remove an out of order view index:"
|
|
+ indexToRemove
|
|
+ " view tag: "
|
|
+ tag
|
|
+ "\n detail: "
|
|
+ constructManageChildrenErrorMessage(
|
|
viewToManage, viewManager, indicesToRemove, viewsToAdd, tagsToDelete));
|
|
}
|
|
|
|
View viewToRemove = viewManager.getChildAt(viewToManage, indexToRemove);
|
|
|
|
if (mLayoutAnimationEnabled
|
|
&& mLayoutAnimator.shouldAnimateLayout(viewToRemove)
|
|
&& arrayContains(tagsToDelete, viewToRemove.getId())) {
|
|
// The view will be removed and dropped by the 'delete' layout animation
|
|
// instead, so do nothing
|
|
} else {
|
|
viewManager.removeViewAt(viewToManage, indexToRemove);
|
|
}
|
|
|
|
lastIndexToRemove = indexToRemove;
|
|
}
|
|
}
|
|
|
|
if (tagsToDelete != null) {
|
|
for (int i = 0; i < tagsToDelete.length; i++) {
|
|
int tagToDelete = tagsToDelete[i];
|
|
final View viewToDestroy = mTagsToViews.get(tagToDelete);
|
|
if (viewToDestroy == null) {
|
|
throw new IllegalViewOperationException(
|
|
"Trying to destroy unknown view tag: "
|
|
+ tagToDelete
|
|
+ "\n detail: "
|
|
+ constructManageChildrenErrorMessage(
|
|
viewToManage, viewManager, indicesToRemove, viewsToAdd, tagsToDelete));
|
|
}
|
|
|
|
if (mLayoutAnimationEnabled && mLayoutAnimator.shouldAnimateLayout(viewToDestroy)) {
|
|
pendingDeletionTags.add(tagToDelete);
|
|
mLayoutAnimator.deleteView(
|
|
viewToDestroy,
|
|
new LayoutAnimationListener() {
|
|
@Override
|
|
public void onAnimationEnd() {
|
|
// This should be called only on the UI thread, because
|
|
// onAnimationEnd is called (indirectly) by Android View Animation.
|
|
UiThreadUtil.assertOnUiThread();
|
|
|
|
viewManager.removeView(viewToManage, viewToDestroy);
|
|
dropView(viewToDestroy);
|
|
pendingDeletionTags.remove(viewToDestroy.getId());
|
|
if (pendingDeletionTags.isEmpty()) {
|
|
mPendingDeletionsForTag.remove(tag);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
dropView(viewToDestroy);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (viewsToAdd != null) {
|
|
for (int i = 0; i < viewsToAdd.length; i++) {
|
|
ViewAtIndex viewAtIndex = viewsToAdd[i];
|
|
View viewToAdd = mTagsToViews.get(viewAtIndex.mTag);
|
|
if (viewToAdd == null) {
|
|
throw new IllegalViewOperationException(
|
|
"Trying to add unknown view tag: "
|
|
+ viewAtIndex.mTag
|
|
+ "\n detail: "
|
|
+ constructManageChildrenErrorMessage(
|
|
viewToManage, viewManager, indicesToRemove, viewsToAdd, tagsToDelete));
|
|
}
|
|
|
|
int normalizedIndex = viewAtIndex.mIndex;
|
|
if (!pendingDeletionTags.isEmpty()) {
|
|
normalizedIndex = 0;
|
|
int counter = 0;
|
|
while (normalizedIndex < viewToManage.getChildCount()) {
|
|
if (counter == viewAtIndex.mIndex) {
|
|
break;
|
|
}
|
|
View v = viewToManage.getChildAt(normalizedIndex);
|
|
if (!pendingDeletionTags.contains(v.getId())) {
|
|
counter++;
|
|
}
|
|
normalizedIndex++;
|
|
}
|
|
}
|
|
|
|
viewManager.addView(viewToManage, viewToAdd, normalizedIndex);
|
|
}
|
|
}
|
|
if (pendingDeletionTags.isEmpty()) {
|
|
mPendingDeletionsForTag.remove(tag);
|
|
}
|
|
}
|
|
|
|
private boolean arrayContains(@Nullable int[] array, int ele) {
|
|
if (array == null) {
|
|
return false;
|
|
}
|
|
for (int curEle : array) {
|
|
if (curEle == ele) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Simplified version of constructManageChildrenErrorMessage that only deals with adding children
|
|
* views
|
|
*/
|
|
private static String constructSetChildrenErrorMessage(
|
|
ViewGroup viewToManage, ViewGroupManager viewManager, ReadableArray childrenTags) {
|
|
ViewAtIndex[] viewsToAdd = new ViewAtIndex[childrenTags.size()];
|
|
for (int i = 0; i < childrenTags.size(); i++) {
|
|
viewsToAdd[i] = new ViewAtIndex(childrenTags.getInt(i), i);
|
|
}
|
|
return constructManageChildrenErrorMessage(viewToManage, viewManager, null, viewsToAdd, null);
|
|
}
|
|
|
|
/** Simplified version of manageChildren that only deals with adding children views */
|
|
public synchronized void setChildren(int tag, ReadableArray childrenTags) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(
|
|
TAG,
|
|
"setChildren[%d]: %s",
|
|
tag,
|
|
(childrenTags != null ? childrenTags.toString() : "<null>"));
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag);
|
|
ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag);
|
|
|
|
for (int i = 0; i < childrenTags.size(); i++) {
|
|
View viewToAdd = mTagsToViews.get(childrenTags.getInt(i));
|
|
if (viewToAdd == null) {
|
|
throw new IllegalViewOperationException(
|
|
"Trying to add unknown view tag: "
|
|
+ childrenTags.getInt(i)
|
|
+ "\n detail: "
|
|
+ constructSetChildrenErrorMessage(viewToManage, viewManager, childrenTags));
|
|
}
|
|
viewManager.addView(viewToManage, viewToAdd, i);
|
|
}
|
|
}
|
|
|
|
/** See {@link UIManagerModule#addRootView}. */
|
|
public synchronized void addRootView(int tag, View view) {
|
|
addRootViewGroup(tag, view);
|
|
}
|
|
|
|
protected final synchronized void addRootViewGroup(int tag, View view) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "addRootViewGroup[%d]: %s", tag, (view != null ? view.toString() : "<null>"));
|
|
}
|
|
if (view.getId() != View.NO_ID) {
|
|
FLog.e(
|
|
TAG,
|
|
"Trying to add a root view with an explicit id ("
|
|
+ view.getId()
|
|
+ ") already "
|
|
+ "set. React Native uses the id field to track react tags and will overwrite this field. "
|
|
+ "If that is fine, explicitly overwrite the id field to View.NO_ID before calling "
|
|
+ "addRootView.");
|
|
}
|
|
|
|
mTagsToViews.put(tag, view);
|
|
mTagsToViewManagers.put(tag, mRootViewManager);
|
|
mRootTags.put(tag, true);
|
|
view.setId(tag);
|
|
}
|
|
|
|
/** Releases all references to given native View. */
|
|
protected synchronized void dropView(View view) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "dropView[%d]", (view != null ? view.getId() : -1));
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
if (view == null) {
|
|
// Ignore this drop operation when view is null.
|
|
return;
|
|
}
|
|
if (mTagsToViewManagers.get(view.getId()) == null) {
|
|
// This view has already been dropped (likely due to a threading issue caused by async js
|
|
// execution). Ignore this drop operation.
|
|
return;
|
|
}
|
|
if (!mRootTags.get(view.getId())) {
|
|
// For non-root views we notify viewmanager with {@link ViewManager#onDropInstance}
|
|
resolveViewManager(view.getId()).onDropViewInstance(view);
|
|
}
|
|
ViewManager viewManager = mTagsToViewManagers.get(view.getId());
|
|
if (view instanceof ViewGroup && viewManager instanceof ViewGroupManager) {
|
|
ViewGroup viewGroup = (ViewGroup) view;
|
|
ViewGroupManager viewGroupManager = (ViewGroupManager) viewManager;
|
|
for (int i = viewGroupManager.getChildCount(viewGroup) - 1; i >= 0; i--) {
|
|
View child = viewGroupManager.getChildAt(viewGroup, i);
|
|
if (child == null) {
|
|
FLog.e(TAG, "Unable to drop null child view");
|
|
} else if (mTagsToViews.get(child.getId()) != null) {
|
|
dropView(child);
|
|
}
|
|
}
|
|
viewGroupManager.removeAllViews(viewGroup);
|
|
}
|
|
mTagsToViews.remove(view.getId());
|
|
mTagsToViewManagers.remove(view.getId());
|
|
}
|
|
|
|
public synchronized void removeRootView(int rootViewTag) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "removeRootView[%d]", rootViewTag);
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
if (!mRootTags.get(rootViewTag)) {
|
|
SoftAssertions.assertUnreachable(
|
|
"View with tag " + rootViewTag + " is not registered as a root view");
|
|
}
|
|
View rootView = mTagsToViews.get(rootViewTag);
|
|
dropView(rootView);
|
|
mRootTags.delete(rootViewTag);
|
|
}
|
|
|
|
/**
|
|
* Returns true on success, false on failure. If successful, after calling, output buffer will be
|
|
* {x, y, width, height}.
|
|
*/
|
|
public synchronized void measure(int tag, int[] outputBuffer) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "measure[%d]", tag);
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
View v = mTagsToViews.get(tag);
|
|
if (v == null) {
|
|
throw new NoSuchNativeViewException("No native view for " + tag + " currently exists");
|
|
}
|
|
|
|
View rootView = (View) RootViewUtil.getRootView(v);
|
|
// It is possible that the RootView can't be found because this view is no longer on the screen
|
|
// and has been removed by clipping
|
|
if (rootView == null) {
|
|
throw new NoSuchNativeViewException("Native view " + tag + " is no longer on screen");
|
|
}
|
|
computeBoundingBox(rootView, outputBuffer);
|
|
int rootX = outputBuffer[0];
|
|
int rootY = outputBuffer[1];
|
|
computeBoundingBox(v, outputBuffer);
|
|
outputBuffer[0] -= rootX;
|
|
outputBuffer[1] -= rootY;
|
|
}
|
|
|
|
private void computeBoundingBox(View view, int[] outputBuffer) {
|
|
mBoundingBox.set(0, 0, view.getWidth(), view.getHeight());
|
|
mapRectFromViewToWindowCoords(view, mBoundingBox);
|
|
|
|
outputBuffer[0] = Math.round(mBoundingBox.left);
|
|
outputBuffer[1] = Math.round(mBoundingBox.top);
|
|
outputBuffer[2] = Math.round(mBoundingBox.right - mBoundingBox.left);
|
|
outputBuffer[3] = Math.round(mBoundingBox.bottom - mBoundingBox.top);
|
|
}
|
|
|
|
private void mapRectFromViewToWindowCoords(View view, RectF rect) {
|
|
Matrix matrix = view.getMatrix();
|
|
if (!matrix.isIdentity()) {
|
|
matrix.mapRect(rect);
|
|
}
|
|
|
|
rect.offset(view.getLeft(), view.getTop());
|
|
|
|
ViewParent parent = view.getParent();
|
|
while (parent instanceof View) {
|
|
View parentView = (View) parent;
|
|
|
|
rect.offset(-parentView.getScrollX(), -parentView.getScrollY());
|
|
|
|
matrix = parentView.getMatrix();
|
|
if (!matrix.isIdentity()) {
|
|
matrix.mapRect(rect);
|
|
}
|
|
|
|
rect.offset(parentView.getLeft(), parentView.getTop());
|
|
|
|
parent = parentView.getParent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the coordinates of a view relative to the window (not just the RootView which is what
|
|
* measure will return)
|
|
*
|
|
* @param tag - the tag for the view
|
|
* @param outputBuffer - output buffer that contains [x,y,width,height] of the view in coordinates
|
|
* relative to the device window
|
|
*/
|
|
public synchronized void measureInWindow(int tag, int[] outputBuffer) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "measureInWindow[%d]", tag);
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
View v = mTagsToViews.get(tag);
|
|
if (v == null) {
|
|
throw new NoSuchNativeViewException("No native view for " + tag + " currently exists");
|
|
}
|
|
|
|
v.getLocationOnScreen(outputBuffer);
|
|
|
|
// we need to subtract visibleWindowCoords - to subtract possible window insets, split screen or
|
|
// multi window
|
|
Rect visibleWindowFrame = new Rect();
|
|
v.getWindowVisibleDisplayFrame(visibleWindowFrame);
|
|
outputBuffer[0] = outputBuffer[0] - visibleWindowFrame.left;
|
|
outputBuffer[1] = outputBuffer[1] - visibleWindowFrame.top;
|
|
|
|
// outputBuffer[0,1] already contain what we want
|
|
outputBuffer[2] = v.getWidth();
|
|
outputBuffer[3] = v.getHeight();
|
|
}
|
|
|
|
public synchronized int findTargetTagForTouch(int reactTag, float touchX, float touchY) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(TAG, "findTargetTagForTouch[%d]: %f %f", reactTag, touchX, touchY);
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
View view = mTagsToViews.get(reactTag);
|
|
if (view == null) {
|
|
throw new JSApplicationIllegalArgumentException("Could not find view with tag " + reactTag);
|
|
}
|
|
return TouchTargetHelper.findTargetTagForTouch(touchX, touchY, (ViewGroup) view);
|
|
}
|
|
|
|
public synchronized void setJSResponder(
|
|
int reactTag, int initialReactTag, boolean blockNativeResponder) {
|
|
if (!blockNativeResponder) {
|
|
mJSResponderHandler.setJSResponder(initialReactTag, null);
|
|
return;
|
|
}
|
|
|
|
View view = mTagsToViews.get(reactTag);
|
|
if (initialReactTag != reactTag && view instanceof ViewParent) {
|
|
// In this case, initialReactTag corresponds to a virtual/layout-only View, and we already
|
|
// have a parent of that View in reactTag, so we can use it.
|
|
mJSResponderHandler.setJSResponder(initialReactTag, (ViewParent) view);
|
|
return;
|
|
}
|
|
|
|
if (mRootTags.get(reactTag)) {
|
|
SoftAssertions.assertUnreachable(
|
|
"Cannot block native responder on " + reactTag + " that is a root view");
|
|
}
|
|
mJSResponderHandler.setJSResponder(initialReactTag, view.getParent());
|
|
}
|
|
|
|
public void clearJSResponder() {
|
|
mJSResponderHandler.clearJSResponder();
|
|
}
|
|
|
|
void configureLayoutAnimation(final ReadableMap config, final Callback onAnimationComplete) {
|
|
mLayoutAnimator.initializeFromConfig(config, onAnimationComplete);
|
|
}
|
|
|
|
void clearLayoutAnimation() {
|
|
mLayoutAnimator.reset();
|
|
}
|
|
|
|
@Deprecated
|
|
public synchronized void dispatchCommand(
|
|
int reactTag, int commandId, @Nullable ReadableArray args) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(
|
|
TAG,
|
|
"dispatchCommand[%d]: %d %s",
|
|
reactTag,
|
|
commandId,
|
|
(args != null ? args.toString() : "<null>"));
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
View view = mTagsToViews.get(reactTag);
|
|
if (view == null) {
|
|
throw new RetryableMountingLayerException(
|
|
"Trying to send command to a non-existing view with tag ["
|
|
+ reactTag
|
|
+ "] and command "
|
|
+ commandId);
|
|
}
|
|
ViewManager viewManager = resolveViewManager(reactTag);
|
|
viewManager.receiveCommand(view, commandId, args);
|
|
}
|
|
|
|
public synchronized void dispatchCommand(
|
|
int reactTag, String commandId, @Nullable ReadableArray args) {
|
|
if (DEBUG_MODE) {
|
|
FLog.d(
|
|
TAG,
|
|
"dispatchCommand[%d]: %s %s",
|
|
reactTag,
|
|
commandId,
|
|
(args != null ? args.toString() : "<null>"));
|
|
}
|
|
UiThreadUtil.assertOnUiThread();
|
|
View view = mTagsToViews.get(reactTag);
|
|
if (view == null) {
|
|
throw new RetryableMountingLayerException(
|
|
"Trying to send command to a non-existing view with tag ["
|
|
+ reactTag
|
|
+ "] and command "
|
|
+ commandId);
|
|
}
|
|
ViewManager viewManager = resolveViewManager(reactTag);
|
|
ViewManagerDelegate delegate;
|
|
if (ReactFeatureFlags.useViewManagerDelegatesForCommands
|
|
&& (delegate = viewManager.getDelegate()) != null) {
|
|
delegate.receiveCommand(view, commandId, args);
|
|
} else {
|
|
viewManager.receiveCommand(view, commandId, args);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show a {@link 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 success will be called with the position of the selected item as the first argument, or
|
|
* no arguments if the menu is dismissed
|
|
*/
|
|
public synchronized void showPopupMenu(
|
|
int reactTag, ReadableArray items, Callback success, Callback error) {
|
|
UiThreadUtil.assertOnUiThread();
|
|
View anchor = mTagsToViews.get(reactTag);
|
|
if (anchor == null) {
|
|
error.invoke("Can't display popup. Could not find view with tag " + reactTag);
|
|
return;
|
|
}
|
|
mPopupMenu = new PopupMenu(getReactContextForView(reactTag), anchor);
|
|
|
|
Menu menu = mPopupMenu.getMenu();
|
|
for (int i = 0; i < items.size(); i++) {
|
|
menu.add(Menu.NONE, Menu.NONE, i, items.getString(i));
|
|
}
|
|
|
|
PopupMenuCallbackHandler handler = new PopupMenuCallbackHandler(success);
|
|
mPopupMenu.setOnMenuItemClickListener(handler);
|
|
mPopupMenu.setOnDismissListener(handler);
|
|
|
|
mPopupMenu.show();
|
|
}
|
|
|
|
/** Dismiss the last opened PopupMenu {@link PopupMenu}. */
|
|
public void dismissPopupMenu() {
|
|
if (mPopupMenu != null) {
|
|
mPopupMenu.dismiss();
|
|
}
|
|
}
|
|
|
|
private static class PopupMenuCallbackHandler
|
|
implements PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
|
|
|
|
final Callback mSuccess;
|
|
boolean mConsumed = false;
|
|
|
|
private PopupMenuCallbackHandler(Callback success) {
|
|
mSuccess = success;
|
|
}
|
|
|
|
@Override
|
|
public void onDismiss(PopupMenu menu) {
|
|
if (!mConsumed) {
|
|
mSuccess.invoke(UIManagerModuleConstants.ACTION_DISMISSED);
|
|
mConsumed = true;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onMenuItemClick(MenuItem item) {
|
|
if (!mConsumed) {
|
|
mSuccess.invoke(UIManagerModuleConstants.ACTION_ITEM_SELECTED, item.getOrder());
|
|
mConsumed = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Themed React context for view with a given {@param reactTag} - it gets the context
|
|
* directly from the view using {@link View#getContext}.
|
|
*/
|
|
private ThemedReactContext getReactContextForView(int reactTag) {
|
|
View view = mTagsToViews.get(reactTag);
|
|
if (view == null) {
|
|
throw new JSApplicationIllegalArgumentException("Could not find view with tag " + reactTag);
|
|
}
|
|
return (ThemedReactContext) view.getContext();
|
|
}
|
|
|
|
public void sendAccessibilityEvent(int tag, int eventType) {
|
|
View view = mTagsToViews.get(tag);
|
|
if (view == null) {
|
|
throw new JSApplicationIllegalArgumentException("Could not find view with tag " + tag);
|
|
}
|
|
view.sendAccessibilityEvent(eventType);
|
|
}
|
|
}
|