/** * 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.Collection; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.VisibleForTesting; import android.support.v4.util.LongSparseArray; import android.support.v4.view.ViewCompat; import android.text.TextUtils; import android.util.SparseArray; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; import com.facebook.infer.annotation.ThreadConfined; import com.facebook.litho.animation.AnimationBinding; import com.facebook.litho.config.ComponentsConfiguration; import com.facebook.litho.reference.Reference; import static android.support.v4.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; import static android.view.View.MeasureSpec.makeMeasureSpec; import static com.facebook.litho.Component.isHostSpec; import static com.facebook.litho.Component.isMountViewSpec; import static com.facebook.litho.ComponentHostUtils.maybeInvalidateAccessibilityState; import static com.facebook.litho.ComponentHostUtils.maybeSetDrawableState; import static com.facebook.litho.FrameworkLogEvents.EVENT_MOUNT; import static com.facebook.litho.FrameworkLogEvents.EVENT_PREPARE_MOUNT; import static com.facebook.litho.FrameworkLogEvents.EVENT_SHOULD_UPDATE_REFERENCE_LAYOUT_MISMATCH; import static com.facebook.litho.FrameworkLogEvents.PARAM_IS_DIRTY; import static com.facebook.litho.FrameworkLogEvents.PARAM_LOG_TAG; import static com.facebook.litho.FrameworkLogEvents.PARAM_MESSAGE; import static com.facebook.litho.FrameworkLogEvents.PARAM_MOUNTED_COUNT; import static com.facebook.litho.FrameworkLogEvents.PARAM_MOVED_COUNT; import static com.facebook.litho.FrameworkLogEvents.PARAM_NO_OP_COUNT; import static com.facebook.litho.FrameworkLogEvents.PARAM_UNCHANGED_COUNT; import static com.facebook.litho.FrameworkLogEvents.PARAM_UNMOUNTED_COUNT; import static com.facebook.litho.FrameworkLogEvents.PARAM_UPDATED_COUNT; import static com.facebook.litho.ThreadUtils.assertMainThread; /** * Encapsulates the mounted state of a {@link Component}. Provides APIs to update state * by recycling existing UI elements e.g. {@link Drawable}s. * * @see #mount(LayoutState, Rect) * @see LithoView * @see LayoutState */ @ThreadConfined(ThreadConfined.UI) class MountState { static final int ROOT_HOST_ID = 0; // Holds the current list of mounted items. // Should always be used within a draw lock. private final LongSparseArray<MountItem> mIndexToItemMap; // Holds a list with information about the components linked to the VisibilityOutputs that are // stored in LayoutState. An item is inserted in this map if its corresponding component is // visible. When the component exits the viewport, the item associated with it is removed from the // map. private final LongSparseArray<VisibilityItem> mVisibilityIdToItemMap; // Holds a list of MountItems that are currently mounted which can mount incrementally. private final LongSparseArray<MountItem> mCanMountIncrementallyMountItems; // A map from test key to a list of one or more `TestItem`s which is only allocated // and populated during test runs. private final Map<String, Deque<TestItem>> mTestItemMap; private long[] mLayoutOutputsIds; // True if we are receiving a new LayoutState and we need to completely // refresh the content of the HostComponent. Always set from the main thread. private boolean mIsDirty; // Holds the list of known component hosts during a mount pass. private final LongSparseArray<ComponentHost> mHostsByMarker = new LongSparseArray<>(); private static final Rect sTempRect = new Rect(); private final ComponentContext mContext; private final LithoView mLithoView; private final Rect mPreviousLocalVisibleRect = new Rect(); private final PrepareMountStats mPrepareMountStats = new PrepareMountStats(); private final MountStats mMountStats = new MountStats(); private DataFlowTransitionManager mTransitionManager; private int mPreviousTopsIndex; private int mPreviousBottomsIndex; private int mLastMountedComponentTreeId; private final HashMap<String, Integer> mMountedTransitionKeys = new HashMap<>(); private final MountItem mRootHostMountItem; public MountState(LithoView view) { mIndexToItemMap = new LongSparseArray<>(); mVisibilityIdToItemMap = new LongSparseArray<>(); mCanMountIncrementallyMountItems = new LongSparseArray<>(); mContext = (ComponentContext) view.getContext(); mLithoView = view; mIsDirty = true; mTestItemMap = ComponentsConfiguration.isEndToEndTestRun ? new HashMap<String, Deque<TestItem>>() : null; // The mount item representing the top-level LithoView which // is always automatically mounted. mRootHostMountItem = ComponentsPools.acquireRootHostMountItem( HostComponent.create(), mLithoView, mLithoView); } /** * To be called whenever the components needs to start the mount process from scratch * e.g. when the component's props or layout change or when the components * gets attached to a host. */ void setDirty() { assertMainThread(); mIsDirty = true; mPreviousLocalVisibleRect.setEmpty(); } boolean isDirty() { assertMainThread(); return mIsDirty; } /** * Mount the layoutState on the pre-set HostView. * @param layoutState * @param localVisibleRect If this variable is null, then mount everything, since incremental * mount is not enabled. * Otherwise mount only what the rect (in local coordinates) contains */ void mount(LayoutState layoutState, Rect localVisibleRect) { assertMainThread(); ComponentsSystrace.beginSection("mount"); final ComponentTree componentTree = mLithoView.getComponentTree(); final ComponentsLogger logger = componentTree.getContext().getLogger(); final int componentTreeId = layoutState.getComponentTreeId(); LogEvent mountEvent = null; if (logger != null) { mountEvent = logger.newPerformanceEvent(EVENT_MOUNT); } // the isDirty check here prevents us from animating for incremental mounts final boolean shouldAnimateTransitions = mIsDirty && layoutState.shouldAnimateTransitions() && layoutState.hasTransitionContext() && mLastMountedComponentTreeId == componentTreeId; prepareTransitionManager(layoutState); if (shouldAnimateTransitions) { createAutoMountTransitions(layoutState); mTransitionManager.onNewTransitionContext(layoutState.getTransitionContext()); recordMountedItemsWithTransitionKeys( mTransitionManager, mIndexToItemMap, true /* isPreMount */); } if (mIsDirty) { suppressInvalidationsOnHosts(true); // Prepare the data structure for the new LayoutState and removes mountItems // that are not present anymore if isUpdateMountInPlace is enabled. prepareMount(layoutState); } mMountStats.reset(); final boolean isIncrementalMountEnabled = localVisibleRect != null; if (!isIncrementalMountEnabled || !performIncrementalMount(layoutState, localVisibleRect)) { for (int i = 0, size = layoutState.getMountableOutputCount(); i < size; i++) { final LayoutOutput layoutOutput = layoutState.getMountableOutputAt(i); final Component component = layoutOutput.getComponent(); ComponentsSystrace.beginSection(component.getSimpleName()); final MountItem currentMountItem = getItemAt(i); final boolean isMounted = currentMountItem != null; final boolean isMountable = !isIncrementalMountEnabled || isMountedHostWithChildContent(currentMountItem) || Rect.intersects(localVisibleRect, layoutOutput.getBounds()); if (isMountable && !isMounted) { mountLayoutOutput(i, layoutOutput, layoutState); } else if (!isMountable && isMounted) { unmountItem(mContext, i, mHostsByMarker); } else if (isMounted) { if (isIncrementalMountEnabled && canMountIncrementally(component)) { mountItemIncrementally(currentMountItem, layoutOutput.getBounds(), localVisibleRect); } if (mIsDirty) { final boolean useUpdateValueFromLayoutOutput = (componentTreeId >= 0) && (componentTreeId == mLastMountedComponentTreeId); final boolean itemUpdated = updateMountItemIfNeeded( layoutOutput, currentMountItem, useUpdateValueFromLayoutOutput, logger, componentTreeId); if (itemUpdated) { mMountStats.updatedCount++; } else { mMountStats.noOpCount++; } } } ComponentsSystrace.endSection(); } if (isIncrementalMountEnabled) { setupPreviousMountableOutputData(layoutState, localVisibleRect); } } mIsDirty = false; if (localVisibleRect != null) { mPreviousLocalVisibleRect.set(localVisibleRect); } processVisibilityOutputs(layoutState, localVisibleRect); if (shouldAnimateTransitions) { recordMountedItemsWithTransitionKeys( mTransitionManager, mIndexToItemMap, false /* isPreMount */); mTransitionManager.runTransitions(); } processTestOutputs(layoutState); suppressInvalidationsOnHosts(false); mLastMountedComponentTreeId = componentTreeId; if (logger != null) { mountEvent.addParam(PARAM_LOG_TAG, componentTree.getContext().getLogTag()); mountEvent.addParam(PARAM_MOUNTED_COUNT, String.valueOf(mMountStats.mountedCount)); mountEvent.addParam(PARAM_UNMOUNTED_COUNT, String.valueOf(mMountStats.unmountedCount)); mountEvent.addParam(PARAM_UPDATED_COUNT, String.valueOf(mMountStats.updatedCount)); mountEvent.addParam(PARAM_NO_OP_COUNT, String.valueOf(mMountStats.noOpCount)); mountEvent.addParam(PARAM_IS_DIRTY, String.valueOf(mIsDirty)); logger.log(mountEvent); } ComponentsSystrace.endSection(); } private void processVisibilityOutputs(LayoutState layoutState, Rect localVisibleRect) { if (localVisibleRect == null) { return; } for (int j = 0, size = layoutState.getVisibilityOutputCount(); j < size; j++) { final VisibilityOutput visibilityOutput = layoutState.getVisibilityOutputAt(j); final EventHandler<VisibleEvent> visibleHandler = visibilityOutput.getVisibleEventHandler(); final EventHandler<FocusedVisibleEvent> focusedHandler = visibilityOutput.getFocusedEventHandler(); final EventHandler<UnfocusedVisibleEvent> unfocusedHandler = visibilityOutput.getUnfocusedEventHandler(); final EventHandler<FullImpressionVisibleEvent> fullImpressionHandler = visibilityOutput.getFullImpressionEventHandler(); final EventHandler<InvisibleEvent> invisibleHandler = visibilityOutput.getInvisibleEventHandler(); final long visibilityOutputId = visibilityOutput.getId(); final Rect visibilityOutputBounds = visibilityOutput.getBounds(); sTempRect.set(visibilityOutputBounds); final boolean isCurrentlyVisible = sTempRect.intersect(localVisibleRect) && isInVisibleRange(visibilityOutput.getVisibleRatio(), visibilityOutputBounds, sTempRect); VisibilityItem visibilityItem = mVisibilityIdToItemMap.get(visibilityOutputId); if (isCurrentlyVisible) { // The component is visible now, but used to be outside the viewport. if (visibilityItem == null) { visibilityItem = ComponentsPools.acquireVisibilityItem(invisibleHandler, unfocusedHandler); mVisibilityIdToItemMap.put(visibilityOutputId, visibilityItem); if (visibleHandler != null) { EventDispatcherUtils.dispatchOnVisible(visibleHandler); } } // Check if the component has entered or exited the focused range. if (focusedHandler != null || unfocusedHandler != null) { if (isInFocusedRange(visibilityOutputBounds, sTempRect)) { if (!visibilityItem.isInFocusedRange()) { visibilityItem.setFocusedRange(true); if (focusedHandler != null) { EventDispatcherUtils.dispatchOnFocused(focusedHandler); } } } else { if (visibilityItem.isInFocusedRange()) { visibilityItem.setFocusedRange(false); if (unfocusedHandler != null) { EventDispatcherUtils.dispatchOnUnfocused(unfocusedHandler); } } } } // If the component has not entered the full impression range yet, make sure to update the // information about the visible edges. if (fullImpressionHandler != null && !visibilityItem.isInFullImpressionRange()) { visibilityItem.setVisibleEdges(visibilityOutputBounds, sTempRect); if (visibilityItem.isInFullImpressionRange()) { EventDispatcherUtils.dispatchOnFullImpression(fullImpressionHandler); } } } else if (visibilityItem != null) { // The component is invisible now, but used to be visible. if (invisibleHandler != null) { EventDispatcherUtils.dispatchOnInvisible(invisibleHandler); } if (unfocusedHandler != null) { visibilityItem.setFocusedRange(false); EventDispatcherUtils.dispatchOnUnfocused(unfocusedHandler); } mVisibilityIdToItemMap.remove(visibilityOutputId); ComponentsPools.release(visibilityItem); } } } /** * Clears and re-populates the test item map if we are in e2e test mode. */ private void processTestOutputs(LayoutState layoutState) { if (mTestItemMap == null) { return; } for (Collection<TestItem> items : mTestItemMap.values()) { for (TestItem item : items) { ComponentsPools.release(item); } } mTestItemMap.clear(); for (int i = 0, size = layoutState.getTestOutputCount(); i < size; i++) { final TestOutput testOutput = layoutState.getTestOutputAt(i); final long hostMarker = testOutput.getHostMarker(); final long layoutOutputId = testOutput.getLayoutOutputId(); final MountItem mountItem = layoutOutputId == -1 ? null : mIndexToItemMap.get(layoutOutputId); final TestItem testItem = ComponentsPools.acquireTestItem(); testItem.setHost(hostMarker == -1 ? null : mHostsByMarker.get(hostMarker)); testItem.setBounds(testOutput.getBounds()); testItem.setTestKey(testOutput.getTestKey()); testItem.setContent(mountItem == null ? null : mountItem.getContent()); final Deque<TestItem> items = mTestItemMap.get(testOutput.getTestKey()); final Deque<TestItem> updatedItems = items == null ? new LinkedList<TestItem>() : items; updatedItems.add(testItem); mTestItemMap.put(testOutput.getTestKey(), updatedItems); } } private boolean isMountedHostWithChildContent(MountItem mountItem) { if (mountItem == null) { return false; } final Object content = mountItem.getContent(); if (!(content instanceof ComponentHost)) { return false; } final ComponentHost host = (ComponentHost) content; return host.getMountItemCount() > 0; } private void setupPreviousMountableOutputData(LayoutState layoutState, Rect localVisibleRect) { if (localVisibleRect.isEmpty()) { return; } final ArrayList<LayoutOutput> layoutOutputTops = layoutState.getMountableOutputTops(); final ArrayList<LayoutOutput> layoutOutputBottoms = layoutState.getMountableOutputBottoms(); final int mountableOutputCount = layoutState.getMountableOutputCount(); mPreviousTopsIndex = layoutState.getMountableOutputCount(); for (int i = 0; i < mountableOutputCount; i++) { if (localVisibleRect.bottom <= layoutOutputTops.get(i).getBounds().top) { mPreviousTopsIndex = i; break; } } mPreviousBottomsIndex = layoutState.getMountableOutputCount(); for (int i = 0; i < mountableOutputCount; i++) { if (localVisibleRect.top < layoutOutputBottoms.get(i).getBounds().bottom) { mPreviousBottomsIndex = i; break; } } } private void clearVisibilityItems() { for (int i = mVisibilityIdToItemMap.size() - 1; i >= 0; i--) { final VisibilityItem visibilityItem = mVisibilityIdToItemMap.valueAt(i); final EventHandler<InvisibleEvent> invisibleHandler = visibilityItem.getInvisibleHandler(); final EventHandler<UnfocusedVisibleEvent> unfocusedHandler = visibilityItem.getUnfocusedHandler(); if (invisibleHandler != null) { EventDispatcherUtils.dispatchOnInvisible(invisibleHandler); } if (unfocusedHandler != null && visibilityItem.isInFocusedRange()) { visibilityItem.setFocusedRange(false); EventDispatcherUtils.dispatchOnUnfocused(unfocusedHandler); } mVisibilityIdToItemMap.removeAt(i); ComponentsPools.release(visibilityItem); } } private void registerHost(long id, ComponentHost host) { host.suppressInvalidations(true); mHostsByMarker.put(id, host); } private boolean isInVisibleRange( float ratio, Rect componentBounds, Rect componentVisibleBounds) { if (ratio <= 0) { return true; } return computeRectArea(componentVisibleBounds) >= ratio * computeRectArea(componentBounds) || isInFocusedRange(componentBounds, componentVisibleBounds); } /** * Returns true if the component is in the focused visible range. */ private boolean isInFocusedRange( Rect componentBounds, Rect componentVisibleBounds) { final View parent = (View) mLithoView.getParent(); final int halfViewportArea = parent.getWidth() * parent.getHeight() / 2; final int totalComponentArea = computeRectArea(componentBounds); final int visibleComponentArea = computeRectArea(componentVisibleBounds); // The component has entered the focused range either if it is larger than half of the viewport // and it occupies at least half of the viewport or if it is smaller than half of the viewport // and it is fully visible. return (totalComponentArea >= halfViewportArea) ? (visibleComponentArea >= halfViewportArea) : componentBounds.equals(componentVisibleBounds); } private static int computeRectArea(Rect rect) { return rect.isEmpty() ? 0 : (rect.width() * rect.height()); } private void suppressInvalidationsOnHosts(boolean suppressInvalidations) { for (int i = mHostsByMarker.size() - 1; i >= 0; i--) { mHostsByMarker.valueAt(i).suppressInvalidations(suppressInvalidations); } } private boolean updateMountItemIfNeeded( LayoutOutput layoutOutput, MountItem currentMountItem, boolean useUpdateValueFromLayoutOutput, ComponentsLogger logger, int componentTreeId) { final Component layoutOutputComponent = layoutOutput.getComponent(); final Component itemComponent = currentMountItem.getComponent(); // 1. Check if the mount item generated from the old component should be updated. final boolean shouldUpdate = shouldUpdateMountItem( layoutOutput, currentMountItem, useUpdateValueFromLayoutOutput, mIndexToItemMap, mLayoutOutputsIds, logger); // 2. Reset all the properties like click handler, content description and tags related to // this item if it needs to be updated. the update mount item will re-set the new ones. if (shouldUpdate) { final String transitionKey = maybeDecrementTransitionKeyMountCount(currentMountItem); // This mount content might be animating and we may be remounting it as a different component // in the same tree, or as a component in a totally different tree so we need to notify the // transition manager. if (transitionKey != null && mTransitionManager != null) { mTransitionManager.onContentUnmounted(transitionKey); } // If we're remounting this ComponentHost for a new ComponentTree, remove all disappearing // mount content that was animating since those disappearing animations belong to the old // ComponentTree if (mLastMountedComponentTreeId != componentTreeId) { final Component<?> component = currentMountItem.getComponent(); if (isHostSpec(component)) { final ComponentHost componentHost = (ComponentHost) currentMountItem.getContent(); removeDisappearingMountContentFromComponentHost(componentHost); } } unsetViewAttributes(currentMountItem); } // 3. We will re-bind this later in 7 regardless so let's make sure it's currently unbound. if (currentMountItem.isBound()) { itemComponent.getLifecycle().onUnbind( getContextForComponent(itemComponent), currentMountItem.getContent(), itemComponent); currentMountItem.setIsBound(false); } // 4. Re initialize the MountItem internal state with the new attributes from LayoutOutput currentMountItem.init(layoutOutput.getComponent(), currentMountItem, layoutOutput); // 5. If the mount item is not valid for this component update its content and view attributes. if (shouldUpdate) { updateMountedContent(currentMountItem, layoutOutput, itemComponent); setViewAttributes(currentMountItem); maybeIncrementTransitionKeyMountCount(currentMountItem); } final Object currentContent = currentMountItem.getContent(); // 6. Set the mounted content on the Component and call the bind callback. layoutOutputComponent.getLifecycle().bind( getContextForComponent(layoutOutputComponent), currentContent, layoutOutputComponent); currentMountItem.setIsBound(true); // 7. Update the bounds of the mounted content. This needs to be done regardless of whether // the component has been updated or not since the mounted item might might have the same // size and content but a different position. updateBoundsForMountedLayoutOutput(layoutOutput, currentMountItem); maybeInvalidateAccessibilityState(currentMountItem); if (currentMountItem.getContent() instanceof Drawable) { maybeSetDrawableState( currentMountItem.getHost(), (Drawable) currentMountItem.getContent(), currentMountItem.getFlags(), currentMountItem.getNodeInfo()); } if (currentMountItem.getDisplayListDrawable() != null) { currentMountItem.getDisplayListDrawable().suppressInvalidations(false); } return shouldUpdate; } private static boolean shouldUpdateMountItem( LayoutOutput layoutOutput, MountItem currentMountItem, boolean useUpdateValueFromLayoutOutput, LongSparseArray<MountItem> indexToItemMap, long[] layoutOutputsIds, ComponentsLogger logger) { final @LayoutOutput.UpdateState int updateState = layoutOutput.getUpdateState(); final Component currentComponent = currentMountItem.getComponent(); final ComponentLifecycle currentLifecycle = currentComponent.getLifecycle(); final Component nextComponent = layoutOutput.getComponent(); final ComponentLifecycle nextLifecycle = nextComponent.getLifecycle(); // If the two components have different sizes and the mounted content depends on the size we // just return true immediately. if (!sameSize(layoutOutput, currentMountItem) && nextLifecycle.isMountSizeDependent()) { return true; } if (useUpdateValueFromLayoutOutput) { if (updateState == LayoutOutput.STATE_UPDATED) { // Check for incompatible ReferenceLifecycle. if (currentLifecycle instanceof DrawableComponent && nextLifecycle instanceof DrawableComponent && currentLifecycle.shouldComponentUpdate(currentComponent, nextComponent)) { if (logger != null) { LayoutOutputLog logObj = new LayoutOutputLog(); logObj.currentId = indexToItemMap.keyAt( indexToItemMap.indexOfValue(currentMountItem)); logObj.currentLifecycle = currentLifecycle.toString(); logObj.nextId = layoutOutput.getId(); logObj.nextLifecycle = nextLifecycle.toString(); for (int i = 0; i < layoutOutputsIds.length; i++) { if (layoutOutputsIds[i] == logObj.currentId) { if (logObj.currentIndex == -1) { logObj.currentIndex = i; } logObj.currentLastDuplicatedIdIndex = i; } } if (logObj.nextId == logObj.currentId) { logObj.nextIndex = logObj.currentIndex; logObj.nextLastDuplicatedIdIndex = logObj.currentLastDuplicatedIdIndex; } else { for (int i = 0; i < layoutOutputsIds.length; i++) { if (layoutOutputsIds[i] == logObj.nextId) { if (logObj.nextIndex == -1) { logObj.nextIndex = i; } logObj.nextLastDuplicatedIdIndex = i; } } } final LogEvent mismatchEvent = logger.newEvent(EVENT_SHOULD_UPDATE_REFERENCE_LAYOUT_MISMATCH); mismatchEvent.addParam(PARAM_MESSAGE, logObj.toString()); logger.log(mismatchEvent); } return true; } return false; } else if (updateState == LayoutOutput.STATE_DIRTY) { return true; } } if (!currentLifecycle.callsShouldUpdateOnMount()) { return true; } return currentLifecycle.shouldComponentUpdate( currentComponent, nextComponent); } private static boolean sameSize(LayoutOutput layoutOutput, MountItem item) { final Rect layoutOutputBounds = layoutOutput.getBounds(); final Object mountedContent = item.getContent(); return layoutOutputBounds.width() == getWidthForMountedContent(mountedContent) && layoutOutputBounds.height() == getHeightForMountedContent(mountedContent); } private static int getWidthForMountedContent(Object content) { return content instanceof Drawable ? ((Drawable) content).getBounds().width() : ((View) content).getWidth(); } private static int getHeightForMountedContent(Object content) { return content instanceof Drawable ? ((Drawable) content).getBounds().height() : ((View) content).getHeight(); } private void updateBoundsForMountedLayoutOutput(LayoutOutput layoutOutput, MountItem item) { // MountState should never update the bounds of the top-level host as this // should be done by the ViewGroup containing the LithoView. if (layoutOutput.getId() == ROOT_HOST_ID) { return; } layoutOutput.getMountBounds(sTempRect); final boolean forceTraversal = Component.isMountViewSpec(layoutOutput.getComponent()) && ((View) item.getContent()).isLayoutRequested(); applyBoundsToMountContent( item.getContent(), sTempRect.left, sTempRect.top, sTempRect.right, sTempRect.bottom, forceTraversal /* force */); } /** * Prepare the {@link MountState} to mount a new {@link LayoutState}. */ @SuppressWarnings("unchecked") private void prepareMount(LayoutState layoutState) { final ComponentTree component = mLithoView.getComponentTree(); final ComponentsLogger logger = component.getContext().getLogger(); final String logTag = component.getContext().getLogTag(); LogEvent prepareEvent = null; if (logger != null) { prepareEvent = logger.newPerformanceEvent(EVENT_PREPARE_MOUNT); } PrepareMountStats stats = unmountOrMoveOldItems(layoutState); if (logger != null) { prepareEvent.addParam(PARAM_LOG_TAG, logTag); prepareEvent.addParam(PARAM_UNMOUNTED_COUNT, String.valueOf(stats.unmountedCount)); prepareEvent.addParam(PARAM_MOVED_COUNT, String.valueOf(stats.movedCount)); prepareEvent.addParam(PARAM_UNCHANGED_COUNT, String.valueOf(stats.unchangedCount)); } if (mHostsByMarker.get(ROOT_HOST_ID) == null) { // Mounting always starts with the root host. registerHost(ROOT_HOST_ID, mLithoView); // Root host is implicitly marked as mounted. mIndexToItemMap.put(ROOT_HOST_ID, mRootHostMountItem); } int outputCount = layoutState.getMountableOutputCount(); if (mLayoutOutputsIds == null || outputCount != mLayoutOutputsIds.length) { mLayoutOutputsIds = new long[layoutState.getMountableOutputCount()]; } for (int i = 0; i < outputCount; i++) { mLayoutOutputsIds[i] = layoutState.getMountableOutputAt(i).getId(); } if (logger != null) { logger.log(prepareEvent); } } /** * Determine whether to apply disappear animation to the given {@link MountItem} */ private static boolean isItemDisappearing( MountItem mountItem, LayoutState newLayoutState, DataFlowTransitionManager transitionManager) { if (mountItem == null || mountItem.getViewNodeInfo() == null) { return false; } final String key = mountItem.getViewNodeInfo().getTransitionKey(); if (key == null) { return false; } final TransitionContext transitionContext = newLayoutState.getTransitionContext(); // If the transition context saw this transition key in this LayoutState, then it's still there // and not disappearing if (transitionContext != null && transitionContext.hasTransitionKey(key)) { return false; } return // for 'first' animation api (transitionContext != null && transitionContext.isDisappearingKey(key)) || // for dataflow api (transitionManager != null && transitionManager.isKeyAnimating(key)); } /** * Go over all the mounted items from the leaves to the root and unmount only the items that are * not present in the new LayoutOutputs. * If an item is still present but in a new position move the item inside its host. * The condition where an item changed host doesn't need any special treatment here since we * mark them as removed and re-added when calculating the new LayoutOutputs */ private PrepareMountStats unmountOrMoveOldItems(LayoutState newLayoutState) { mPrepareMountStats.reset(); if (mLayoutOutputsIds == null) { return mPrepareMountStats; } // Traversing from the beginning since mLayoutOutputsIds unmounting won't remove entries there // but only from mIndexToItemMap. If an host changes we're going to unmount it and recursively // all its mounted children. for (int i = 0; i < mLayoutOutputsIds.length; i++) { final int newPosition = newLayoutState.getLayoutOutputPositionForId(mLayoutOutputsIds[i]); final MountItem oldItem = getItemAt(i); // If an item is being unmounted, has a disappearing animation, and we're still rendering the // same component tree, don't actually unmount so that we can perform the disappear animation. if (mLastMountedComponentTreeId == newLayoutState.getComponentTreeId() && isItemDisappearing(oldItem, newLayoutState, mTransitionManager)) { startUnmountDisappearingItem(i, oldItem.getViewNodeInfo().getTransitionKey()); final int lastDescendantOfItem = findLastDescendantOfItem(i, oldItem); // Disassociate disappearing items from current mounted items. The layout tree will not // contain disappearing items anymore, however they are kept separately in their hosts. removeDisappearingItemMappings(i, lastDescendantOfItem); // Skip this disappearing item and all its descendants. Do not unmount or move them yet. // We will unmount them after animation is completed. i = lastDescendantOfItem; continue; } if (newPosition == -1) { unmountItem(mContext, i, mHostsByMarker); mPrepareMountStats.unmountedCount++; } else { final long newHostMarker = newLayoutState.getMountableOutputAt(newPosition).getHostMarker(); if (oldItem == null) { // This was previously unmounted. mPrepareMountStats.unmountedCount++; } else if (oldItem.getHost() != mHostsByMarker.get(newHostMarker)) { // If the id is the same but the parent host is different we simply unmount the item and // re-mount it later. If the item to unmount is a ComponentHost, all the children will be // recursively unmounted. unmountItem(mContext, i, mHostsByMarker); mPrepareMountStats.unmountedCount++; } else if (newPosition != i) { // If a MountItem for this id exists and the hostMarker has not changed but its position // in the outputs array has changed we need to update the position in the Host to ensure // the z-ordering. oldItem.getHost().moveItem(oldItem, i, newPosition); mPrepareMountStats.movedCount++; } else { mPrepareMountStats.unchangedCount++; } } } return mPrepareMountStats; } private void removeDisappearingItemMappings(int fromIndex, int toIndex) { for (int i = fromIndex; i <= toIndex; i++) { final MountItem item = getItemAt(i); // We do not need this mapping for disappearing items. mIndexToItemMap.remove(mLayoutOutputsIds[i]); maybeDecrementTransitionKeyMountCount(item); // Likewise we no longer need host mapping for disappearing items. if (isHostSpec(item.getComponent())) { mHostsByMarker .removeAt(mHostsByMarker.indexOfValue((ComponentHost) item.getContent())); } } } /** * Find the index of last descendant of given {@link MountItem} */ private int findLastDescendantOfItem(int disappearingItemIndex, MountItem item) { for (int i = disappearingItemIndex + 1; i < mLayoutOutputsIds.length; i++) { if (!ComponentHostUtils.hasAncestorHost( getItemAt(i).getHost(), (ComponentHost) item.getContent())) { // No need to go further as the items that have common ancestor hosts are co-located. // This is the first non-descendant of given MountItem, therefore last descendant is the // item before. return i - 1; } } return mLayoutOutputsIds.length - 1; } private void updateMountedContent( MountItem item, LayoutOutput layoutOutput, Component previousComponent) { final Component<?> component = layoutOutput.getComponent(); if (isHostSpec(component)) { return; } final Object previousContent = item.getContent(); final ComponentLifecycle lifecycle = component.getLifecycle(); // Call unmount and mount in sequence to make sure all the the resources are correctly // de-allocated. It's possible for previousContent to equal null - when the root is // interactive we create a LayoutOutput without content in order to set up click handling. lifecycle.unmount( getContextForComponent(previousComponent), previousContent, previousComponent); lifecycle.mount(getContextForComponent(component), previousContent, component); } private void mountLayoutOutput(int index, LayoutOutput layoutOutput, LayoutState layoutState) { // 1. Resolve the correct host to mount our content to. ComponentHost host = resolveComponentHost(layoutOutput, mHostsByMarker); if (host == null) { // Host has not yet been mounted - mount it now. for (int hostMountIndex = 0, size = mLayoutOutputsIds.length; hostMountIndex < size; hostMountIndex++) { if (mLayoutOutputsIds[hostMountIndex] == layoutOutput.getHostMarker()) { final LayoutOutput hostLayoutOutput = layoutState.getMountableOutputAt(hostMountIndex); mountLayoutOutput(hostMountIndex, hostLayoutOutput, layoutState); break; } } host = resolveComponentHost(layoutOutput, mHostsByMarker); } final Component<?> component = layoutOutput.getComponent(); final ComponentContext context = getContextForComponent(component); final ComponentLifecycle lifecycle = component.getLifecycle(); // 2. Generate the component's mount state (this might also be a ComponentHost View). Object content = acquireMountContent(component, host); if (content == null) { content = lifecycle.createMountContent(mContext); } lifecycle.mount( context, content, component); // 3. If it's a ComponentHost, add the mounted View to the list of Hosts. if (isHostSpec(component)) { ComponentHost componentHost = (ComponentHost) content; componentHost.setParentHostMarker(layoutOutput.getHostMarker()); registerHost(layoutOutput.getId(), componentHost); } // 4. Mount the content into the selected host. final MountItem item = mountContent(index, component, content, host, layoutOutput); // 5. Notify the component that mounting has completed lifecycle.bind(context, content, component); item.setIsBound(true); // 6. Apply the bounds to the Mount content now. It's important to do so after bind as calling // bind might have triggered a layout request within a View. layoutOutput.getMountBounds(sTempRect); applyBoundsToMountContent( content, sTempRect.left, sTempRect.top, sTempRect.right, sTempRect.bottom, true /* force */); if (item.getDisplayListDrawable() != null) { item.getDisplayListDrawable().suppressInvalidations(false); } // 6. Update the mount stats mMountStats.mountedCount++; } // The content might be null because it's the LayoutSpec for the root host // (the very first LayoutOutput). private MountItem mountContent( int index, Component<?> component, Object content, ComponentHost host, LayoutOutput layoutOutput) { final MountItem item = ComponentsPools.acquireMountItem( component, host, content, layoutOutput); // Create and keep a MountItem even for the layoutSpec with null content // that sets the root host interactions. mIndexToItemMap.put(mLayoutOutputsIds[index], item); maybeIncrementTransitionKeyMountCount(item); if (component.getLifecycle().canMountIncrementally()) { mCanMountIncrementallyMountItems.put(mLayoutOutputsIds[index], item); } layoutOutput.getMountBounds(sTempRect); host.mount(index, item, sTempRect); setViewAttributes(item); return item; } private Object acquireMountContent(Component<?> component, ComponentHost host) { final ComponentLifecycle lifecycle = component.getLifecycle(); if (isHostSpec(component)) { return host.recycleHost(); } return ComponentsPools.acquireMountContent(mContext, lifecycle.getId()); } private static void applyBoundsToMountContent( Object content, int left, int top, int right, int bottom, boolean force) { assertMainThread(); if (content instanceof View) { View view = (View) content; int width = right - left; int height = bottom - top; if (force || view.getMeasuredHeight() != height || view.getMeasuredWidth() != width) { view.measure( makeMeasureSpec(right - left, MeasureSpec.EXACTLY), makeMeasureSpec(bottom - top, MeasureSpec.EXACTLY)); } if (force || view.getLeft() != left || view.getTop() != top || view.getRight() != right || view.getBottom() != bottom) { view.layout(left, top, right, bottom); } } else if (content instanceof Drawable) { ((Drawable) content).setBounds(left, top, right, bottom); } else { throw new IllegalStateException("Unsupported mounted content " + content); } } private static boolean canMountIncrementally(Component<?> component) { return component.getLifecycle().canMountIncrementally(); } /** * Resolves the component host that will be used for the given layout output * being mounted. */ private static ComponentHost resolveComponentHost( LayoutOutput layoutOutput, LongSparseArray<ComponentHost> hostsByMarker) { final long hostMarker = layoutOutput.getHostMarker(); return hostsByMarker.get(hostMarker); } private static void setViewAttributes(MountItem item) { final Component<?> component = item.getComponent(); if (!isMountViewSpec(component)) { return; } final View view = (View) item.getContent(); final NodeInfo nodeInfo = item.getNodeInfo(); if (nodeInfo != null) { setClickHandler(nodeInfo.getClickHandler(), view); setLongClickHandler(nodeInfo.getLongClickHandler(), view); setTouchHandler(nodeInfo.getTouchHandler(), view); setInterceptTouchHandler(nodeInfo.getInterceptTouchHandler(), view); setAccessibilityDelegate(view, nodeInfo); setViewTag(view, nodeInfo.getViewTag()); setViewTags(view, nodeInfo.getViewTags()); setContentDescription(view, nodeInfo.getContentDescription()); setFocusable(view, nodeInfo.getFocusState()); } setImportantForAccessibility(view, item.getImportantForAccessibility()); final ViewNodeInfo viewNodeInfo = item.getViewNodeInfo(); if (viewNodeInfo != null && !isHostSpec(component)) { // Set view background, if applicable. Do this before padding // as it otherwise overrides the padding. setViewBackground(view, viewNodeInfo); setViewPadding(view, viewNodeInfo); setViewForeground(view, viewNodeInfo); setViewLayoutDirection(view, viewNodeInfo); } } private static void unsetViewAttributes(MountItem item) { final Component<?> component = item.getComponent(); if (!isMountViewSpec(component)) { return; } final View view = (View) item.getContent(); final NodeInfo nodeInfo = item.getNodeInfo(); if (nodeInfo != null) { if (nodeInfo.getClickHandler() != null) { unsetClickHandler(view); } if (nodeInfo.getLongClickHandler() != null) { unsetLongClickHandler(view); } if (nodeInfo.getTouchHandler() != null) { unsetTouchHandler(view); } if (nodeInfo.getInterceptTouchHandler() != null) { unsetInterceptTouchEventHandler(view); } unsetViewTag(view); unsetViewTags(view, nodeInfo.getViewTags()); if (!TextUtils.isEmpty(nodeInfo.getContentDescription())) { unsetContentDescription(view); } } view.setClickable(MountItem.isViewClickable(item.getFlags())); view.setLongClickable(MountItem.isViewLongClickable(item.getFlags())); unsetFocusable(view, item); if (item.getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_AUTO) { unsetImportantForAccessibility(view); } unsetAccessibilityDelegate(view); final ViewNodeInfo viewNodeInfo = item.getViewNodeInfo(); if (viewNodeInfo != null && !isHostSpec(component)) { unsetViewPadding(view, viewNodeInfo); unsetViewBackground(view, viewNodeInfo); unsetViewForeground(view, viewNodeInfo); unsetViewLayoutDirection(view, viewNodeInfo); } } /** * Store a {@link ComponentAccessibilityDelegate} as a tag in {@code view}. {@link LithoView} * contains the logic for setting/unsetting it whenever accessibility is enabled/disabled * * For non {@link ComponentHost}s * this is only done if any {@link EventHandler}s for accessibility events have been implemented, * we want to preserve the original behaviour since {@code view} might have had * a default delegate. */ private static void setAccessibilityDelegate(View view, NodeInfo nodeInfo) { if (!(view instanceof ComponentHost) && !nodeInfo.hasAccessibilityHandlers()) { return; } view.setTag( R.id.component_node_info, nodeInfo); } private static void unsetAccessibilityDelegate(View view) { if (!(view instanceof ComponentHost) && view.getTag(R.id.component_node_info) == null) { return; } view.setTag(R.id.component_node_info, null); if (!(view instanceof ComponentHost)) { ViewCompat.setAccessibilityDelegate(view, null); } } /** * Installs the click listeners that will dispatch the click handler * defined in the component's props. Unconditionally set the clickable * flag on the view. */ private static void setClickHandler(EventHandler<ClickEvent> clickHandler, View view) { if (clickHandler == null) { return; } ComponentClickListener listener = getComponentClickListener(view); if (listener == null) { listener = new ComponentClickListener(); setComponentClickListener(view, listener); } listener.setEventHandler(clickHandler); view.setClickable(true); } private static void unsetClickHandler(View view) { final ComponentClickListener listener = getComponentClickListener(view); if (listener != null) { listener.setEventHandler(null); } } static ComponentClickListener getComponentClickListener(View v) { if (v instanceof ComponentHost) { return ((ComponentHost) v).getComponentClickListener(); } else { return (ComponentClickListener) v.getTag(R.id.component_click_listener); } } static void setComponentClickListener(View v, ComponentClickListener listener) { if (v instanceof ComponentHost) { ((ComponentHost) v).setComponentClickListener(listener); } else { v.setOnClickListener(listener); v.setTag(R.id.component_click_listener, listener); } } /** * Installs the long click listeners that will dispatch the click handler * defined in the component's props. Unconditionally set the clickable * flag on the view. */ private static void setLongClickHandler( EventHandler<LongClickEvent> longClickHandler, View view) { if (longClickHandler != null) { ComponentLongClickListener listener = getComponentLongClickListener(view); if (listener == null) { listener = new ComponentLongClickListener(); setComponentLongClickListener(view, listener); } listener.setEventHandler(longClickHandler); view.setLongClickable(true); } } private static void unsetLongClickHandler(View view) { final ComponentLongClickListener listener = getComponentLongClickListener(view); if (listener != null) { listener.setEventHandler(null); } } static ComponentLongClickListener getComponentLongClickListener(View v) { if (v instanceof ComponentHost) { return ((ComponentHost) v).getComponentLongClickListener(); } else { return (ComponentLongClickListener) v.getTag(R.id.component_long_click_listener); } } static void setComponentLongClickListener(View v, ComponentLongClickListener listener) { if (v instanceof ComponentHost) { ((ComponentHost) v).setComponentLongClickListener(listener); } else { v.setOnLongClickListener(listener); v.setTag(R.id.component_long_click_listener, listener); } } /** * Installs the touch listeners that will dispatch the touch handler * defined in the component's props. */ private static void setTouchHandler(EventHandler<TouchEvent> touchHandler, View view) { if (touchHandler != null) { ComponentTouchListener listener = getComponentTouchListener(view); if (listener == null) { listener = new ComponentTouchListener(); setComponentTouchListener(view, listener); } listener.setEventHandler(touchHandler); } } private static void unsetTouchHandler(View view) { final ComponentTouchListener listener = getComponentTouchListener(view); if (listener != null) { listener.setEventHandler(null); } } /** * Sets the intercept touch handler defined in the component's props. */ private static void setInterceptTouchHandler( EventHandler<InterceptTouchEvent> interceptTouchHandler, View view) { if (interceptTouchHandler == null) { return; } if (view instanceof ComponentHost) { ((ComponentHost) view).setInterceptTouchEventHandler(interceptTouchHandler); } } private static void unsetInterceptTouchEventHandler(View view) { if (view instanceof ComponentHost) { ((ComponentHost) view).setInterceptTouchEventHandler(null); } } static ComponentTouchListener getComponentTouchListener(View v) { if (v instanceof ComponentHost) { return ((ComponentHost) v).getComponentTouchListener(); } else { return (ComponentTouchListener) v.getTag(R.id.component_touch_listener); } } static void setComponentTouchListener(View v, ComponentTouchListener listener) { if (v instanceof ComponentHost) { ((ComponentHost) v).setComponentTouchListener(listener); } else { v.setOnTouchListener(listener); v.setTag(R.id.component_touch_listener, listener); } } private static void setViewTag(View view, Object viewTag) { if (view instanceof ComponentHost) { final ComponentHost host = (ComponentHost) view; host.setViewTag(viewTag); } else { view.setTag(viewTag); } } private static void setViewTags(View view, SparseArray<Object> viewTags) { if (viewTags == null) { return; } if (view instanceof ComponentHost) { final ComponentHost host = (ComponentHost) view; host.setViewTags(viewTags); } else { for (int i = 0, size = viewTags.size(); i < size; i++) { view.setTag(viewTags.keyAt(i), viewTags.valueAt(i)); } } } private static void unsetViewTag(View view) { if (view instanceof ComponentHost) { final ComponentHost host = (ComponentHost) view; host.setViewTag(null); } else { view.setTag(null); } } private static void unsetViewTags(View view, SparseArray<Object> viewTags) { if (view instanceof ComponentHost) { final ComponentHost host = (ComponentHost) view; host.setViewTags(null); } else { if (viewTags != null) { for (int i = 0, size = viewTags.size(); i < size; i++) { view.setTag(viewTags.keyAt(i), null); } } } } private static void setContentDescription(View view, CharSequence contentDescription) { if (TextUtils.isEmpty(contentDescription)) { return; } view.setContentDescription(contentDescription); } private static void unsetContentDescription(View view) { view.setContentDescription(null); } private static void setImportantForAccessibility(View view, int importantForAccessibility) { if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { return; } ViewCompat.setImportantForAccessibility(view, importantForAccessibility); } private static void unsetImportantForAccessibility(View view) { ViewCompat.setImportantForAccessibility(view, IMPORTANT_FOR_ACCESSIBILITY_AUTO); } private static void setFocusable(View view, @NodeInfo.FocusState short focusState) { if (focusState == NodeInfo.FOCUS_SET_TRUE) { view.setFocusable(true); } else if (focusState == NodeInfo.FOCUS_SET_FALSE) { view.setFocusable(false); } } private static void unsetFocusable(View view, MountItem mountItem) { view.setFocusable(MountItem.isViewFocusable(mountItem.getFlags())); } private static void setViewPadding(View view, ViewNodeInfo viewNodeInfo) { if (!viewNodeInfo.hasPadding()) { return; } view.setPadding( viewNodeInfo.getPaddingLeft(), viewNodeInfo.getPaddingTop(), viewNodeInfo.getPaddingRight(), viewNodeInfo.getPaddingBottom()); } private static void unsetViewPadding(View view, ViewNodeInfo viewNodeInfo) { if (!viewNodeInfo.hasPadding()) { return; } view.setPadding(0, 0, 0, 0); } private static void setViewBackground(View view, ViewNodeInfo viewNodeInfo) { final Reference<Drawable> backgroundReference = viewNodeInfo.getBackground(); if (backgroundReference != null) { setBackgroundCompat( view, Reference.acquire((ComponentContext) view.getContext(), backgroundReference)); } } private static void unsetViewBackground(View view, ViewNodeInfo viewNodeInfo) { final Reference<Drawable> backgroundReference = viewNodeInfo.getBackground(); if (backgroundReference != null) { Reference.release( (ComponentContext) view.getContext(), view.getBackground(), backgroundReference); setBackgroundCompat(view, null); } } @SuppressWarnings("deprecation") private static void setBackgroundCompat(View view, Drawable drawable) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { view.setBackgroundDrawable(drawable); } else { view.setBackground(drawable); } } private static void setViewForeground(View view, ViewNodeInfo viewNodeInfo) { final Drawable foreground = viewNodeInfo.getForeground(); if (foreground != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { throw new IllegalStateException("MountState has a ViewNodeInfo with foreground however " + "the current Android version doesn't support foreground on Views"); } view.setForeground(foreground); } } private static void unsetViewForeground(View view, ViewNodeInfo viewNodeInfo) { final Drawable foreground = viewNodeInfo.getForeground(); if (foreground != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { throw new IllegalStateException("MountState has a ViewNodeInfo with foreground however " + "the current Android version doesn't support foreground on Views"); } view.setForeground(null); } } private static void setViewLayoutDirection(View view, ViewNodeInfo viewNodeInfo) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { return; } final int viewLayoutDirection; switch (viewNodeInfo.getLayoutDirection()) { case LTR: viewLayoutDirection = View.LAYOUT_DIRECTION_LTR; break; case RTL: viewLayoutDirection = View.LAYOUT_DIRECTION_RTL; break; default: viewLayoutDirection = View.LAYOUT_DIRECTION_INHERIT; } view.setLayoutDirection(viewLayoutDirection); } private static void unsetViewLayoutDirection(View view, ViewNodeInfo viewNodeInfo) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { return; } view.setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT); } private static void mountItemIncrementally( MountItem item, Rect itemBounds, Rect localVisibleRect) { final Component<?> component = item.getComponent(); if (!isMountViewSpec(component)) { return; } // We can't just use the bounds of the View since we need the bounds relative to the // hosting LithoView (which is what the localVisibleRect is measured relative to). final View view = (View) item.getContent(); final Rect rect = ComponentsPools.acquireRect(); rect.set( Math.max(0, localVisibleRect.left - itemBounds.left), Math.max(0, localVisibleRect.top - itemBounds.top), itemBounds.width() - Math.max(0, itemBounds.right - localVisibleRect.right), itemBounds.height() - Math.max(0, itemBounds.bottom - localVisibleRect.bottom)); mountViewIncrementally(view, rect); ComponentsPools.release(rect); } private static void mountViewIncrementally(View view, Rect localVisibleRect) { assertMainThread(); if (view instanceof LithoView) { final LithoView lithoView = (LithoView) view; lithoView.performIncrementalMount(localVisibleRect); } else if (view instanceof ViewGroup) { final ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { final View childView = viewGroup.getChildAt(i); if (localVisibleRect.intersects( childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom())) { final Rect rect = ComponentsPools.acquireRect(); rect.set( Math.max(0, localVisibleRect.left - childView.getLeft()), Math.max(0, localVisibleRect.top - childView.getTop()), childView.getWidth() - Math.max(0, childView.getRight() - localVisibleRect.right), childView.getHeight() - Math.max(0, childView.getBottom() - localVisibleRect.bottom)); mountViewIncrementally(childView, rect); ComponentsPools.release(rect); } } } } private void unmountDisappearingItemChild(ComponentContext context, MountItem item) { final Object content = item.getContent(); // Recursively unmount mounted children items. if (content instanceof ComponentHost) { final ComponentHost host = (ComponentHost) content; for (int i = host.getMountItemCount() - 1; i >= 0; i--) { final MountItem mountItem = host.getMountItemAt(i); unmountDisappearingItemChild(context, mountItem); } if (host.getMountItemCount() > 0) { throw new IllegalStateException("Recursively unmounting items from a ComponentHost, left" + " some items behind maybe because not tracked by its MountState"); } } final ComponentHost host = item.getHost(); host.unmount(item); unsetViewAttributes(item); unbindAndUnmountLifecycle(context, item); if (item.getComponent().getLifecycle().canMountIncrementally()) { final int index = mCanMountIncrementallyMountItems.indexOfValue(item); if (index > 0) { mCanMountIncrementallyMountItems.removeAt(index); } } ComponentsPools.release(context, item); } private void unmountItem( ComponentContext context, int index, LongSparseArray<ComponentHost> hostsByMarker) { final MountItem item = getItemAt(index); // The root host item should never be unmounted as it's a reference // to the top-level LithoView. if (item == null || mLayoutOutputsIds[index] == ROOT_HOST_ID) { return; } final Object content = item.getContent(); // Recursively unmount mounted children items. // This is the case when mountDiffing is enabled and unmountOrMoveOldItems() has a matching // sub tree. However, traversing the tree bottom-up, it needs to unmount a node holding that // sub tree, that will still have mounted items. (Different sequence number on LayoutOutput id) if ((content instanceof ComponentHost) && !(content instanceof LithoView)) { final ComponentHost host = (ComponentHost) content; // Concurrently remove items therefore traverse backwards. for (int i = host.getMountItemCount() - 1; i >= 0; i--) { final MountItem mountItem = host.getMountItemAt(i); final long layoutOutputId = mIndexToItemMap.keyAt(mIndexToItemMap.indexOfValue(mountItem)); for (int mountIndex = mLayoutOutputsIds.length - 1; mountIndex >= 0; mountIndex--) { if (mLayoutOutputsIds[mountIndex] == layoutOutputId) { unmountItem(context, mountIndex, hostsByMarker); break; } } } if (host.getMountItemCount() > 0) { throw new IllegalStateException("Recursively unmounting items from a ComponentHost, left" + " some items behind maybe because not tracked by its MountState"); } } final ComponentHost host = item.getHost(); host.unmount(index, item); unsetViewAttributes(item); final Component<?> component = item.getComponent(); if (isHostSpec(component)) { final ComponentHost componentHost = (ComponentHost) content; hostsByMarker.removeAt(hostsByMarker.indexOfValue(componentHost)); removeDisappearingMountContentFromComponentHost(componentHost); } unbindAndUnmountLifecycle(context, item); mIndexToItemMap.remove(mLayoutOutputsIds[index]); final String transitionKey = maybeDecrementTransitionKeyMountCount(item); if (transitionKey != null && mTransitionManager != null) { mTransitionManager.onContentUnmounted(transitionKey); } if (component.getLifecycle().canMountIncrementally()) { mCanMountIncrementallyMountItems.delete(mLayoutOutputsIds[index]); } ComponentsPools.release(context, item); mMountStats.unmountedCount++; } private void unbindAndUnmountLifecycle( ComponentContext context, MountItem item) { final Component component = item.getComponent(); final Object content = item.getContent(); final ComponentLifecycle lifecycle = component.getLifecycle(); // Call the component's unmount() method. if (item.isBound()) { lifecycle.onUnbind(context, content, component); item.setIsBound(false); } lifecycle.unmount(context, content, component); } private void startUnmountDisappearingItem(int index, String key) { final MountItem item = getItemAt(index); if (item == null) { throw new RuntimeException("Item at index=" + index +" does not exist"); } if (!(item.getContent() instanceof ComponentHost)) { throw new RuntimeException("Only host components can be used as disappearing items"); } final ComponentHost host = item.getHost(); host.startUnmountDisappearingItem(index, item); mTransitionManager.addMountItemAnimationCompleteListener( key, new DataFlowTransitionManager.OnMountItemAnimationComplete() { @Override public void onMountItemAnimationComplete(Object currentMountItem) { if (item.getContent() != currentMountItem) { throw new RuntimeException( "Got animation complete callback for wrong mount item (expected " + item.getContent() + ", got " + currentMountItem + ")"); } endUnmountDisappearingItem(mContext, item); } }); } private void endUnmountDisappearingItem(ComponentContext context, MountItem item) { final ComponentHost content = (ComponentHost) item.getContent(); // Unmount descendant items in reverse order. for (int i = content.getMountItemCount() - 1; i >= 0; i--) { final MountItem mountItem = content.getMountItemAt(i); unmountDisappearingItemChild(context, mountItem); } if (content.getMountItemCount() > 0) { throw new IllegalStateException("Recursively unmounting items from a ComponentHost, left" + " some items behind maybe because not tracked by its MountState"); } final ComponentHost host = item.getHost(); host.unmountDisappearingItem(item); unsetViewAttributes(item); unbindAndUnmountLifecycle(context, item); if (item.getComponent().getLifecycle().canMountIncrementally()) { final int index = mCanMountIncrementallyMountItems.indexOfValue(item); if (index > 0) { mCanMountIncrementallyMountItems.removeAt(index); } } ComponentsPools.release(context, item); } int getItemCount() { return mIndexToItemMap.size(); } MountItem getItemAt(int i) { return mIndexToItemMap.get(mLayoutOutputsIds[i]); } private static class PrepareMountStats { private int unmountedCount = 0; private int movedCount = 0; private int unchangedCount = 0; private PrepareMountStats() {} private void reset() { unchangedCount = 0; movedCount = 0; unmountedCount = 0; } } private static class MountStats { private int mountedCount; private int unmountedCount; private int updatedCount; private int noOpCount; private void reset() { mountedCount = 0; unmountedCount = 0; updatedCount = 0; noOpCount = 0; } } /** * Unbinds all the MountItems currently mounted on this MountState. Unbinding a MountItem means * calling unbind on its {@link Component}. The MountItem is not yet unmounted after unbind is * called and can be re-used in place to re-mount another {@link Component} with the same * {@link ComponentLifecycle}. */ void unbind() { if (mLayoutOutputsIds == null) { return; } for (int i = 0, size = mLayoutOutputsIds.length; i < size; i++) { MountItem mountItem = getItemAt(i); if (mountItem == null || !mountItem.isBound()) { continue; } final Component component = mountItem.getComponent(); component.getLifecycle().unbind( mContext, mountItem.getContent(), component); mountItem.setIsBound(false); } clearVisibilityItems(); } void detach() { unbind(); } /** * This is called when the {@link MountItem}s mounted on this {@link MountState} need to be * re-bound with the same component. The common case here is a detach/attach happens on the * {@link LithoView} that owns the MountState. */ void rebind() { if (mLayoutOutputsIds == null) { return; } for (int i = 0, size = mLayoutOutputsIds.length; i < size; i++) { final MountItem mountItem = getItemAt(i); if (mountItem == null || mountItem.isBound()) { continue; } final Component component = mountItem.getComponent(); final Object content = mountItem.getContent(); component.getLifecycle().bind( mContext, content, component); mountItem.setIsBound(true); if (content instanceof View && !(content instanceof ComponentHost) && ((View) content).isLayoutRequested()) { final View view = (View) content; applyBoundsToMountContent( view, view.getLeft(), view.getTop(), view.getRight(), view.getBottom(), true); } } } /** * @return true if this method did all the work that was necessary and there is no other * content that needs mounting/unmounting in this mount step. If false then a full mount step * should take place. */ private boolean performIncrementalMount(LayoutState layoutState, Rect localVisibleRect) { if (mPreviousLocalVisibleRect.isEmpty()) { return false; } if (localVisibleRect.left != mPreviousLocalVisibleRect.left || localVisibleRect.right != mPreviousLocalVisibleRect.right) { return false; } final ArrayList<LayoutOutput> layoutOutputTops = layoutState.getMountableOutputTops(); final ArrayList<LayoutOutput> layoutOutputBottoms = layoutState.getMountableOutputBottoms(); final int count = layoutState.getMountableOutputCount(); if (localVisibleRect.top > 0 || mPreviousLocalVisibleRect.top > 0) { // View is going on/off the top of the screen. Check the bottoms to see if there is anything // that has moved on/off the top of the screen. while (mPreviousBottomsIndex < count && localVisibleRect.top >= layoutOutputBottoms.get(mPreviousBottomsIndex).getBounds().bottom) { final long id = layoutOutputBottoms.get(mPreviousBottomsIndex).getId(); unmountItem(mContext, layoutState.getLayoutOutputPositionForId(id), mHostsByMarker); mPreviousBottomsIndex++; } while (mPreviousBottomsIndex > 0 && localVisibleRect.top < layoutOutputBottoms.get(mPreviousBottomsIndex - 1).getBounds().bottom) { mPreviousBottomsIndex--; final LayoutOutput layoutOutput = layoutOutputBottoms.get(mPreviousBottomsIndex); final int layoutOutputIndex = layoutState.getLayoutOutputPositionForId(layoutOutput.getId()); if (getItemAt(layoutOutputIndex) == null) { mountLayoutOutput( layoutState.getLayoutOutputPositionForId(layoutOutput.getId()), layoutOutput, layoutState); } } } final int height = mLithoView.getHeight(); if (localVisibleRect.bottom < height || mPreviousLocalVisibleRect.bottom < height) { // View is going on/off the bottom of the screen. Check the tops to see if there is anything // that has changed. while (mPreviousTopsIndex < count && localVisibleRect.bottom > layoutOutputTops.get(mPreviousTopsIndex).getBounds().top) { final LayoutOutput layoutOutput = layoutOutputTops.get(mPreviousTopsIndex); final int layoutOutputIndex = layoutState.getLayoutOutputPositionForId(layoutOutput.getId()); if (getItemAt(layoutOutputIndex) == null) { mountLayoutOutput( layoutState.getLayoutOutputPositionForId(layoutOutput.getId()), layoutOutput, layoutState); } mPreviousTopsIndex++; } while (mPreviousTopsIndex > 0 && localVisibleRect.bottom <= layoutOutputTops.get(mPreviousTopsIndex - 1).getBounds().top) { mPreviousTopsIndex--; final long id = layoutOutputTops.get(mPreviousTopsIndex).getId(); unmountItem(mContext, layoutState.getLayoutOutputPositionForId(id), mHostsByMarker); } } for (int i = 0, size = mCanMountIncrementallyMountItems.size(); i < size; i++) { final MountItem mountItem = mCanMountIncrementallyMountItems.valueAt(i); final int layoutOutputPosition = layoutState.getLayoutOutputPositionForId(mCanMountIncrementallyMountItems.keyAt(i)); mountItemIncrementally( mountItem, layoutState.getMountableOutputAt(layoutOutputPosition).getBounds(), localVisibleRect); } return true; } private void prepareTransitionManager(LayoutState layoutState) { if (layoutState.hasTransitionContext() && mTransitionManager == null) { mTransitionManager = new DataFlowTransitionManager(); } } private static void recordMountedItemsWithTransitionKeys( DataFlowTransitionManager transitionManager, LongSparseArray<MountItem> indexToItemMap, boolean isPreMount) { for (int i = 0, size = indexToItemMap.size(); i < size; i++) { final MountItem item = indexToItemMap.valueAt(i); final ViewNodeInfo viewNodeInfo = item.getViewNodeInfo(); final String transitionKey = viewNodeInfo != null ? viewNodeInfo.getTransitionKey() : null; if (transitionKey != null) { if (isPreMount) { transitionManager.onPreMountItem(transitionKey, (View) item.getContent()); } else { transitionManager.onPostMountItem(transitionKey, (View) item.getContent()); } } } } /** * Given the transition keys currently mounted and the transition keys that are going to be * mounted in the new animation state, create the proper appear/disappear/change animations for * this update. */ private void createAutoMountTransitions(LayoutState layoutState) { final TransitionContext transitionContext = layoutState.getTransitionContext(); final TransitionSet transitionSet = transitionContext.getAutoTransitionSet(); final ArrayList<Transition> transitions = transitionSet.getTransitions(); transitionContext.getTransitionAnimationBindings().clear(); for (int i = 0, size = transitions.size(); i < size; i++) { final Transition transition = transitions.get(i); final String key = transition.getTransitionKey(); final boolean lastMountHadKey = mMountedTransitionKeys.containsKey(key); final boolean thisMountWillHaveKey = transitionContext.hasTransitionKey(key); AnimationBinding animation = null; if (lastMountHadKey && thisMountWillHaveKey) { animation = transition.createChangeAnimation(); } else if (lastMountHadKey && !thisMountWillHaveKey) { if (transition.hasDisappearAnimation()) { animation = transition.createDisappearAnimation(); } } else if (!lastMountHadKey && thisMountWillHaveKey) { if (transition.hasAppearAnimation()) { animation = transition.createAppearAnimation(); } } if (animation != null) { transitionContext.addTransitionAnimationBinding(animation); } } } private void removeDisappearingMountContentFromComponentHost(ComponentHost componentHost) { if (componentHost.hasDisappearingItems()) { List<String> disappearingKeys = componentHost.getDisappearingItemKeys(); for (int i = 0, size = disappearingKeys.size(); i < size; i++) { mTransitionManager.onContentUnmounted(disappearingKeys.get(i)); } } } // These increment and decrement methods are necessary because when a transition key changes what // content it mounts to, it can be re-added to the mount state before its old content is removed // (so we can't use a simple set). private void maybeIncrementTransitionKeyMountCount(MountItem mountItem) { final ViewNodeInfo viewNodeInfo = mountItem.getViewNodeInfo(); if (viewNodeInfo == null) { return; } final String transitionKey = viewNodeInfo.getTransitionKey(); if (transitionKey == null) { return; } final Integer currentCount = mMountedTransitionKeys.get(transitionKey); mMountedTransitionKeys.put(transitionKey, currentCount == null ? 1 : currentCount + 1); } private String maybeDecrementTransitionKeyMountCount(MountItem mountItem) { final ViewNodeInfo viewNodeInfo = mountItem.getViewNodeInfo(); if (viewNodeInfo == null) { return null; } final String transitionKey = viewNodeInfo.getTransitionKey(); if (transitionKey == null) { return null; } final Integer currentCount = mMountedTransitionKeys.remove(transitionKey); if (currentCount == null) { throw new RuntimeException("Tried to decrement mount count below 0 for key " + transitionKey); } if (currentCount != 1) { mMountedTransitionKeys.put(transitionKey, currentCount - 1); } return transitionKey; } /** * @see LithoViewTestHelper#findTestItems(LithoView, String) */ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) Deque<TestItem> findTestItems(String testKey) { if (mTestItemMap == null) { throw new UnsupportedOperationException("Trying to access TestItems while " + "ComponentsConfiguration.isEndToEndTestRun is false."); } final Deque<TestItem> items = mTestItemMap.get(testKey); return items == null ? new LinkedList<TestItem>() : items; } /** * For HostComponents, we don't set a scoped context during layout calculation because we don't * need one, as we could never call a state update on it. Instead it's okay to use the context * that is passed to MountState from the LithoView, which is not scoped. */ private ComponentContext getContextForComponent(Component component) { final ComponentContext c = component.getScopedContext(); return c == null ? mContext : c; } private static class LayoutOutputLog { long currentId = -1; String currentLifecycle; int currentIndex = -1; int currentLastDuplicatedIdIndex = -1; long nextId = -1; String nextLifecycle; int nextIndex = -1; int nextLastDuplicatedIdIndex = -1; @Override public String toString() { return "id: [" + currentId + " - " + nextId + "], " + "lifecycle: [" + currentLifecycle + " - " + nextLifecycle + "], " + "index: [" + currentIndex + " - " + nextIndex + "], " + "lastDuplicatedIdIndex: [" + currentLastDuplicatedIdIndex + " - " + nextLastDuplicatedIdIndex + "]"; } } }