1138 lines
38 KiB
C++
1138 lines
38 KiB
C++
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#include "NativeAnimatedNodesManager.h"
|
|
|
|
#include <cxxreact/TraceSection.h>
|
|
#include <folly/json.h>
|
|
#include <glog/logging.h>
|
|
#include <react/debug/react_native_assert.h>
|
|
#include <react/featureflags/ReactNativeFeatureFlags.h>
|
|
#include <react/renderer/animated/drivers/AnimationDriver.h>
|
|
#include <react/renderer/animated/drivers/AnimationDriverUtils.h>
|
|
#include <react/renderer/animated/drivers/DecayAnimationDriver.h>
|
|
#include <react/renderer/animated/drivers/FrameAnimationDriver.h>
|
|
#include <react/renderer/animated/drivers/SpringAnimationDriver.h>
|
|
#include <react/renderer/animated/nodes/AdditionAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/AnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/ColorAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/DiffClampAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/DivisionAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/InterpolationAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/ModulusAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/MultiplicationAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/ObjectAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/PropsAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/RoundAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/StyleAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/SubtractionAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/TrackingAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/TransformAnimatedNode.h>
|
|
#include <react/renderer/animated/nodes/ValueAnimatedNode.h>
|
|
#include <react/renderer/core/EventEmitter.h>
|
|
|
|
#ifdef RN_USE_ANIMATION_BACKEND
|
|
#include <react/renderer/animationbackend/AnimatedPropsBuilder.h>
|
|
#endif
|
|
|
|
namespace facebook::react {
|
|
|
|
// Global function pointer for getting current time. Current time
|
|
// can be injected for testing purposes.
|
|
static TimePointFunction g_now = &std::chrono::steady_clock::now;
|
|
void g_setNativeAnimatedNowTimestampFunction(TimePointFunction nowFunction) {
|
|
g_now = nowFunction;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct NodesQueueItem {
|
|
AnimatedNode* node;
|
|
bool connectedToFinishedAnimation;
|
|
};
|
|
|
|
void mergeObjects(folly::dynamic& out, const folly::dynamic& objectToMerge) {
|
|
react_native_assert(objectToMerge.isObject());
|
|
if (out.isObject() && !out.empty()) {
|
|
for (const auto& pair : objectToMerge.items()) {
|
|
out[pair.first] = pair.second;
|
|
}
|
|
} else {
|
|
out = objectToMerge;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
thread_local bool NativeAnimatedNodesManager::isOnRenderThread_{false};
|
|
|
|
NativeAnimatedNodesManager::NativeAnimatedNodesManager(
|
|
DirectManipulationCallback&& directManipulationCallback,
|
|
FabricCommitCallback&& fabricCommitCallback,
|
|
StartOnRenderCallback&& startOnRenderCallback,
|
|
StopOnRenderCallback&& stopOnRenderCallback,
|
|
FrameRateListenerCallback&& frameRateListenerCallback) noexcept
|
|
: directManipulationCallback_(std::move(directManipulationCallback)),
|
|
fabricCommitCallback_(std::move(fabricCommitCallback)),
|
|
startOnRenderCallback_(std::move(startOnRenderCallback)),
|
|
stopOnRenderCallback_(std::move(stopOnRenderCallback)),
|
|
frameRateListenerCallback_(std::move(frameRateListenerCallback)) {
|
|
if (!fabricCommitCallback_) {
|
|
LOG(WARNING)
|
|
<< "C++ Animated was setup without commit callback. This may lead to issue where buttons are not tappable when animation is driven by onScroll event.";
|
|
}
|
|
|
|
if (!directManipulationCallback_) {
|
|
LOG(WARNING)
|
|
<< "C++ Animated was setup without direct manipulation callback. This may lead to suboptimal performance.";
|
|
}
|
|
|
|
if (!directManipulationCallback_ && fabricCommitCallback_) {
|
|
LOG(ERROR)
|
|
<< "C++ Animated was setup without a way to update UI. Animations will not work.";
|
|
}
|
|
}
|
|
|
|
NativeAnimatedNodesManager::NativeAnimatedNodesManager(
|
|
std::shared_ptr<UIManagerAnimationBackend> animationBackend) noexcept
|
|
: animationBackend_(animationBackend) {}
|
|
|
|
NativeAnimatedNodesManager::~NativeAnimatedNodesManager() noexcept {
|
|
stopRenderCallbackIfNeeded(true);
|
|
}
|
|
|
|
std::optional<double> NativeAnimatedNodesManager::getValue(Tag tag) noexcept {
|
|
auto node = getAnimatedNode<ValueAnimatedNode>(tag);
|
|
if (node != nullptr) {
|
|
return node->getValue();
|
|
} else {
|
|
LOG(WARNING)
|
|
<< "Cannot get value from AnimatedNode, it's not a ValueAnimatedNode";
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Graph
|
|
|
|
std::unique_ptr<AnimatedNode> NativeAnimatedNodesManager::animatedNode(
|
|
Tag tag,
|
|
const folly::dynamic& config) noexcept {
|
|
auto typeName = config["type"].asString();
|
|
|
|
auto type = AnimatedNode::getNodeTypeByName(typeName);
|
|
if (!type) {
|
|
LOG(WARNING) << "Invalid AnimatedNode type " << typeName;
|
|
return nullptr;
|
|
}
|
|
|
|
switch (type.value()) {
|
|
case AnimatedNodeType::Style:
|
|
return std::make_unique<StyleAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Value:
|
|
return std::make_unique<ValueAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Color:
|
|
return std::make_unique<ColorAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Props:
|
|
return std::make_unique<PropsAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Tracking:
|
|
return std::make_unique<TrackingAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Interpolation:
|
|
return std::make_unique<InterpolationAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Transform:
|
|
return std::make_unique<TransformAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Subtraction:
|
|
return std::make_unique<SubtractionAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Addition:
|
|
return std::make_unique<AdditionAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Multiplication:
|
|
return std::make_unique<MultiplicationAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Division:
|
|
return std::make_unique<DivisionAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Modulus:
|
|
return std::make_unique<ModulusAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Diffclamp:
|
|
return std::make_unique<DiffClampAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Round:
|
|
return std::make_unique<RoundAnimatedNode>(tag, config, *this);
|
|
case AnimatedNodeType::Object:
|
|
return std::make_unique<ObjectAnimatedNode>(tag, config, *this);
|
|
default:
|
|
LOG(WARNING) << "Cannot create AnimatedNode of type " << typeName
|
|
<< ", it's not implemented yet";
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::createAnimatedNodeAsync(
|
|
Tag tag,
|
|
const folly::dynamic& config) noexcept {
|
|
if (isOnRenderThread_) {
|
|
LOG(ERROR)
|
|
<< "createAnimatedNodeAsync should not be called on render thread";
|
|
return;
|
|
}
|
|
auto node = animatedNode(tag, config);
|
|
if (node) {
|
|
std::lock_guard<std::mutex> lock(animatedNodesCreatedAsyncMutex_);
|
|
animatedNodesCreatedAsync_.emplace(tag, std::move(node));
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::createAnimatedNode(
|
|
Tag tag,
|
|
const folly::dynamic& config) noexcept {
|
|
if (!isOnRenderThread_) {
|
|
LOG(ERROR) << "createAnimatedNode should only be called on render thread";
|
|
return;
|
|
}
|
|
auto node = animatedNode(tag, config);
|
|
if (node) {
|
|
std::lock_guard<std::mutex> lock(connectedAnimatedNodesMutex_);
|
|
animatedNodes_.emplace(tag, std::move(node));
|
|
updatedNodeTags_.insert(tag);
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::connectAnimatedNodes(
|
|
Tag parentTag,
|
|
Tag childTag) noexcept {
|
|
react_native_assert(parentTag);
|
|
react_native_assert(childTag);
|
|
|
|
auto parentNode = getAnimatedNode<AnimatedNode>(parentTag);
|
|
auto childNode = getAnimatedNode<AnimatedNode>(childTag);
|
|
|
|
if ((parentNode != nullptr) && (childNode != nullptr)) {
|
|
parentNode->addChild(childTag);
|
|
updatedNodeTags_.insert(childTag);
|
|
} else {
|
|
LOG(WARNING) << "Cannot ConnectAnimatedNodes, parentTag = " << parentTag
|
|
<< ", childTag = " << childTag
|
|
<< ", not all of them are created";
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::connectAnimatedNodeToView(
|
|
Tag propsNodeTag,
|
|
Tag viewTag) noexcept {
|
|
react_native_assert(propsNodeTag);
|
|
react_native_assert(viewTag);
|
|
|
|
auto node = getAnimatedNode<PropsAnimatedNode>(propsNodeTag);
|
|
if (node != nullptr) {
|
|
node->connectToView(viewTag);
|
|
{
|
|
std::lock_guard<std::mutex> lock(connectedAnimatedNodesMutex_);
|
|
connectedAnimatedNodes_.insert({viewTag, propsNodeTag});
|
|
}
|
|
updatedNodeTags_.insert(node->tag());
|
|
} else {
|
|
LOG(WARNING)
|
|
<< "Cannot ConnectAnimatedNodeToView, animated node has to be props type";
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::disconnectAnimatedNodeFromView(
|
|
Tag propsNodeTag,
|
|
Tag viewTag) noexcept {
|
|
react_native_assert(propsNodeTag);
|
|
react_native_assert(viewTag);
|
|
|
|
auto node = getAnimatedNode<PropsAnimatedNode>(propsNodeTag);
|
|
if (node != nullptr) {
|
|
node->disconnectFromView(viewTag);
|
|
{
|
|
std::lock_guard<std::mutex> lock(connectedAnimatedNodesMutex_);
|
|
connectedAnimatedNodes_.erase(viewTag);
|
|
}
|
|
updatedNodeTags_.insert(node->tag());
|
|
|
|
onManagedPropsRemoved(viewTag);
|
|
} else {
|
|
LOG(WARNING)
|
|
<< "Cannot DisconnectAnimatedNodeToView, animated node has to be props type";
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::disconnectAnimatedNodes(
|
|
Tag parentTag,
|
|
Tag childTag) noexcept {
|
|
react_native_assert(parentTag);
|
|
react_native_assert(childTag);
|
|
|
|
auto parentNode = getAnimatedNode<AnimatedNode>(parentTag);
|
|
auto childNode = getAnimatedNode<AnimatedNode>(childTag);
|
|
|
|
if ((parentNode != nullptr) && (childNode != nullptr)) {
|
|
parentNode->removeChild(childTag);
|
|
} else {
|
|
LOG(WARNING) << "Cannot DisconnectAnimatedNodes, parentTag = " << parentTag
|
|
<< ", childTag = " << childTag
|
|
<< ", not all of them are created";
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::restoreDefaultValues(Tag tag) noexcept {
|
|
if (auto propsNode = getAnimatedNode<PropsAnimatedNode>(tag)) {
|
|
propsNode->restoreDefaultValues();
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::dropAnimatedNode(Tag tag) noexcept {
|
|
std::lock_guard<std::mutex> lock(connectedAnimatedNodesMutex_);
|
|
animatedNodes_.erase(tag);
|
|
}
|
|
|
|
#pragma mark - Mutations
|
|
|
|
void NativeAnimatedNodesManager::setAnimatedNodeValue(Tag tag, double value) {
|
|
if (auto node = getAnimatedNode<ValueAnimatedNode>(tag)) {
|
|
stopAnimationsForNode(node->tag());
|
|
if (node->setRawValue(value)) {
|
|
updatedNodeTags_.insert(node->tag());
|
|
}
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::setAnimatedNodeOffset(Tag tag, double offset) {
|
|
if (auto node = getAnimatedNode<ValueAnimatedNode>(tag)) {
|
|
if (node->setOffset(offset)) {
|
|
updatedNodeTags_.insert(node->tag());
|
|
}
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::flattenAnimatedNodeOffset(Tag tag) {
|
|
if (auto node = getAnimatedNode<ValueAnimatedNode>(tag)) {
|
|
node->flattenOffset();
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::extractAnimatedNodeOffsetOp(Tag tag) {
|
|
if (auto node = getAnimatedNode<ValueAnimatedNode>(tag)) {
|
|
node->extractOffset();
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::stopAnimationsForNode(Tag nodeTag) {
|
|
std::vector<int> discardedAnimIds{};
|
|
|
|
for (const auto& [animationId, driver] : activeAnimations_) {
|
|
if (driver->getAnimatedValueTag() == nodeTag) {
|
|
discardedAnimIds.emplace_back(animationId);
|
|
}
|
|
}
|
|
for (const auto& id : discardedAnimIds) {
|
|
activeAnimations_.at(id)->stopAnimation();
|
|
activeAnimations_.erase(id);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Drivers
|
|
|
|
void NativeAnimatedNodesManager::startAnimatingNode(
|
|
int animationId,
|
|
Tag animatedNodeTag,
|
|
folly::dynamic config,
|
|
std::optional<AnimationEndCallback> endCallback) noexcept {
|
|
if (auto iter = activeAnimations_.find(animationId);
|
|
iter != activeAnimations_.end()) {
|
|
// reset animation config
|
|
auto& animation = iter->second;
|
|
animation->updateConfig(config);
|
|
} else if (animatedNodes_.contains(animatedNodeTag)) {
|
|
auto type = config["type"].asString();
|
|
auto typeEnum = AnimationDriver::getDriverTypeByName(type);
|
|
std::unique_ptr<AnimationDriver> animation = nullptr;
|
|
if (typeEnum) {
|
|
switch (typeEnum.value()) {
|
|
case AnimationDriverType::Frames: {
|
|
animation = std::make_unique<FrameAnimationDriver>(
|
|
animationId,
|
|
animatedNodeTag,
|
|
std::move(endCallback),
|
|
std::move(config),
|
|
this);
|
|
} break;
|
|
case AnimationDriverType::Spring: {
|
|
animation = std::make_unique<SpringAnimationDriver>(
|
|
animationId,
|
|
animatedNodeTag,
|
|
std::move(endCallback),
|
|
std::move(config),
|
|
this);
|
|
} break;
|
|
case AnimationDriverType::Decay: {
|
|
animation = std::make_unique<DecayAnimationDriver>(
|
|
animationId,
|
|
animatedNodeTag,
|
|
std::move(endCallback),
|
|
std::move(config),
|
|
this);
|
|
} break;
|
|
}
|
|
if (animation) {
|
|
animation->startAnimation();
|
|
activeAnimations_.insert({animationId, std::move(animation)});
|
|
}
|
|
} else {
|
|
LOG(ERROR) << "Unknown AnimationDriver type " << type;
|
|
}
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::stopAnimation(
|
|
int animationId,
|
|
bool /*isTrackingAnimation*/) noexcept {
|
|
if (auto iter = activeAnimations_.find(animationId);
|
|
iter != activeAnimations_.end()) {
|
|
iter->second->stopAnimation();
|
|
activeAnimations_.erase(iter);
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::addAnimatedEventToView(
|
|
Tag viewTag,
|
|
const std::string& eventName,
|
|
const folly::dynamic& eventMapping) noexcept {
|
|
const auto animatedValueTag = (eventMapping.count("animatedValueTag") != 0u)
|
|
? static_cast<Tag>(eventMapping["animatedValueTag"].asInt())
|
|
: 0;
|
|
const auto& pathList = eventMapping["nativeEventPath"];
|
|
auto numPaths = pathList.size();
|
|
std::vector<std::string> eventPath(numPaths);
|
|
for (size_t i = 0; i < numPaths; i++) {
|
|
eventPath[i] = pathList[i].asString();
|
|
}
|
|
|
|
const auto key = EventAnimationDriverKey{
|
|
.viewTag = viewTag,
|
|
.eventName = EventEmitter::normalizeEventType(eventName)};
|
|
if (auto driversIter = eventDrivers_.find(key);
|
|
driversIter != eventDrivers_.end()) {
|
|
auto& drivers = driversIter->second;
|
|
drivers.emplace_back(
|
|
std::make_unique<EventAnimationDriver>(eventPath, animatedValueTag));
|
|
} else {
|
|
std::vector<std::unique_ptr<EventAnimationDriver>> drivers(1);
|
|
drivers[0] =
|
|
std::make_unique<EventAnimationDriver>(eventPath, animatedValueTag);
|
|
eventDrivers_.insert({key, std::move(drivers)});
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::removeAnimatedEventFromView(
|
|
Tag viewTag,
|
|
const std::string& eventName,
|
|
Tag animatedValueTag) noexcept {
|
|
const auto key = EventAnimationDriverKey{
|
|
.viewTag = viewTag,
|
|
.eventName = EventEmitter::normalizeEventType(eventName)};
|
|
auto driversIter = eventDrivers_.find(key);
|
|
if (driversIter != eventDrivers_.end()) {
|
|
auto& drivers = driversIter->second;
|
|
std::erase_if(drivers, [animatedValueTag](auto& it) {
|
|
return it->getAnimatedNodeTag() == animatedValueTag;
|
|
});
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::handleAnimatedEvent(
|
|
Tag viewTag,
|
|
const std::string& eventName,
|
|
const EventPayload& eventPayload) noexcept {
|
|
// We currently reject events that are not on the same thread as `onRender`
|
|
// callbacks, as the assumption is these events can synchronously update
|
|
// UI components or otherwise Animated nodes with single-threaded assumptions.
|
|
// While we could dispatch event handling back to the UI thread using the
|
|
// scheduleOnUI helper, we are not yet doing that because it would violate
|
|
// the assumption that the events have synchronous side-effects. We can
|
|
// revisit this decision later.
|
|
if (!isOnRenderThread_) {
|
|
return;
|
|
}
|
|
if (eventDrivers_.empty()) {
|
|
return;
|
|
}
|
|
|
|
bool foundAtLeastOneDriver = false;
|
|
|
|
const auto key = EventAnimationDriverKey{
|
|
.viewTag = viewTag,
|
|
.eventName = EventEmitter::normalizeEventType(eventName)};
|
|
if (auto driversIter = eventDrivers_.find(key);
|
|
driversIter != eventDrivers_.end()) {
|
|
auto& drivers = driversIter->second;
|
|
if (!drivers.empty()) {
|
|
foundAtLeastOneDriver = true;
|
|
}
|
|
for (const auto& driver : drivers) {
|
|
if (auto value = driver->getValueFromPayload(eventPayload)) {
|
|
auto node =
|
|
getAnimatedNode<ValueAnimatedNode>(driver->getAnimatedNodeTag());
|
|
if (node == nullptr) {
|
|
continue;
|
|
}
|
|
stopAnimationsForNode(node->tag());
|
|
if (node->setRawValue(value.value())) {
|
|
updatedNodeTags_.insert(node->tag());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundAtLeastOneDriver && !isEventAnimationInProgress_) {
|
|
// There is an animation driver handling this event and
|
|
// event driven animation has not been started yet.
|
|
isEventAnimationInProgress_ = true;
|
|
// Some platforms (e.g. iOS) have UI tick listener disable
|
|
// when there are no active animations. Calling
|
|
// `startRenderCallbackIfNeeded` will call platform specific code to
|
|
// register UI tick listener.
|
|
startRenderCallbackIfNeeded(false);
|
|
// Calling startOnRenderCallback_ will register a UI tick listener.
|
|
// The UI ticker listener will not be called until the next frame.
|
|
// That's why, in case this is called from the UI thread, we need to
|
|
// proactivelly trigger the animation loop to avoid showing stale
|
|
// frames.
|
|
onRender();
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<EventEmitterListener>
|
|
NativeAnimatedNodesManager::ensureEventEmitterListener() noexcept {
|
|
if (!eventEmitterListener_) {
|
|
eventEmitterListener_ = std::make_shared<EventEmitterListener>(
|
|
[this](
|
|
Tag tag,
|
|
const std::string& eventName,
|
|
const EventPayload& payload) -> bool {
|
|
handleAnimatedEvent(tag, eventName, payload);
|
|
return false;
|
|
});
|
|
}
|
|
return eventEmitterListener_;
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) {
|
|
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
|
|
#ifdef RN_USE_ANIMATION_BACKEND
|
|
if (auto animationBackend = animationBackend_.lock()) {
|
|
std::static_pointer_cast<AnimationBackend>(animationBackend)
|
|
->start(
|
|
[this](float /*f*/) { return pullAnimationMutations(); },
|
|
isAsync);
|
|
}
|
|
#endif
|
|
|
|
return;
|
|
}
|
|
// This method can be called from either the UI thread or JavaScript thread.
|
|
// It ensures `startOnRenderCallback_` is called exactly once using atomic
|
|
// operations. We use std::atomic_bool rather than std::mutex to avoid
|
|
// potential deadlocks that could occur if we called external code while
|
|
// holding a mutex.
|
|
auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(true);
|
|
if (isRenderCallbackStarted) {
|
|
// onRender callback is already started.
|
|
return;
|
|
}
|
|
|
|
if (startOnRenderCallback_) {
|
|
startOnRenderCallback_([this]() { onRender(); }, isAsync);
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded(
|
|
bool isAsync) noexcept {
|
|
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
|
|
if (auto animationBackend = animationBackend_.lock()) {
|
|
animationBackend->stop(isAsync);
|
|
}
|
|
return;
|
|
}
|
|
// When multiple threads reach this point, only one thread should call
|
|
// stopOnRenderCallback_. This synchronization is primarily needed during
|
|
// destruction of NativeAnimatedNodesManager. In normal operation,
|
|
// stopRenderCallbackIfNeeded is always called from the UI thread.
|
|
auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(false);
|
|
|
|
if (isRenderCallbackStarted) {
|
|
if (stopOnRenderCallback_) {
|
|
stopOnRenderCallback_(isAsync);
|
|
}
|
|
|
|
if (frameRateListenerCallback_) {
|
|
frameRateListenerCallback_(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NativeAnimatedNodesManager::isAnimationUpdateNeeded() const noexcept {
|
|
return !activeAnimations_.empty() || !updatedNodeTags_.empty() ||
|
|
isEventAnimationInProgress_;
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::updateNodes(
|
|
const std::set<int>& finishedAnimationValueNodes) noexcept {
|
|
auto nodesQueue = std::deque<NodesQueueItem>{};
|
|
|
|
const auto is_node_connected_to_finished_animation =
|
|
[&finishedAnimationValueNodes](
|
|
AnimatedNode* node,
|
|
int nodeTag,
|
|
bool parentFinishedAnimation) -> bool {
|
|
return parentFinishedAnimation ||
|
|
(node->type() == AnimatedNodeType::Value &&
|
|
finishedAnimationValueNodes.contains(nodeTag));
|
|
};
|
|
|
|
#ifdef REACT_NATIVE_DEBUG
|
|
int activeNodesCount = 0;
|
|
int updatedNodesCount = 0;
|
|
#endif
|
|
|
|
// STEP 1.
|
|
// BFS over graph of nodes. Update `mIncomingNodes` attribute for each node
|
|
// during that BFS. Store number of visited nodes in `activeNodesCount`. We
|
|
// "execute" active animations as a part of this step.
|
|
|
|
animatedGraphBFSColor_++;
|
|
if (animatedGraphBFSColor_ == AnimatedNode::INITIAL_BFS_COLOR) {
|
|
animatedGraphBFSColor_++;
|
|
}
|
|
|
|
for (const auto& nodeTag : updatedNodeTags_) {
|
|
if (auto node = getAnimatedNode<AnimatedNode>(nodeTag)) {
|
|
if (node->bfsColor != animatedGraphBFSColor_) {
|
|
node->bfsColor = animatedGraphBFSColor_;
|
|
#ifdef REACT_NATIVE_DEBUG
|
|
activeNodesCount++;
|
|
#endif
|
|
const auto connectedToFinishedAnimation =
|
|
is_node_connected_to_finished_animation(node, nodeTag, false);
|
|
nodesQueue.emplace_back(
|
|
NodesQueueItem{
|
|
.node = node,
|
|
.connectedToFinishedAnimation = connectedToFinishedAnimation});
|
|
}
|
|
}
|
|
}
|
|
|
|
while (!nodesQueue.empty()) {
|
|
auto nextNode = nodesQueue.front();
|
|
nodesQueue.pop_front();
|
|
// in Animated, value nodes like RGBA are parents and Color node is child
|
|
// (the opposite of tree structure)
|
|
for (const auto childTag : nextNode.node->getChildren()) {
|
|
auto child = getAnimatedNode<AnimatedNode>(childTag);
|
|
child->activeIncomingNodes++;
|
|
if (child->bfsColor != animatedGraphBFSColor_) {
|
|
child->bfsColor = animatedGraphBFSColor_;
|
|
#ifdef REACT_NATIVE_DEBUG
|
|
activeNodesCount++;
|
|
#endif
|
|
const auto connectedToFinishedAnimation =
|
|
is_node_connected_to_finished_animation(
|
|
child, childTag, nextNode.connectedToFinishedAnimation);
|
|
nodesQueue.emplace_back(
|
|
NodesQueueItem{
|
|
.node = child,
|
|
.connectedToFinishedAnimation = connectedToFinishedAnimation});
|
|
}
|
|
}
|
|
}
|
|
|
|
// STEP 2
|
|
// BFS over the graph of active nodes in topological order -> visit node only
|
|
// when all its "predecessors" in the graph have already been visited. It is
|
|
// important to visit nodes in that order as they may often use values of
|
|
// their predecessors in order to calculate "next state" of their own. We
|
|
// start by determining the starting set of nodes by looking for nodes with
|
|
// `activeIncomingNodes = 0` (those can only be the ones that we start BFS in
|
|
// the previous step). We store number of visited nodes in this step in
|
|
// `updatedNodesCount`
|
|
|
|
animatedGraphBFSColor_++;
|
|
if (animatedGraphBFSColor_ == AnimatedNode::INITIAL_BFS_COLOR) {
|
|
animatedGraphBFSColor_++;
|
|
}
|
|
|
|
for (const auto& nodeTag : updatedNodeTags_) {
|
|
if (auto node = getAnimatedNode<AnimatedNode>(nodeTag)) {
|
|
if (node->activeIncomingNodes == 0 &&
|
|
node->bfsColor != animatedGraphBFSColor_) {
|
|
node->bfsColor = animatedGraphBFSColor_;
|
|
#ifdef REACT_NATIVE_DEBUG
|
|
updatedNodesCount++;
|
|
#endif
|
|
const auto connectedToFinishedAnimation =
|
|
is_node_connected_to_finished_animation(node, nodeTag, false);
|
|
nodesQueue.emplace_back(
|
|
NodesQueueItem{
|
|
.node = node,
|
|
.connectedToFinishedAnimation = connectedToFinishedAnimation});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run main "update" loop
|
|
#ifdef REACT_NATIVE_DEBUG
|
|
int cyclesDetected = 0;
|
|
#endif
|
|
while (!nodesQueue.empty()) {
|
|
auto nextNode = nodesQueue.front();
|
|
nodesQueue.pop_front();
|
|
if (nextNode.connectedToFinishedAnimation &&
|
|
nextNode.node->type() == AnimatedNodeType::Props) {
|
|
if (auto propsNode = dynamic_cast<PropsAnimatedNode*>(nextNode.node)) {
|
|
propsNode->update(/*forceFabricCommit*/ true);
|
|
};
|
|
} else {
|
|
nextNode.node->update();
|
|
}
|
|
|
|
for (auto childTag : nextNode.node->getChildren()) {
|
|
auto child = getAnimatedNode<AnimatedNode>(childTag);
|
|
child->activeIncomingNodes--;
|
|
if (child->activeIncomingNodes == 0 && child->activeIncomingNodes == 0) {
|
|
child->bfsColor = animatedGraphBFSColor_;
|
|
#ifdef REACT_NATIVE_DEBUG
|
|
updatedNodesCount++;
|
|
#endif
|
|
const auto connectedToFinishedAnimation =
|
|
is_node_connected_to_finished_animation(
|
|
child, childTag, nextNode.connectedToFinishedAnimation);
|
|
nodesQueue.emplace_back(
|
|
NodesQueueItem{
|
|
.node = child,
|
|
.connectedToFinishedAnimation = connectedToFinishedAnimation});
|
|
}
|
|
#ifdef REACT_NATIVE_DEBUG
|
|
else if (child->bfsColor == animatedGraphBFSColor_) {
|
|
cyclesDetected++;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#ifdef REACT_NATIVE_DEBUG
|
|
// Verify that we've visited *all* active nodes. Throw otherwise as this could
|
|
// mean there is a cycle in animated node graph, or that the graph is only
|
|
// partially set up. We also take advantage of the fact that all active nodes
|
|
// are visited in the step above so that all the nodes properties
|
|
// `activeIncomingNodes` are set to zero. In Fabric there can be race
|
|
// conditions between the JS thread setting up or tearing down animated nodes,
|
|
// and Fabric executing them on the UI thread, leading to temporary
|
|
// inconsistent states.
|
|
if (activeNodesCount != updatedNodesCount) {
|
|
if (warnedAboutGraphTraversal_) {
|
|
return;
|
|
}
|
|
warnedAboutGraphTraversal_ = true;
|
|
auto reason = cyclesDetected > 0
|
|
? ("cycles (" + std::to_string(cyclesDetected) + ")")
|
|
: "disconnected regions";
|
|
LOG(ERROR) << "Detected animation cycle or disconnected graph. "
|
|
<< "Looks like animated nodes graph has " << reason
|
|
<< ", there are " << activeNodesCount
|
|
<< " but toposort visited only " << updatedNodesCount;
|
|
} else {
|
|
warnedAboutGraphTraversal_ = false;
|
|
}
|
|
#endif
|
|
|
|
updatedNodeTags_.clear();
|
|
}
|
|
|
|
bool NativeAnimatedNodesManager::onAnimationFrame(double timestamp) {
|
|
// Run all active animations
|
|
auto hasFinishedAnimations = false;
|
|
std::set<int> finishedAnimationValueNodes;
|
|
for (const auto& [_id, driver] : activeAnimations_) {
|
|
driver->runAnimationStep(timestamp);
|
|
|
|
if (driver->getIsComplete()) {
|
|
hasFinishedAnimations = true;
|
|
const auto shouldRemoveJsSync =
|
|
ReactNativeFeatureFlags::cxxNativeAnimatedRemoveJsSync() &&
|
|
!ReactNativeFeatureFlags::disableFabricCommitInCXXAnimated();
|
|
if (shouldRemoveJsSync) {
|
|
finishedAnimationValueNodes.insert(driver->getAnimatedValueTag());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update all animated nodes
|
|
updateNodes(finishedAnimationValueNodes);
|
|
|
|
// remove finished animations
|
|
if (hasFinishedAnimations) {
|
|
std::vector<int> finishedAnimations;
|
|
for (const auto& [animationId, driver] : activeAnimations_) {
|
|
if (driver->getIsComplete()) {
|
|
if (getAnimatedNode<ValueAnimatedNode>(driver->getAnimatedValueTag()) !=
|
|
nullptr) {
|
|
driver->stopAnimation();
|
|
}
|
|
finishedAnimations.emplace_back(animationId);
|
|
}
|
|
}
|
|
for (const auto& id : finishedAnimations) {
|
|
activeAnimations_.erase(id);
|
|
}
|
|
}
|
|
|
|
return commitProps();
|
|
}
|
|
|
|
folly::dynamic NativeAnimatedNodesManager::managedProps(
|
|
Tag tag) const noexcept {
|
|
std::lock_guard<std::mutex> lock(connectedAnimatedNodesMutex_);
|
|
if (const auto iter = connectedAnimatedNodes_.find(tag);
|
|
iter != connectedAnimatedNodes_.end()) {
|
|
if (const auto node = getAnimatedNode<PropsAnimatedNode>(iter->second)) {
|
|
return node->props();
|
|
}
|
|
} else if (!ReactNativeFeatureFlags::
|
|
overrideBySynchronousMountPropsAtMountingAndroid()) {
|
|
std::lock_guard<std::mutex> lockUnsyncedDirectViewProps(
|
|
unsyncedDirectViewPropsMutex_);
|
|
if (auto it = unsyncedDirectViewProps_.find(tag);
|
|
it != unsyncedDirectViewProps_.end()) {
|
|
return it->second;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool NativeAnimatedNodesManager::hasManagedProps() const noexcept {
|
|
{
|
|
std::lock_guard<std::mutex> lock(connectedAnimatedNodesMutex_);
|
|
if (!connectedAnimatedNodes_.empty()) {
|
|
return true;
|
|
}
|
|
}
|
|
if (!ReactNativeFeatureFlags::
|
|
overrideBySynchronousMountPropsAtMountingAndroid()) {
|
|
std::lock_guard<std::mutex> lock(unsyncedDirectViewPropsMutex_);
|
|
if (!unsyncedDirectViewProps_.empty()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::onManagedPropsRemoved(Tag tag) noexcept {
|
|
if (!ReactNativeFeatureFlags::
|
|
overrideBySynchronousMountPropsAtMountingAndroid()) {
|
|
std::lock_guard<std::mutex> lock(unsyncedDirectViewPropsMutex_);
|
|
if (auto iter = unsyncedDirectViewProps_.find(tag);
|
|
iter != unsyncedDirectViewProps_.end()) {
|
|
unsyncedDirectViewProps_.erase(iter);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NativeAnimatedNodesManager::isOnRenderThread() const noexcept {
|
|
return isOnRenderThread_;
|
|
}
|
|
|
|
#pragma mark - Listeners
|
|
|
|
void NativeAnimatedNodesManager::startListeningToAnimatedNodeValue(
|
|
Tag tag,
|
|
ValueListenerCallback&& callback) noexcept {
|
|
if (auto iter = animatedNodes_.find(tag); iter != animatedNodes_.end() &&
|
|
iter->second->type() == AnimatedNodeType::Value) {
|
|
static_cast<ValueAnimatedNode*>(iter->second.get())
|
|
->setValueListener(std::move(callback));
|
|
} else {
|
|
LOG(ERROR) << "startListeningToAnimatedNodeValue: Animated node [" << tag
|
|
<< "] does not exist, or is not a 'value' node";
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::stopListeningToAnimatedNodeValue(
|
|
Tag tag) noexcept {
|
|
if (auto iter = animatedNodes_.find(tag); iter != animatedNodes_.end() &&
|
|
iter->second->type() == AnimatedNodeType::Value) {
|
|
static_cast<ValueAnimatedNode*>(iter->second.get())
|
|
->setValueListener(nullptr);
|
|
} else {
|
|
LOG(ERROR) << "stopListeningToAnimatedNodeValue: Animated node [" << tag
|
|
<< "] does not exist, or is not a 'value' node";
|
|
}
|
|
}
|
|
|
|
void NativeAnimatedNodesManager::schedulePropsCommit(
|
|
Tag viewTag,
|
|
const folly::dynamic& props,
|
|
bool layoutStyleUpdated,
|
|
bool forceFabricCommit) noexcept {
|
|
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
|
|
if (layoutStyleUpdated) {
|
|
mergeObjects(updateViewProps_[viewTag], props);
|
|
} else {
|
|
mergeObjects(updateViewPropsDirect_[viewTag], props);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// When fabricCommitCallback_ & directManipulationCallback_ are both
|
|
// available, we commit layout props via Fabric and the other using direct
|
|
// manipulation. If only fabricCommitCallback_ is available, we commit all
|
|
// props using that; if only directManipulationCallback_ is available, we
|
|
// commit all except for layout props.
|
|
if (fabricCommitCallback_ != nullptr &&
|
|
(layoutStyleUpdated || forceFabricCommit ||
|
|
directManipulationCallback_ == nullptr)) {
|
|
mergeObjects(updateViewProps_[viewTag], props);
|
|
|
|
// Must call direct manipulation to set final values on components.
|
|
mergeObjects(updateViewPropsDirect_[viewTag], props);
|
|
} else if (!layoutStyleUpdated && directManipulationCallback_ != nullptr) {
|
|
mergeObjects(updateViewPropsDirect_[viewTag], props);
|
|
if (!ReactNativeFeatureFlags::
|
|
overrideBySynchronousMountPropsAtMountingAndroid()) {
|
|
std::lock_guard<std::mutex> lock(unsyncedDirectViewPropsMutex_);
|
|
mergeObjects(unsyncedDirectViewProps_[viewTag], props);
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef RN_USE_ANIMATION_BACKEND
|
|
AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations() {
|
|
if (!ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
|
|
return {};
|
|
}
|
|
TraceSection s(
|
|
"NativeAnimatedNodesManager::pullAnimations",
|
|
"numActiveAnimations",
|
|
activeAnimations_.size());
|
|
|
|
isOnRenderThread_ = true;
|
|
|
|
// Run operations scheduled from AnimatedModule
|
|
std::vector<UiTask> operations;
|
|
{
|
|
std::lock_guard<std::mutex> lock(uiTasksMutex_);
|
|
std::swap(operations_, operations);
|
|
}
|
|
|
|
for (auto& task : operations) {
|
|
task();
|
|
}
|
|
|
|
AnimationMutations mutations;
|
|
|
|
// Step through the animation loop
|
|
if (isAnimationUpdateNeeded()) {
|
|
auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(
|
|
g_now().time_since_epoch())
|
|
.count();
|
|
|
|
auto timestamp = static_cast<double>(microseconds) / 1000.0;
|
|
bool containsChange = false;
|
|
AnimatedPropsBuilder propsBuilder;
|
|
{
|
|
// copied from onAnimationFrame
|
|
// Run all active animations
|
|
auto hasFinishedAnimations = false;
|
|
std::set<int> finishedAnimationValueNodes;
|
|
for (const auto& [_id, driver] : activeAnimations_) {
|
|
driver->runAnimationStep(timestamp);
|
|
|
|
if (driver->getIsComplete()) {
|
|
hasFinishedAnimations = true;
|
|
const auto shouldRemoveJsSync =
|
|
ReactNativeFeatureFlags::cxxNativeAnimatedRemoveJsSync() &&
|
|
!ReactNativeFeatureFlags::disableFabricCommitInCXXAnimated();
|
|
if (shouldRemoveJsSync) {
|
|
finishedAnimationValueNodes.insert(driver->getAnimatedValueTag());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update all animated nodes
|
|
updateNodes(finishedAnimationValueNodes);
|
|
|
|
// remove finished animations
|
|
if (hasFinishedAnimations) {
|
|
std::vector<int> finishedAnimations;
|
|
for (const auto& [animationId, driver] : activeAnimations_) {
|
|
if (driver->getIsComplete()) {
|
|
if (getAnimatedNode<ValueAnimatedNode>(
|
|
driver->getAnimatedValueTag()) != nullptr) {
|
|
driver->stopAnimation();
|
|
}
|
|
finishedAnimations.emplace_back(animationId);
|
|
}
|
|
}
|
|
for (const auto& id : finishedAnimations) {
|
|
activeAnimations_.erase(id);
|
|
}
|
|
}
|
|
|
|
for (auto& [tag, props] : updateViewPropsDirect_) {
|
|
// TODO: also handle layout props (updateViewProps_). It is skipped for
|
|
// now, because the backend requires shadowNodeFamilies to be able to
|
|
// commit to the ShadowTree
|
|
propsBuilder.storeDynamic(props);
|
|
mutations.push_back(
|
|
AnimationMutation{tag, nullptr, propsBuilder.get()});
|
|
containsChange = true;
|
|
}
|
|
}
|
|
|
|
if (!containsChange) {
|
|
// The last animation tick didn't result in any changes to the UI.
|
|
// It is safe to assume any event animation that was in progress has
|
|
// completed.
|
|
|
|
// Step 1: gather all animations driven by events.
|
|
std::set<int> finishedAnimationValueNodes;
|
|
for (auto& [key, drivers] : eventDrivers_) {
|
|
for (auto& driver : drivers) {
|
|
finishedAnimationValueNodes.insert(driver->getAnimatedNodeTag());
|
|
if (auto node = getAnimatedNode<ValueAnimatedNode>(
|
|
driver->getAnimatedNodeTag())) {
|
|
updatedNodeTags_.insert(node->tag());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 2: update all nodes that are connected to the finished animations.
|
|
updateNodes(finishedAnimationValueNodes);
|
|
|
|
isEventAnimationInProgress_ = false;
|
|
|
|
for (auto& [tag, props] : updateViewPropsDirect_) {
|
|
// TODO: handle layout props
|
|
propsBuilder.storeDynamic(props);
|
|
mutations.push_back(
|
|
AnimationMutation{tag, nullptr, propsBuilder.get()});
|
|
}
|
|
}
|
|
} else {
|
|
// There is no active animation. Stop the render callback.
|
|
stopRenderCallbackIfNeeded(false);
|
|
}
|
|
return mutations;
|
|
}
|
|
#endif
|
|
|
|
void NativeAnimatedNodesManager::onRender() {
|
|
if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) {
|
|
return;
|
|
}
|
|
TraceSection s(
|
|
"NativeAnimatedNodesManager::onRender",
|
|
"numActiveAnimations",
|
|
activeAnimations_.size());
|
|
|
|
if (frameRateListenerCallback_) {
|
|
frameRateListenerCallback_(true);
|
|
}
|
|
|
|
isOnRenderThread_ = true;
|
|
|
|
{
|
|
// Flush async created animated nodes
|
|
std::unordered_map<Tag, std::unique_ptr<AnimatedNode>>
|
|
animatedNodesCreatedAsync;
|
|
{
|
|
std::lock_guard<std::mutex> lock(animatedNodesCreatedAsyncMutex_);
|
|
std::swap(animatedNodesCreatedAsync, animatedNodesCreatedAsync_);
|
|
}
|
|
|
|
if (!animatedNodesCreatedAsync.empty()) {
|
|
std::lock_guard<std::mutex> lock(connectedAnimatedNodesMutex_);
|
|
for (auto& [tag, node] : animatedNodesCreatedAsync) {
|
|
animatedNodes_.insert({tag, std::move(node)});
|
|
updatedNodeTags_.insert(tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run operations scheduled from AnimatedModule
|
|
std::vector<UiTask> operations;
|
|
{
|
|
std::lock_guard<std::mutex> lock(uiTasksMutex_);
|
|
std::swap(operations_, operations);
|
|
}
|
|
|
|
for (auto& task : operations) {
|
|
task();
|
|
}
|
|
|
|
// Step through the animation loop
|
|
if (isAnimationUpdateNeeded()) {
|
|
auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(
|
|
g_now().time_since_epoch())
|
|
.count();
|
|
|
|
auto timestamp = static_cast<double>(microseconds) / 1000.0;
|
|
auto containsChange = onAnimationFrame(timestamp);
|
|
|
|
if (!containsChange) {
|
|
// The last animation tick didn't result in any changes to the UI.
|
|
// It is safe to assume any event animation that was in progress has
|
|
// completed.
|
|
|
|
// Step 1: gather all animations driven by events.
|
|
std::set<int> finishedAnimationValueNodes;
|
|
for (auto& [key, drivers] : eventDrivers_) {
|
|
for (auto& driver : drivers) {
|
|
finishedAnimationValueNodes.insert(driver->getAnimatedNodeTag());
|
|
if (auto node = getAnimatedNode<ValueAnimatedNode>(
|
|
driver->getAnimatedNodeTag())) {
|
|
updatedNodeTags_.insert(node->tag());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 2: update all nodes that are connected to the finished animations.
|
|
updateNodes(finishedAnimationValueNodes);
|
|
|
|
isEventAnimationInProgress_ = false;
|
|
|
|
// Step 3: commit the changes to the UI.
|
|
commitProps();
|
|
}
|
|
} else {
|
|
// There is no active animation. Stop the render callback.
|
|
stopRenderCallbackIfNeeded(false);
|
|
}
|
|
}
|
|
|
|
bool NativeAnimatedNodesManager::commitProps() {
|
|
bool containsChange =
|
|
!updateViewProps_.empty() || !updateViewPropsDirect_.empty();
|
|
|
|
if (fabricCommitCallback_ != nullptr) {
|
|
if (!updateViewProps_.empty()) {
|
|
fabricCommitCallback_(updateViewProps_);
|
|
}
|
|
}
|
|
|
|
updateViewProps_.clear();
|
|
|
|
if (directManipulationCallback_ != nullptr) {
|
|
for (const auto& [viewTag, props] : updateViewPropsDirect_) {
|
|
directManipulationCallback_(viewTag, folly::dynamic(props));
|
|
}
|
|
}
|
|
|
|
updateViewPropsDirect_.clear();
|
|
|
|
return containsChange;
|
|
}
|
|
|
|
} // namespace facebook::react
|