com.facebook.react.uimanager.UIImplementation.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.react.uimanager.UIImplementation.java

Source

/*
 * 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.os.SystemClock;
import android.view.View;
import android.view.View.MeasureSpec;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.modules.i18nmanager.I18nUtil;
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.systrace.Systrace;
import com.facebook.systrace.SystraceMessage;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaDirection;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * A class that is used to receive React commands from JS and translate them into a shadow node
 * hierarchy that is then mapped to a native view hierarchy.
 */
public class UIImplementation {
    protected Object uiImplementationThreadLock = new Object();

    protected final EventDispatcher mEventDispatcher;
    protected final ReactApplicationContext mReactContext;
    protected final ShadowNodeRegistry mShadowNodeRegistry = new ShadowNodeRegistry();
    private final ViewManagerRegistry mViewManagers;
    private final UIViewOperationQueue mOperationsQueue;
    private final NativeViewHierarchyOptimizer mNativeViewHierarchyOptimizer;
    private final int[] mMeasureBuffer = new int[4];

    private long mLastCalculateLayoutTime = 0;
    protected @Nullable LayoutUpdateListener mLayoutUpdateListener;

    /** Interface definition for a callback to be invoked when the layout has been updated */
    public interface LayoutUpdateListener {

        /** Called when the layout has been updated */
        void onLayoutUpdated(ReactShadowNode root);
    }

    public UIImplementation(ReactApplicationContext reactContext,
            UIManagerModule.ViewManagerResolver viewManagerResolver, EventDispatcher eventDispatcher,
            int minTimeLeftInFrameForNonBatchedOperationMs) {
        this(reactContext, new ViewManagerRegistry(viewManagerResolver), eventDispatcher,
                minTimeLeftInFrameForNonBatchedOperationMs);
    }

    public UIImplementation(ReactApplicationContext reactContext, List<ViewManager> viewManagers,
            EventDispatcher eventDispatcher, int minTimeLeftInFrameForNonBatchedOperationMs) {
        this(reactContext, new ViewManagerRegistry(viewManagers), eventDispatcher,
                minTimeLeftInFrameForNonBatchedOperationMs);
    }

    UIImplementation(ReactApplicationContext reactContext, ViewManagerRegistry viewManagers,
            EventDispatcher eventDispatcher, int minTimeLeftInFrameForNonBatchedOperationMs) {
        this(reactContext, viewManagers, new UIViewOperationQueue(reactContext,
                new NativeViewHierarchyManager(viewManagers), minTimeLeftInFrameForNonBatchedOperationMs),
                eventDispatcher);
    }

    protected UIImplementation(ReactApplicationContext reactContext, ViewManagerRegistry viewManagers,
            UIViewOperationQueue operationsQueue, EventDispatcher eventDispatcher) {
        mReactContext = reactContext;
        mViewManagers = viewManagers;
        mOperationsQueue = operationsQueue;
        mNativeViewHierarchyOptimizer = new NativeViewHierarchyOptimizer(mOperationsQueue, mShadowNodeRegistry);
        mEventDispatcher = eventDispatcher;
    }

    protected ReactShadowNode createRootShadowNode() {
        ReactShadowNode rootCSSNode = new ReactShadowNodeImpl();
        I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
        if (sharedI18nUtilInstance.isRTL(mReactContext)) {
            rootCSSNode.setLayoutDirection(YogaDirection.RTL);
        }
        rootCSSNode.setViewClassName("Root");
        return rootCSSNode;
    }

    protected ReactShadowNode createShadowNode(String className) {
        ViewManager viewManager = mViewManagers.get(className);
        return viewManager.createShadowNodeInstance(mReactContext);
    }

    public final ReactShadowNode resolveShadowNode(int reactTag) {
        return mShadowNodeRegistry.getNode(reactTag);
    }

    protected final ViewManager resolveViewManager(String className) {
        return mViewManagers.get(className);
    }

    /*package*/ UIViewOperationQueue getUIViewOperationQueue() {
        return mOperationsQueue;
    }

