/** * 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.Arrays; import java.util.BitSet; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import android.support.annotation.AttrRes; import android.support.annotation.StyleRes; import com.facebook.infer.annotation.ThreadConfined; import com.facebook.infer.annotation.ThreadSafe; import com.facebook.litho.ComponentLifecycle.MountType; import com.facebook.litho.ComponentLifecycle.StateContainer; /** * Represents a unique instance of a component that is driven by its matching * {@link ComponentLifecycle}. To create new {@link Component} instances, use the * {@code create()} method in the generated {@link ComponentLifecycle} subclass which * returns a builder that allows you to set values for individual props. {@link Component} * instances are immutable after creation. */ public abstract class Component<L extends ComponentLifecycle> implements HasEventDispatcher { public static abstract class Builder<L extends ComponentLifecycle> extends ResourceResolver { private ComponentContext mContext; private @AttrRes int mDefStyleAttr; private @StyleRes int mDefStyleRes; private Component mComponent; protected void init( ComponentContext c, @AttrRes int defStyleAttr, @StyleRes int defStyleRes, Component<L> component) { super.init(c, c.getResourceCache()); mComponent = component; mContext = c; mDefStyleAttr = defStyleAttr; mDefStyleRes = defStyleRes; if (defStyleAttr != 0 || defStyleRes != 0) { component.mLifecycle.loadStyle(c, defStyleAttr, defStyleRes, component); } } /** * Set a key on the component that is local to its parent. */ protected void setKey(String key) { mComponent.setKey(key); } public abstract Component.Builder<L> key(String key); @Override protected void release() { super.release(); mContext = null; mDefStyleAttr = 0; mDefStyleRes = 0; mComponent = null; } /** * Checks that all the required props are supplied, and if not throws a useful exception * * @param requiredPropsCount expected number of props * @param required the bit set that identifies which props have been supplied * @param requiredPropsNames the names of all props used for a useful error message */ protected static void checkArgs( int requiredPropsCount, BitSet required, String[] requiredPropsNames) { if (required != null && required.nextClearBit(0) < requiredPropsCount) { List<String> missingProps = new ArrayList<>(); for (int i = 0; i < requiredPropsCount; i++) { if (!required.get(i)) { missingProps.add(requiredPropsNames[i]); } } throw new IllegalStateException( "The following props are not marked as optional and were not supplied: " + Arrays.toString(missingProps.toArray())); } } public final ComponentLayout buildWithLayout() { return this.withLayout().build(); } public final ComponentLayout.Builder withLayout() { // calling build() which will release this builder setting these members to null/0. // We must capture their value before that happens. final ComponentContext context = mContext; final int defStyleAttr = mDefStyleAttr; final int defStyleRes = mDefStyleRes; return Layout.create(context, build(), defStyleAttr, defStyleRes); } public abstract Component<L> build(); } private static final AtomicInteger sIdGenerator = new AtomicInteger(0); private int mId = sIdGenerator.getAndIncrement(); private String mGlobalKey; private String mKey; private final L mLifecycle; private @ThreadConfined(ThreadConfined.ANY) ComponentContext mScopedContext; private boolean mIsLayoutStarted = false; // If we have a cachedLayout, onPrepare and onMeasure would have been called on it already. private @ThreadConfined(ThreadConfined.ANY) InternalNode mLastMeasuredLayout; abstract public String getSimpleName(); protected StateContainer getStateContainer() { return null; } public ComponentContext getScopedContext() { return mScopedContext; } public void setScopedContext(ComponentContext scopedContext) { mScopedContext = scopedContext; } synchronized void markLayoutStarted() { if (mIsLayoutStarted) { throw new IllegalStateException("Duplicate layout of a component: " + this); } mIsLayoutStarted = true; } // Get an id that is identical across cloned instances, but otherwise unique protected int getId() { return mId; } /** * Get a key that is unique to this component within its tree. * @return */ String getGlobalKey() { return mGlobalKey; } /** * Set a key for this component that is unique within its tree. * @param key * */ // thread-safe because the one write is before all the reads @ThreadSafe(enableChecks = false) private void setGlobalKey(String key) { mGlobalKey = key; } /** * * @return a key that is local to the component's parent. */ String getKey() { return mKey; } /** * Set a key that is local to the parent of this component. * @param key key */ void setKey(String key) { mKey = key; } Component<L> makeCopyWithNullContext() { try { Component<L> component = (Component<L>) super.clone(); component.mScopedContext = null; return component; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } public Component<L> makeShallowCopy() { try { Component<L> component = (Component<L>) super.clone(); component.mIsLayoutStarted = false; return component; } catch (CloneNotSupportedException e) { // Subclasses implement Cloneable, so this is impossible throw new RuntimeException(e); } } Component<L> makeShallowCopyWithNewId() { final Component<L> component = makeShallowCopy(); component.mId = sIdGenerator.incrementAndGet(); return component; } boolean hasCachedLayout() { return (mLastMeasuredLayout != null); } InternalNode getCachedLayout() { return mLastMeasuredLayout; } void releaseCachedLayout() { if (mLastMeasuredLayout != null) { LayoutState.releaseNodeTree(mLastMeasuredLayout, true /* isNestedTree */); mLastMeasuredLayout = null; } } void clearCachedLayout() { mLastMeasuredLayout = null; } void release() { mIsLayoutStarted = false; } protected Component(L lifecycle) { mLifecycle = lifecycle; mKey = Integer.toString(mLifecycle.getId()); } public L getLifecycle() { return mLifecycle; } /** * Measure a component with the given {@link SizeSpec} constrain. * * @param c {@link ComponentContext}. * @param widthSpec Width {@link SizeSpec} constrain. * @param heightSpec Height {@link SizeSpec} constrain. * @param outputSize Size object that will be set with the measured dimensions. */ public void measure(ComponentContext c, int widthSpec, int heightSpec, Size outputSize) { releaseCachedLayout(); mLastMeasuredLayout = LayoutState.createAndMeasureTreeForComponent( c, this, widthSpec, heightSpec); // This component resolution won't be deferred nor onMeasure called if it's a layout spec. // In that case it needs to manually save the latest saze specs. // The size specs will be checked during the calculation (or collection) of the main tree. if (Component.isLayoutSpec(this)) { mLastMeasuredLayout.setLastWidthSpec(widthSpec); mLastMeasuredLayout.setLastHeightSpec(heightSpec); } outputSize.width = mLastMeasuredLayout.getWidth(); outputSize.height = mLastMeasuredLayout.getHeight(); } protected void copyInterStageImpl(Component<L> component) { } static boolean isHostSpec(Component<?> component) { return (component != null && component.mLifecycle instanceof HostComponent); } static boolean isLayoutSpec(Component<?> component) { return (component != null && component.mLifecycle.getMountType() == MountType.NONE); } static boolean isMountSpec(Component<?> component) { return (component != null && component.mLifecycle.getMountType() != MountType.NONE); } static boolean isMountDrawableSpec(Component<?> component) { return (component != null && component.mLifecycle.getMountType() == MountType.DRAWABLE); } static boolean isMountViewSpec(Component<?> component) { return (component != null && component.mLifecycle.getMountType() == MountType.VIEW); } static boolean isLayoutSpecWithSizeSpec(Component<?> component) { return (isLayoutSpec(component) && component.mLifecycle.canMeasure()); } static boolean isNestedTree(Component<?> component) { return (isLayoutSpecWithSizeSpec(component) || (component != null && component.hasCachedLayout())); } /** * Prepares a component for calling any pending state updates on it by setting a global key and * a scoped component context and applies the pending state updates. * @param c component context */ void applyStateUpdates(ComponentContext c) { final Component<?> parentScope = c.getComponentScope(); final String key = getKey(); setGlobalKey(parentScope == null ? key : parentScope.getGlobalKey() + key); setScopedContext(ComponentContext.withComponentScope(c, this)); if (getLifecycle().hasState()) { c.getStateHandler().applyStateUpdatesForComponent(this); } } @Override public EventDispatcher getEventDispatcher() { return mLifecycle; } }