/** * 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.ArrayList; import java.util.Map; import com.facebook.csslayout.CSSNode; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; /** * Base node class for representing virtual tree of React nodes. Shadow nodes are used primarily * for layouting therefore it extends {@link CSSNode} to allow that. They also help with handling * Common base subclass of {@link CSSNode} for all layout nodes for react-based view. It extends * {@link CSSNode} by adding additional capabilities. * * Instances of this class receive property updates from JS via @{link UIManagerModule}. Subclasses * may use {@link #updateShadowNode} to persist some of the updated fields in the node instance that * corresponds to a particular view type. * * Subclasses of {@link ReactShadowNode} should be created only from {@link ViewManager} that * corresponds to a certain type of native view. They will be updated and accessed only from JS * thread. Subclasses of {@link ViewManager} may choose to use base class {@link ReactShadowNode} or * custom subclass of it if necessary. * * The primary use-case for {@link ReactShadowNode} nodes is to calculate layouting. Although this * might be extended. For some examples please refer to ARTGroupCSSNode or ReactTextCSSNode. * * This class allows for the native view hierarchy to not be an exact copy of the hierarchy received * from JS by keeping track of both JS children (e.g. {@link #getChildCount()} and separately native * children (e.g. {@link #getNativeChildCount()}). See {@link NativeViewHierarchyOptimizer} for more * information. */ public class ReactShadowNode extends CSSNode { private int mReactTag; private @Nullable String mViewClassName; private @Nullable ReactShadowNode mRootNode; private @Nullable ThemedReactContext mThemedContext; private boolean mShouldNotifyOnLayout; private boolean mNodeUpdated = true; // layout-only nodes private boolean mIsLayoutOnly; private int mTotalNativeChildren = 0; private @Nullable ReactShadowNode mNativeParent; private @Nullable ArrayList<ReactShadowNode> mNativeChildren; private float mAbsoluteLeft; private float mAbsoluteTop; private float mAbsoluteRight; private float mAbsoluteBottom; /** * Nodes that return {@code true} will be treated as "virtual" nodes. That is, nodes that are not * mapped into native views (e.g. nested text node). By default this method returns {@code false}. */ public boolean isVirtual() { return false; } /** * Nodes that return {@code true} will be treated as a root view for the virtual nodes tree. It * means that {@link NativeViewHierarchyManager} will not try to perform {@code manageChildren} * operation on such views. Good example is {@code InputText} view that may have children * {@code Text} nodes but this whole hierarchy will be mapped to a single android {@link EditText} * view. */ public boolean isVirtualAnchor() { return false; } public final String getViewClass() { return Assertions.assertNotNull(mViewClassName); } public final boolean hasUpdates() { return mNodeUpdated || hasNewLayout() || isDirty(); } public final void markUpdateSeen() { mNodeUpdated = false; if (hasNewLayout()) { markLayoutSeen(); } } protected void markUpdated() { if (mNodeUpdated) { return; } mNodeUpdated = true; ReactShadowNode parent = getParent(); if (parent != null) { parent.markUpdated(); } } @Override protected void dirty() { if (!isVirtual()) { super.dirty(); } } @Override public void addChildAt(CSSNode child, int i) { super.addChildAt(child, i); markUpdated(); ReactShadowNode node = (ReactShadowNode) child; int increase = node.mIsLayoutOnly ? node.mTotalNativeChildren : 1; mTotalNativeChildren += increase; if (mIsLayoutOnly) { ReactShadowNode parent = getParent(); while (parent != null) { parent.mTotalNativeChildren += increase; if (!parent.mIsLayoutOnly) { break; } parent = parent.getParent(); } } } @Override public ReactShadowNode removeChildAt(int i) { ReactShadowNode removed = (ReactShadowNode) super.removeChildAt(i); markUpdated(); int decrease = removed.mIsLayoutOnly ? removed.mTotalNativeChildren : 1; mTotalNativeChildren -= decrease; if (mIsLayoutOnly) { ReactShadowNode parent = getParent(); while (parent != null) { parent.mTotalNativeChildren -= decrease; if (!parent.mIsLayoutOnly) { break; } parent = parent.getParent(); } } return removed; } /** * This method will be called by {@link UIManagerModule} once per batch, before calculating * layout. Will be only called for nodes that are marked as updated with {@link #markUpdated()} * or require layouting (marked with {@link #dirty()}). */ public void onBeforeLayout() { } public final void updateProperties(CatalystStylesDiffMap props) { Map<String, ViewManagersPropertyCache.PropSetter> propSetters = ViewManagersPropertyCache.getNativePropSettersForShadowNodeClass(getClass()); ReadableMap propMap = props.mBackingMap; ReadableMapKeySetIterator iterator = propMap.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); ViewManagersPropertyCache.PropSetter setter = propSetters.get(key); if (setter != null) { setter.updateShadowNodeProp(this, props); } } onAfterUpdateTransaction(); } public void onAfterUpdateTransaction() { // no-op } /** * Called after layout step at the end of the UI batch from {@link UIManagerModule}. May be used * to enqueue additional ui operations for the native view. Will only be called on nodes marked * as updated either with {@link #dirty()} or {@link #markUpdated()}. * * @param uiViewOperationQueue interface for enqueueing UI operations */ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { } /* package */ void dispatchUpdates( float absoluteX, float absoluteY, UIViewOperationQueue uiViewOperationQueue, NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) { if (mNodeUpdated) { onCollectExtraUpdates(uiViewOperationQueue); } if (hasNewLayout()) { mAbsoluteLeft = Math.round(absoluteX + getLayoutX()); mAbsoluteTop = Math.round(absoluteY + getLayoutY()); mAbsoluteRight = Math.round(absoluteX + getLayoutX() + getLayoutWidth()); mAbsoluteBottom = Math.round(absoluteY + getLayoutY() + getLayoutHeight()); nativeViewHierarchyOptimizer.handleUpdateLayout(this); } } public final int getReactTag() { return mReactTag; } /* package */ final void setReactTag(int reactTag) { mReactTag = reactTag; } public final ReactShadowNode getRootNode() { return Assertions.assertNotNull(mRootNode); } /* package */ final void setRootNode(ReactShadowNode rootNode) { mRootNode = rootNode; } /* package */ final void setViewClassName(String viewClassName) { mViewClassName = viewClassName; } @Override public final ReactShadowNode getChildAt(int i) { return (ReactShadowNode) super.getChildAt(i); } @Override public final @Nullable ReactShadowNode getParent() { return (ReactShadowNode) super.getParent(); } /** * Get the {@link ThemedReactContext} associated with this {@link ReactShadowNode}. This will * never change during the lifetime of a {@link ReactShadowNode} instance, but different instances * can have different contexts; don't cache any calculations based on theme values globally. */ public ThemedReactContext getThemedContext() { return Assertions.assertNotNull(mThemedContext); } protected void setThemedContext(ThemedReactContext themedContext) { mThemedContext = themedContext; } public void setShouldNotifyOnLayout(boolean shouldNotifyOnLayout) { mShouldNotifyOnLayout = shouldNotifyOnLayout; } /* package */ boolean shouldNotifyOnLayout() { return mShouldNotifyOnLayout; } /** * Adds a child that the native view hierarchy will have at this index in the native view * corresponding to this node. */ public void addNativeChildAt(ReactShadowNode child, int nativeIndex) { Assertions.assertCondition(!mIsLayoutOnly); Assertions.assertCondition(!child.mIsLayoutOnly); if (mNativeChildren == null) { mNativeChildren = new ArrayList<>(4); } mNativeChildren.add(nativeIndex, child); child.mNativeParent = this; } public ReactShadowNode removeNativeChildAt(int i) { Assertions.assertNotNull(mNativeChildren); ReactShadowNode removed = mNativeChildren.remove(i); removed.mNativeParent = null; return removed; } public int getNativeChildCount() { return mNativeChildren == null ? 0 : mNativeChildren.size(); } public int indexOfNativeChild(ReactShadowNode nativeChild) { Assertions.assertNotNull(mNativeChildren); return mNativeChildren.indexOf(nativeChild); } public @Nullable ReactShadowNode getNativeParent() { return mNativeParent; } /** * Sets whether this node only contributes to the layout of its children without doing any * drawing or functionality itself. */ public void setIsLayoutOnly(boolean isLayoutOnly) { Assertions.assertCondition(getParent() == null, "Must remove from no opt parent first"); Assertions.assertCondition(mNativeParent == null, "Must remove from native parent first"); Assertions.assertCondition(getNativeChildCount() == 0, "Must remove all native children first"); mIsLayoutOnly = isLayoutOnly; } public boolean isLayoutOnly() { return mIsLayoutOnly; } public int getTotalNativeChildren() { return mTotalNativeChildren; } /** * Returns the offset within the native children owned by all layout-only nodes in the subtree * rooted at this node for the given child. Put another way, this returns the number of native * nodes (nodes not optimized out of the native tree) that are a) to the left (visited before by a * DFS) of the given child in the subtree rooted at this node and b) do not have a native parent * in this subtree (which means that the given child will be a sibling of theirs in the final * native hierarchy since they'll get attached to the same native parent). * * Basically, a view might have children that have been optimized away by * {@link NativeViewHierarchyOptimizer}. Since those children will then add their native children * to this view, we now have ranges of native children that correspond to single unoptimized * children. The purpose of this method is to return the index within the native children that * corresponds to the **start** of the native children that belong to the given child. Also, note * that all of the children of a view might be optimized away, so this could return the same value * for multiple different children. * * Example. Native children are represented by (N) where N is the no-opt child they came from. If * no children are optimized away it'd look like this: (0) (1) (2) (3) ... (n) * * In case some children are optimized away, it might look like this: * (0) (1) (1) (1) (3) (3) (4) * * In that case: * getNativeOffsetForChild(Node 0) => 0 * getNativeOffsetForChild(Node 1) => 1 * getNativeOffsetForChild(Node 2) => 4 * getNativeOffsetForChild(Node 3) => 4 * getNativeOffsetForChild(Node 4) => 6 */ public int getNativeOffsetForChild(ReactShadowNode child) { int index = 0; boolean found = false; for (int i = 0; i < getChildCount(); i++) { ReactShadowNode current = getChildAt(i); if (child == current) { found = true; break; } index += (current.mIsLayoutOnly ? current.getTotalNativeChildren() : 1); } if (!found) { throw new RuntimeException("Child " + child.mReactTag + " was not a child of " + mReactTag); } return index; } /** * @return the x position of the corresponding view on the screen, rounded to pixels */ public int getScreenX() { return Math.round(getLayoutX()); } /** * @return the y position of the corresponding view on the screen, rounded to pixels */ public int getScreenY() { return Math.round(getLayoutY()); } /** * @return width corrected for rounding to pixels. */ public int getScreenWidth() { return Math.round(mAbsoluteRight - mAbsoluteLeft); } /** * @return height corrected for rounding to pixels. */ public int getScreenHeight() { return Math.round(mAbsoluteBottom - mAbsoluteTop); } }