    /**
     * Updates the styles of the {@link ReactShadowNode} based on the Measure specs received by
     * parameters.
     */
    public void updateRootView(int tag, int widthMeasureSpec, int heightMeasureSpec) {
        ReactShadowNode rootCSSNode = mShadowNodeRegistry.getNode(tag);
        if (rootCSSNode == null) {
            FLog.w(ReactConstants.TAG, "Tried to update non-existent root tag: " + tag);
            return;
        }
        updateRootView(rootCSSNode, widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * Updates the styles of the {@link ReactShadowNode} based on the Measure specs received by
     * parameters.
     */
    public void updateRootView(ReactShadowNode rootCSSNode, int widthMeasureSpec, int heightMeasureSpec) {
        rootCSSNode.setMeasureSpecs(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * Registers a root node with a given tag, size and ThemedReactContext and adds it to a node
     * registry.
     */
    public <T extends View> void registerRootView(T rootView, int tag, ThemedReactContext context) {
        synchronized (uiImplementationThreadLock) {
            final ReactShadowNode rootCSSNode = createRootShadowNode();
            rootCSSNode.setReactTag(tag); // Thread safety needed here
            rootCSSNode.setThemedContext(context);

            context.runOnNativeModulesQueueThread(new Runnable() {
                @Override
                public void run() {
                    mShadowNodeRegistry.addRootNode(rootCSSNode);
                }
            });

            // register it within NativeViewHierarchyManager
            mOperationsQueue.addRootView(tag, rootView);
        }
    }

    /** Unregisters a root node with a given tag. */
    public void removeRootView(int rootViewTag) {
        removeRootShadowNode(rootViewTag);
        mOperationsQueue.enqueueRemoveRootView(rootViewTag);
    }

    /** Unregisters a root node with a given tag from the shadow node registry */
    public void removeRootShadowNode(int rootViewTag) {
        synchronized (uiImplementationThreadLock) {
            mShadowNodeRegistry.removeRootNode(rootViewTag); // Thread safety needed here
        }
    }

    /**
     * Invoked when native view that corresponds to a root node, or acts as a root view (ie. Modals)
     * has its size changed.
     */
    public void updateNodeSize(int nodeViewTag, int newWidth, int newHeight) {
        ReactShadowNode cssNode = mShadowNodeRegistry.getNode(nodeViewTag);
        if (cssNode == null) {
            FLog.w(ReactConstants.TAG, "Tried to update size of non-existent tag: " + nodeViewTag);
            return;
        }
        cssNode.setStyleWidth(newWidth);
        cssNode.setStyleHeight(newHeight);

        dispatchViewUpdatesIfNeeded();
    }

    public void setViewLocalData(int tag, Object data) {
        ReactShadowNode shadowNode = mShadowNodeRegistry.getNode(tag);

        if (shadowNode == null) {
            FLog.w(ReactConstants.TAG, "Attempt to set local data for view with unknown tag: " + tag);
            return;
        }

        shadowNode.setLocalData(data);

        dispatchViewUpdatesIfNeeded();
    }

    public void profileNextBatch() {
        mOperationsQueue.profileNextBatch();
    }

    public Map<String, Long> getProfiledBatchPerfCounters() {
        return mOperationsQueue.getProfiledBatchPerfCounters();
    }

    /** Invoked by React to create a new node with a given tag, class name and properties. */
    public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
        synchronized (uiImplementationThreadLock) {
            ReactShadowNode cssNode = createShadowNode(className);
            ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);
            Assertions.assertNotNull(rootNode, "Root node with tag " + rootViewTag + " doesn't exist");
            cssNode.setReactTag(tag); // Thread safety needed here
            cssNode.setViewClassName(className);
            cssNode.setRootTag(rootNode.getReactTag());
            cssNode.setThemedContext(rootNode.getThemedContext());

            mShadowNodeRegistry.addNode(cssNode);

            ReactStylesDiffMap styles = null;
            if (props != null) {
                styles = new ReactStylesDiffMap(props);
                cssNode.updateProperties(styles);
            }

            handleCreateView(cssNode, rootViewTag, styles);
        }
    }

    protected void handleCreateView(ReactShadowNode cssNode, int rootViewTag, @Nullable ReactStylesDiffMap styles) {
        if (!cssNode.isVirtual()) {
            mNativeViewHierarchyOptimizer.handleCreateView(cssNode, cssNode.getThemedContext(), styles);
        }
    }

    /** Invoked by React to create a new node with a given tag has its properties changed. */
    public void updateView(int tag, String className, ReadableMap props) {
        ViewManager viewManager = mViewManagers.get(className);
        if (viewManager == null) {
            throw new IllegalViewOperationException("Got unknown view type: " + className);
        }
        ReactShadowNode cssNode = mShadowNodeRegistry.getNode(tag);
        if (cssNode == null) {
            throw new IllegalViewOperationException("Trying to update non-existent view with tag " + tag);
        }

        if (props != null) {
            ReactStylesDiffMap styles = new ReactStylesDiffMap(props);
            cssNode.updateProperties(styles);
            handleUpdateView(cssNode, className, styles);
        }
    }

    /**
     * Used by native animated module to bypass the process of updating the values through the shadow
     * view hierarchy. This method will directly update native views, which means that updates for
     * layout-related propertied won't be handled properly. Make sure you know what you're doing
     * before calling this method :)
     */
    public void synchronouslyUpdateViewOnUIThread(int tag, ReactStylesDiffMap props) {
        UiThreadUtil.assertOnUiThread();
        mOperationsQueue.getNativeViewHierarchyManager().updateProperties(tag, props);
    }

    protected void handleUpdateView(ReactShadowNode cssNode, String className, ReactStylesDiffMap styles) {
        if (!cssNode.isVirtual()) {
            mNativeViewHierarchyOptimizer.handleUpdateView(cssNode, className, styles);
        }
    }

    /**
     * Invoked when there is a mutation in a node tree.
     *
     * @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 void manageChildren(int viewTag, @Nullable ReadableArray moveFrom, @Nullable ReadableArray moveTo,
            @Nullable ReadableArray addChildTags, @Nullable ReadableArray addAtIndices,
            @Nullable ReadableArray removeFrom) {
        synchronized (uiImplementationThreadLock) {
            ReactShadowNode cssNodeToManage = mShadowNodeRegistry.getNode(viewTag);

            int numToMove = moveFrom == null ? 0 : moveFrom.size();
            int numToAdd = addChildTags == null ? 0 : addChildTags.size();
            int numToRemove = removeFrom == null ? 0 : removeFrom.size();

            if (numToMove != 0 && (moveTo == null || numToMove != moveTo.size())) {
                throw new IllegalViewOperationException("Size of moveFrom != size of moveTo!");
            }

            if (numToAdd != 0 && (addAtIndices == null || numToAdd != addAtIndices.size())) {
                throw new IllegalViewOperationException("Size of addChildTags != size of addAtIndices!");
            }

            // We treat moves as an add and a delete
            ViewAtIndex[] viewsToAdd = new ViewAtIndex[numToMove + numToAdd];
            int[] indicesToRemove = new int[numToMove + numToRemove];
            int[] tagsToRemove = new int[indicesToRemove.length];
            int[] tagsToDelete = new int[numToRemove];
            int[] indicesToDelete = new int[numToRemove];

            if (numToMove > 0) {
                Assertions.assertNotNull(moveFrom);
                Assertions.assertNotNull(moveTo);
                for (int i = 0; i < numToMove; i++) {
                    int moveFromIndex = moveFrom.getInt(i);
                    int tagToMove = cssNodeToManage.getChildAt(moveFromIndex).getReactTag();
                    viewsToAdd[i] = new ViewAtIndex(tagToMove, moveTo.getInt(i));
                    indicesToRemove[i] = moveFromIndex;
                    tagsToRemove[i] = tagToMove;
                }
            }

            if (numToAdd > 0) {
                Assertions.assertNotNull(addChildTags);
                Assertions.assertNotNull(addAtIndices);
                for (int i = 0; i < numToAdd; i++) {
                    int viewTagToAdd = addChildTags.getInt(i);
                    int indexToAddAt = addAtIndices.getInt(i);
                    viewsToAdd[numToMove + i] = new ViewAtIndex(viewTagToAdd, indexToAddAt);
                }
            }

            if (numToRemove > 0) {
                Assertions.assertNotNull(removeFrom);
                for (int i = 0; i < numToRemove; i++) {
                    int indexToRemove = removeFrom.getInt(i);
                    int tagToRemove = cssNodeToManage.getChildAt(indexToRemove).getReactTag();
                    indicesToRemove[numToMove + i] = indexToRemove;
                    tagsToRemove[numToMove + i] = tagToRemove;
                    tagsToDelete[i] = tagToRemove;
                    indicesToDelete[i] = indexToRemove;
                }
            }

            // NB: moveFrom and removeFrom are both relative to the starting state of the View's children.
            // moveTo and addAt are both relative to the final state of the View's children.
            //
            // 1) Sort the views to add and indices to remove by index
            // 2) Iterate the indices being removed from high to low and remove them. Going high to low
            //    makes sure we remove the correct index when there are multiple to remove.
            // 3) Iterate the views being added by index low to high and add them. Like the view removal,
            //    iteration direction is important to preserve the correct index.

            Arrays.sort(viewsToAdd, ViewAtIndex.COMPARATOR);
            Arrays.sort(indicesToRemove);

            // Apply changes to CSSNodeDEPRECATED hierarchy
            int lastIndexRemoved = -1;
            for (int i = indicesToRemove.length - 1; i >= 0; i--) {
                int indexToRemove = indicesToRemove[i];
                if (indexToRemove == lastIndexRemoved) {
                    throw new IllegalViewOperationException(
                            "Repeated indices in Removal list for view tag: " + viewTag);
                }
                cssNodeToManage.removeChildAt(indicesToRemove[i]); // Thread safety needed here

                lastIndexRemoved = indicesToRemove[i];
            }

            for (int i = 0; i < viewsToAdd.length; i++) {
                ViewAtIndex viewAtIndex = viewsToAdd[i];
                ReactShadowNode cssNodeToAdd = mShadowNodeRegistry.getNode(viewAtIndex.mTag);
                if (cssNodeToAdd == null) {
                    throw new IllegalViewOperationException("Trying to add unknown view tag: " + viewAtIndex.mTag);
                }
                cssNodeToManage.addChildAt(cssNodeToAdd, viewAtIndex.mIndex);
            }

            mNativeViewHierarchyOptimizer.handleManageChildren(cssNodeToManage, indicesToRemove, tagsToRemove,
                    viewsToAdd, tagsToDelete, indicesToDelete);

            for (int i = 0; i < tagsToDelete.length; i++) {
                removeShadowNode(mShadowNodeRegistry.getNode(tagsToDelete[i]));
            }
        }
    }

    /**
     * An optimized version of manageChildren that is used for initial setting of child views. The
     * children are assumed to be in index order
     *
     * @param viewTag tag of the parent
     * @param childrenTags tags of the children
     */
    public void setChildren(int viewTag, ReadableArray childrenTags) {
        synchronized (uiImplementationThreadLock) {
            ReactShadowNode cssNodeToManage = mShadowNodeRegistry.getNode(viewTag);

            for (int i = 0; i < childrenTags.size(); i++) {
                ReactShadowNode cssNodeToAdd = mShadowNodeRegistry.getNode(childrenTags.getInt(i));
                if (cssNodeToAdd == null) {
                    throw new IllegalViewOperationException(
                            "Trying to add unknown view tag: " + childrenTags.getInt(i));
                }
                cssNodeToManage.addChildAt(cssNodeToAdd, i);
            }

            mNativeViewHierarchyOptimizer.handleSetChildren(cssNodeToManage, childrenTags);
        }
    }

    /**
     * Replaces the View specified by oldTag with the View specified by newTag within oldTag's parent.
     */
    public void replaceExistingNonRootView(int oldTag, int newTag) {
        if (mShadowNodeRegistry.isRootNode(oldTag) || mShadowNodeRegistry.isRootNode(newTag)) {
            throw new IllegalViewOperationException("Trying to add or replace a root tag!");
        }

        ReactShadowNode oldNode = mShadowNodeRegistry.getNode(oldTag);
        if (oldNode == null) {
            throw new IllegalViewOperationException("Trying to replace unknown view tag: " + oldTag);
        }

        ReactShadowNode parent = oldNode.getParent();
        if (parent == null) {
            throw new IllegalViewOperationException("Node is not attached to a parent: " + oldTag);
        }

        int oldIndex = parent.indexOf(oldNode);
        if (oldIndex < 0) {
            throw new IllegalStateException("Didn't find child tag in parent");
        }

        WritableArray tagsToAdd = Arguments.createArray();
        tagsToAdd.pushInt(newTag);

        WritableArray addAtIndices = Arguments.createArray();
        addAtIndices.pushInt(oldIndex);

        WritableArray indicesToRemove = Arguments.createArray();
        indicesToRemove.pushInt(oldIndex);

        manageChildren(parent.getReactTag(), null, null, tagsToAdd, addAtIndices, indicesToRemove);
    }

    /**
     * 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
     */
    public void removeSubviewsFromContainerWithID(int containerTag) {
        ReactShadowNode containerNode = mShadowNodeRegistry.getNode(containerTag);
        if (containerNode == null) {
            throw new IllegalViewOperationException(
                    "Trying to remove subviews of an unknown view tag: " + containerTag);
        }

        WritableArray indicesToRemove = Arguments.createArray();
        for (int childIndex = 0; childIndex < containerNode.getChildCount(); childIndex++) {
            indicesToRemove.pushInt(childIndex);
        }

        manageChildren(containerTag, null, null, null, null, indicesToRemove);
    }

    /**
     * Find the touch target child native view in the supplied root view hierarchy, given a react
     * target location.
     *
     * <p>This method is currently used only by Element Inspector DevTool.
     *
     * @param reactTag the tag of the root view to traverse
     * @param targetX target X location
     * @param targetY target Y 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.
     */
    public void findSubviewIn(int reactTag, float targetX, float targetY, Callback callback) {
        mOperationsQueue.enqueueFindTargetForTouch(reactTag, targetX, targetY, callback);
    }

    /**
     * Check if the first shadow node is the descendant of the second shadow node
     *
     * @deprecated This method will not be implemented in Fabric.
     */
    @Deprecated
    public void viewIsDescendantOf(final int reactTag, final int ancestorReactTag, final Callback callback) {
        ReactShadowNode node = mShadowNodeRegistry.getNode(reactTag);
        ReactShadowNode ancestorNode = mShadowNodeRegistry.getNode(ancestorReactTag);
        if (node == null || ancestorNode == null) {
            callback.invoke(false);
            return;
        }
        callback.invoke(node.isDescendantOf(ancestorNode));
    }

    /**
     * Determines the location on screen, width, and height of the given view relative to the root
     * view and returns the values via an async callback.
     */
    public void measure(int reactTag, Callback callback) {
        // This method is called by the implementation of JS touchable interface (see Touchable.js for
        // more details) at the moment of touch activation. That is after user starts the gesture from
        // a touchable view with a given reactTag, or when user drag finger back into the press
        // activation area of a touchable view that have been activated before.
        mOperationsQueue.enqueueMeasure(reactTag, callback);
    }

    /**
     * Determines the location on screen, width, and height of the given view relative to the device
     * screen and returns the values via an async callback. This is the absolute position including
     * things like the status bar
     */
    public void measureInWindow(int reactTag, Callback callback) {
        mOperationsQueue.enqueueMeasureInWindow(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.
     */
    public void measureLayout(int tag, int ancestorTag, Callback errorCallback, Callback successCallback) {
        try {
            measureLayout(tag, ancestorTag, mMeasureBuffer);
            float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
            float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
            float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
            float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
            successCallback.invoke(relativeX, relativeY, width, height);
        } catch (IllegalViewOperationException e) {
            errorCallback.invoke(e.getMessage());
        }
    }

    /**
     * Like {@link #measure} and {@link #measureLayout} but measures relative to the immediate parent.
     */
    public void measureLayoutRelativeToParent(int tag, Callback errorCallback, Callback successCallback) {
        try {
            measureLayoutRelativeToParent(tag, mMeasureBuffer);
            float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
            float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
            float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
            float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
            successCallback.invoke(relativeX, relativeY, width, height);
        } catch (IllegalViewOperationException e) {
            errorCallback.invoke(e.getMessage());
        }
    }

    /** Invoked at the end of the transaction to commit any updates to the node hierarchy. */
    public void dispatchViewUpdates(int batchId) {
        SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "UIImplementation.dispatchViewUpdates")
                .arg("batchId", batchId).flush();
        final long commitStartTime = SystemClock.uptimeMillis();
        try {
            updateViewHierarchy();
            mNativeViewHierarchyOptimizer.onBatchComplete();
            mOperationsQueue.dispatchViewUpdates(batchId, commitStartTime, mLastCalculateLayoutTime);
        } finally {
            Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
        }
    }

    private void dispatchViewUpdatesIfNeeded() {
        // If we are in the middle of a batch update, any additional changes
        // will automatically be dispatched at the end of the batch.
        // If we are not, we have to initiate new batch update.
        // As all batches are executed as a single runnable on the event queue
        // this should always be empty, but that calling architecture is an implementation detail.
        if (mOperationsQueue.isEmpty()) {
            dispatchViewUpdates(-1); // "-1" means "no associated batch id"
        }
    }

    protected void updateViewHierarchy() {
        Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "UIImplementation.updateViewHierarchy");
        try {
            for (int i = 0; i < mShadowNodeRegistry.getRootNodeCount(); i++) {
                int tag = mShadowNodeRegistry.getRootTag(i);
                ReactShadowNode cssRoot = mShadowNodeRegistry.getNode(tag);

                if (cssRoot.getWidthMeasureSpec() != null && cssRoot.getHeightMeasureSpec() != null) {
                    SystraceMessage
                            .beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
                                    "UIImplementation.notifyOnBeforeLayoutRecursive")
                            .arg("rootTag", cssRoot.getReactTag()).flush();
                    try {
                        notifyOnBeforeLayoutRecursive(cssRoot);
                    } finally {
                        Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
                    }

                    calculateRootLayout(cssRoot);
                    SystraceMessage
                            .beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
                                    "UIImplementation.applyUpdatesRecursive")
                            .arg("rootTag", cssRoot.getReactTag()).flush();
                    try {
                        applyUpdatesRecursive(cssRoot, 0f, 0f);
                    } finally {
                        Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
                    }

                    if (mLayoutUpdateListener != null) {
                        mOperationsQueue.enqueueLayoutUpdateFinished(cssRoot, mLayoutUpdateListener);
                    }
                }
            }
        } finally {
            Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
        }
    }

