/**
* 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.annotation.VisibleForTesting;
import com.facebook.litho.internal.ArraySet;
/**
* Defines the relationship of a set of input values to a set of output values where the values from
* the input nodes 'flow into' the output nodes. For example, input values could be a touch X/Y or a
* layout value, and output values could be the X/Y position of a View or its opacity. Input and
* output values can be connected to each other via intermediate operators like springs or timing.
*
* NB: ValueNodes can be referenced by multiple GraphBindings (e.g. a view property).
*/
public final class GraphBinding {
private final DataFlowGraph mDataFlowGraph;
private final Bindings mBindings = new Bindings();
private final ArraySet<ValueNode> mAllNodes = new ArraySet<>();
private BindingListener mListener;
private boolean mIsActive = false;
private boolean mHasBeenActivated = false;
/**
* Creates a {@link GraphBinding} associated with the default {@link DataFlowGraph} instance.
*/
public static GraphBinding create() {
return new GraphBinding(DataFlowGraph.getInstance());
}
@VisibleForTesting
public static GraphBinding create(DataFlowGraph dataFlowGraph) {
return new GraphBinding(dataFlowGraph);
}
private GraphBinding(DataFlowGraph dataFlowGraph) {
mDataFlowGraph = dataFlowGraph;
}
/**
* Adds a connection between two nodes to this graph. The connection will not be actualized until
* {@link #activate} is called, and will be removed once {@link #deactivate} is called.
*/
public void addBinding(ValueNode fromNode, ValueNode toNode, String name) {
if (mHasBeenActivated) {
throw new RuntimeException(
"Trying to add binding after DataFlowGraph has already been activated.");
}
mBindings.addBinding(fromNode, toNode, name);
mAllNodes.add(fromNode);
mAllNodes.add(toNode);
}
public void addBinding(ValueNode fromNode, ValueNode toNode) {
addBinding(fromNode, toNode, ValueNode.DEFAULT_INPUT);
}
/**
* @return all nodes that have a binding defined in this {@link GraphBinding}.
*/
ArraySet<ValueNode> getAllNodes() {
return mAllNodes;
}
/**
* Activates a binding, adding the sub-graph defined by this binding to the main
* {@link DataFlowGraph} associated with this binding. This is expected to be called from
* framework code and should not be called by the end developer.
*/
public void activate() {
mBindings.applyBindings();
mHasBeenActivated = true;
mIsActive = true;
mDataFlowGraph.register(this);
}
/**
* Deactivates this binding which, as you might guess, is the reverse of activating it: the
* sub-graph associated with this binding is removed from the main {@link DataFlowGraph}. As with
* {@link #activate()}, this is expected to only be called by framework code and not the end
* developer.
*/
public void deactivate() {
mIsActive = false;
mDataFlowGraph.unregister(this);
mBindings.removeBindings();
}
/**
* @return whether this binding has been activated and not yet deactivated.
*/
public boolean isActive() {
return mIsActive;
}
void notifyNodesHaveFinished() {
if (mListener != null) {
mListener.onAllNodesFinished(this);
}
}
/**
* Sets the {@link BindingListener}.
*/
public void setListener(BindingListener listener) {
if (mListener != null && listener != null) {
throw new RuntimeException("Overriding existing listener!");
}
mListener = listener;
}
private static class Bindings {
private final ArrayList<ValueNode> mFromNodes = new ArrayList<>();
private final ArrayList<ValueNode> mToNodes = new ArrayList<>();
private final ArrayList<String> mInputNames = new ArrayList<>();
public void addBinding(ValueNode fromNode, ValueNode toNode, String name) {
mFromNodes.add(fromNode);
mToNodes.add(toNode);
mInputNames.add(name);
}
public void applyBindings() {
for (int i = 0; i < mFromNodes.size(); i++) {
final ValueNode fromNode = mFromNodes.get(i);
final ValueNode toNode = mToNodes.get(i);
final String name = mInputNames.get(i);
final ValueNode currentInput = toNode.getInputUnsafe(name);
if (currentInput != null) {
unbindNodes(currentInput, toNode, name);
}
fromNode.addOutput(toNode);
toNode.setInput(name, fromNode);
}
}
public void removeBindings() {
for (int i = 0; i < mFromNodes.size(); i++) {
final ValueNode fromNode = mFromNodes.get(i);
final ValueNode toNode = mToNodes.get(i);
final String name = mInputNames.get(i);
// Nodes may have already been re-bound
if (toNode.getInputUnsafe(name) == fromNode) {
unbindNodes(fromNode, toNode, name);
}
}
}
private static void unbindNodes(ValueNode fromNode, ValueNode toNode, String name) {
fromNode.removeOutput(toNode);
toNode.removeInput(name);
}
}
}