/** * Copyright (c) 2017-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.litho; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.concurrent.atomic.AtomicInteger; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.util.LongSparseArray; import android.support.v4.view.accessibility.AccessibilityManagerCompat; import android.text.TextUtils; import android.view.View; import android.view.Window; import android.view.accessibility.AccessibilityManager; import com.facebook.infer.annotation.ThreadConfined; import com.facebook.litho.config.ComponentsConfiguration; import com.facebook.litho.displaylist.DisplayList; import com.facebook.litho.displaylist.DisplayListException; import com.facebook.litho.reference.BorderColorDrawableReference; import com.facebook.litho.reference.DrawableReference; import com.facebook.litho.reference.Reference; import com.facebook.infer.annotation.ThreadSafe; import com.facebook.yoga.YogaConstants; import com.facebook.yoga.YogaDirection; import com.facebook.yoga.YogaEdge; import static android.content.Context.ACCESSIBILITY_SERVICE; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.M; import static android.support.v4.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; import static android.support.v4.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO; import static com.facebook.litho.Component.isHostSpec; import static com.facebook.litho.Component.isLayoutSpecWithSizeSpec; import static com.facebook.litho.Component.isMountSpec; import static com.facebook.litho.Component.isMountViewSpec; import static com.facebook.litho.ComponentContext.NULL_LAYOUT; import static com.facebook.litho.ComponentLifecycle.MountType.NONE; import static com.facebook.litho.FrameworkLogEvents.EVENT_COLLECT_RESULTS; import static com.facebook.litho.FrameworkLogEvents.EVENT_CREATE_LAYOUT; import static com.facebook.litho.FrameworkLogEvents.EVENT_CSS_LAYOUT; import static com.facebook.litho.FrameworkLogEvents.PARAM_COMPONENT; import static com.facebook.litho.FrameworkLogEvents.PARAM_LOG_TAG; import static com.facebook.litho.FrameworkLogEvents.PARAM_TREE_DIFF_ENABLED; import static com.facebook.litho.MountItem.FLAG_DUPLICATE_PARENT_STATE; import static com.facebook.litho.MountState.ROOT_HOST_ID; import static com.facebook.litho.NodeInfo.FOCUS_SET_TRUE; import static com.facebook.litho.SizeSpec.EXACTLY; /** * The main role of {@link LayoutState} is to hold the output of layout calculation. This includes * mountable outputs and visibility outputs. A centerpiece of the class is {@link * #collectResults(InternalNode, LayoutState, DiffNode)} which prepares the before-mentioned outputs * based on the provided {@link InternalNode} for later use in {@link MountState}. */ class LayoutState { static final Comparator<LayoutOutput> sTopsComparator = new Comparator<LayoutOutput>() { @Override public int compare(LayoutOutput lhs, LayoutOutput rhs) { final int lhsTop = lhs.getBounds().top; final int rhsTop = rhs.getBounds().top; return lhsTop < rhsTop ? -1 : lhsTop > rhsTop ? 1 // Hosts should be higher for tops so that they are mounted first if possible. : isHostSpec(lhs.getComponent()) == isHostSpec(rhs.getComponent()) ? 0 : isHostSpec(lhs.getComponent()) ? -1 : 1; } }; static final Comparator<LayoutOutput> sBottomsComparator = new Comparator<LayoutOutput>() { @Override public int compare(LayoutOutput lhs, LayoutOutput rhs) { final int lhsBottom = lhs.getBounds().bottom; final int rhsBottom = rhs.getBounds().bottom; return lhsBottom < rhsBottom ? -1 : lhsBottom > rhsBottom ? 1 // Hosts should be lower for bottoms so that they are mounted first if possible. : isHostSpec(lhs.getComponent()) == isHostSpec(rhs.getComponent()) ? 0 : isHostSpec(lhs.getComponent()) ? 1 : -1; } }; @ThreadConfined(ThreadConfined.UI) private final Rect mDisplayListCreateRect = new Rect(); @ThreadConfined(ThreadConfined.ANY) private final Rect mDisplayListQueueRect = new Rect(); private static final int[] DRAWABLE_STATE_ENABLED = new int[]{android.R.attr.state_enabled}; private static final int[] DRAWABLE_STATE_NOT_ENABLED = new int[]{}; private ComponentContext mContext; private TransitionContext mTransitionContext; private Component<?> mComponent; private int mWidthSpec; private int mHeightSpec; private final List<LayoutOutput> mMountableOutputs = new ArrayList<>(8); private final List<VisibilityOutput> mVisibilityOutputs = new ArrayList<>(8); private final LongSparseArray<Integer> mOutputsIdToPositionMap = new LongSparseArray<>(8); private final LayoutStateOutputIdCalculator mLayoutStateOutputIdCalculator; private final ArrayList<LayoutOutput> mMountableOutputTops = new ArrayList<>(); private final ArrayList<LayoutOutput> mMountableOutputBottoms = new ArrayList<>(); private final Queue<Integer> mDisplayListsToPrefetch = new LinkedList<>(); private final List<TestOutput> mTestOutputs; private InternalNode mLayoutRoot; private DiffNode mDiffTreeRoot; // Reference count will be initialized to 1 in init(). private final AtomicInteger mReferenceCount = new AtomicInteger(-1); private int mWidth; private int mHeight; private int mCurrentX; private int mCurrentY; private int mCurrentLevel = 0; // Holds the current host marker in the layout tree. private long mCurrentHostMarker = -1; private int mCurrentHostOutputPosition = -1; private boolean mShouldDuplicateParentState = true; private boolean mShouldGenerateDiffTree = false; private int mComponentTreeId = -1; private AccessibilityManager mAccessibilityManager; private boolean mAccessibilityEnabled = false; private boolean mShouldAnimateTransitions = false; private StateHandler mStateHandler; private boolean mCanPrefetchDisplayLists; LayoutState() { mLayoutStateOutputIdCalculator = new LayoutStateOutputIdCalculator(); mTestOutputs = ComponentsConfiguration.isEndToEndTestRun ? new ArrayList<TestOutput>(8) : null; } /** * Acquires a new layout output for the internal node and its associated component. It returns * null if there's no component associated with the node as the mount pass only cares about nodes * that will potentially mount content into the component host. */ @Nullable private static LayoutOutput createGenericLayoutOutput( InternalNode node, LayoutState layoutState) { final Component<?> component = node.getRootComponent(); // Skip empty nodes and layout specs because they don't mount anything. if (component == null || component.getLifecycle().getMountType() == NONE) { return null; } return createLayoutOutput( component, layoutState, node, true /* useNodePadding */, node.getImportantForAccessibility(), layoutState.mShouldDuplicateParentState); } private static LayoutOutput createHostLayoutOutput(LayoutState layoutState, InternalNode node) { final LayoutOutput hostOutput = createLayoutOutput( HostComponent.create(), layoutState, node, false /* useNodePadding */, node.getImportantForAccessibility(), node.isDuplicateParentStateEnabled()); hostOutput.getViewNodeInfo().setTransitionKey(node.getTransitionKey()); return hostOutput; } private static LayoutOutput createDrawableLayoutOutput( Component<?> component, LayoutState layoutState, InternalNode node) { return createLayoutOutput( component, layoutState, node, false /* useNodePadding */, IMPORTANT_FOR_ACCESSIBILITY_NO, layoutState.mShouldDuplicateParentState); } private static LayoutOutput createLayoutOutput( Component<?> component, LayoutState layoutState, InternalNode node, boolean useNodePadding, int importantForAccessibility, boolean duplicateParentState) { final boolean isMountViewSpec = isMountViewSpec(component); final LayoutOutput layoutOutput = ComponentsPools.acquireLayoutOutput(); layoutOutput.setComponent(component); layoutOutput.setImportantForAccessibility(importantForAccessibility); // The mount operation will need both the marker for the target host and its matching // parent host to ensure the correct hierarchy when nesting the host views. layoutOutput.setHostMarker(layoutState.mCurrentHostMarker); if (layoutState.mCurrentHostOutputPosition >= 0) { final LayoutOutput hostOutput = layoutState.mMountableOutputs.get(layoutState.mCurrentHostOutputPosition); final Rect hostBounds = hostOutput.getBounds(); layoutOutput.setHostTranslationX(hostBounds.left); layoutOutput.setHostTranslationY(hostBounds.top); } int l = layoutState.mCurrentX + node.getX(); int t = layoutState.mCurrentY + node.getY(); int r = l + node.getWidth(); int b = t + node.getHeight(); final int paddingLeft = useNodePadding ? node.getPaddingLeft() : 0; final int paddingTop = useNodePadding ? node.getPaddingTop() : 0; final int paddingRight = useNodePadding ? node.getPaddingRight() : 0; final int paddingBottom = useNodePadding ? node.getPaddingBottom() : 0; // View mount specs are able to set their own attributes when they're mounted. // Non-view specs (drawable and layout) always transfer their view attributes // to their respective hosts. // Moreover, if the component mounts a view, then we apply padding to the view itself later on. // Otherwise, apply the padding to the bounds of the layout output. if (isMountViewSpec) { layoutOutput.setNodeInfo(node.getNodeInfo()); // Acquire a ViewNodeInfo, set it up and release it after passing it to the LayoutOutput. final ViewNodeInfo viewNodeInfo = ViewNodeInfo.acquire(); viewNodeInfo.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); viewNodeInfo.setLayoutDirection(node.getResolvedLayoutDirection()); viewNodeInfo.setExpandedTouchBounds(node, l, t, r, b); layoutOutput.setViewNodeInfo(viewNodeInfo); viewNodeInfo.release(); } else { l += paddingLeft; t += paddingTop; r -= paddingRight; b -= paddingBottom; } layoutOutput.setBounds(l, t, r, b); int flags = 0; if (duplicateParentState) { flags |= FLAG_DUPLICATE_PARENT_STATE; } layoutOutput.setFlags(flags); return layoutOutput; } /** * Acquires a {@link VisibilityOutput} object and computes the bounds for it using the information * stored in the {@link InternalNode}. */ private static VisibilityOutput createVisibilityOutput( InternalNode node, LayoutState layoutState) { final int l = layoutState.mCurrentX + node.getX(); final int t = layoutState.mCurrentY + node.getY(); final int r = l + node.getWidth(); final int b = t + node.getHeight(); final EventHandler<VisibleEvent> visibleHandler = node.getVisibleHandler(); final EventHandler<FocusedVisibleEvent> focusedHandler = node.getFocusedHandler(); final EventHandler<UnfocusedVisibleEvent> unfocusedHandler = node.getUnfocusedHandler(); final EventHandler<FullImpressionVisibleEvent> fullImpressionHandler = node.getFullImpressionHandler(); final EventHandler<InvisibleEvent> invisibleHandler = node.getInvisibleHandler(); final VisibilityOutput visibilityOutput = ComponentsPools.acquireVisibilityOutput(); final Component<?> handlerComponent; // Get the component from the handler that is not null. If more than one is not null, then // getting the component from any of them works. if (visibleHandler != null) { handlerComponent = (Component<?>) visibleHandler.mHasEventDispatcher; } else if (focusedHandler != null) { handlerComponent = (Component<?>) focusedHandler.mHasEventDispatcher; } else if (unfocusedHandler != null) { handlerComponent = (Component<?>) unfocusedHandler.mHasEventDispatcher; } else if (fullImpressionHandler != null) { handlerComponent = (Component<?>) fullImpressionHandler.mHasEventDispatcher; } else { handlerComponent = (Component<?>) invisibleHandler.mHasEventDispatcher; } visibilityOutput.setComponent(handlerComponent); visibilityOutput.setBounds(l, t, r, b); visibilityOutput.setVisibleRatio(node.getVisibleRatio()); visibilityOutput.setVisibleEventHandler(visibleHandler); visibilityOutput.setFocusedEventHandler(focusedHandler); visibilityOutput.setUnfocusedEventHandler(unfocusedHandler); visibilityOutput.setFullImpressionEventHandler(fullImpressionHandler); visibilityOutput.setInvisibleEventHandler(invisibleHandler); return visibilityOutput; } private static TestOutput createTestOutput( InternalNode node, LayoutState layoutState, LayoutOutput layoutOutput) { final int l = layoutState.mCurrentX + node.getX(); final int t = layoutState.mCurrentY + node.getY(); final int r = l + node.getWidth(); final int b = t + node.getHeight(); final TestOutput output = ComponentsPools.acquireTestOutput(); output.setTestKey(node.getTestKey()); output.setBounds(l, t, r, b); output.setHostMarker(layoutState.mCurrentHostMarker); if (layoutOutput != null) { output.setLayoutOutputId(layoutOutput.getId()); } return output; } private static boolean isLayoutDirectionRTL(Context context) { ApplicationInfo applicationInfo = context.getApplicationInfo(); if ((SDK_INT >= JELLY_BEAN_MR1) && (applicationInfo.flags & ApplicationInfo.FLAG_SUPPORTS_RTL) != 0) { int layoutDirection = getLayoutDirection(context); return layoutDirection == View.LAYOUT_DIRECTION_RTL; } return false; } @TargetApi(JELLY_BEAN_MR1) private static int getLayoutDirection(Context context) { return context.getResources().getConfiguration().getLayoutDirection(); } /** * Determine if a given {@link InternalNode} within the context of a given {@link LayoutState} * requires to be wrapped inside a view. * * @see #needsHostView(InternalNode, LayoutState) */ private static boolean hasViewContent(InternalNode node, LayoutState layoutState) { final Component<?> component = node.getRootComponent(); final NodeInfo nodeInfo = node.getNodeInfo(); final boolean implementsAccessibility = (nodeInfo != null && nodeInfo.hasAccessibilityHandlers()) || (component != null && component.getLifecycle().implementsAccessibility()); final int importantForAccessibility = node.getImportantForAccessibility(); // A component has accessibility content if: // 1. Accessibility is currently enabled. // 2. Accessibility hasn't been explicitly disabled on it // i.e. IMPORTANT_FOR_ACCESSIBILITY_NO. // 3. Any of these conditions are true: // - It implements accessibility support. // - It has a content description. // - It has importantForAccessibility set as either IMPORTANT_FOR_ACCESSIBILITY_YES // or IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS. // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS should trigger an inner host // so that such flag is applied in the resulting view hierarchy after the component // tree is mounted. Click handling is also considered accessibility content but // this is already covered separately i.e. click handler is not null. final boolean hasAccessibilityContent = layoutState.mAccessibilityEnabled && importantForAccessibility != IMPORTANT_FOR_ACCESSIBILITY_NO && (implementsAccessibility || (nodeInfo != null && !TextUtils.isEmpty(nodeInfo.getContentDescription())) || importantForAccessibility != IMPORTANT_FOR_ACCESSIBILITY_AUTO); final boolean hasTouchEventHandlers = (nodeInfo != null && nodeInfo.hasTouchEventHandlers()); final boolean hasViewTag = (nodeInfo != null && nodeInfo.getViewTag() != null); final boolean hasViewTags = (nodeInfo != null && nodeInfo.getViewTags() != null); final boolean isFocusableSetTrue = (nodeInfo != null && nodeInfo.getFocusState() == FOCUS_SET_TRUE); return hasTouchEventHandlers || hasViewTag || hasViewTags || hasAccessibilityContent || isFocusableSetTrue; } /** * Collects layout outputs and release the layout tree. The layout outputs hold necessary * information to be used by {@link MountState} to mount components into a {@link ComponentHost}. * <p/> * Whenever a component has view content (view tags, click handler, etc), a new host 'marker' * is added for it. The mount pass will use the markers to decide which host should be used * for each layout output. The root node unconditionally generates a layout output corresponding * to the root host. * <p/> * The order of layout outputs follows a depth-first traversal in the tree to ensure the hosts * will be created at the right order when mounting. The host markers will be define which host * each mounted artifacts will be attached to. * <p/> * At this stage all the {@link InternalNode} for which we have LayoutOutputs that can be recycled * will have a DiffNode associated. If the CachedMeasures are valid we'll try to recycle both the * host and the contents (including background/foreground). In all other cases instead we'll only * try to re-use the hosts. In some cases the host's structure might change between two updates * even if the component is of the same type. This can happen for example when a click listener is * added. To avoid trying to re-use the wrong host type we explicitly check that after all the * children for a subtree have been added (this is when the actual host type is resolved). If the * host type changed compared to the one in the DiffNode we need to refresh the ids for the whole * subtree in order to ensure that the MountState will unmount the subtree and mount it again on * the correct host. * <p/> * * @param node InternalNode to process. * @param layoutState the LayoutState currently operating. * @param parentDiffNode whether this method also populates the diff tree and assigns the root * to mDiffTreeRoot. */ private static void collectResults( InternalNode node, LayoutState layoutState, DiffNode parentDiffNode) { if (node.hasNewLayout()) { node.markLayoutSeen(); } final Component<?> component = node.getRootComponent(); // Early return if collecting results of a node holding a nested tree. if (node.isNestedTreeHolder()) { // If the nested tree is defined, it has been resolved during a measure call during // layout calculation. InternalNode nestedTree = resolveNestedTree( node, SizeSpec.makeSizeSpec(node.getWidth(), EXACTLY), SizeSpec.makeSizeSpec(node.getHeight(), EXACTLY)); if (nestedTree == NULL_LAYOUT) { return; } // Account for position of the holder node. layoutState.mCurrentX += node.getX(); layoutState.mCurrentY += node.getY(); collectResults(nestedTree, layoutState, parentDiffNode); layoutState.mCurrentX -= node.getX(); layoutState.mCurrentY -= node.getY(); return; } final boolean shouldGenerateDiffTree = layoutState.mShouldGenerateDiffTree; final DiffNode currentDiffNode = node.getDiffNode(); final boolean shouldUseCachedOutputs = isMountSpec(component) && currentDiffNode != null; final boolean isCachedOutputUpdated = shouldUseCachedOutputs && node.areCachedMeasuresValid(); final DiffNode diffNode; if (shouldGenerateDiffTree) { diffNode = createDiffNode(node, parentDiffNode); if (parentDiffNode == null) { layoutState.mDiffTreeRoot = diffNode; } } else { diffNode = null; } final boolean needsHostView = needsHostView(node, layoutState); final long currentHostMarker = layoutState.mCurrentHostMarker; final int currentHostOutputPosition = layoutState.mCurrentHostOutputPosition; int hostLayoutPosition = -1; // 1. Insert a host LayoutOutput if we have some interactive content to be attached to. if (needsHostView) { hostLayoutPosition = addHostLayoutOutput(node, layoutState, diffNode); layoutState.mCurrentLevel++; layoutState.mCurrentHostMarker = layoutState.mMountableOutputs.get(hostLayoutPosition).getId(); layoutState.mCurrentHostOutputPosition = hostLayoutPosition; } // We need to take into account flattening when setting duplicate parent state. The parent after // flattening may no longer exist. Therefore the value of duplicate parent state should only be // true if the path between us (inclusive) and our inner/root host (exclusive) all are // duplicate parent state. final boolean shouldDuplicateParentState = layoutState.mShouldDuplicateParentState; layoutState.mShouldDuplicateParentState = needsHostView || (shouldDuplicateParentState && node.isDuplicateParentStateEnabled()); // Generate the layoutOutput for the given node. final LayoutOutput layoutOutput = createGenericLayoutOutput(node, layoutState); if (layoutOutput != null) { final long previousId = shouldUseCachedOutputs ? currentDiffNode.getContent().getId() : -1; layoutState.mLayoutStateOutputIdCalculator.calculateAndSetLayoutOutputIdAndUpdateState( layoutOutput, layoutState.mCurrentLevel, LayoutOutput.TYPE_CONTENT, previousId, isCachedOutputUpdated); } // If we don't need to update this output we can safely re-use the display list from the // previous output. if (ThreadUtils.isMainThread() && isCachedOutputUpdated) { layoutOutput.setDisplayList(currentDiffNode.getContent().getDisplayList()); } // 2. Add background if defined. final Reference<? extends Drawable> background = node.getBackground(); if (background != null) { if (layoutOutput != null && layoutOutput.hasViewNodeInfo()) { layoutOutput.getViewNodeInfo().setBackground(background); } else { final LayoutOutput convertBackground = (currentDiffNode != null) ? currentDiffNode.getBackground() : null; final LayoutOutput backgroundOutput = addDrawableComponent( node, layoutState, convertBackground, background, LayoutOutput.TYPE_BACKGROUND); if (diffNode != null) { diffNode.setBackground(backgroundOutput); } } } // 3. Now add the MountSpec (either View or Drawable) to the Outputs. if (isMountSpec(component)) { // Notify component about its final size. component.getLifecycle().onBoundsDefined(layoutState.mContext, node, component); addMountableOutput(layoutState, layoutOutput); addLayoutOutputIdToPositionsMap( layoutState.mOutputsIdToPositionMap, layoutOutput, layoutState.mMountableOutputs.size() - 1); if (diffNode != null) { diffNode.setContent(layoutOutput); } } // 4. Add border color if defined. if (node.shouldDrawBorders()) { final LayoutOutput convertBorder = (currentDiffNode != null) ? currentDiffNode.getBorder() : null; final LayoutOutput borderOutput = addDrawableComponent( node, layoutState, convertBorder, getBorderColorDrawable(node), LayoutOutput.TYPE_BORDER); if (diffNode != null) { diffNode.setBorder(borderOutput); } } // 5. Extract the Transitions. if (SDK_INT >= ICE_CREAM_SANDWICH) { if (node.getTransitionKey() != null) { layoutState .getOrCreateTransitionContext() .addTransitionKey(node.getTransitionKey()); } if (component != null) { final TransitionSet transitionSet = component.getLifecycle().onCreateTransition(layoutState.mContext, component); if (transitionSet != null) { layoutState.getOrCreateTransitionContext().addAutoTransitions(transitionSet); } } } layoutState.mCurrentX += node.getX(); layoutState.mCurrentY += node.getY(); // We must process the nodes in order so that the layout state output order is correct. for (int i = 0, size = node.getChildCount(); i < size; i++) { collectResults( node.getChildAt(i), layoutState, diffNode); } layoutState.mCurrentX -= node.getX(); layoutState.mCurrentY -= node.getY(); // 6. Add foreground if defined. final Drawable foreground = node.getForeground(); if (foreground != null) { if (layoutOutput != null && layoutOutput.hasViewNodeInfo() && SDK_INT >= M) { layoutOutput.getViewNodeInfo().setForeground(foreground); } else { final LayoutOutput convertForeground = (currentDiffNode != null) ? currentDiffNode.getForeground() : null; final LayoutOutput foregroundOutput = addDrawableComponent( node, layoutState, convertForeground, DrawableReference.create().drawable(foreground).build(), LayoutOutput.TYPE_FOREGROUND); if (diffNode != null) { diffNode.setForeground(foregroundOutput); } } } // 7. Add VisibilityOutputs if any visibility-related event handlers are present. if (node.hasVisibilityHandlers()) { final VisibilityOutput visibilityOutput = createVisibilityOutput(node, layoutState); final long previousId = shouldUseCachedOutputs ? currentDiffNode.getVisibilityOutput().getId() : -1; layoutState.mLayoutStateOutputIdCalculator.calculateAndSetVisibilityOutputId( visibilityOutput, layoutState.mCurrentLevel, previousId); layoutState.mVisibilityOutputs.add(visibilityOutput); if (diffNode != null) { diffNode.setVisibilityOutput(visibilityOutput); } } // 8. If we're in a testing environment, maintain an additional data structure with // information about nodes that we can query later. if (layoutState.mTestOutputs != null && !TextUtils.isEmpty(node.getTestKey())) { final TestOutput testOutput = createTestOutput(node, layoutState, layoutOutput); layoutState.mTestOutputs.add(testOutput); } // All children for the given host have been added, restore the previous // host, level, and duplicate parent state value in the recursive queue. if (layoutState.mCurrentHostMarker != currentHostMarker) { layoutState.mCurrentHostMarker = currentHostMarker; layoutState.mCurrentHostOutputPosition = currentHostOutputPosition; layoutState.mCurrentLevel--; } layoutState.mShouldDuplicateParentState = shouldDuplicateParentState; Collections.sort(layoutState.mMountableOutputTops, sTopsComparator); Collections.sort(layoutState.mMountableOutputBottoms, sBottomsComparator); } private static void calculateAndSetHostOutputIdAndUpdateState( InternalNode node, LayoutOutput hostOutput, LayoutState layoutState, boolean isCachedOutputUpdated) { if (layoutState.isLayoutRoot(node)) { // The root host (LithoView) always has ID 0 and is unconditionally // set as dirty i.e. no need to use shouldComponentUpdate(). hostOutput.setId(ROOT_HOST_ID); // Special case where the host marker of the root host is pointing to itself. hostOutput.setHostMarker(ROOT_HOST_ID); hostOutput.setUpdateState(LayoutOutput.STATE_DIRTY); } else { layoutState.mLayoutStateOutputIdCalculator.calculateAndSetLayoutOutputIdAndUpdateState( hostOutput, layoutState.mCurrentLevel, LayoutOutput.TYPE_HOST, -1, isCachedOutputUpdated); } } private static LayoutOutput addDrawableComponent( InternalNode node, LayoutState layoutState, LayoutOutput recycle, Reference<? extends Drawable> reference, @LayoutOutput.LayoutOutputType int type) { final Component<DrawableComponent> drawableComponent = DrawableComponent.create(reference); drawableComponent.setScopedContext( ComponentContext.withComponentScope(node.getContext(), drawableComponent)); final boolean isOutputUpdated; if (recycle != null) { isOutputUpdated = !drawableComponent.getLifecycle().shouldComponentUpdate( recycle.getComponent(), drawableComponent); } else { isOutputUpdated = false; } final long previousId = recycle != null ? recycle.getId() : -1; final LayoutOutput output = addDrawableLayoutOutput( drawableComponent, layoutState, node, type, previousId, isOutputUpdated); return output; } private static Reference<? extends Drawable> getBorderColorDrawable(InternalNode node) { if (!node.shouldDrawBorders()) { throw new RuntimeException("This node does not support drawing border color"); } return BorderColorDrawableReference.create(node.getContext()) .color(node.getBorderColor()) .borderLeft(FastMath.round(node.mYogaNode.getLayoutBorder(YogaEdge.LEFT))) .borderTop(FastMath.round(node.mYogaNode.getLayoutBorder(YogaEdge.TOP))) .borderRight(FastMath.round(node.mYogaNode.getLayoutBorder(YogaEdge.RIGHT))) .borderBottom(FastMath.round(node.mYogaNode.getLayoutBorder(YogaEdge.BOTTOM))) .build(); } private static void addLayoutOutputIdToPositionsMap( LongSparseArray outputsIdToPositionMap, LayoutOutput layoutOutput, int position) { if (outputsIdToPositionMap != null) { outputsIdToPositionMap.put(layoutOutput.getId(), position); } } private static LayoutOutput addDrawableLayoutOutput( Component<DrawableComponent> drawableComponent, LayoutState layoutState, InternalNode node, @LayoutOutput.LayoutOutputType int layoutOutputType, long previousId, boolean isCachedOutputUpdated) { drawableComponent.getLifecycle().onBoundsDefined( layoutState.mContext, node, drawableComponent); final LayoutOutput drawableLayoutOutput = createDrawableLayoutOutput( drawableComponent, layoutState, node); layoutState.mLayoutStateOutputIdCalculator.calculateAndSetLayoutOutputIdAndUpdateState( drawableLayoutOutput, layoutState.mCurrentLevel, layoutOutputType, previousId, isCachedOutputUpdated); addMountableOutput(layoutState, drawableLayoutOutput); addLayoutOutputIdToPositionsMap( layoutState.mOutputsIdToPositionMap, drawableLayoutOutput, layoutState.mMountableOutputs.size() - 1); return drawableLayoutOutput; } static void releaseNodeTree(InternalNode node, boolean isNestedTree) { if (node == NULL_LAYOUT) { throw new IllegalArgumentException("Cannot release a null node tree"); } for (int i = node.getChildCount() - 1; i >= 0; i--) { final InternalNode child = node.getChildAt(i); if (isNestedTree && node.hasNewLayout()) { node.markLayoutSeen(); } // A node must be detached from its parent *before* being released (otherwise the parent would // retain a reference to a node that may get re-used by another thread) node.removeChildAt(i); releaseNodeTree(child, isNestedTree); } if (node.hasNestedTree() && node.getNestedTree() != NULL_LAYOUT) { releaseNodeTree(node.getNestedTree(), true); } ComponentsPools.release(node); } /** * If we have an interactive LayoutSpec or a MountSpec Drawable, we need to insert an * HostComponent in the Outputs such as it will be used as a HostView at Mount time. View * MountSpec are not allowed. * * @return The position the HostLayoutOutput was inserted. */ private static int addHostLayoutOutput( InternalNode node, LayoutState layoutState, DiffNode diffNode) { final Component<?> component = node.getRootComponent(); // Only the root host is allowed to wrap view mount specs as a layout output // is unconditionally added for it. if (isMountViewSpec(component) && !layoutState.isLayoutRoot(node)) { throw new IllegalArgumentException("We shouldn't insert a host as a parent of a View"); } final LayoutOutput hostLayoutOutput = createHostLayoutOutput(layoutState, node); // The component of the hostLayoutOutput will be set later after all the // children got processed. addMountableOutput(layoutState, hostLayoutOutput); final int hostOutputPosition = layoutState.mMountableOutputs.size() - 1; if (diffNode != null) { diffNode.setHost(hostLayoutOutput); } calculateAndSetHostOutputIdAndUpdateState( node, hostLayoutOutput, layoutState, false); addLayoutOutputIdToPositionsMap( layoutState.mOutputsIdToPositionMap, hostLayoutOutput, hostOutputPosition); return hostOutputPosition; } static <T extends ComponentLifecycle> LayoutState calculate( ComponentContext c, Component<T> component, int componentTreeId, int widthSpec, int heightSpec, boolean shouldGenerateDiffTree, boolean shouldAnimatedTransitions, DiffNode previousDiffTreeRoot, boolean canPrefetchDisplayLists) { // Detect errors internal to components component.markLayoutStarted(); LayoutState layoutState = ComponentsPools.acquireLayoutState(c); layoutState.mShouldGenerateDiffTree = shouldGenerateDiffTree; layoutState.mComponentTreeId = componentTreeId; layoutState.mAccessibilityManager = (AccessibilityManager) c.getSystemService(ACCESSIBILITY_SERVICE); layoutState.mAccessibilityEnabled = isAccessibilityEnabled(layoutState.mAccessibilityManager); layoutState.mComponent = component; layoutState.mWidthSpec = widthSpec; layoutState.mHeightSpec = heightSpec; layoutState.mShouldAnimateTransitions = shouldAnimatedTransitions; layoutState.mCanPrefetchDisplayLists = canPrefetchDisplayLists; component.applyStateUpdates(c); final InternalNode root = createAndMeasureTreeForComponent( component.getScopedContext(), component, null, // nestedTreeHolder is null because this is measuring the root component tree. widthSpec, heightSpec, previousDiffTreeRoot); switch (SizeSpec.getMode(widthSpec)) { case SizeSpec.EXACTLY: layoutState.mWidth = SizeSpec.getSize(widthSpec); break; case SizeSpec.AT_MOST: layoutState.mWidth = Math.min(root.getWidth(), SizeSpec.getSize(widthSpec)); break; case SizeSpec.UNSPECIFIED: layoutState.mWidth = root.getWidth(); break; } switch (SizeSpec.getMode(heightSpec)) { case SizeSpec.EXACTLY: layoutState.mHeight = SizeSpec.getSize(heightSpec); break; case SizeSpec.AT_MOST: layoutState.mHeight = Math.min(root.getHeight(), SizeSpec.getSize(heightSpec)); break; case SizeSpec.UNSPECIFIED: layoutState.mHeight = root.getHeight(); break; } layoutState.mLayoutStateOutputIdCalculator.clear(); // Reset markers before collecting layout outputs. layoutState.mCurrentHostMarker = -1; final ComponentsLogger logger = c.getLogger(); if (root == NULL_LAYOUT) { return layoutState; } layoutState.mLayoutRoot = root; ComponentsSystrace.beginSection("collectResults:" + component.getSimpleName()); LogEvent collectResultsEvent = null; if (logger != null) { collectResultsEvent = logger.newPerformanceEvent(EVENT_COLLECT_RESULTS); collectResultsEvent.addParam(PARAM_LOG_TAG, c.getLogTag()); } collectResults(root, layoutState, null); if (logger != null) { logger.log(collectResultsEvent); } ComponentsSystrace.endSection(); if (!ComponentsConfiguration.isDebugModeEnabled && layoutState.mLayoutRoot != null) { releaseNodeTree(layoutState.mLayoutRoot, false /* isNestedTree */); layoutState.mLayoutRoot = null; } final Activity activity = getValidActivityForContext(c); if (activity != null && isEligibleForCreatingDisplayLists()) { if (ThreadUtils.isMainThread() && !layoutState.mCanPrefetchDisplayLists && canCollectDisplayListsSync(activity)) { collectDisplayLists(layoutState); } else if (layoutState.mCanPrefetchDisplayLists) { queueDisplayListsForPrefetch(layoutState); } } return layoutState; } void preAllocateMountContent() { if (mMountableOutputs != null && !mMountableOutputs.isEmpty()) { for (int i = 0, size = mMountableOutputs.size(); i < size; i++) { final Component component = mMountableOutputs.get(i).getComponent(); if (Component.isMountViewSpec(component)) { final ComponentLifecycle lifecycle = component.getLifecycle(); if (!lifecycle.hasBeenPreallocated()) { final int poolSize = lifecycle.poolSize(); int insertedCount = 0; while (insertedCount < poolSize && ComponentsPools.canAddMountContentToPool(mContext, lifecycle)) { ComponentsPools.release( mContext, lifecycle, lifecycle.createMountContent(mContext)); insertedCount++; } lifecycle.setWasPreallocated(); } } } } } private static void collectDisplayLists(LayoutState layoutState) { ComponentsSystrace.beginSection( "collectDisplayLists:" + layoutState.mComponent.getSimpleName()); final Rect rect = layoutState.mDisplayListCreateRect; for (int i = 0, count = layoutState.getMountableOutputCount(); i < count; i++) { final LayoutOutput output = layoutState.getMountableOutputAt(i); if (shouldCreateDisplayList(output, rect)) { layoutState.createDisplayList(output); } } ComponentsSystrace.endSection(); } private static boolean shouldCreateDisplayList(LayoutOutput output, Rect rect) { final Component component = output.getComponent(); final ComponentLifecycle lifecycle = component.getLifecycle(); if (!lifecycle.shouldUseDisplayList()) { return false; } output.getMountBounds(rect); if (output.getDisplayList() != null && output.getDisplayList().isValid()) { // This output already has a valid DisplayList from diffing. No need to re-create it. // Just update its bounds. try { output.getDisplayList().setBounds(rect.left, rect.top, rect.right, rect.bottom); return false; } catch (DisplayListException e) { // Nothing to do here. } } return true; } private static boolean canCollectDisplayListsSync(Activity activity) { // If we have no window or the hierarchy has never been drawn before we cannot guarantee that // a valid GL context exists. In this case just bail. final Window window = activity.getWindow(); if (window == null) { return false; } final View decorView = window.getDecorView(); if (decorView == null || decorView.getDrawingTime() == 0) { return false; } return true; } void createDisplayList(LayoutOutput output) { ThreadUtils.assertMainThread(); final Component component = output.getComponent(); ComponentsSystrace.beginSection("createDisplayList: "+component.getSimpleName()); final ComponentLifecycle lifecycle = component.getLifecycle(); final DisplayList displayList = DisplayList.createDisplayList( lifecycle.getClass().getSimpleName()); if (displayList == null) { ComponentsSystrace.endSection(); return; } final ComponentContext context = mContext; Drawable drawable = (Drawable) ComponentsPools.acquireMountContent(context, lifecycle.getId()); if (drawable == null) { drawable = (Drawable) lifecycle.createMountContent(context); } final LayoutOutput clickableOutput = findInteractiveRoot(this, output); boolean isStateEnabled = false; if (clickableOutput != null && clickableOutput.getNodeInfo() != null) { final NodeInfo nodeInfo = clickableOutput.getNodeInfo(); if (nodeInfo.hasTouchEventHandlers() || nodeInfo.getFocusState() == FOCUS_SET_TRUE) { isStateEnabled = true; } } if (isStateEnabled) { drawable.setState(DRAWABLE_STATE_ENABLED); } else { drawable.setState(DRAWABLE_STATE_NOT_ENABLED); } lifecycle.mount( context, drawable, component); lifecycle.bind(context, drawable, component); final Rect rect = mDisplayListCreateRect; output.getMountBounds(rect); drawable.setBounds(0, 0, rect.width(), rect.height()); try { final Canvas canvas = displayList.start(rect.width(), rect.height()); drawable.draw(canvas); displayList.end(canvas); displayList.setBounds(rect.left, rect.top, rect.right, rect.bottom); output.setDisplayList(displayList); } catch (DisplayListException e) { // Display list creation failed. Make sure the DisplayList for this output is set // to null. output.setDisplayList(null); } lifecycle.unbind(context, drawable, component); lifecycle.unmount(context, drawable, component); ComponentsPools.release(context, lifecycle, drawable); ComponentsSystrace.endSection(); } private static void queueDisplayListsForPrefetch(LayoutState layoutState) { final Rect rect = layoutState.mDisplayListQueueRect; for (int i = 0, count = layoutState.getMountableOutputCount(); i < count; i++) { final LayoutOutput output = layoutState.getMountableOutputAt(i); if (shouldCreateDisplayList(output, rect)) { layoutState.mDisplayListsToPrefetch.add(i); } } if (!layoutState.mDisplayListsToPrefetch.isEmpty()) { DisplayListPrefetcher.getInstance().addLayoutState(layoutState); } } public static boolean isEligibleForCreatingDisplayLists() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } private static LayoutOutput findInteractiveRoot(LayoutState layoutState, LayoutOutput output) { if (output.getId() == ROOT_HOST_ID) { return output; } if ((output.getFlags() & FLAG_DUPLICATE_PARENT_STATE) != 0) { final int parentPosition = layoutState.getLayoutOutputPositionForId(output.getHostMarker()); if (parentPosition >= 0) { final LayoutOutput parent = layoutState.mMountableOutputs.get(parentPosition); if (parent == null) { return null; } return findInteractiveRoot(layoutState, parent); } return null; } return output; } private static boolean isActivityDestroyed(Activity activity) { if (SDK_INT >= JELLY_BEAN_MR1) { return activity.isDestroyed(); } return false; } private static Activity findActivityInContext(Context context) { if (context instanceof Activity) { return (Activity) context; } else if (context instanceof ContextWrapper) { return findActivityInContext(((ContextWrapper) context).getBaseContext()); } return null; } private static Activity getValidActivityForContext(Context context) { final Activity activity = findActivityInContext(context); if (activity == null || activity.isFinishing() || isActivityDestroyed(activity)) { return null; } return activity; } @VisibleForTesting static <T extends ComponentLifecycle> InternalNode createTree( Component<T> component, ComponentContext context) { final ComponentsLogger logger = context.getLogger(); LogEvent createLayoutEvent = null; if (logger != null) { createLayoutEvent = logger.newPerformanceEvent(EVENT_CREATE_LAYOUT); createLayoutEvent.addParam(PARAM_LOG_TAG, context.getLogTag()); createLayoutEvent.addParam(PARAM_COMPONENT, component.getSimpleName()); } final InternalNode root = (InternalNode) component.getLifecycle().createLayout( context, component, true /* resolveNestedTree */); if (logger != null) { logger.log(createLayoutEvent); } return root; } @VisibleForTesting static void measureTree( InternalNode root, int widthSpec, int heightSpec, DiffNode previousDiffTreeRoot) { final ComponentContext context = root.getContext(); final Component component = root.getRootComponent(); ComponentsSystrace.beginSection("measureTree:" + component.getSimpleName()); if (YogaConstants.isUndefined(root.getStyleWidth())) { root.setStyleWidthFromSpec(widthSpec); } if (YogaConstants.isUndefined(root.getStyleHeight())) { root.setStyleHeightFromSpec(heightSpec); } if (previousDiffTreeRoot != null) { ComponentsSystrace.beginSection("applyDiffNode"); applyDiffNodeToUnchangedNodes(root, previousDiffTreeRoot); ComponentsSystrace.endSection(/* applyDiffNode */); } final ComponentsLogger logger = context.getLogger(); LogEvent layoutEvent = null; if (logger != null) { layoutEvent = logger.newPerformanceEvent(EVENT_CSS_LAYOUT); layoutEvent.addParam(PARAM_LOG_TAG, context.getLogTag()); layoutEvent.addParam(PARAM_TREE_DIFF_ENABLED, String.valueOf(previousDiffTreeRoot != null)); } root.calculateLayout( SizeSpec.getMode(widthSpec) == SizeSpec.UNSPECIFIED ? YogaConstants.UNDEFINED : SizeSpec.getSize(widthSpec), SizeSpec.getMode(heightSpec) == SizeSpec.UNSPECIFIED ? YogaConstants.UNDEFINED : SizeSpec.getSize(heightSpec)); if (logger != null) { logger.log(layoutEvent); } ComponentsSystrace.endSection(/* measureTree */); } /** * Create and measure the nested tree or return the cached one for the same size specs. */ static InternalNode resolveNestedTree( InternalNode nestedTreeHolder, int widthSpec, int heightSpec) { final ComponentContext context = nestedTreeHolder.getContext(); final Component<?> component = nestedTreeHolder.getRootComponent(); InternalNode nestedTree = nestedTreeHolder.getNestedTree(); if (nestedTree == null || !hasCompatibleSizeSpec( nestedTree.getLastWidthSpec(), nestedTree.getLastHeightSpec(), widthSpec, heightSpec, nestedTree.getLastMeasuredWidth(), nestedTree.getLastMeasuredHeight())) { if (nestedTree != null) { if (nestedTree != NULL_LAYOUT) { releaseNodeTree(nestedTree, true /* isNestedTree */); } nestedTree = null; } if (component.hasCachedLayout()) { final InternalNode cachedLayout = component.getCachedLayout(); final boolean hasCompatibleLayoutDirection = InternalNode.hasValidLayoutDirectionInNestedTree(nestedTreeHolder, cachedLayout); // Transfer the cached layout to the node without releasing it if it's compatible. if (hasCompatibleLayoutDirection && hasCompatibleSizeSpec( cachedLayout.getLastWidthSpec(), cachedLayout.getLastHeightSpec(), widthSpec, heightSpec, cachedLayout.getLastMeasuredWidth(), cachedLayout.getLastMeasuredHeight())) { nestedTree = cachedLayout; component.clearCachedLayout(); } else { component.releaseCachedLayout(); } } if (nestedTree == null) { nestedTree = createAndMeasureTreeForComponent( context, component, nestedTreeHolder, widthSpec, heightSpec, nestedTreeHolder.getDiffNode()); // Previously set while traversing the holder's tree. nestedTree.setLastWidthSpec(widthSpec); nestedTree.setLastHeightSpec(heightSpec); nestedTree.setLastMeasuredHeight(nestedTree.getHeight()); nestedTree.setLastMeasuredWidth(nestedTree.getWidth()); } nestedTreeHolder.setNestedTree(nestedTree); } // This is checking only nested tree roots however it will be moved to check all the tree roots. InternalNode.assertContextSpecificStyleNotSet(nestedTree); return nestedTree; } /** * Create and measure a component with the given size specs. */ static InternalNode createAndMeasureTreeForComponent( ComponentContext c, Component component, int widthSpec, int heightSpec) { return createAndMeasureTreeForComponent(c, component, null, widthSpec, heightSpec, null); } private static InternalNode createAndMeasureTreeForComponent( ComponentContext c, Component component, InternalNode nestedTreeHolder, // This will be set only if we are resolving a nested tree. int widthSpec, int heightSpec, DiffNode diffTreeRoot) { final boolean isTest = "robolectric".equals(Build.FINGERPRINT); // Copy the context so that it can have its own set of tree props. // Robolectric tests keep the context so that tree props can be set externally. if (!isTest) { c = c.makeNewCopy(); } final boolean hasNestedTreeHolder = nestedTreeHolder != null; if (hasNestedTreeHolder) { c.setTreeProps(nestedTreeHolder.getPendingTreeProps()); } else if (!isTest) { c.setTreeProps(null); } // Account for the size specs in ComponentContext in case the tree is a NestedTree. final int previousWidthSpec = c.getWidthSpec(); final int previousHeightSpec = c.getHeightSpec(); c.setWidthSpec(widthSpec); c.setHeightSpec(heightSpec); final InternalNode root = createTree( component, c); if (hasNestedTreeHolder) { c.setTreeProps(null); } c.setWidthSpec(previousWidthSpec); c.setHeightSpec(previousHeightSpec); if (root == NULL_LAYOUT) { return root; } // If measuring a ComponentTree with a LayoutSpecWithSizeSpec at the root, the nested tree // holder argument will be null. if (hasNestedTreeHolder && isLayoutSpecWithSizeSpec(component)) { // Transfer information from the holder node to the nested tree root before measurement. nestedTreeHolder.copyInto(root); diffTreeRoot = nestedTreeHolder.getDiffNode(); } else if (root.getStyleDirection() == com.facebook.yoga.YogaDirection.INHERIT && LayoutState.isLayoutDirectionRTL(c)) { root.layoutDirection(YogaDirection.RTL); } measureTree( root, widthSpec, heightSpec, diffTreeRoot); return root; } static DiffNode createDiffNode(InternalNode node, DiffNode parent) { ComponentsSystrace.beginSection("diff_node_creation"); DiffNode diffNode = ComponentsPools.acquireDiffNode(); diffNode.setLastWidthSpec(node.getLastWidthSpec()); diffNode.setLastHeightSpec(node.getLastHeightSpec()); diffNode.setLastMeasuredWidth(node.getLastMeasuredWidth()); diffNode.setLastMeasuredHeight(node.getLastMeasuredHeight()); diffNode.setComponent(node.getRootComponent()); if (parent != null) { parent.addChild(diffNode); } ComponentsSystrace.endSection(); return diffNode; } boolean isCompatibleSpec(int widthSpec, int heightSpec) { final boolean widthIsCompatible = MeasureComparisonUtils.isMeasureSpecCompatible( mWidthSpec, widthSpec, mWidth); final boolean heightIsCompatible = MeasureComparisonUtils.isMeasureSpecCompatible( mHeightSpec, heightSpec, mHeight); return widthIsCompatible && heightIsCompatible; } boolean isCompatibleAccessibility() { return isAccessibilityEnabled(mAccessibilityManager) == mAccessibilityEnabled; } private static boolean isAccessibilityEnabled(AccessibilityManager accessibilityManager) { return accessibilityManager.isEnabled() && AccessibilityManagerCompat.isTouchExplorationEnabled(accessibilityManager); } /** * Traverses the layoutTree and the diffTree recursively. If a layoutNode has a compatible host * type {@link LayoutState#hostIsCompatible} it assigns the DiffNode to the layout node in order * to try to re-use the LayoutOutputs that will be generated by {@link * LayoutState#collectResults(InternalNode, LayoutState, DiffNode)}. If a layoutNode * component returns false when shouldComponentUpdate is called with the DiffNode Component it * also tries to re-use the old measurements and therefore marks as valid the cachedMeasures for * the whole component subtree. * * @param layoutNode the root of the LayoutTree * @param diffNode the root of the diffTree * * @return true if the layout node requires updating, false if it can re-use the measurements * from the diff node. */ static boolean applyDiffNodeToUnchangedNodes(InternalNode layoutNode, DiffNode diffNode) { // Root of the main tree or of a nested tree. final boolean isTreeRoot = layoutNode.getParent() == null; if (isLayoutSpecWithSizeSpec(layoutNode.getRootComponent()) && !isTreeRoot) { layoutNode.setDiffNode(diffNode); return true; } if (!hostIsCompatible(layoutNode, diffNode)) { return true; } layoutNode.setDiffNode(diffNode); final int layoutCount = layoutNode.getChildCount(); final int diffCount = diffNode.getChildCount(); // Layout node needs to be updated if: // - it has a different number of children. // - one of its children needs updating. // - the node itself declares that it needs updating. boolean shouldUpdate = layoutCount != diffCount; for (int i = 0; i < layoutCount && i < diffCount; i++) { // ensure that we always run for all children. boolean shouldUpdateChild = applyDiffNodeToUnchangedNodes( layoutNode.getChildAt(i), diffNode.getChildAt(i)); shouldUpdate |= shouldUpdateChild; } shouldUpdate |= shouldComponentUpdate(layoutNode, diffNode); if (!shouldUpdate) { applyDiffNodeToLayoutNode(layoutNode, diffNode); } return shouldUpdate; } /** * Copies the inter stage state (if any) from the DiffNode's component to the layout node's * component, and declares that the cached measures on the diff node are valid for the layout * node. */ private static void applyDiffNodeToLayoutNode(InternalNode layoutNode, DiffNode diffNode) { final Component component = layoutNode.getRootComponent(); if (component != null) { component.copyInterStageImpl(diffNode.getComponent()); } layoutNode.setCachedMeasuresValid(true); } /** * Returns true either if the two nodes have the same Component type or if both don't have a * Component. */ private static boolean hostIsCompatible(InternalNode node, DiffNode diffNode) { if (diffNode == null) { return false; } return isSameComponentType(node.getRootComponent(), diffNode.getComponent()); } private static boolean isSameComponentType(Component a, Component b) { if (a == b) { return true; } else if (a == null || b == null) { return false; } return a.getLifecycle().getClass().equals(b.getLifecycle().getClass()); } private static boolean shouldComponentUpdate(InternalNode layoutNode, DiffNode diffNode) { if (diffNode == null) { return true; } final Component component = layoutNode.getRootComponent(); if (component != null) { return component.getLifecycle().shouldComponentUpdate(component, diffNode.getComponent()); } return true; } boolean isCompatibleComponentAndSpec( int componentId, int widthSpec, int heightSpec) { return mComponent.getId() == componentId && isCompatibleSpec(widthSpec, heightSpec); } boolean isCompatibleSize(int width, int height) { return mWidth == width && mHeight == height; } boolean isComponentId(int componentId) { return mComponent.getId() == componentId; } int getMountableOutputCount() { return mMountableOutputs.size(); } LayoutOutput getMountableOutputAt(int index) { return mMountableOutputs.get(index); } ArrayList<LayoutOutput> getMountableOutputTops() { return mMountableOutputTops; } ArrayList<LayoutOutput> getMountableOutputBottoms() { return mMountableOutputBottoms; } int getVisibilityOutputCount() { return mVisibilityOutputs.size(); } VisibilityOutput getVisibilityOutputAt(int index) { return mVisibilityOutputs.get(index); } int getTestOutputCount() { return mTestOutputs == null ? 0 : mTestOutputs.size(); } TestOutput getTestOutputAt(int index) { return mTestOutputs == null ? null : mTestOutputs.get(index); } public DiffNode getDiffTree() { return mDiffTreeRoot; } int getWidth() { return mWidth; } int getHeight() { return mHeight; } /** * @return The id of the {@link ComponentTree} that generated this {@link LayoutState} */ int getComponentTreeId() { return mComponentTreeId; } /** * See {@link LayoutState#acquireRef} Call this when you are done using the reference to the * LayoutState. */ @ThreadSafe(enableChecks = false) void releaseRef() { int count = mReferenceCount.decrementAndGet(); if (count < 0) { throw new IllegalStateException("Trying to releaseRef a recycled LayoutState"); } if (count == 0) { mContext = null; mComponent = null; mWidth = 0; mHeight = 0; mCurrentX = 0; mCurrentY = 0; mCurrentHostMarker = -1; mCurrentHostOutputPosition = -1; mComponentTreeId = -1; mShouldDuplicateParentState = true; for (int i = 0, size = mMountableOutputs.size(); i < size; i++) { ComponentsPools.release(mMountableOutputs.get(i)); } mMountableOutputs.clear(); mMountableOutputTops.clear(); mMountableOutputBottoms.clear(); mOutputsIdToPositionMap.clear(); mDisplayListsToPrefetch.clear(); for (int i = 0, size = mVisibilityOutputs.size(); i < size; i++) { ComponentsPools.release(mVisibilityOutputs.get(i)); } mVisibilityOutputs.clear(); if (mTestOutputs != null) { for (int i = 0, size = mTestOutputs.size(); i < size; i++) { ComponentsPools.release(mTestOutputs.get(i)); } mTestOutputs.clear(); } mShouldGenerateDiffTree = false; mAccessibilityManager = null; mAccessibilityEnabled = false; mShouldAnimateTransitions = false; if (mDiffTreeRoot != null) { ComponentsPools.release(mDiffTreeRoot); mDiffTreeRoot = null; } mLayoutStateOutputIdCalculator.clear(); if (mTransitionContext != null) { ComponentsPools.release(mTransitionContext); mTransitionContext = null; } // This should only ever be true in non-release builds as we need this for Stetho integration. // In release builds the node tree is released in calculateLayout(). if (mLayoutRoot != null) { releaseNodeTree(mLayoutRoot, false /* isNestedTree */); mLayoutRoot = null; } ComponentsPools.release(this); } } /** * The lifecycle of LayoutState is generally controlled by ComponentTree. Since sometimes we need * an old LayoutState to be passed to a new LayoutState to implement Tree diffing, We use * reference counting to avoid releasing a LayoutState that is not used by ComponentTree anymore * but could be used by another LayoutState. The rule is that whenever you need to pass the * LayoutState outside of ComponentTree, you acquire a reference and then you release it as soon * as you are done with it * * @return The same LayoutState instance with an higher reference count. */ LayoutState acquireRef() { if (mReferenceCount.getAndIncrement() == 0) { throw new IllegalStateException("Trying to use a released LayoutState"); } return this; } void init(ComponentContext context) { mContext = context; mStateHandler = mContext.getStateHandler(); mReferenceCount.set(1); } /** * Returns the state handler instance currently held by LayoutState and nulls it afterwards. * @return the state handler */ StateHandler consumeStateHandler() { final StateHandler stateHandler = mStateHandler; mStateHandler = null; return stateHandler; } InternalNode getLayoutRoot() { return mLayoutRoot; } // If the layout root is a nested tree holder node, it gets skipped immediately while // collecting the LayoutOutputs. The nested tree itself effectively becomes the layout // root in this case. private boolean isLayoutRoot(InternalNode node) { return mLayoutRoot.isNestedTreeHolder() ? node == mLayoutRoot.getNestedTree() : node == mLayoutRoot; } /** * Check if a cached nested tree has compatible SizeSpec to be reused as is or * if it needs to be recomputed. * * The conditions to be able to re-use previous measurements are: * 1) The measureSpec is the same * 2) The new measureSpec is EXACTLY and the last measured size matches the measureSpec size. * 3) The old measureSpec is UNSPECIFIED, the new one is AT_MOST and the old measured size is * smaller that the maximum size the new measureSpec will allow. * 4) Both measure specs are AT_MOST. The old measure spec allows a bigger size than the new and * the old measured size is smaller than the allowed max size for the new sizeSpec. */ public static boolean hasCompatibleSizeSpec( int oldWidthSpec, int oldHeightSpec, int newWidthSpec, int newHeightSpec, float oldMeasuredWidth, float oldMeasuredHeight) { final boolean widthIsCompatible = MeasureComparisonUtils.isMeasureSpecCompatible( oldWidthSpec, newWidthSpec, (int) oldMeasuredWidth); final boolean heightIsCompatible = MeasureComparisonUtils.isMeasureSpecCompatible( oldHeightSpec, newHeightSpec, (int) oldMeasuredHeight); return widthIsCompatible && heightIsCompatible; } /** * Returns true if this is the root node (which always generates a matching layout * output), if the node has view attributes e.g. tags, content description, etc, or if * the node has explicitly been forced to be wrapped in a view. */ private static boolean needsHostView(InternalNode node, LayoutState layoutState) { return layoutState.isLayoutRoot(node) || (!isMountViewSpec(node.getRootComponent()) && (hasViewContent(node, layoutState) || node.isForceViewWrapping())); } /** * @return the position of the {@link LayoutOutput} with id layoutOutputId in the * {@link LayoutState} list of outputs or -1 if no {@link LayoutOutput} with that id exists in * the {@link LayoutState} */ int getLayoutOutputPositionForId(long layoutOutputId) { return mOutputsIdToPositionMap.get(layoutOutputId, -1); } TransitionContext getTransitionContext() { return mTransitionContext; } boolean hasTransitionContext() { return (mTransitionContext != null); } boolean shouldAnimateTransitions() { return mShouldAnimateTransitions; } private static void addMountableOutput(LayoutState layoutState, LayoutOutput layoutOutput) { layoutState.mMountableOutputs.add(layoutOutput); layoutState.mMountableOutputTops.add(layoutOutput); layoutState.mMountableOutputBottoms.add(layoutOutput); } private TransitionContext getOrCreateTransitionContext() { if (mTransitionContext == null) { mTransitionContext = ComponentsPools.acquireTransitionContext(); } return mTransitionContext; } /** * @return whether there are any items in the queue for Display Lists prefetching. */ boolean hasItemsForDLPrefetch() { return !mDisplayListsToPrefetch.isEmpty(); } /** * Removes and returns next {@link LayoutOutput} from the queue for Display Lists. * Note that it is callers responsibility to make sure queue is not empty. */ LayoutOutput getNextLayoutOutputForDLPrefetch() { final int layoutOutputIndex = mDisplayListsToPrefetch.poll(); return getMountableOutputAt(layoutOutputIndex); } }