    /**
     * LayoutAnimation API on Android is currently experimental. Therefore, it needs to be enabled
     * explicitly in order to avoid regression in existing application written for iOS using this API.
     *
     * <p>Warning : This method will be removed in future version of React Native, and layout
     * animation will be enabled by default, so always check for its existence before invoking it.
     *
     * <p>TODO(9139831) : remove this method once layout animation is fully stable.
     *
     * @param enabled whether layout animation is enabled or not
     */
    public void setLayoutAnimationEnabledExperimental(boolean enabled) {
        mOperationsQueue.enqueueSetLayoutAnimationEnabled(enabled);
    }

    /**
     * Configure an animation to be used for the native layout changes, and native views creation. The
     * animation will only apply during the current batch operations.
     *
     * <p>TODO(7728153) : animating view deletion is currently not supported. TODO(7613721) :
     * callbacks are not supported, this feature will likely be killed.
     *
     * @param config the configuration of the animation for view addition/removal/update.
     * @param success will be called when the animation completes, or when the animation get
     *     interrupted. In this case, callback parameter will be false.
     * @param error will be called if there was an error processing the animation
     */
    public void configureNextLayoutAnimation(ReadableMap config, Callback success) {
        mOperationsQueue.enqueueConfigureLayoutAnimation(config, success);
    }

