/**
* 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);
}
}