/** * 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.dataflow; import java.util.ArrayList; import android.support.v4.util.SimpleArrayMap; /** * A single node in a {@link DataFlowGraph}. Nodes are added to a {@link DataFlowGraph} using * by creating a {@link GraphBinding} and calling {@link GraphBinding#addBinding}. The nodes will * actually be added to the graph when {@link GraphBinding#activate} is called. * * The nodes in an active graph are visited on each frame in dependency order and given a chance to * compute a new value based on frame time and their parent (dependency) nodes. * * A ValueNode should be able to return its latest value at any point in time. * * Sub-classes are expected to implement {@link #calculateValue}, which handles calculating * the new value for this frame based on the node's parents (i.e. nodes it depends on) and the * current frame time. */ public abstract class ValueNode { public static final String DEFAULT_INPUT = "default_input"; private SimpleArrayMap<String, ValueNode> mInputs = null; private ArrayList<ValueNode> mOutputs = null; private float mValue; private long mTimeNs = 0; /** * @return the most recently calculated value from {@link #calculateValue}. */ public float getValue() { return mValue; } /** * This node should calculate and set a new value based on frame time and its parents (the nodes * it depends on). When this is called, it's guaranteed that the parent nodes have already been * updated for this frame. */ protected abstract float calculateValue(long frameTimeNanos); /** * @return the input node for the given input name */ protected ValueNode getInput(String name) { final ValueNode input = getInputUnsafe(name); if (input == null) { throw new RuntimeException( "Tried to get non-existent input '" + name + "'. Node only has these inputs: " + buildDebugInputsString()); } return input; } /** * @return the default input node. This should only be used for nodes that expect a single input. */ protected ValueNode getInput() { if (getInputCount() > 1) { throw new RuntimeException("Trying to get single input of node with multiple inputs!"); } return getInput(DEFAULT_INPUT); } /** * @return whether this node has an input with the given name */ protected boolean hasInput(String name) { if (mInputs == null) { return false; } return mInputs.containsKey(name); } /** * @return whether this node has a default input node. This should only be used for nodes that * expect a single input. */ protected boolean hasInput() { if (getInputCount() > 1) { throw new RuntimeException("Trying to check for single input of node with multiple inputs!"); } return hasInput(DEFAULT_INPUT); } private String buildDebugInputsString() { if (mInputs == null) { return "[]"; } String inputNames = ""; for (int i = 0; i < mInputs.size(); i++) { inputNames += "'" + mInputs.keyAt(i) + "'"; if (i != mInputs.size() - 1) { inputNames += ", "; } } return "[" + inputNames + "]"; } ValueNode getInputUnsafe(String name) { if (mInputs == null) { return null; } return mInputs.get(name); } final void doCalculateValue(long frameTimeNanos) { final float value = calculateValue(frameTimeNanos); if (frameTimeNanos == mTimeNs) { throw new RuntimeException( "Got a calculate value call multiple times in the same frame. This isn't expected."); } mTimeNs = frameTimeNanos; mValue = value; } void addOutput(ValueNode node) { if (mOutputs == null) { mOutputs = new ArrayList<>(); } mOutputs.add(node); } int getOutputCount() { return mOutputs == null ? 0 : mOutputs.size(); } ValueNode getOutputAt(int i) { return mOutputs.get(i); } ValueNode getOutput() { if (getOutputCount() != 1) { throw new RuntimeException("Node does not have inputs of size 1: " + getOutputCount()); } return getOutputAt(0); } void removeOutputAt(int i) { if (i >= getOutputCount()) { throw new RuntimeException("Bad index: " + i + " >= " + getOutputCount()); } mOutputs.remove(i); } void removeOutput(ValueNode output) { if (!mOutputs.remove(output)) { throw new RuntimeException("Tried to remove non-existent input!"); } } int getInputCount() { return mInputs == null ? 0 : mInputs.size(); } ValueNode getInputAt(int i) { if (getInputCount() <= i) { throw new RuntimeException("Bad index: " + i + " > " + getInputCount()); } return mInputs.valueAt(i); } void setInput(String name, ValueNode input) { if (mInputs == null) { mInputs = new SimpleArrayMap<>(); } mInputs.put(name, input); } void removeInput(String name) { if (mInputs == null || mInputs.remove(name) == null) { throw new RuntimeException("Tried to remove non-existent input with name: " + name); } } }