    public void setJSResponder(int reactTag, boolean blockNativeResponder) {
        ReactShadowNode node = mShadowNodeRegistry.getNode(reactTag);

        if (node == null) {
            // TODO: this should only happen when using Fabric renderer. This is a temporary approach
            // and it will be refactored when fabric supports JS Responder.
            return;
        }

        while (node.getNativeKind() == NativeKind.NONE) {
            node = node.getParent();
        }
        mOperationsQueue.enqueueSetJSResponder(node.getReactTag(), reactTag, blockNativeResponder);
    }

    public void clearJSResponder() {
        mOperationsQueue.enqueueClearJSResponder();
    }

    @Deprecated
    public void dispatchViewManagerCommand(int reactTag, int commandId, @Nullable ReadableArray commandArgs) {
        assertViewExists(reactTag, "dispatchViewManagerCommand");
        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.
     *
     * @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
     */
    public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Callback success) {
        assertViewExists(reactTag, "showPopupMenu");
        mOperationsQueue.enqueueShowPopupMenu(reactTag, items, error, success);
    }

    public void dismissPopupMenu() {
        mOperationsQueue.enqueueDismissPopupMenu();
    }

    public void sendAccessibilityEvent(int tag, int eventType) {
        mOperationsQueue.enqueueSendAccessibilityEvent(tag, eventType);
    }

