/** * 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.concurrent.atomic.AtomicInteger; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.support.annotation.AttrRes; import android.support.annotation.StyleRes; import android.support.v4.util.Pools; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.widget.ExploreByTouchHelper; import android.view.View; import com.facebook.litho.annotations.OnCreateTreeProp; import com.facebook.yoga.YogaBaselineFunction; import com.facebook.yoga.YogaMeasureMode; import com.facebook.yoga.YogaMeasureFunction; import com.facebook.yoga.YogaMeasureOutput; import com.facebook.yoga.YogaNode; /** * {@link ComponentLifecycle} is a stateless singleton object that defines how {@link Component} * instances calculate their layout bounds and mount elements, among other things. This is the * base class from which all new component types inherit. * * In most cases, the {@link ComponentLifecycle} class will be automatically generated by the * annotation processor at build-time based on your spec class and you won't have to deal with * it directly when implementing new component types. */ public abstract class ComponentLifecycle implements EventDispatcher { private static final AtomicInteger sComponentId = new AtomicInteger(); private static final int DEFAULT_MAX_PREALLOCATION = 15; private boolean mPreallocationDone; public enum MountType { NONE, DRAWABLE, VIEW, } public interface StateContainer {} private static final YogaBaselineFunction sBaselineFunction = new YogaBaselineFunction() { public float baseline(YogaNode cssNode, float width, float height) { final InternalNode node = (InternalNode) cssNode.getData(); return node.getRootComponent() .getLifecycle() .onMeasureBaseline(node.getContext(), (int) width, (int) height); } }; private static final YogaMeasureFunction sMeasureFunction = new YogaMeasureFunction() { private final Pools.SynchronizedPool<Size> mSizePool = new Pools.SynchronizedPool<>(2); private Size acquireSize(int initialValue) { Size size = mSizePool.acquire(); if (size == null) { size = new Size(); } size.width = initialValue; size.height = initialValue; return size; } private void releaseSize(Size size) { mSizePool.release(size); } @Override @SuppressLint("WrongCall") @SuppressWarnings("unchecked") public long measure( YogaNode cssNode, float width, YogaMeasureMode widthMode, float height, YogaMeasureMode heightMode) { final InternalNode node = (InternalNode) cssNode.getData(); final DiffNode diffNode = node.areCachedMeasuresValid() ? node.getDiffNode() : null; final Component<?> component = node.getRootComponent(); final int widthSpec; final int heightSpec; ComponentsSystrace.beginSection("measure:" + component.getSimpleName()); widthSpec = SizeSpec.makeSizeSpecFromCssSpec(width, widthMode); heightSpec = SizeSpec.makeSizeSpecFromCssSpec(height, heightMode); node.setLastWidthSpec(widthSpec); node.setLastHeightSpec(heightSpec); int outputWidth = 0; int outputHeight = 0; if (Component.isNestedTree(component) || node.hasNestedTree()) { final InternalNode nestedTree = LayoutState.resolveNestedTree(node, widthSpec, heightSpec); outputWidth = nestedTree.getWidth(); outputHeight = nestedTree.getHeight(); } else if (diffNode != null && diffNode.getLastWidthSpec() == widthSpec && diffNode.getLastHeightSpec() == heightSpec) { outputWidth = (int) diffNode.getLastMeasuredWidth(); outputHeight = (int) diffNode.getLastMeasuredHeight(); } else { final Size size = acquireSize(Integer.MIN_VALUE /* initialValue */); try { component.getLifecycle().onMeasure( node.getContext(), node, widthSpec, heightSpec, size, component); if (size.width < 0 || size.height < 0) { throw new IllegalStateException( "MeasureOutput not set, ComponentLifecycle is: " + component.getLifecycle()); } outputWidth = size.width; outputHeight = size.height; if (node.getDiffNode() != null) { node.getDiffNode().setLastWidthSpec(widthSpec); node.getDiffNode().setLastHeightSpec(heightSpec); node.getDiffNode().setLastMeasuredWidth(outputWidth); node.getDiffNode().setLastMeasuredHeight(outputHeight); } } finally { releaseSize(size); } } node.setLastMeasuredWidth(outputWidth); node.setLastMeasuredHeight(outputHeight); ComponentsSystrace.endSection(); return YogaMeasureOutput.make(outputWidth, outputHeight); } }; private final int mId; protected ComponentLifecycle() { mId = sComponentId.incrementAndGet(); } int getId() { return mId; } Object createMountContent(ComponentContext c) { return onCreateMountContent(c); } void mount(ComponentContext c, Object convertContent, Component<?> component) { c.enterNoStateUpdatesMethod("mount"); onMount(c, convertContent, component); c.exitNoStateUpdatesMethod(); } void bind(ComponentContext c, Object mountedContent, Component<?> component) { c.enterNoStateUpdatesMethod("bind"); onBind(c, mountedContent, component); c.exitNoStateUpdatesMethod(); } void unbind(ComponentContext c, Object mountedContent, Component<?> component) { onUnbind(c, mountedContent, component); } void unmount(ComponentContext c, Object mountedContent, Component<?> component) { onUnmount(c, mountedContent, component); } /** * Create a layout from the given component. * * @param context ComponentContext associated with the current ComponentTree. * @param component Component to process the layout for. * @param resolveNestedTree if the component's layout tree should be resolved as part of this * call. * @return New InternalNode associated with the given component. */ ComponentLayout createLayout( ComponentContext context, Component<?> component, boolean resolveNestedTree) { final boolean deferNestedTreeResolution = Component.isNestedTree(component) && !resolveNestedTree; final TreeProps parentTreeProps = context.getTreeProps(); populateTreeProps(component, parentTreeProps); context.setTreeProps(getTreePropsForChildren(context, component, parentTreeProps)); ComponentsSystrace.beginSection("createLayout:" + component.getSimpleName()); final InternalNode node; if (deferNestedTreeResolution) { node = ComponentsPools.acquireInternalNode(context, context.getResources()); node.markIsNestedTreeHolder(context.getTreeProps()); } else if (Component.isLayoutSpecWithSizeSpec(component)) { node = (InternalNode) onCreateLayoutWithSizeSpec( context, context.getWidthSpec(), context.getHeightSpec(), component); } else { node = (InternalNode) onCreateLayout(context, component); } ComponentsSystrace.endSection(); if (node == null) { return ComponentContext.NULL_LAYOUT; } // Set component on the root node of the generated tree so that the mount calls use // those (see Controller.mountNodeTree()). Handle the case where the component simply // delegates its layout creation to another component i.e. the root node belongs to // another component. if (node.getRootComponent() == null) { node.setBaselineFunction(sBaselineFunction); final boolean isMountSpecWithMeasure = canMeasure() && Component.isMountSpec(component); if (isMountSpecWithMeasure || deferNestedTreeResolution) { node.setMeasureFunction(sMeasureFunction); } } node.appendComponent(component); if (!deferNestedTreeResolution) { onPrepare(context, component); } if (context.getTreeProps() != parentTreeProps) { ComponentsPools.release(context.getTreeProps()); context.setTreeProps(parentTreeProps); } return node; } void loadStyle( ComponentContext c, @AttrRes int defStyleAttr, @StyleRes int defStyleRes, Component<?> component) { c.setDefStyle(defStyleAttr, defStyleRes); onLoadStyle(c, component); c.setDefStyle(0, 0); } void loadStyle(ComponentContext c, Component<?> component) { onLoadStyle(c, component); } protected Output acquireOutput() { return ComponentsPools.acquireOutput(); } protected void releaseOutput(Output output) { ComponentsPools.release(output); } protected final <T> Diff<T> acquireDiff(T previousValue, T nextValue) { Diff<T> diff = ComponentsPools.acquireDiff(previousValue, nextValue); return diff; } protected void releaseDiff(Diff diff) { ComponentsPools.release(diff); } /** * Retrieves all of the tree props used by this Component from the TreeProps map * and sets the tree props as fields on the ComponentImpl. */ protected void populateTreeProps(Component<?> component, TreeProps parentTreeProps) { } /** * Updates the TreeProps map with outputs from all {@link OnCreateTreeProp} methods. */ protected TreeProps getTreePropsForChildren( ComponentContext c, Component<?> component, TreeProps previousTreeProps) { return previousTreeProps; } /** * Generate a tree of {@link ComponentLayout} representing the layout structure of * the {@link Component} and its sub-components. You should use * {@link ComponentContext#newLayoutBuilder} to build the layout tree. * * @param c The {@link ComponentContext} to build a {@link ComponentLayout} tree. * @param component The component to create the {@link ComponentLayout} tree from. */ protected ComponentLayout onCreateLayout(ComponentContext c, Component<?> component) { return Column.create(c).build(); } protected ComponentLayout onCreateLayoutWithSizeSpec( ComponentContext c, int widthSpec, int heightSpec, Component<?> component) { return Column.create(c).build(); } protected void onPrepare(ComponentContext c, Component<?> component) { // do nothing, by default } protected void onLoadStyle(ComponentContext c, Component<?> component) { } /** * Called after the layout calculation is finished and the given {@link ComponentLayout} * has its bounds defined. You can use {@link ComponentLayout#getX()}, * {@link ComponentLayout#getY()}, {@link ComponentLayout#getWidth()}, and * {@link ComponentLayout#getHeight()} to get the size and position of the component * in the layout tree. * * @param c The {@link Context} used by this component. * @param layout The {@link ComponentLayout} with defined position and size. * @param component The {@link Component} for this component. */ protected void onBoundsDefined( ComponentContext c, ComponentLayout layout, Component<?> component) { } /** * Called during layout calculation to determine the baseline of a component. * * @param c The {@link Context} used by this component. * @param width The width of this component. * @param height The height of this component. */ protected int onMeasureBaseline(ComponentContext c, int width, int height) { return height; } /** * Whether this {@link ComponentLifecycle} is able to measure itself according * to specific size constraints. */ protected boolean canMeasure() { return false; } protected void onMeasure( ComponentContext c, ComponentLayout layout, int widthSpec, int heightSpec, Size size, Component<?> component) { throw new IllegalStateException( "You must override onMeasure() if you return true in canMeasure(), " + "ComponentLifecycle is: " + component.getLifecycle()); } /** * Whether this {@link ComponentLifecycle} mounts views that contain component-based * content that can be incrementally mounted e.g. if the mounted view has a * LithoView with incremental mount enabled. */ protected boolean canMountIncrementally() { return false; } /** * Whether this drawable mount spec should cache its drawing in a display list. */ protected boolean shouldUseDisplayList() { return false; } /** * Create the object that will be mounted in the {@link LithoView}. * * @param context The {@link ComponentContext} to be used to create the content. * @return an Object that can be mounted for this component. */ protected Object onCreateMountContent(ComponentContext context) { throw new RuntimeException( "Trying to mount a MountSpec that doesn't implement @OnCreateMountContent"); } /** * Deploy all UI elements representing the final bounds defined in the given * {@link ComponentLayout}. Return either a {@link Drawable} or a {@link View} or * {@code null} to be mounted. * * @param c The {@link ComponentContext} to mount the component into. * @param component The {@link Component} for this component. */ protected void onMount(ComponentContext c, Object convertContent, Component<?> component) { // Do nothing by default. } /** * Unload UI elements associated with this component. * * @param c The {@link Context} for this mount operation. * @param mountedContent The {@link Drawable} or {@link View} mounted by this component. * @param component The {@link Component} for this component. */ protected void onUnmount(ComponentContext c, Object mountedContent, Component<?> component) { // Do nothing by default. } protected void onBind(ComponentContext c, Object mountedContent, Component<?> component) { // Do nothing by default. } protected void onUnbind(ComponentContext c, Object mountedContent, Component<?> component) { // Do nothing by default. } /** * This indicates the type of the {@link java.lang.Object} that will be returned by * {@link ComponentLifecycle#mount}. * * @return one of {@link ComponentLifecycle.MountType} */ public MountType getMountType() { return MountType.NONE; } /** * Populate an accessibility node with information about the component. * @param accessibilityNode node to populate * @param component The {@link Component} for this component. */ protected void onPopulateAccessibilityNode( AccessibilityNodeInfoCompat accessibilityNode, Component<?> component) { } /** * Populate an extra accessibility node. * @param accessibilityNode node to populate * @param extraNodeIndex index of extra node * @param componentBoundsX left bound of the mounted component * @param componentBoundsY top bound of the mounted component * @param component The {@link Component} for this component. */ protected void onPopulateExtraAccessibilityNode( AccessibilityNodeInfoCompat accessibilityNode, int extraNodeIndex, int componentBoundsX, int componentBoundsY, Component<?> component) { } /** * Get extra accessibility node id at a given point within the component. * @param x x co-ordinate within the mounted component * @param y y co-ordinate within the mounted component * @param component the {@link Component} for this component * @return the extra virtual view id if one is found, otherwise * {@code ExploreByTouchHelper#INVALID_ID} */ protected int getExtraAccessibilityNodeAt(int x, int y, Component<?> component) { return ExploreByTouchHelper.INVALID_ID; } /** * The number of extra accessibility nodes that this component wishes to provides to the * accessibility system. * @param component the {@link Component} for this component * @return the number of extra nodes */ protected int getExtraAccessibilityNodesCount(Component<?> component) { return 0; } /** * Whether this component will expose any virtual views to the accessibility framework * @return true if the component exposes extra accessibility nodes */ protected boolean implementsExtraAccessibilityNodes() { return false; } /** * Whether this component will populate any accessibility nodes or events that are passed to it. * @return true if the component implements accessibility info */ protected boolean implementsAccessibility() { return false; } /** * Call this to transfer the {@link com.facebook.litho.annotations.State} annotated values * between two {@link Component} with the same global scope. */ protected void transferState( ComponentContext c, StateContainer previousStateContainer, Component component) { } protected void createInitialState(ComponentContext c, Component<?> component) { } @Override public Object dispatchOnEvent(EventHandler eventHandler, Object eventState) { // Do nothing by default. return null; } boolean hasBeenPreallocated() { return mPreallocationDone; } void setWasPreallocated() { mPreallocationDone = true; } protected boolean isPureRender() { return false; } protected boolean callsShouldUpdateOnMount() { return false; } /** * @return true if Mount uses @FromMeasure or @FromOnBoundsDefined parameters. */ protected boolean isMountSizeDependent() { return false; } protected int poolSize() { return DEFAULT_MAX_PREALLOCATION; } final boolean shouldComponentUpdate(Component previous, Component next) { if (isPureRender()) { return shouldUpdate(previous, next); } return true; } /** * Whether the component needs updating. * <p> * For layout components, the framework will verify that none of the children of the component * need updating, and that both components have the same number of children. Therefore this * method just needs to determine any changes to the top-level component that would cause it to * need to be updated (for example, a click handler was added). * <p> * For mount specs, the framework does nothing extra and this method alone determines whether the * component is updated or not. * @param previous the previous component to compare against. * @param next the component that is now in use. * @return true if the component needs an update, false otherwise. */ protected boolean shouldUpdate(Component previous, Component next) { return !previous.equals(next); } /** * @return a {@link TransitionSet} specifying how to animate this component to its new layout * and props. */ protected TransitionSet onCreateTransition( ComponentContext c, Component<?> component) { return null; } protected static <E> EventHandler<E> newEventHandler( ComponentContext c, int id, Object[] params) { return c.newEventHandler(id, params); } protected static <E> EventHandler<E> newEventHandler( Component<?> c, int id, Object[] params) { return new EventHandler<E>(c, id, params); } public interface StateUpdate { void updateState(StateContainer stateContainer, Component newComponent); } /** * @return true if the Component is using state, false otherwise. */ protected boolean hasState() { return false; } }