    public void onHostResume() {
        mOperationsQueue.resumeFrameCallback();
    }

    public void onHostPause() {
        mOperationsQueue.pauseFrameCallback();
    }

    public void onHostDestroy() {
    }

    public void setViewHierarchyUpdateDebugListener(
            @Nullable NotThreadSafeViewHierarchyUpdateDebugListener listener) {
        mOperationsQueue.setViewHierarchyUpdateDebugListener(listener);
    }

    protected final void removeShadowNode(ReactShadowNode nodeToRemove) {
        removeShadowNodeRecursive(nodeToRemove);
        nodeToRemove.dispose();
    }

    private void removeShadowNodeRecursive(ReactShadowNode nodeToRemove) {
        NativeViewHierarchyOptimizer.handleRemoveNode(nodeToRemove);
        mShadowNodeRegistry.removeNode(nodeToRemove.getReactTag());
        for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) {
            removeShadowNodeRecursive(nodeToRemove.getChildAt(i));
        }
        nodeToRemove.removeAndDisposeAllChildren();
    }

    private void measureLayout(int tag, int ancestorTag, int[] outputBuffer) {
        ReactShadowNode node = mShadowNodeRegistry.getNode(tag);
        ReactShadowNode ancestor = mShadowNodeRegistry.getNode(ancestorTag);
        if (node == null || ancestor == null) {
            throw new IllegalViewOperationException(
                    "Tag " + (node == null ? tag : ancestorTag) + " does not exist");
        }

        if (node != ancestor) {
            ReactShadowNode currentParent = node.getParent();
            while (currentParent != ancestor) {
                if (currentParent == null) {
                    throw new IllegalViewOperationException(
                            "Tag " + ancestorTag + " is not an ancestor of tag " + tag);
                }
                currentParent = currentParent.getParent();
            }
        }

        measureLayoutRelativeToVerifiedAncestor(node, ancestor, outputBuffer);
    }

    private void measureLayoutRelativeToParent(int tag, int[] outputBuffer) {
        ReactShadowNode node = mShadowNodeRegistry.getNode(tag);
        if (node == null) {
            throw new IllegalViewOperationException("No native view for tag " + tag + " exists!");
        }
        ReactShadowNode parent = node.getParent();
        if (parent == null) {
            throw new IllegalViewOperationException("View with tag " + tag + " doesn't have a parent!");
        }

        measureLayoutRelativeToVerifiedAncestor(node, parent, outputBuffer);
    }

    private void measureLayoutRelativeToVerifiedAncestor(ReactShadowNode node, ReactShadowNode ancestor,
            int[] outputBuffer) {
        int offsetX = 0;
        int offsetY = 0;
        if (node != ancestor) {
            offsetX = Math.round(node.getLayoutX());
            offsetY = Math.round(node.getLayoutY());
            ReactShadowNode current = node.getParent();
            while (current != ancestor) {
                Assertions.assertNotNull(current);
                assertNodeDoesNotNeedCustomLayoutForChildren(current);
                offsetX += Math.round(current.getLayoutX());
                offsetY += Math.round(current.getLayoutY());
                current = current.getParent();
            }
            assertNodeDoesNotNeedCustomLayoutForChildren(ancestor);
        }

        outputBuffer[0] = offsetX;
        outputBuffer[1] = offsetY;
        outputBuffer[2] = node.getScreenWidth();
        outputBuffer[3] = node.getScreenHeight();
    }

    private void assertViewExists(int reactTag, String operationNameForExceptionMessage) {
        if (mShadowNodeRegistry.getNode(reactTag) == null) {
            throw new IllegalViewOperationException(
                    "Unable to execute operation " + operationNameForExceptionMessage + " on view with " + "tag: "
                            + reactTag + ", since the view does not exists");
        }
    }

    private void assertNodeDoesNotNeedCustomLayoutForChildren(ReactShadowNode node) {
        ViewManager viewManager = Assertions.assertNotNull(mViewManagers.get(node.getViewClass()));
        IViewManagerWithChildren viewManagerWithChildren;
        if (viewManager instanceof IViewManagerWithChildren) {
            viewManagerWithChildren = (IViewManagerWithChildren) viewManager;
        } else {
            throw new IllegalViewOperationException("Trying to use view " + node.getViewClass()
                    + " as a parent, but its Manager doesn't extends ViewGroupManager");
        }
        if (viewManagerWithChildren != null && viewManagerWithChildren.needsCustomLayoutForChildren()) {
            throw new IllegalViewOperationException(
                    "Trying to measure a view using measureLayout/measureLayoutRelativeToParent relative to"
                            + " an ancestor that requires custom layout for it's children (" + node.getViewClass()
                            + "). Use measure instead.");
        }
    }

    private void notifyOnBeforeLayoutRecursive(ReactShadowNode cssNode) {
        if (!cssNode.hasUpdates()) {
            return;
        }
        for (int i = 0; i < cssNode.getChildCount(); i++) {
            notifyOnBeforeLayoutRecursive(cssNode.getChildAt(i));
        }
        cssNode.onBeforeLayout(mNativeViewHierarchyOptimizer);
    }

    protected void calculateRootLayout(ReactShadowNode cssRoot) {
        SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "cssRoot.calculateLayout")
                .arg("rootTag", cssRoot.getReactTag()).flush();
        long startTime = SystemClock.uptimeMillis();
        try {
            int widthSpec = cssRoot.getWidthMeasureSpec();
            int heightSpec = cssRoot.getHeightMeasureSpec();
            cssRoot.calculateLayout(
                    MeasureSpec.getMode(widthSpec) == MeasureSpec.UNSPECIFIED ? YogaConstants.UNDEFINED
                            : MeasureSpec.getSize(widthSpec),
                    MeasureSpec.getMode(heightSpec) == MeasureSpec.UNSPECIFIED ? YogaConstants.UNDEFINED
                            : MeasureSpec.getSize(heightSpec));
        } finally {
            Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
            mLastCalculateLayoutTime = SystemClock.uptimeMillis() - startTime;
        }
    }

    protected void applyUpdatesRecursive(ReactShadowNode cssNode, float absoluteX, float absoluteY) {
        if (!cssNode.hasUpdates()) {
            return;
        }

        Iterable<? extends ReactShadowNode> cssChildren = cssNode.calculateLayoutOnChildren();
        if (cssChildren != null) {
            for (ReactShadowNode cssChild : cssChildren) {
                applyUpdatesRecursive(cssChild, absoluteX + cssNode.getLayoutX(), absoluteY + cssNode.getLayoutY());
            }
        }

        int tag = cssNode.getReactTag();
        if (!mShadowNodeRegistry.isRootNode(tag)) {
            boolean frameDidChange = cssNode.dispatchUpdates(absoluteX, absoluteY, mOperationsQueue,
                    mNativeViewHierarchyOptimizer);

            // Notify JS about layout event if requested
            // and if the position or dimensions actually changed
            // (consistent with iOS).
            if (frameDidChange && cssNode.shouldNotifyOnLayout()) {
                mEventDispatcher.dispatchEvent(OnLayoutEvent.obtain(tag, cssNode.getScreenX(), cssNode.getScreenY(),
                        cssNode.getScreenWidth(), cssNode.getScreenHeight()));
            }
        }
        cssNode.markUpdateSeen();
    }

    public void addUIBlock(UIBlock block) {
        mOperationsQueue.enqueueUIBlock(block);
    }

    public void prependUIBlock(UIBlock block) {
        mOperationsQueue.prependUIBlock(block);
    }

    public int resolveRootTagFromReactTag(int reactTag) {
        if (mShadowNodeRegistry.isRootNode(reactTag)) {
            return reactTag;
        }

        ReactShadowNode node = resolveShadowNode(reactTag);
        int rootTag = 0;
        if (node != null) {
            rootTag = node.getRootTag();
        } else {
            FLog.w(ReactConstants.TAG,
                    "Warning : attempted to resolve a non-existent react shadow node. reactTag=" + reactTag);
        }

        return rootTag;
    }

    public void setLayoutUpdateListener(LayoutUpdateListener listener) {
        mLayoutUpdateListener = listener;
    }

    public void removeLayoutUpdateListener() {
        mLayoutUpdateListener = null;
    }
}