feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,25 @@
package com.swmansion.reanimated;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.common.UIManagerType;
import com.facebook.react.fabric.FabricUIManager;
class ReaCompatibility {
private FabricUIManager fabricUIManager;
public ReaCompatibility(ReactApplicationContext reactApplicationContext) {
fabricUIManager = (FabricUIManager) UIManagerHelper.getUIManager(reactApplicationContext, UIManagerType.FABRIC);
}
public void registerFabricEventListener(NodesManager nodesManager) {
if (fabricUIManager != null) {
fabricUIManager.getEventDispatcher().addListener(nodesManager);
}
}
public void synchronouslyUpdateUIProps(int viewTag, ReadableMap uiProps) {
fabricUIManager.synchronouslyUpdateViewOnUIThread(viewTag, uiProps);
}
}

View File

@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.swmansion.reanimated">
</manifest>

View File

@@ -0,0 +1,34 @@
#include <android/log.h>
#include <memory>
#include "AndroidLogger.h"
#include "Logger.h"
#define APPNAME "NATIVE_REANIMATED"
namespace reanimated {
std::unique_ptr<LoggerInterface> Logger::instance =
std::make_unique<AndroidLogger>();
void AndroidLogger::log(const char *str) {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "%s", str);
}
void AndroidLogger::log(const std::string &str) {
log(str.c_str());
}
void AndroidLogger::log(double d) {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "%f", d);
}
void AndroidLogger::log(int i) {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "%d", i);
}
void AndroidLogger::log(bool b) {
log(b ? "true" : "false");
}
} // namespace reanimated

View File

@@ -0,0 +1,18 @@
#pragma once
#include <string>
#include "LoggerInterface.h"
namespace reanimated {
class AndroidLogger : public LoggerInterface {
public:
void log(const char *str) override;
void log(const std::string &str) override;
void log(double d) override;
void log(int i) override;
void log(bool b) override;
};
} // namespace reanimated

View File

@@ -0,0 +1,63 @@
#include "AndroidUIScheduler.h"
#include <android/log.h>
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <memory>
#include <string>
namespace reanimated {
using namespace facebook;
using namespace react;
class UISchedulerWrapper : public UIScheduler {
private:
jni::global_ref<AndroidUIScheduler::javaobject> androidUiScheduler_;
public:
explicit UISchedulerWrapper(
jni::global_ref<AndroidUIScheduler::javaobject> androidUiScheduler)
: androidUiScheduler_(androidUiScheduler) {}
void scheduleOnUI(std::function<void()> job) override {
UIScheduler::scheduleOnUI(job);
if (!scheduledOnUI_) {
scheduledOnUI_ = true;
androidUiScheduler_->cthis()->scheduleTriggerOnUI();
}
}
~UISchedulerWrapper() {}
};
AndroidUIScheduler::AndroidUIScheduler(
jni::alias_ref<AndroidUIScheduler::javaobject> jThis)
: javaPart_(jni::make_global(jThis)),
uiScheduler_(
std::make_shared<UISchedulerWrapper>(jni::make_global(jThis))) {}
jni::local_ref<AndroidUIScheduler::jhybriddata> AndroidUIScheduler::initHybrid(
jni::alias_ref<jhybridobject> jThis) {
return makeCxxInstance(jThis);
}
void AndroidUIScheduler::triggerUI() {
uiScheduler_->triggerUI();
}
void AndroidUIScheduler::scheduleTriggerOnUI() {
static const auto method =
javaPart_->getClass()->getMethod<void()>("scheduleTriggerOnUI");
method(javaPart_.get());
}
void AndroidUIScheduler::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", AndroidUIScheduler::initHybrid),
makeNativeMethod("triggerUI", AndroidUIScheduler::triggerUI),
});
}
} // namespace reanimated

View File

@@ -0,0 +1,43 @@
#pragma once
#include <fbjni/fbjni.h>
#include <jni.h>
#include <jsi/jsi.h>
#include <react/jni/CxxModuleWrapper.h>
#include <react/jni/JMessageQueueThread.h>
#include <memory>
#include "UIScheduler.h"
namespace reanimated {
using namespace facebook;
class AndroidUIScheduler : public jni::HybridClass<AndroidUIScheduler> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/swmansion/reanimated/AndroidUIScheduler;";
static jni::local_ref<jhybriddata> initHybrid(
jni::alias_ref<jhybridobject> jThis);
static void registerNatives();
std::shared_ptr<UIScheduler> getUIScheduler() {
return uiScheduler_;
}
void scheduleTriggerOnUI();
private:
friend HybridBase;
void triggerUI();
jni::global_ref<AndroidUIScheduler::javaobject> javaPart_;
std::shared_ptr<UIScheduler> uiScheduler_;
explicit AndroidUIScheduler(
jni::alias_ref<AndroidUIScheduler::jhybridobject> jThis);
};
} // namespace reanimated

View File

@@ -0,0 +1,59 @@
#include "JNIHelper.h"
namespace reanimated {
using namespace facebook::jni;
using namespace facebook;
using namespace react;
local_ref<JNIHelper::PropsMap> JNIHelper::PropsMap::create() {
return newInstance();
}
void JNIHelper::PropsMap::put(
const std::string &key,
jni::local_ref<JObject> object) {
static auto method =
getClass()
->getMethod<jobject(
jni::local_ref<JObject>, jni::local_ref<JObject>)>("put");
method(self(), jni::make_jstring(key), object);
}
jni::local_ref<JNIHelper::PropsMap> JNIHelper::ConvertToPropsMap(
jsi::Runtime &rt,
const jsi::Object &props) {
auto map = PropsMap::create();
auto propNames = props.getPropertyNames(rt);
for (size_t i = 0, size = propNames.size(rt); i < size; i++) {
auto jsiKey = propNames.getValueAtIndex(rt, i).asString(rt);
auto value = props.getProperty(rt, jsiKey);
auto key = jsiKey.utf8(rt);
if (value.isUndefined() || value.isNull()) {
map->put(key, nullptr);
} else if (value.isBool()) {
map->put(key, JBoolean::valueOf(value.getBool()));
} else if (value.isNumber()) {
map->put(key, jni::autobox(value.asNumber()));
} else if (value.isString()) {
map->put(key, jni::make_jstring(value.asString(rt).utf8(rt)));
} else if (value.isObject()) {
if (value.asObject(rt).isArray(rt)) {
map->put(
key,
ReadableNativeArray::newObjectCxxArgs(
jsi::dynamicFromValue(rt, value)));
} else {
map->put(
key,
ReadableNativeMap::newObjectCxxArgs(
jsi::dynamicFromValue(rt, value)));
}
}
}
return map;
}
}; // namespace reanimated

View File

@@ -0,0 +1,30 @@
#pragma once
#include <fbjni/fbjni.h>
#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <react/jni/CxxModuleWrapper.h>
#include <react/jni/JMessageQueueThread.h>
#include <react/jni/WritableNativeMap.h>
#include <string>
namespace reanimated {
using namespace facebook::jni;
using namespace facebook;
using namespace react;
struct JNIHelper {
struct PropsMap : jni::JavaClass<PropsMap, JMap<JString, JObject>> {
static constexpr auto kJavaDescriptor = "Ljava/util/HashMap;";
static local_ref<PropsMap> create();
void put(const std::string &key, jni::local_ref<JObject> object);
};
static jni::local_ref<PropsMap> ConvertToPropsMap(
jsi::Runtime &rt,
const jsi::Object &props);
};
}; // namespace reanimated

View File

@@ -0,0 +1,133 @@
#include "LayoutAnimations.h"
#include "FeaturesConfig.h"
#include "Logger.h"
namespace reanimated {
LayoutAnimations::LayoutAnimations(
jni::alias_ref<LayoutAnimations::javaobject> jThis)
: javaPart_(jni::make_global(jThis)) {}
jni::local_ref<LayoutAnimations::jhybriddata> LayoutAnimations::initHybrid(
jni::alias_ref<jhybridobject> jThis) {
return makeCxxInstance(jThis);
}
void LayoutAnimations::setAnimationStartingBlock(
AnimationStartingBlock animationStartingBlock) {
this->animationStartingBlock_ = animationStartingBlock;
}
void LayoutAnimations::startAnimationForTag(
int tag,
int type,
alias_ref<JMap<jstring, jstring>> values) {
this->animationStartingBlock_(tag, type, values);
}
void LayoutAnimations::progressLayoutAnimation(
int tag,
const jni::local_ref<JNIHelper::PropsMap> &updates,
bool isSharedTransition) {
static const auto method =
javaPart_->getClass()
->getMethod<void(int, JMap<JString, JObject>::javaobject, bool)>(
"progressLayoutAnimation");
method(javaPart_.get(), tag, updates.get(), isSharedTransition);
}
void LayoutAnimations::endLayoutAnimation(int tag, bool removeView) {
static const auto method =
javaPart_->getClass()->getMethod<void(int, bool)>("endLayoutAnimation");
method(javaPart_.get(), tag, removeView);
}
void LayoutAnimations::setHasAnimationBlock(
HasAnimationBlock hasAnimationBlock) {
this->hasAnimationBlock_ = hasAnimationBlock;
}
void LayoutAnimations::setShouldAnimateExitingBlock(
ShouldAnimateExitingBlock shouldAnimateExitingBlock) {
this->shouldAnimateExitingBlock_ = shouldAnimateExitingBlock;
}
#ifndef NDEBUG
void LayoutAnimations::setCheckDuplicateSharedTag(
CheckDuplicateSharedTag checkDuplicateSharedTag) {
checkDuplicateSharedTag_ = checkDuplicateSharedTag;
}
void LayoutAnimations::checkDuplicateSharedTag(int viewTag, int screenTag) {
checkDuplicateSharedTag_(viewTag, screenTag);
}
#endif
bool LayoutAnimations::hasAnimationForTag(int tag, int type) {
return hasAnimationBlock_(tag, type);
}
bool LayoutAnimations::shouldAnimateExiting(int tag, bool shouldAnimate) {
return shouldAnimateExitingBlock_(tag, shouldAnimate);
}
void LayoutAnimations::setClearAnimationConfigBlock(
ClearAnimationConfigBlock clearAnimationConfigBlock) {
this->clearAnimationConfigBlock_ = clearAnimationConfigBlock;
}
void LayoutAnimations::clearAnimationConfigForTag(int tag) {
clearAnimationConfigBlock_(tag);
}
void LayoutAnimations::setCancelAnimationForTag(
CancelAnimationBlock cancelAnimationBlock) {
this->cancelAnimationBlock_ = cancelAnimationBlock;
}
void LayoutAnimations::cancelAnimationForTag(int tag) {
this->cancelAnimationBlock_(tag);
}
bool LayoutAnimations::isLayoutAnimationEnabled() {
return FeaturesConfig::isLayoutAnimationEnabled();
}
void LayoutAnimations::setFindPrecedingViewTagForTransition(
FindPrecedingViewTagForTransitionBlock
findPrecedingViewTagForTransitionBlock) {
findPrecedingViewTagForTransitionBlock_ =
findPrecedingViewTagForTransitionBlock;
}
int LayoutAnimations::findPrecedingViewTagForTransition(int tag) {
return findPrecedingViewTagForTransitionBlock_(tag);
}
void LayoutAnimations::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", LayoutAnimations::initHybrid),
makeNativeMethod(
"startAnimationForTag", LayoutAnimations::startAnimationForTag),
makeNativeMethod(
"hasAnimationForTag", LayoutAnimations::hasAnimationForTag),
makeNativeMethod(
"shouldAnimateExiting", LayoutAnimations::shouldAnimateExiting),
makeNativeMethod(
"clearAnimationConfigForTag",
LayoutAnimations::clearAnimationConfigForTag),
makeNativeMethod(
"cancelAnimationForTag", LayoutAnimations::cancelAnimationForTag),
makeNativeMethod(
"isLayoutAnimationEnabled",
LayoutAnimations::isLayoutAnimationEnabled),
makeNativeMethod(
"findPrecedingViewTagForTransition",
LayoutAnimations::findPrecedingViewTagForTransition),
#ifndef NDEBUG
makeNativeMethod(
"checkDuplicateSharedTag", LayoutAnimations::checkDuplicateSharedTag),
#endif
});
}
}; // namespace reanimated

View File

@@ -0,0 +1,84 @@
#pragma once
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <memory>
#include <string>
#include "JNIHelper.h"
namespace reanimated {
using namespace facebook::jni;
using namespace facebook;
class LayoutAnimations : public jni::HybridClass<LayoutAnimations> {
using AnimationStartingBlock =
std::function<void(int, int, alias_ref<JMap<jstring, jstring>>)>;
using HasAnimationBlock = std::function<bool(int, int)>;
using ShouldAnimateExitingBlock = std::function<bool(int, bool)>;
#ifndef NDEBUG
using CheckDuplicateSharedTag = std::function<void(int, int)>;
#endif
using ClearAnimationConfigBlock = std::function<void(int)>;
using CancelAnimationBlock = std::function<void(int)>;
using FindPrecedingViewTagForTransitionBlock = std::function<int(int)>;
public:
static auto constexpr kJavaDescriptor =
"Lcom/swmansion/reanimated/layoutReanimation/LayoutAnimations;";
static jni::local_ref<jhybriddata> initHybrid(
jni::alias_ref<jhybridobject> jThis);
static void registerNatives();
void startAnimationForTag(
int tag,
int type,
alias_ref<JMap<jstring, jstring>> values);
bool hasAnimationForTag(int tag, int type);
bool shouldAnimateExiting(int tag, bool shouldAnimate);
bool isLayoutAnimationEnabled();
void setAnimationStartingBlock(AnimationStartingBlock animationStartingBlock);
void setHasAnimationBlock(HasAnimationBlock hasAnimationBlock);
void setShouldAnimateExitingBlock(
ShouldAnimateExitingBlock shouldAnimateExitingBlock);
#ifndef NDEBUG
void setCheckDuplicateSharedTag(
CheckDuplicateSharedTag checkDuplicateSharedTag);
void checkDuplicateSharedTag(int viewTag, int screenTag);
#endif
void setClearAnimationConfigBlock(
ClearAnimationConfigBlock clearAnimationConfigBlock);
void setCancelAnimationForTag(CancelAnimationBlock cancelAnimationBlock);
void setFindPrecedingViewTagForTransition(
FindPrecedingViewTagForTransitionBlock
findPrecedingViewTagForTransitionBlock);
void progressLayoutAnimation(
int tag,
const jni::local_ref<JNIHelper::PropsMap> &updates,
bool isSharedTransition);
void endLayoutAnimation(int tag, bool removeView);
void clearAnimationConfigForTag(int tag);
void cancelAnimationForTag(int tag);
int findPrecedingViewTagForTransition(int tag);
private:
friend HybridBase;
jni::global_ref<LayoutAnimations::javaobject> javaPart_;
AnimationStartingBlock animationStartingBlock_;
HasAnimationBlock hasAnimationBlock_;
ShouldAnimateExitingBlock shouldAnimateExitingBlock_;
ClearAnimationConfigBlock clearAnimationConfigBlock_;
CancelAnimationBlock cancelAnimationBlock_;
FindPrecedingViewTagForTransitionBlock
findPrecedingViewTagForTransitionBlock_;
#ifndef NDEBUG
CheckDuplicateSharedTag checkDuplicateSharedTag_;
#endif
explicit LayoutAnimations(
jni::alias_ref<LayoutAnimations::jhybridobject> jThis);
};
}; // namespace reanimated

View File

@@ -0,0 +1,657 @@
#include <android/log.h>
#include <fbjni/fbjni.h>
#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <react/jni/JMessageQueueThread.h>
#include <react/jni/ReadableNativeArray.h>
#include <react/jni/ReadableNativeMap.h>
#ifdef RCT_NEW_ARCH_ENABLED
#include <react/fabric/Binding.h>
#endif
#include <memory>
#include <string>
#include "AndroidUIScheduler.h"
#include "LayoutAnimationsManager.h"
#include "NativeProxy.h"
#include "PlatformDepMethodsHolder.h"
#include "RNRuntimeDecorator.h"
#include "ReanimatedJSIUtils.h"
#include "ReanimatedRuntime.h"
#include "ReanimatedVersion.h"
#include "WorkletRuntime.h"
#include "WorkletRuntimeCollector.h"
namespace reanimated {
using namespace facebook;
using namespace react;
NativeProxy::NativeProxy(
jni::alias_ref<NativeProxy::javaobject> jThis,
jsi::Runtime *rnRuntime,
const std::shared_ptr<facebook::react::CallInvoker> &jsCallInvoker,
const std::shared_ptr<UIScheduler> &uiScheduler,
jni::global_ref<LayoutAnimations::javaobject> layoutAnimations,
jni::alias_ref<JavaMessageQueueThread::javaobject> messageQueueThread,
#ifdef RCT_NEW_ARCH_ENABLED
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
fabricUIManager,
#endif
const std::string &valueUnpackerCode)
: javaPart_(jni::make_global(jThis)),
rnRuntime_(rnRuntime),
nativeReanimatedModule_(std::make_shared<NativeReanimatedModule>(
*rnRuntime,
std::make_shared<JSScheduler>(*rnRuntime, jsCallInvoker),
std::make_shared<JMessageQueueThread>(messageQueueThread),
uiScheduler,
getPlatformDependentMethods(),
valueUnpackerCode,
/* isBridgeless */ false)),
layoutAnimations_(std::move(layoutAnimations)) {
#ifdef RCT_NEW_ARCH_ENABLED
commonInit(fabricUIManager);
#endif // RCT_NEW_ARCH_ENABLED
}
#if REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED)
NativeProxy::NativeProxy(
jni::alias_ref<NativeProxy::javaobject> jThis,
jsi::Runtime *rnRuntime,
RuntimeExecutor runtimeExecutor,
const std::shared_ptr<UIScheduler> &uiScheduler,
jni::global_ref<LayoutAnimations::javaobject> layoutAnimations,
jni::alias_ref<JavaMessageQueueThread::javaobject> messageQueueThread,
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
fabricUIManager,
const std::string &valueUnpackerCode)
: javaPart_(jni::make_global(jThis)),
rnRuntime_(rnRuntime),
nativeReanimatedModule_(std::make_shared<NativeReanimatedModule>(
*rnRuntime,
std::make_shared<JSScheduler>(*rnRuntime, runtimeExecutor),
std::make_shared<JMessageQueueThread>(messageQueueThread),
uiScheduler,
getPlatformDependentMethods(),
valueUnpackerCode,
/* isBridgeless */ true)),
layoutAnimations_(std::move(layoutAnimations)) {
commonInit(fabricUIManager);
}
#endif // REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED
#ifdef RCT_NEW_ARCH_ENABLED
void NativeProxy::commonInit(
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
&fabricUIManager) {
const auto &uiManager =
fabricUIManager->getBinding()->getScheduler()->getUIManager();
nativeReanimatedModule_->initializeFabric(uiManager);
// removed temporarily, event listener mechanism needs to be fixed on RN side
// eventListener_ = std::make_shared<EventListener>(
// [nativeReanimatedModule,
// getAnimationTimestamp](const RawEvent &rawEvent) {
// return nativeReanimatedModule->handleRawEvent(
// rawEvent, getAnimationTimestamp());
// });
// reactScheduler_ = binding->getScheduler();
// reactScheduler_->addEventListener(eventListener_);
}
#endif // RCT_NEW_ARCH_ENABLED
NativeProxy::~NativeProxy() {
// removed temporary, new event listener mechanism need fix on the RN side
// reactScheduler_->removeEventListener(eventListener_);
// cleanup all animated sensors here, since NativeProxy
// has already been destroyed when AnimatedSensorModule's
// destructor is ran
nativeReanimatedModule_->cleanupSensors();
}
jni::local_ref<NativeProxy::jhybriddata> NativeProxy::initHybrid(
jni::alias_ref<jhybridobject> jThis,
jlong jsContext,
jni::alias_ref<facebook::react::CallInvokerHolder::javaobject>
jsCallInvokerHolder,
jni::alias_ref<AndroidUIScheduler::javaobject> androidUiScheduler,
jni::alias_ref<LayoutAnimations::javaobject> layoutAnimations,
jni::alias_ref<JavaMessageQueueThread::javaobject> messageQueueThread,
#ifdef RCT_NEW_ARCH_ENABLED
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
fabricUIManager,
#endif
const std::string &valueUnpackerCode) {
auto jsCallInvoker = jsCallInvokerHolder->cthis()->getCallInvoker();
auto uiScheduler = androidUiScheduler->cthis()->getUIScheduler();
return makeCxxInstance(
jThis,
(jsi::Runtime *)jsContext,
jsCallInvoker,
uiScheduler,
make_global(layoutAnimations),
messageQueueThread,
#ifdef RCT_NEW_ARCH_ENABLED
fabricUIManager,
#endif
valueUnpackerCode);
}
#if REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED)
jni::local_ref<NativeProxy::jhybriddata> NativeProxy::initHybridBridgeless(
jni::alias_ref<jhybridobject> jThis,
jlong jsContext,
jni::alias_ref<react::JRuntimeExecutor::javaobject> runtimeExecutorHolder,
jni::alias_ref<AndroidUIScheduler::javaobject> androidUiScheduler,
jni::alias_ref<LayoutAnimations::javaobject> layoutAnimations,
jni::alias_ref<JavaMessageQueueThread::javaobject> messageQueueThread,
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
fabricUIManager,
const std::string &valueUnpackerCode) {
auto uiScheduler = androidUiScheduler->cthis()->getUIScheduler();
auto runtimeExecutor = runtimeExecutorHolder->cthis()->get();
return makeCxxInstance(
jThis,
(jsi::Runtime *)jsContext,
runtimeExecutor,
uiScheduler,
make_global(layoutAnimations),
messageQueueThread,
fabricUIManager,
valueUnpackerCode);
}
#endif // REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED
#ifndef NDEBUG
void NativeProxy::checkJavaVersion(jsi::Runtime &rnRuntime) {
std::string javaVersion;
try {
javaVersion =
getJniMethod<jstring()>("getReanimatedJavaVersion")(javaPart_.get())
->toStdString();
} catch (std::exception &) {
throw std::runtime_error(
std::string(
"[Reanimated] C++ side failed to resolve Java code version.\n") +
"See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#c-side-failed-to-resolve-java-code-version` for more details.");
}
auto cppVersion = getReanimatedCppVersion();
if (cppVersion != javaVersion) {
throw std::runtime_error(
std::string(
"[Reanimated] Mismatch between C++ code version and Java code version (") +
cppVersion + " vs. " + javaVersion + " respectively).\n" +
"See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-c-code-version-and-java-code-version` for more details.");
}
}
void NativeProxy::injectCppVersion() {
auto cppVersion = getReanimatedCppVersion();
try {
static const auto method =
getJniMethod<void(jni::local_ref<JString>)>("setCppVersion");
method(javaPart_.get(), make_jstring(cppVersion));
} catch (std::exception &) {
throw std::runtime_error(
std::string(
"[Reanimated] C++ side failed to resolve Java code version (injection).\n") +
"See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#c-side-failed-to-resolve-java-code-version` for more details.");
}
}
#endif // NDEBUG
void NativeProxy::installJSIBindings() {
jsi::Runtime &rnRuntime = *rnRuntime_;
WorkletRuntimeCollector::install(rnRuntime);
auto isReducedMotion = getIsReducedMotion();
RNRuntimeDecorator::decorate(
rnRuntime, nativeReanimatedModule_, isReducedMotion);
#ifndef NDEBUG
checkJavaVersion(rnRuntime);
injectCppVersion();
#endif // NDEBUG
registerEventHandler();
setupLayoutAnimations();
}
bool NativeProxy::isAnyHandlerWaitingForEvent(
const std::string &eventName,
const int emitterReactTag) {
return nativeReanimatedModule_->isAnyHandlerWaitingForEvent(
eventName, emitterReactTag);
}
void NativeProxy::performOperations() {
#ifdef RCT_NEW_ARCH_ENABLED
nativeReanimatedModule_->performOperations();
#endif
}
bool NativeProxy::getIsReducedMotion() {
static const auto method = getJniMethod<jboolean()>("getIsReducedMotion");
return method(javaPart_.get());
}
void NativeProxy::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", NativeProxy::initHybrid),
#if REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED)
makeNativeMethod(
"initHybridBridgeless", NativeProxy::initHybridBridgeless),
#endif // REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED)
makeNativeMethod("installJSIBindings", NativeProxy::installJSIBindings),
makeNativeMethod(
"isAnyHandlerWaitingForEvent",
NativeProxy::isAnyHandlerWaitingForEvent),
makeNativeMethod("performOperations", NativeProxy::performOperations)
});
}
void NativeProxy::requestRender(
std::function<void(double)> onRender,
jsi::Runtime &) {
static const auto method =
getJniMethod<void(AnimationFrameCallback::javaobject)>("requestRender");
method(
javaPart_.get(),
AnimationFrameCallback::newObjectCxxArgs(std::move(onRender)).get());
}
void NativeProxy::registerEventHandler() {
auto eventHandler = bindThis(&NativeProxy::handleEvent);
static const auto method =
getJniMethod<void(EventHandler::javaobject)>("registerEventHandler");
method(
javaPart_.get(),
EventHandler::newObjectCxxArgs(std::move(eventHandler)).get());
}
void NativeProxy::maybeFlushUIUpdatesQueue() {
static const auto method = getJniMethod<void()>("maybeFlushUIUpdatesQueue");
method(javaPart_.get());
}
#ifdef RCT_NEW_ARCH_ENABLED
// nothing
#else
jsi::Value NativeProxy::obtainProp(
jsi::Runtime &rt,
const int viewTag,
const jsi::Value &propName) {
static const auto method =
getJniMethod<jni::local_ref<JString>(int, jni::local_ref<JString>)>(
"obtainProp");
local_ref<JString> propNameJStr =
jni::make_jstring(propName.asString(rt).utf8(rt).c_str());
auto result = method(javaPart_.get(), viewTag, propNameJStr);
std::string str = result->toStdString();
return jsi::Value(rt, jsi::String::createFromAscii(rt, str));
}
void NativeProxy::configureProps(
jsi::Runtime &rt,
const jsi::Value &uiProps,
const jsi::Value &nativeProps) {
static const auto method = getJniMethod<void(
ReadableNativeArray::javaobject, ReadableNativeArray::javaobject)>(
"configureProps");
method(
javaPart_.get(),
ReadableNativeArray::newObjectCxxArgs(jsi::dynamicFromValue(rt, uiProps))
.get(),
ReadableNativeArray::newObjectCxxArgs(
jsi::dynamicFromValue(rt, nativeProps))
.get());
}
void NativeProxy::updateProps(jsi::Runtime &rt, const jsi::Value &operations) {
static const auto method =
getJniMethod<void(int, JMap<JString, JObject>::javaobject)>(
"updateProps");
auto array = operations.asObject(rt).asArray(rt);
size_t length = array.size(rt);
for (size_t i = 0; i < length; ++i) {
auto item = array.getValueAtIndex(rt, i).asObject(rt);
int viewTag = item.getProperty(rt, "tag").asNumber();
const jsi::Object &props = item.getProperty(rt, "updates").asObject(rt);
method(
javaPart_.get(),
viewTag,
JNIHelper::ConvertToPropsMap(rt, props).get());
}
}
void NativeProxy::scrollTo(int viewTag, double x, double y, bool animated) {
static const auto method =
getJniMethod<void(int, double, double, bool)>("scrollTo");
method(javaPart_.get(), viewTag, x, y, animated);
}
inline jni::local_ref<ReadableArray::javaobject> castReadableArray(
jni::local_ref<ReadableNativeArray::javaobject> const &nativeArray) {
return make_local(
reinterpret_cast<ReadableArray::javaobject>(nativeArray.get()));
}
void NativeProxy::dispatchCommand(
jsi::Runtime &rt,
const int viewTag,
const jsi::Value &commandNameValue,
const jsi::Value &argsValue) {
static const auto method = getJniMethod<void(
int, jni::local_ref<JString>, jni::local_ref<ReadableArray::javaobject>)>(
"dispatchCommand");
local_ref<JString> commandId =
jni::make_jstring(commandNameValue.asString(rt).utf8(rt).c_str());
jni::local_ref<ReadableArray::javaobject> commandArgs =
castReadableArray(ReadableNativeArray::newObjectCxxArgs(
jsi::dynamicFromValue(rt, argsValue)));
method(javaPart_.get(), viewTag, commandId, commandArgs);
}
std::vector<std::pair<std::string, double>> NativeProxy::measure(int viewTag) {
static const auto method =
getJniMethod<local_ref<JArrayFloat>(int)>("measure");
local_ref<JArrayFloat> output = method(javaPart_.get(), viewTag);
size_t size = output->size();
auto elements = output->getRegion(0, size);
return {
{"x", elements[0]},
{"y", elements[1]},
{"pageX", elements[2]},
{"pageY", elements[3]},
{"width", elements[4]},
{"height", elements[5]},
};
}
#endif // RCT_NEW_ARCH_ENABLED
#ifdef RCT_NEW_ARCH_ENABLED
inline jni::local_ref<ReadableMap::javaobject> castReadableMap(
jni::local_ref<ReadableNativeMap::javaobject> const &nativeMap) {
return make_local(reinterpret_cast<ReadableMap::javaobject>(nativeMap.get()));
}
void NativeProxy::synchronouslyUpdateUIProps(
jsi::Runtime &rt,
Tag tag,
const jsi::Object &props) {
static const auto method =
getJniMethod<void(int, jni::local_ref<ReadableMap::javaobject>)>(
"synchronouslyUpdateUIProps");
jni::local_ref<ReadableMap::javaobject> uiProps =
castReadableMap(ReadableNativeMap::newObjectCxxArgs(
jsi::dynamicFromValue(rt, jsi::Value(rt, props))));
method(javaPart_.get(), tag, uiProps);
}
#endif
int NativeProxy::registerSensor(
int sensorType,
int interval,
int,
std::function<void(double[], int)> setter) {
static const auto method =
getJniMethod<int(int, int, SensorSetter::javaobject)>("registerSensor");
return method(
javaPart_.get(),
sensorType,
interval,
SensorSetter::newObjectCxxArgs(std::move(setter)).get());
}
void NativeProxy::unregisterSensor(int sensorId) {
static const auto method = getJniMethod<void(int)>("unregisterSensor");
method(javaPart_.get(), sensorId);
}
void NativeProxy::setGestureState(int handlerTag, int newState) {
static const auto method = getJniMethod<void(int, int)>("setGestureState");
method(javaPart_.get(), handlerTag, newState);
}
int NativeProxy::subscribeForKeyboardEvents(
std::function<void(int, int)> callback,
bool isStatusBarTranslucent) {
static const auto method =
getJniMethod<int(KeyboardWorkletWrapper::javaobject, bool)>(
"subscribeForKeyboardEvents");
return method(
javaPart_.get(),
KeyboardWorkletWrapper::newObjectCxxArgs(std::move(callback)).get(),
isStatusBarTranslucent);
}
void NativeProxy::unsubscribeFromKeyboardEvents(int listenerId) {
static const auto method =
getJniMethod<void(int)>("unsubscribeFromKeyboardEvents");
method(javaPart_.get(), listenerId);
}
double NativeProxy::getAnimationTimestamp() {
static const auto method = getJniMethod<jlong()>("getAnimationTimestamp");
jlong output = method(javaPart_.get());
return static_cast<double>(output);
}
void NativeProxy::handleEvent(
jni::alias_ref<JString> eventName,
jint emitterReactTag,
jni::alias_ref<react::WritableMap> event) {
// handles RCTEvents from RNGestureHandler
if (event.get() == nullptr) {
// Ignore events with null payload.
return;
}
// TODO: convert event directly to jsi::Value without JSON serialization
std::string eventAsString;
try {
eventAsString = event->toString();
} catch (std::exception &) {
// Events from other libraries may contain NaN or INF values which
// cannot be represented in JSON. See
// https://github.com/software-mansion/react-native-reanimated/issues/1776
// for details.
return;
}
#if REACT_NATIVE_MINOR_VERSION >= 72
std::string eventJSON = eventAsString;
#else
// remove "{ NativeMap: " and " }"
std::string eventJSON = eventAsString.substr(13, eventAsString.length() - 15);
#endif
if (eventJSON == "null") {
return;
}
jsi::Runtime &rt = nativeReanimatedModule_->getUIRuntime();
jsi::Value payload;
try {
payload = jsi::Value::createFromJsonUtf8(
rt, reinterpret_cast<uint8_t *>(&eventJSON[0]), eventJSON.size());
} catch (std::exception &) {
// Ignore events with malformed JSON payload.
return;
}
nativeReanimatedModule_->handleEvent(
eventName->toString(), emitterReactTag, payload, getAnimationTimestamp());
}
void NativeProxy::progressLayoutAnimation(
jsi::Runtime &rt,
int tag,
const jsi::Object &newProps,
bool isSharedTransition) {
auto newPropsJNI = JNIHelper::ConvertToPropsMap(rt, newProps);
layoutAnimations_->cthis()->progressLayoutAnimation(
tag, newPropsJNI, isSharedTransition);
}
PlatformDepMethodsHolder NativeProxy::getPlatformDependentMethods() {
#ifdef RCT_NEW_ARCH_ENABLED
// nothing
#else
auto updatePropsFunction = bindThis(&NativeProxy::updateProps);
auto measureFunction = bindThis(&NativeProxy::measure);
auto scrollToFunction = bindThis(&NativeProxy::scrollTo);
auto dispatchCommandFunction = bindThis(&NativeProxy::dispatchCommand);
auto obtainPropFunction = bindThis(&NativeProxy::obtainProp);
#endif
auto getAnimationTimestamp = bindThis(&NativeProxy::getAnimationTimestamp);
auto requestRender = bindThis(&NativeProxy::requestRender);
#ifdef RCT_NEW_ARCH_ENABLED
auto synchronouslyUpdateUIPropsFunction =
bindThis(&NativeProxy::synchronouslyUpdateUIProps);
#else
auto configurePropsFunction = bindThis(&NativeProxy::configureProps);
#endif
auto registerSensorFunction = bindThis(&NativeProxy::registerSensor);
auto unregisterSensorFunction = bindThis(&NativeProxy::unregisterSensor);
auto setGestureStateFunction = bindThis(&NativeProxy::setGestureState);
auto subscribeForKeyboardEventsFunction =
bindThis(&NativeProxy::subscribeForKeyboardEvents);
auto unsubscribeFromKeyboardEventsFunction =
bindThis(&NativeProxy::unsubscribeFromKeyboardEvents);
auto progressLayoutAnimation =
bindThis(&NativeProxy::progressLayoutAnimation);
auto endLayoutAnimation = [this](int tag, bool removeView) {
this->layoutAnimations_->cthis()->endLayoutAnimation(tag, removeView);
};
auto maybeFlushUiUpdatesQueueFunction =
bindThis(&NativeProxy::maybeFlushUIUpdatesQueue);
return {
requestRender,
#ifdef RCT_NEW_ARCH_ENABLED
synchronouslyUpdateUIPropsFunction,
#else
updatePropsFunction,
scrollToFunction,
dispatchCommandFunction,
measureFunction,
configurePropsFunction,
obtainPropFunction,
#endif
getAnimationTimestamp,
progressLayoutAnimation,
endLayoutAnimation,
registerSensorFunction,
unregisterSensorFunction,
setGestureStateFunction,
subscribeForKeyboardEventsFunction,
unsubscribeFromKeyboardEventsFunction,
maybeFlushUiUpdatesQueueFunction,
};
}
void NativeProxy::setupLayoutAnimations() {
auto weakNativeReanimatedModule =
std::weak_ptr<NativeReanimatedModule>(nativeReanimatedModule_);
layoutAnimations_->cthis()->setAnimationStartingBlock(
[weakNativeReanimatedModule](
int tag, int type, alias_ref<JMap<jstring, jstring>> values) {
if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
jsi::Runtime &rt = nativeReanimatedModule->getUIRuntime();
jsi::Object yogaValues(rt);
for (const auto &entry : *values) {
try {
std::string keyString = entry.first->toStdString();
std::string valueString = entry.second->toStdString();
auto key = jsi::String::createFromAscii(rt, keyString);
if (keyString == "currentTransformMatrix" ||
keyString == "targetTransformMatrix") {
jsi::Array matrix =
jsi_utils::convertStringToArray(rt, valueString, 9);
yogaValues.setProperty(rt, key, matrix);
} else {
auto value = stod(valueString);
yogaValues.setProperty(rt, key, value);
}
} catch (std::invalid_argument e) {
throw std::runtime_error(
"[Reanimated] Failed to convert value to number.");
}
}
nativeReanimatedModule->layoutAnimationsManager()
.startLayoutAnimation(
rt, tag, static_cast<LayoutAnimationType>(type), yogaValues);
}
});
layoutAnimations_->cthis()->setHasAnimationBlock(
[weakNativeReanimatedModule](int tag, int type) {
if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
return nativeReanimatedModule->layoutAnimationsManager()
.hasLayoutAnimation(tag, static_cast<LayoutAnimationType>(type));
}
return false;
});
layoutAnimations_->cthis()->setShouldAnimateExitingBlock(
[weakNativeReanimatedModule](int tag, bool shouldAnimate) {
if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
return nativeReanimatedModule->layoutAnimationsManager()
.shouldAnimateExiting(tag, shouldAnimate);
}
return false;
});
#ifndef NDEBUG
layoutAnimations_->cthis()->setCheckDuplicateSharedTag(
[weakNativeReanimatedModule](int viewTag, int screenTag) {
if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
nativeReanimatedModule->layoutAnimationsManager()
.checkDuplicateSharedTag(viewTag, screenTag);
}
});
#endif
layoutAnimations_->cthis()->setClearAnimationConfigBlock(
[weakNativeReanimatedModule](int tag) {
if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
nativeReanimatedModule->layoutAnimationsManager()
.clearLayoutAnimationConfig(tag);
}
});
layoutAnimations_->cthis()->setCancelAnimationForTag(
[weakNativeReanimatedModule](int tag) {
if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
jsi::Runtime &rt = nativeReanimatedModule->getUIRuntime();
nativeReanimatedModule->layoutAnimationsManager()
.cancelLayoutAnimation(rt, tag);
}
});
layoutAnimations_->cthis()->setFindPrecedingViewTagForTransition(
[weakNativeReanimatedModule](int tag) {
if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
return nativeReanimatedModule->layoutAnimationsManager()
.findPrecedingViewTagForTransition(tag);
} else {
return -1;
}
});
}
} // namespace reanimated

View File

@@ -0,0 +1,308 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#include <react/fabric/JFabricUIManager.h>
#include <react/renderer/scheduler/Scheduler.h>
#if REACT_NATIVE_MINOR_VERSION >= 74
#include <react/jni/JRuntimeExecutor.h>
#endif // REACT_NATIVE_MINOR_VERSION >= 74
#endif
#include <ReactCommon/CallInvokerHolder.h>
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <react/jni/CxxModuleWrapper.h>
#include <react/jni/JMessageQueueThread.h>
#include <react/jni/JavaScriptExecutorHolder.h>
#include <react/jni/WritableNativeMap.h>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "AndroidUIScheduler.h"
#include "JNIHelper.h"
#include "LayoutAnimations.h"
#include "NativeReanimatedModule.h"
#include "UIScheduler.h"
namespace reanimated {
using namespace facebook;
using namespace facebook::jni;
class AnimationFrameCallback : public HybridClass<AnimationFrameCallback> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/swmansion/reanimated/nativeProxy/AnimationFrameCallback;";
void onAnimationFrame(double timestampMs) {
callback_(timestampMs);
}
static void registerNatives() {
javaClassStatic()->registerNatives({
makeNativeMethod(
"onAnimationFrame", AnimationFrameCallback::onAnimationFrame),
});
}
private:
friend HybridBase;
explicit AnimationFrameCallback(std::function<void(double)> callback)
: callback_(std::move(callback)) {}
std::function<void(double)> callback_;
};
class EventHandler : public HybridClass<EventHandler> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/swmansion/reanimated/nativeProxy/EventHandler;";
void receiveEvent(
jni::alias_ref<JString> eventKey,
jint emitterReactTag,
jni::alias_ref<react::WritableMap> event) {
handler_(eventKey, emitterReactTag, event);
}
static void registerNatives() {
javaClassStatic()->registerNatives({
makeNativeMethod("receiveEvent", EventHandler::receiveEvent),
});
}
private:
friend HybridBase;
explicit EventHandler(std::function<void(
jni::alias_ref<JString>,
jint emitterReactTag,
jni::alias_ref<react::WritableMap>)> handler)
: handler_(std::move(handler)) {}
std::function<
void(jni::alias_ref<JString>, jint, jni::alias_ref<react::WritableMap>)>
handler_;
};
class SensorSetter : public HybridClass<SensorSetter> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/swmansion/reanimated/nativeProxy/SensorSetter;";
void sensorSetter(jni::alias_ref<JArrayFloat> value, int orientationDegrees) {
size_t size = value->size();
auto elements = value->getRegion(0, size);
double array[7];
for (size_t i = 0; i < size; i++) {
array[i] = elements[i];
}
callback_(array, orientationDegrees);
}
static void registerNatives() {
javaClassStatic()->registerNatives({
makeNativeMethod("sensorSetter", SensorSetter::sensorSetter),
});
}
private:
friend HybridBase;
explicit SensorSetter(std::function<void(double[], int)> callback)
: callback_(std::move(callback)) {}
std::function<void(double[], int)> callback_;
};
class KeyboardWorkletWrapper : public HybridClass<KeyboardWorkletWrapper> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/swmansion/reanimated/keyboard/KeyboardWorkletWrapper;";
void invoke(int keyboardState, int height) {
callback_(keyboardState, height);
}
static void registerNatives() {
javaClassStatic()->registerNatives({
makeNativeMethod("invoke", KeyboardWorkletWrapper::invoke),
});
}
private:
friend HybridBase;
explicit KeyboardWorkletWrapper(std::function<void(int, int)> callback)
: callback_(std::move(callback)) {}
std::function<void(int, int)> callback_;
};
class NativeProxy : public jni::HybridClass<NativeProxy> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/swmansion/reanimated/NativeProxy;";
static jni::local_ref<jhybriddata> initHybrid(
jni::alias_ref<jhybridobject> jThis,
jlong jsContext,
jni::alias_ref<facebook::react::CallInvokerHolder::javaobject>
jsCallInvokerHolder,
jni::alias_ref<AndroidUIScheduler::javaobject> androidUiScheduler,
jni::alias_ref<LayoutAnimations::javaobject> layoutAnimations,
jni::alias_ref<JavaMessageQueueThread::javaobject> messageQueueThread,
#ifdef RCT_NEW_ARCH_ENABLED
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
fabricUIManager,
#endif
const std::string &valueUnpackerCode);
#if REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED)
static jni::local_ref<jhybriddata> initHybridBridgeless(
jni::alias_ref<jhybridobject> jThis,
jlong jsContext,
jni::alias_ref<react::JRuntimeExecutor::javaobject> runtimeExecutorHolder,
jni::alias_ref<AndroidUIScheduler::javaobject> androidUiScheduler,
jni::alias_ref<LayoutAnimations::javaobject> layoutAnimations,
jni::alias_ref<JavaMessageQueueThread::javaobject> messageQueueThread,
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
fabricUIManager,
const std::string &valueUnpackerCode);
#endif // REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED
static void registerNatives();
~NativeProxy();
private:
friend HybridBase;
jni::global_ref<NativeProxy::javaobject> javaPart_;
jsi::Runtime *rnRuntime_;
std::shared_ptr<NativeReanimatedModule> nativeReanimatedModule_;
jni::global_ref<LayoutAnimations::javaobject> layoutAnimations_;
#ifndef NDEBUG
void checkJavaVersion(jsi::Runtime &);
void injectCppVersion();
#endif // NDEBUG
#ifdef RCT_NEW_ARCH_ENABLED
// removed temporarily, event listener mechanism needs to be fixed on RN side
// std::shared_ptr<facebook::react::Scheduler> reactScheduler_;
// std::shared_ptr<EventListener> eventListener_;
#endif
void installJSIBindings();
#ifdef RCT_NEW_ARCH_ENABLED
void synchronouslyUpdateUIProps(
jsi::Runtime &rt,
Tag viewTag,
const jsi::Object &props);
#endif
PlatformDepMethodsHolder getPlatformDependentMethods();
void setupLayoutAnimations();
double getAnimationTimestamp();
bool isAnyHandlerWaitingForEvent(
const std::string &eventName,
const int emitterReactTag);
void performOperations();
bool getIsReducedMotion();
void requestRender(std::function<void(double)> onRender, jsi::Runtime &rt);
void registerEventHandler();
void maybeFlushUIUpdatesQueue();
void setGestureState(int handlerTag, int newState);
int registerSensor(
int sensorType,
int interval,
int iosReferenceFrame,
std::function<void(double[], int)> setter);
void unregisterSensor(int sensorId);
int subscribeForKeyboardEvents(
std::function<void(int, int)> callback,
bool isStatusBarTranslucent);
void unsubscribeFromKeyboardEvents(int listenerId);
#ifdef RCT_NEW_ARCH_ENABLED
// nothing
#else
jsi::Value
obtainProp(jsi::Runtime &rt, const int viewTag, const jsi::Value &propName);
void configureProps(
jsi::Runtime &rt,
const jsi::Value &uiProps,
const jsi::Value &nativeProps);
void updateProps(jsi::Runtime &rt, const jsi::Value &operations);
void scrollTo(int viewTag, double x, double y, bool animated);
void dispatchCommand(
jsi::Runtime &rt,
const int viewTag,
const jsi::Value &commandNameValue,
const jsi::Value &argsValue);
std::vector<std::pair<std::string, double>> measure(int viewTag);
#endif
void handleEvent(
jni::alias_ref<JString> eventName,
jint emitterReactTag,
jni::alias_ref<react::WritableMap> event);
void progressLayoutAnimation(
jsi::Runtime &rt,
int tag,
const jsi::Object &newProps,
bool isSharedTransition);
/***
* Wraps a method of `NativeProxy` in a function object capturing `this`
* @tparam TReturn return type of passed method
* @tparam TParams paramater types of passed method
* @param methodPtr pointer to method to be wrapped
* @return a function object with the same signature as the method, calling
* that method on `this`
*/
template <class TReturn, class... TParams>
std::function<TReturn(TParams...)> bindThis(
TReturn (NativeProxy::*methodPtr)(TParams...)) {
return [this, methodPtr](TParams &&...args) {
return (this->*methodPtr)(std::forward<TParams>(args)...);
};
}
template <class Signature>
JMethod<Signature> getJniMethod(std::string const &methodName) {
return javaPart_->getClass()->getMethod<Signature>(methodName.c_str());
}
explicit NativeProxy(
jni::alias_ref<NativeProxy::jhybridobject> jThis,
jsi::Runtime *rnRuntime,
const std::shared_ptr<facebook::react::CallInvoker> &jsCallInvoker,
const std::shared_ptr<UIScheduler> &uiScheduler,
jni::global_ref<LayoutAnimations::javaobject> layoutAnimations,
jni::alias_ref<JavaMessageQueueThread::javaobject> messageQueueThread,
#ifdef RCT_NEW_ARCH_ENABLED
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
fabricUIManager,
#endif
const std::string &valueUnpackerCode);
#if REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED)
explicit NativeProxy(
jni::alias_ref<NativeProxy::jhybridobject> jThis,
jsi::Runtime *rnRuntime,
RuntimeExecutor runtimeExecutor,
const std::shared_ptr<UIScheduler> &uiScheduler,
jni::global_ref<LayoutAnimations::javaobject> layoutAnimations,
jni::alias_ref<JavaMessageQueueThread::javaobject> messageQueueThread,
jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
fabricUIManager,
const std::string &valueUnpackerCode);
#endif // REACT_NATIVE_MINOR_VERSION >= 74 && defined(RCT_NEW_ARCH_ENABLED
#ifdef RCT_NEW_ARCH_ENABLED
void commonInit(jni::alias_ref<facebook::react::JFabricUIManager::javaobject>
&fabricUIManager);
#endif // RCT_NEW_ARCH_ENABLED
};
} // namespace reanimated

View File

@@ -0,0 +1,18 @@
#include <fbjni/fbjni.h>
#include "AndroidUIScheduler.h"
#include "LayoutAnimations.h"
#include "Logger.h"
#include "NativeProxy.h"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(vm, [] {
reanimated::NativeProxy::registerNatives();
reanimated::AnimationFrameCallback::registerNatives();
reanimated::EventHandler::registerNatives();
reanimated::AndroidUIScheduler::registerNatives();
reanimated::LayoutAnimations::registerNatives();
reanimated::SensorSetter::registerNatives();
reanimated::KeyboardWorkletWrapper::registerNatives();
});
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "TurboModule.h"
using namespace facebook;
namespace facebook {
namespace react {
TurboModule::TurboModule(
const std::string &name,
const std::shared_ptr<CallInvoker> &jsInvoker)
: name_(name), jsInvoker_(jsInvoker) {}
TurboModule::~TurboModule() {}
jsi::Value TurboModule::get(
jsi::Runtime &runtime,
const jsi::PropNameID &propName) {
std::string propNameUtf8 = propName.utf8(runtime);
auto p = methodMap_.find(propNameUtf8);
if (p == methodMap_.end()) {
// Method was not found, let JS decide what to do.
return jsi::Value::undefined();
}
MethodMetadata meta = p->second;
return jsi::Function::createFromHostFunction(
runtime,
propName,
meta.argCount,
[this, meta](
facebook::jsi::Runtime &rt,
const facebook::jsi::Value &,
const facebook::jsi::Value *args,
size_t count) { return meta.invoker(rt, *this, args, count); });
}
} // namespace react
} // namespace facebook

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <ReactCommon/CallInvoker.h>
#include <jsi/jsi.h>
#include <memory>
#include <string>
#include <unordered_map>
namespace facebook {
namespace react {
/**
* For now, support the same set of return types as existing impl.
* This can be improved to support richer typed objects.
*/
enum TurboModuleMethodValueKind {
VoidKind,
BooleanKind,
NumberKind,
StringKind,
ObjectKind,
ArrayKind,
FunctionKind,
PromiseKind,
};
/**
* Base HostObject class for every module to be exposed to JS
*/
class JSI_EXPORT TurboModule : public facebook::jsi::HostObject {
public:
TurboModule(
const std::string &name,
const std::shared_ptr<CallInvoker> &jsInvoker);
virtual ~TurboModule();
facebook::jsi::Value get(
facebook::jsi::Runtime &runtime,
const facebook::jsi::PropNameID &propName) override;
const std::string name_;
const std::shared_ptr<CallInvoker> jsInvoker_;
protected:
struct MethodMetadata {
size_t argCount;
facebook::jsi::Value (*invoker)(
facebook::jsi::Runtime &rt,
TurboModule &turboModule,
const facebook::jsi::Value *args,
size_t count);
};
std::unordered_map<std::string, MethodMetadata> methodMap_;
};
/**
* An app/platform-specific provider function to get an instance of a module
* given a name.
*/
using TurboModuleProviderFunctionType =
std::function<std::shared_ptr<TurboModule>(const std::string &name)>;
} // namespace react
} // namespace facebook

View File

@@ -0,0 +1,15 @@
package com.facebook.react.uimanager;
/**
* This class provides a way to workaround limited visibility of
* UIViewOperationQueue#getUIViewOperationQueue. We rely on accessing that method to check if
* operation queue is empty or not. This in turn indicates if we are in a middle of processing batch
* of operations from JS. In such a case we can rely on the enqueued update operations to be flushed
* onto the shadow view hierarchy. Otherwise we want to trigger "dispatchViewUpdates" and enforce
* flush immediately.
*/
public class UIManagerReanimatedHelper {
public static boolean isOperationQueueEmpty(UIImplementation uiImplementation) {
return uiImplementation.getUIViewOperationQueue().isEmpty();
}
}

View File

@@ -0,0 +1,5 @@
package com.swmansion.common;
public interface GestureHandlerStateManager {
void setGestureHandlerState(int handlerTag, int newState);
}

View File

@@ -0,0 +1,51 @@
package com.swmansion.reanimated;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.UiThreadUtil;
import java.util.concurrent.atomic.AtomicBoolean;
public class AndroidUIScheduler {
@DoNotStrip
@SuppressWarnings("unused")
private final HybridData mHybridData;
private final ReactApplicationContext mContext;
private final AtomicBoolean mActive = new AtomicBoolean(true);
private final Runnable mUIThreadRunnable =
new Runnable() {
@Override
public void run() {
if (mActive.get()) {
triggerUI();
}
}
};
public AndroidUIScheduler(ReactApplicationContext context) {
mHybridData = initHybrid();
mContext = context;
}
private native HybridData initHybrid();
public native void triggerUI();
@DoNotStrip
private void scheduleTriggerOnUI() {
UiThreadUtil.runOnUiThread(
new GuardedRunnable(mContext.getExceptionHandler()) {
public void runGuarded() {
mUIThreadRunnable.run();
}
});
}
public void deactivate() {
mActive.set(false);
}
}

View File

@@ -0,0 +1,43 @@
package com.swmansion.reanimated;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class CopiedEvent {
private int targetTag;
private String eventName;
private WritableMap payload;
CopiedEvent(Event event) {
event.dispatch(
new RCTEventEmitter() {
@Override
public void receiveEvent(int targetTag, String eventName, @Nullable WritableMap event) {
CopiedEvent.this.targetTag = targetTag;
CopiedEvent.this.eventName = eventName;
CopiedEvent.this.payload = event.copy();
}
@Override
public void receiveTouches(
String eventName, WritableArray touches, WritableArray changedIndices) {
// noop
}
});
}
public int getTargetTag() {
return targetTag;
}
public String getEventName() {
return eventName;
}
public WritableMap getPayload() {
return payload;
}
}

View File

@@ -0,0 +1,26 @@
package com.swmansion.reanimated;
import com.facebook.react.bridge.JSApplicationCausedNativeException;
import com.facebook.react.bridge.NoSuchKeyException;
import com.facebook.react.bridge.ReadableMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class MapUtils {
public static int getInt(ReadableMap map, @Nonnull String name, String errorMsg) {
try {
return map.getInt(name);
} catch (NoSuchKeyException e) {
throw new JSApplicationCausedNativeException(errorMsg);
}
}
@Nullable
public static String getString(ReadableMap map, @Nonnull String name, String errorMsg) {
try {
return map.getString(name);
} catch (NoSuchKeyException e) {
throw new JSApplicationCausedNativeException(errorMsg);
}
}
}

View File

@@ -0,0 +1,112 @@
package com.swmansion.reanimated;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.util.Log;
import android.view.View;
import android.view.ViewParent;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.RootViewUtil;
import com.facebook.react.views.scroll.ReactHorizontalScrollView;
import com.facebook.react.views.scroll.ReactScrollView;
import com.facebook.react.views.swiperefresh.ReactSwipeRefreshLayout;
public class NativeMethodsHelper {
public static float[] measure(View view) {
View rootView = (View) RootViewUtil.getRootView(view);
if (rootView == null || view == null) {
float result[] = new float[6];
result[0] = -1234567;
return result;
}
int buffer[] = new int[4];
computeBoundingBox(rootView, buffer);
int rootX = buffer[0];
int rootY = buffer[1];
computeBoundingBox(view, buffer);
buffer[0] -= rootX;
buffer[1] -= rootY;
float result[] = new float[6];
result[0] = result[1] = 0;
for (int i = 2; i < 6; ++i) result[i] = PixelUtil.toDIPFromPixel(buffer[i - 2]);
return result;
}
public static void scrollTo(View view, double argX, double argY, boolean animated) {
int x = Math.round(PixelUtil.toPixelFromDIP(argX));
int y = Math.round(PixelUtil.toPixelFromDIP(argY));
boolean isHorizontal = view instanceof ReactHorizontalScrollView;
if (!isHorizontal) {
if (view instanceof ReactSwipeRefreshLayout) {
view = findScrollView((ReactSwipeRefreshLayout) view);
}
if (!(view instanceof ReactScrollView)) {
Log.w(
"REANIMATED",
"NativeMethodsHelper: Unhandled scroll view type - allowed only {ReactScrollView, ReactHorizontalScrollView}");
return;
}
}
if (animated) {
final View finalView = view;
if (isHorizontal) {
view.post(() -> ((ReactHorizontalScrollView) finalView).smoothScrollTo(x, y));
} else {
view.post(() -> ((ReactScrollView) finalView).smoothScrollTo(x, y));
}
} else {
view.scrollTo(x, y);
}
}
private static ReactScrollView findScrollView(ReactSwipeRefreshLayout view) {
for (int i = 0; i < view.getChildCount(); i++) {
if (view.getChildAt(i) instanceof ReactScrollView) {
return (ReactScrollView) view.getChildAt(i);
}
}
return null;
}
private static void computeBoundingBox(View view, int[] outputBuffer) {
RectF boundingBox = new RectF();
boundingBox.set(0, 0, view.getWidth(), view.getHeight());
mapRectFromViewToWindowCoords(view, boundingBox);
outputBuffer[0] = Math.round(boundingBox.left);
outputBuffer[1] = Math.round(boundingBox.top);
outputBuffer[2] = Math.round(boundingBox.right - boundingBox.left);
outputBuffer[3] = Math.round(boundingBox.bottom - boundingBox.top);
}
private static void mapRectFromViewToWindowCoords(View view, RectF rect) {
Matrix matrix = view.getMatrix();
if (!matrix.isIdentity()) {
matrix.mapRect(rect);
}
rect.offset(view.getLeft(), view.getTop());
ViewParent parent = view.getParent();
while (parent instanceof View) {
View parentView = (View) parent;
rect.offset(-parentView.getScrollX(), -parentView.getScrollY());
matrix = parentView.getMatrix();
if (!matrix.isIdentity()) {
matrix.mapRect(rect);
}
rect.offset(parentView.getLeft(), parentView.getTop());
parent = parentView.getParent();
}
}
}

View File

@@ -0,0 +1,510 @@
package com.swmansion.reanimated;
import static java.lang.Float.NaN;
import android.graphics.drawable.Drawable;
import android.view.View;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.core.ReactChoreographer;
import com.facebook.react.uimanager.GuardedFrameCallback;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIManagerReanimatedHelper;
import com.facebook.react.uimanager.common.UIManagerType;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcherListener;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
import com.swmansion.reanimated.layoutReanimation.AnimationsManager;
import com.swmansion.reanimated.nativeProxy.NoopEventHandler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
public class NodesManager implements EventDispatcherListener {
public void scrollTo(int viewTag, double x, double y, boolean animated) {
View view;
try {
view = mUIManager.resolveView(viewTag);
} catch (IllegalViewOperationException e) {
e.printStackTrace();
return;
}
NativeMethodsHelper.scrollTo(view, x, y, animated);
}
public void dispatchCommand(int viewTag, String commandId, ReadableArray commandArgs) {
// mUIManager.dispatchCommand must be called from native modules queue thread
// because of an assert in ShadowNodeRegistry.getNode
mContext.runOnNativeModulesQueueThread(
new GuardedRunnable(mContext.getExceptionHandler()) {
@Override
public void runGuarded() {
mUIManager.dispatchCommand(viewTag, commandId, commandArgs);
}
});
}
public float[] measure(int viewTag) {
View view;
try {
view = mUIManager.resolveView(viewTag);
} catch (IllegalViewOperationException e) {
e.printStackTrace();
return (new float[] {NaN, NaN, NaN, NaN, NaN, NaN});
}
return NativeMethodsHelper.measure(view);
}
public interface OnAnimationFrame {
void onAnimationFrame(double timestampMs);
}
private final AnimationsManager mAnimationManager;
private final UIImplementation mUIImplementation;
private final DeviceEventManagerModule.RCTDeviceEventEmitter mEventEmitter;
private final ReactChoreographer mReactChoreographer;
private final GuardedFrameCallback mChoreographerCallback;
protected final UIManagerModule.CustomEventNamesResolver mCustomEventNamesResolver;
private final AtomicBoolean mCallbackPosted = new AtomicBoolean();
private final ReactContext mContext;
private final UIManager mUIManager;
private ReactApplicationContext mReactApplicationContext;
private RCTEventEmitter mCustomEventHandler = new NoopEventHandler();
private List<OnAnimationFrame> mFrameCallbacks = new ArrayList<>();
private ConcurrentLinkedQueue<CopiedEvent> mEventQueue = new ConcurrentLinkedQueue<>();
private double lastFrameTimeMs;
public Set<String> uiProps = Collections.emptySet();
public Set<String> nativeProps = Collections.emptySet();
private ReaCompatibility compatibility;
public NativeProxy getNativeProxy() {
return mNativeProxy;
}
private NativeProxy mNativeProxy;
public AnimationsManager getAnimationsManager() {
return mAnimationManager;
}
public void invalidate() {
if (mAnimationManager != null) {
mAnimationManager.invalidate();
}
if (mNativeProxy != null) {
mNativeProxy.invalidate();
mNativeProxy = null;
}
}
public void initWithContext(
ReactApplicationContext reactApplicationContext, String valueUnpackerCode) {
mReactApplicationContext = reactApplicationContext;
mNativeProxy = new NativeProxy(reactApplicationContext, valueUnpackerCode);
mAnimationManager.setAndroidUIScheduler(getNativeProxy().getAndroidUIScheduler());
compatibility = new ReaCompatibility(reactApplicationContext);
compatibility.registerFabricEventListener(this);
}
private final class NativeUpdateOperation {
public int mViewTag;
public WritableMap mNativeProps;
public NativeUpdateOperation(int viewTag, WritableMap nativeProps) {
mViewTag = viewTag;
mNativeProps = nativeProps;
}
}
private Queue<NativeUpdateOperation> mOperationsInBatch = new LinkedList<>();
private boolean mTryRunBatchUpdatesSynchronously = false;
public NodesManager(ReactContext context) {
mContext = context;
int uiManagerType =
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED ? UIManagerType.FABRIC : UIManagerType.DEFAULT;
mUIManager = UIManagerHelper.getUIManager(context, uiManagerType);
assert mUIManager != null;
mUIImplementation =
mUIManager instanceof UIManagerModule
? ((UIManagerModule) mUIManager).getUIImplementation()
: null;
mCustomEventNamesResolver = mUIManager::resolveCustomDirectEventName;
mEventEmitter = context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
mReactChoreographer = ReactChoreographer.getInstance();
mChoreographerCallback =
new GuardedFrameCallback(context) {
@Override
protected void doFrameGuarded(long frameTimeNanos) {
onAnimationFrame(frameTimeNanos);
}
};
// We register as event listener at the end, because we pass `this` and we haven't finished
// constructing an object yet.
// This lead to a crash described in
// https://github.com/software-mansion/react-native-reanimated/issues/604 which was caused by
// Nodes Manager being constructed on UI thread and registering for events.
// Events are handled in the native modules thread in the `onEventDispatch()` method.
// This method indirectly uses `mChoreographerCallback` which was created after event
// registration, creating race condition
Objects.requireNonNull(UIManagerHelper.getEventDispatcher(context, uiManagerType))
.addListener(this);
mAnimationManager = new AnimationsManager(mContext, mUIManager);
}
public void onHostPause() {
if (mCallbackPosted.get()) {
stopUpdatingOnAnimationFrame();
mCallbackPosted.set(true);
}
}
public boolean isAnimationRunning() {
return mCallbackPosted.get();
}
public void onHostResume() {
if (mCallbackPosted.getAndSet(false)) {
startUpdatingOnAnimationFrame();
}
}
public void startUpdatingOnAnimationFrame() {
if (!mCallbackPosted.getAndSet(true)) {
mReactChoreographer.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, mChoreographerCallback);
}
}
private void stopUpdatingOnAnimationFrame() {
if (mCallbackPosted.getAndSet(false)) {
mReactChoreographer.removeFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, mChoreographerCallback);
}
}
public void performOperations() {
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
if (mNativeProxy != null) {
mNativeProxy.performOperations();
}
} else if (!mOperationsInBatch.isEmpty()) {
final Queue<NativeUpdateOperation> copiedOperationsQueue = mOperationsInBatch;
mOperationsInBatch = new LinkedList<>();
final boolean trySynchronously = mTryRunBatchUpdatesSynchronously;
mTryRunBatchUpdatesSynchronously = false;
final Semaphore semaphore = new Semaphore(0);
mContext.runOnNativeModulesQueueThread(
new GuardedRunnable(mContext.getExceptionHandler()) {
@Override
public void runGuarded() {
boolean queueWasEmpty =
UIManagerReanimatedHelper.isOperationQueueEmpty(mUIImplementation);
boolean shouldDispatchUpdates = trySynchronously && queueWasEmpty;
if (!shouldDispatchUpdates) {
semaphore.release();
}
while (!copiedOperationsQueue.isEmpty()) {
NativeUpdateOperation op = copiedOperationsQueue.remove();
ReactShadowNode shadowNode = mUIImplementation.resolveShadowNode(op.mViewTag);
if (shadowNode != null) {
((UIManagerModule) mUIManager)
.updateView(op.mViewTag, shadowNode.getViewClass(), op.mNativeProps);
}
}
if (queueWasEmpty) {
mUIImplementation.dispatchViewUpdates(-1); // no associated batchId
}
if (shouldDispatchUpdates) {
semaphore.release();
}
}
});
if (trySynchronously) {
try {
semaphore.tryAcquire(16, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// if the thread is interrupted we just continue and let the layout update happen
// asynchronously
}
}
}
}
private void onAnimationFrame(long frameTimeNanos) {
// Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onAnimationFrame");
double currentFrameTimeMs = frameTimeNanos / 1000000.;
if (currentFrameTimeMs > lastFrameTimeMs) {
// It is possible for ChoreographerCallback to be executed twice within the same frame
// due to frame drops. If this occurs, the additional callback execution should be ignored.
lastFrameTimeMs = currentFrameTimeMs;
while (!mEventQueue.isEmpty()) {
CopiedEvent copiedEvent = mEventQueue.poll();
handleEvent(
copiedEvent.getTargetTag(), copiedEvent.getEventName(), copiedEvent.getPayload());
}
if (!mFrameCallbacks.isEmpty()) {
List<OnAnimationFrame> frameCallbacks = mFrameCallbacks;
mFrameCallbacks = new ArrayList<>(frameCallbacks.size());
for (int i = 0, size = frameCallbacks.size(); i < size; i++) {
frameCallbacks.get(i).onAnimationFrame(currentFrameTimeMs);
}
}
performOperations();
}
mCallbackPosted.set(false);
if (!mFrameCallbacks.isEmpty() || !mEventQueue.isEmpty()) {
// enqueue next frame
startUpdatingOnAnimationFrame();
}
// Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
}
public void enqueueUpdateViewOnNativeThread(
int viewTag, WritableMap nativeProps, boolean trySynchronously) {
if (trySynchronously) {
mTryRunBatchUpdatesSynchronously = true;
}
mOperationsInBatch.add(new NativeUpdateOperation(viewTag, nativeProps));
}
public void configureProps(Set<String> uiPropsSet, Set<String> nativePropsSet) {
uiProps = uiPropsSet;
nativeProps = nativePropsSet;
}
public void postOnAnimation(OnAnimationFrame onAnimationFrame) {
mFrameCallbacks.add(onAnimationFrame);
startUpdatingOnAnimationFrame();
}
@Override
public void onEventDispatch(Event event) {
if (mNativeProxy == null) {
return;
}
// Events can be dispatched from any thread so we have to make sure handleEvent is run from the
// UI thread.
if (UiThreadUtil.isOnUiThread()) {
handleEvent(event);
performOperations();
} else {
String eventName = mCustomEventNamesResolver.resolveCustomEventName(event.getEventName());
int viewTag = event.getViewTag();
boolean shouldSaveEvent = mNativeProxy.isAnyHandlerWaitingForEvent(eventName, viewTag);
if (shouldSaveEvent) {
mEventQueue.offer(new CopiedEvent(event));
}
startUpdatingOnAnimationFrame();
}
}
private void handleEvent(Event event) {
event.dispatch(mCustomEventHandler);
}
private void handleEvent(int targetTag, String eventName, @Nullable WritableMap event) {
mCustomEventHandler.receiveEvent(targetTag, eventName, event);
}
public UIManagerModule.CustomEventNamesResolver getEventNameResolver() {
return mCustomEventNamesResolver;
}
public void registerEventHandler(RCTEventEmitter handler) {
mCustomEventHandler = handler;
}
public void sendEvent(String name, WritableMap body) {
mEventEmitter.emit(name, body);
}
public void updateProps(int viewTag, Map<String, Object> props) {
// TODO: update PropsNode to use this method instead of its own way of updating props
boolean hasUIProps = false;
boolean hasNativeProps = false;
boolean hasJSProps = false;
JavaOnlyMap newUIProps = new JavaOnlyMap();
WritableMap newJSProps = Arguments.createMap();
WritableMap newNativeProps = Arguments.createMap();
for (Map.Entry<String, Object> entry : props.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (uiProps.contains(key)) {
hasUIProps = true;
addProp(newUIProps, key, value);
} else if (nativeProps.contains(key)) {
hasNativeProps = true;
addProp(newNativeProps, key, value);
} else {
hasJSProps = true;
addProp(newJSProps, key, value);
}
}
if (viewTag != View.NO_ID) {
if (hasUIProps) {
mUIImplementation.synchronouslyUpdateViewOnUIThread(
viewTag, new ReactStylesDiffMap(newUIProps));
}
if (hasNativeProps) {
enqueueUpdateViewOnNativeThread(viewTag, newNativeProps, true);
}
if (hasJSProps) {
WritableMap evt = Arguments.createMap();
evt.putInt("viewTag", viewTag);
evt.putMap("props", newJSProps);
sendEvent("onReanimatedPropsChange", evt);
}
}
}
public void synchronouslyUpdateUIProps(int viewTag, ReadableMap uiProps) {
compatibility.synchronouslyUpdateUIProps(viewTag, uiProps);
}
public String obtainProp(int viewTag, String propName) {
View view;
try {
view = mUIManager.resolveView(viewTag);
} catch (Exception e) {
throw new IllegalStateException("[Reanimated] Unable to resolve view");
}
switch (propName) {
case "opacity":
return Float.toString(view.getAlpha());
case "zIndex":
return Float.toString(view.getElevation());
case "width":
return Float.toString(PixelUtil.toDIPFromPixel(view.getWidth()));
case "height":
return Float.toString(PixelUtil.toDIPFromPixel(view.getHeight()));
case "top":
return Float.toString(PixelUtil.toDIPFromPixel(view.getTop()));
case "left":
return Float.toString(PixelUtil.toDIPFromPixel(view.getLeft()));
case "backgroundColor":
Drawable background = view.getBackground();
if (!(background instanceof ReactViewBackgroundDrawable)) {
return "unable to resolve background color";
}
int actualColor = ((ReactViewBackgroundDrawable) background).getColor();
String invertedColor = String.format("%08x", (0xFFFFFFFF & actualColor));
// By default transparency is first, color second
return "#" + invertedColor.substring(2, 8) + invertedColor.substring(0, 2);
default:
throw new IllegalArgumentException(
"[Reanimated] Attempted to get unsupported property"
+ propName
+ " with function `getViewProp`");
}
}
private static WritableMap copyReadableMap(ReadableMap map) {
WritableMap copy = Arguments.createMap();
copy.merge(map);
return copy;
}
private static WritableArray copyReadableArray(ReadableArray array) {
WritableArray copy = Arguments.createArray();
for (int i = 0; i < array.size(); i++) {
ReadableType type = array.getType(i);
switch (type) {
case Boolean:
copy.pushBoolean(array.getBoolean(i));
break;
case String:
copy.pushString(array.getString(i));
break;
case Null:
copy.pushNull();
break;
case Number:
copy.pushDouble(array.getDouble(i));
break;
case Map:
copy.pushMap(copyReadableMap(array.getMap(i)));
break;
case Array:
copy.pushArray(copyReadableArray(array.getArray(i)));
break;
default:
throw new IllegalStateException("[Reanimated] Unknown type of ReadableArray.");
}
}
return copy;
}
private static void addProp(WritableMap propMap, String key, Object value) {
if (value == null) {
propMap.putNull(key);
} else if (value instanceof Double) {
propMap.putDouble(key, (Double) value);
} else if (value instanceof Integer) {
propMap.putInt(key, (Integer) value);
} else if (value instanceof Number) {
propMap.putDouble(key, ((Number) value).doubleValue());
} else if (value instanceof Boolean) {
propMap.putBoolean(key, (Boolean) value);
} else if (value instanceof String) {
propMap.putString(key, (String) value);
} else if (value instanceof ReadableArray) {
if (!(value instanceof WritableArray)) {
propMap.putArray(key, copyReadableArray((ReadableArray) value));
} else {
propMap.putArray(key, (ReadableArray) value);
}
} else if (value instanceof ReadableMap) {
if (!(value instanceof WritableMap)) {
propMap.putMap(key, copyReadableMap((ReadableMap) value));
} else {
propMap.putMap(key, (ReadableMap) value);
}
} else {
throw new IllegalStateException("[Reanimated] Unknown type of animated value.");
}
}
}

View File

@@ -0,0 +1,37 @@
package com.swmansion.reanimated;
import android.graphics.drawable.Drawable;
import android.view.View;
import com.facebook.react.views.image.ReactImageView;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
import java.lang.reflect.Field;
public class ReactNativeUtils {
private static Field mBorderRadiusField;
public static float getBorderRadius(View view) {
if (view.getBackground() != null) {
Drawable background = view.getBackground();
if (background instanceof ReactViewBackgroundDrawable) {
return ((ReactViewBackgroundDrawable) background).getFullBorderRadius();
}
} else if (view instanceof ReactImageView) {
try {
if (mBorderRadiusField == null) {
mBorderRadiusField = ReactImageView.class.getDeclaredField("mBorderRadius");
mBorderRadiusField.setAccessible(true);
}
float borderRadius = mBorderRadiusField.getFloat(view);
if (Float.isNaN(borderRadius)) {
return 0;
}
return borderRadius;
} catch (NullPointerException | NoSuchFieldException | IllegalAccessException ignored) {
// In case of non-standard view is better to not support the border animation
// instead of throwing exception
}
}
return 0;
}
}

View File

@@ -0,0 +1,72 @@
package com.swmansion.reanimated;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.bridge.queue.MessageQueueThreadImpl;
import com.facebook.react.bridge.queue.MessageQueueThreadPerfStats;
import com.facebook.react.bridge.queue.MessageQueueThreadSpec;
import java.lang.reflect.Field;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
// This class is an almost exact copy of MessageQueueThreadImpl taken from here:
// https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java
// The only method that has changed is `quitSynchronous()` (see comment above
// function implementation for details).
@DoNotStrip
public abstract class ReanimatedMessageQueueThreadBase implements MessageQueueThread {
protected final MessageQueueThreadImpl messageQueueThread;
public ReanimatedMessageQueueThreadBase() {
messageQueueThread =
MessageQueueThreadImpl.create(
MessageQueueThreadSpec.mainThreadSpec(),
exception -> {
throw new RuntimeException(exception);
});
}
@Override
public <T> Future<T> callOnQueue(Callable<T> callable) {
return messageQueueThread.callOnQueue(callable);
}
@Override
public boolean isOnThread() {
return messageQueueThread.isOnThread();
}
@Override
public void assertIsOnThread() {
messageQueueThread.assertIsOnThread();
}
@Override
public void assertIsOnThread(String s) {
messageQueueThread.assertIsOnThread(s);
}
// We don't want to quit the main looper (which is what MessageQueueThreadImpl would have done),
// but we still want to prevent anything else from executing.
@Override
public void quitSynchronous() {
try {
Field mIsFinished = messageQueueThread.getClass().getDeclaredField("mIsFinished");
mIsFinished.setAccessible(true);
mIsFinished.set(messageQueueThread, true);
mIsFinished.setAccessible(false);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public MessageQueueThreadPerfStats getPerfStats() {
return messageQueueThread.getPerfStats();
}
@Override
public void resetPerfStats() {
messageQueueThread.resetPerfStats();
}
}

View File

@@ -0,0 +1,102 @@
package com.swmansion.reanimated;
import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_END;
import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_START;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.module.annotations.ReactModuleList;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.uimanager.ReanimatedUIManager;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.systrace.Systrace;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ReactModuleList(
nativeModules = {
ReanimatedModule.class,
ReanimatedUIManager.class,
})
public class ReanimatedPackage extends TurboReactPackage implements ReactPackage {
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(ReanimatedModule.NAME)) {
return new ReanimatedModule(reactContext);
}
if (name.equals(ReanimatedUIManager.NAME)) {
return createUIManager(reactContext);
}
return null;
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
Class<? extends NativeModule>[] moduleList =
new Class[] {
ReanimatedModule.class, ReanimatedUIManager.class,
};
final Map<String, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
for (Class<? extends NativeModule> moduleClass : moduleList) {
ReactModule reactModule = moduleClass.getAnnotation(ReactModule.class);
reactModuleInfoMap.put(
reactModule.name(),
new ReactModuleInfo(
reactModule.name(),
moduleClass.getName(),
true,
reactModule.needsEagerInit(),
reactModule.hasConstants(),
reactModule.isCxxModule(),
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED));
}
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
return reactModuleInfoMap;
}
};
}
private UIManagerModule createUIManager(final ReactApplicationContext reactContext) {
ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_START);
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createUIManagerModule");
final ReactInstanceManager reactInstanceManager = getReactInstanceManager(reactContext);
List<ViewManager> viewManagers = reactInstanceManager.getOrCreateViewManagers(reactContext);
int minTimeLeftInFrameForNonBatchedOperationMs = -1;
try {
return ReanimatedUIManagerFactory.create(
reactContext, viewManagers, minTimeLeftInFrameForNonBatchedOperationMs);
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_END);
}
}
/**
* Get the {@link ReactInstanceManager} used by this app. By default, assumes {@link
* ReactApplicationContext#getApplicationContext()} is an instance of {@link ReactApplication} and
* calls {@link ReactApplication#getReactNativeHost().getReactInstanceManager()}. Override this
* method if your application class does not implement {@code ReactApplication} or you simply have
* a different mechanism for storing a {@code ReactInstanceManager}, e.g. as a static field
* somewhere.
*/
public ReactInstanceManager getReactInstanceManager(ReactApplicationContext reactContext) {
return ((ReactApplication) reactContext.getApplicationContext())
.getReactNativeHost()
.getReactInstanceManager();
}
}

View File

@@ -0,0 +1,63 @@
package com.swmansion.reanimated;
import android.os.Build;
import android.util.Log;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ReanimatedUIImplementation;
import com.facebook.react.uimanager.ReanimatedUIManager;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.ViewManagerRegistry;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
public class ReanimatedUIManagerFactory {
static UIManagerModule create(
ReactApplicationContext reactContext,
List<ViewManager> viewManagers,
int minTimeLeftInFrameForNonBatchedOperationMs) {
ViewManagerRegistry viewManagerRegistry = new ViewManagerRegistry(viewManagers);
UIManagerModule uiManagerModule =
new ReanimatedUIManager(
reactContext, viewManagers, minTimeLeftInFrameForNonBatchedOperationMs);
UIImplementation uiImplementation =
new ReanimatedUIImplementation(
reactContext,
viewManagerRegistry,
uiManagerModule.getEventDispatcher(),
minTimeLeftInFrameForNonBatchedOperationMs);
Class clazz = uiManagerModule.getClass().getSuperclass();
if (clazz == null) {
Log.e("reanimated", "unable to resolve super class of ReanimatedUIManager");
return uiManagerModule;
}
try {
Field uiImplementationField = clazz.getDeclaredField("mUIImplementation");
uiImplementationField.setAccessible(true);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
// accessFlags is supported only by API >=23
Field modifiersField = Field.class.getDeclaredField("accessFlags");
modifiersField.setAccessible(true);
modifiersField.setInt(
uiImplementationField, uiImplementationField.getModifiers() & ~Modifier.FINAL);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
uiImplementationField.set(uiManagerModule, uiImplementation);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return uiManagerModule;
}
}

View File

@@ -0,0 +1,49 @@
package com.swmansion.reanimated;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import java.util.HashMap;
import java.util.Map;
public class Utils {
public static boolean isChromeDebugger = false;
public static Map<String, Integer> processMapping(ReadableMap style) {
ReadableMapKeySetIterator iter = style.keySetIterator();
HashMap<String, Integer> mapping = new HashMap<>();
while (iter.hasNextKey()) {
String propKey = iter.nextKey();
int nodeIndex = style.getInt(propKey);
mapping.put(propKey, nodeIndex);
}
return mapping;
}
public static int[] processIntArray(ReadableArray ary) {
int size = ary.size();
int[] res = new int[size];
for (int i = 0; i < size; i++) {
res[i] = ary.getInt(i);
}
return res;
}
public static String simplifyStringNumbersList(String list) {
// transforms string: '[1, 2, 3]' -> '1 2 3'
// to make usage of std::istringstream in C++ easier
return list.replace(",", "").replace("[", "").replace("]", "");
}
public static float convertToFloat(Object value) {
if (value instanceof Integer) {
return ((Integer) value).floatValue();
} else if (value instanceof Float) {
return (float) value;
} else if (value instanceof Double) {
return ((Double) value).floatValue();
}
return 0;
}
}

View File

@@ -0,0 +1,52 @@
package com.swmansion.reanimated.keyboard;
import androidx.core.view.WindowInsetsCompat;
import com.facebook.react.uimanager.PixelUtil;
public class Keyboard {
private KeyboardState mState;
private int mHeight = 0;
private int mActiveTransitionCounter = 0;
private static final int CONTENT_TYPE_MASK = WindowInsetsCompat.Type.ime();
private static final int SYSTEM_BAR_TYPE_MASK = WindowInsetsCompat.Type.systemBars();
public KeyboardState getState() {
return mState;
}
public int getHeight() {
return mHeight;
}
public void updateHeight(WindowInsetsCompat insets) {
int contentBottomInset = insets.getInsets(CONTENT_TYPE_MASK).bottom;
int systemBarBottomInset = insets.getInsets(SYSTEM_BAR_TYPE_MASK).bottom;
int keyboardHeightDip = contentBottomInset - systemBarBottomInset;
int keyboardHeight = (int) PixelUtil.toDIPFromPixel(Math.max(0, keyboardHeightDip));
if (keyboardHeight == 0 && mState == KeyboardState.OPEN) {
/*
When the keyboard is being canceling, for one frame the insets show a keyboard height of 0,
causing a jump of the keyboard. We can avoid it by ignoring that frame and calling
the listeners on the following frame.
*/
return;
}
mHeight = keyboardHeight;
}
public void onAnimationStart() {
if (mActiveTransitionCounter > 0) {
mState = mState == KeyboardState.OPENING ? KeyboardState.CLOSING : KeyboardState.OPENING;
} else {
mState = mHeight == 0 ? KeyboardState.OPENING : KeyboardState.CLOSING;
}
mActiveTransitionCounter++;
}
public void onAnimationEnd() {
mActiveTransitionCounter--;
if (mActiveTransitionCounter == 0) {
mState = mHeight == 0 ? KeyboardState.CLOSED : KeyboardState.OPEN;
}
}
}

View File

@@ -0,0 +1,63 @@
package com.swmansion.reanimated.keyboard;
import androidx.annotation.NonNull;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat;
import java.util.List;
public class KeyboardAnimationCallback extends WindowInsetsAnimationCompat.Callback {
private final Keyboard mKeyboard;
private final NotifyAboutKeyboardChangeFunction mNotifyAboutKeyboardChange;
private static final int CONTENT_TYPE_MASK = WindowInsetsCompat.Type.ime();
public KeyboardAnimationCallback(
Keyboard keyboard, NotifyAboutKeyboardChangeFunction notifyAboutKeyboardChange) {
super(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE);
mNotifyAboutKeyboardChange = notifyAboutKeyboardChange;
mKeyboard = keyboard;
}
@NonNull
@Override
public WindowInsetsAnimationCompat.BoundsCompat onStart(
@NonNull WindowInsetsAnimationCompat animation,
@NonNull WindowInsetsAnimationCompat.BoundsCompat bounds) {
if (!isKeyboardAnimation(animation)) {
return bounds;
}
mKeyboard.onAnimationStart();
mNotifyAboutKeyboardChange.call();
return super.onStart(animation, bounds);
}
@NonNull
@Override
public WindowInsetsCompat onProgress(
@NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
boolean isAnyKeyboardAnimationRunning = false;
for (WindowInsetsAnimationCompat animation : runningAnimations) {
if (isKeyboardAnimation(animation)) {
isAnyKeyboardAnimationRunning = true;
break;
}
}
if (isAnyKeyboardAnimationRunning) {
mKeyboard.updateHeight(insets);
mNotifyAboutKeyboardChange.call();
}
return insets;
}
@Override
public void onEnd(@NonNull WindowInsetsAnimationCompat animation) {
if (isKeyboardAnimation(animation)) {
mKeyboard.onAnimationEnd();
mNotifyAboutKeyboardChange.call();
}
}
private static boolean isKeyboardAnimation(@NonNull WindowInsetsAnimationCompat animation) {
return (animation.getTypeMask() & CONTENT_TYPE_MASK) != 0;
}
}

View File

@@ -0,0 +1,49 @@
package com.swmansion.reanimated.keyboard;
import com.facebook.react.bridge.ReactApplicationContext;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
@FunctionalInterface
interface NotifyAboutKeyboardChangeFunction {
void call();
}
public class KeyboardAnimationManager {
private int mNextListenerId = 0;
private final ConcurrentHashMap<Integer, KeyboardWorkletWrapper> mListeners =
new ConcurrentHashMap<>();
private final Keyboard mKeyboard = new Keyboard();
private final WindowsInsetsManager mWindowsInsetsManager;
public KeyboardAnimationManager(WeakReference<ReactApplicationContext> reactContext) {
mWindowsInsetsManager =
new WindowsInsetsManager(reactContext, mKeyboard, this::notifyAboutKeyboardChange);
}
public int subscribeForKeyboardUpdates(
KeyboardWorkletWrapper callback, boolean isStatusBarTranslucent) {
int listenerId = mNextListenerId++;
if (mListeners.isEmpty()) {
KeyboardAnimationCallback keyboardAnimationCallback =
new KeyboardAnimationCallback(mKeyboard, this::notifyAboutKeyboardChange);
mWindowsInsetsManager.startObservingChanges(
keyboardAnimationCallback, isStatusBarTranslucent);
}
mListeners.put(listenerId, callback);
return listenerId;
}
public void unsubscribeFromKeyboardUpdates(int listenerId) {
mListeners.remove(listenerId);
if (mListeners.isEmpty()) {
mWindowsInsetsManager.stopObservingChanges();
}
}
public void notifyAboutKeyboardChange() {
for (KeyboardWorkletWrapper listener : mListeners.values()) {
listener.invoke(mKeyboard.getState().asInt(), mKeyboard.getHeight());
}
}
}

View File

@@ -0,0 +1,19 @@
package com.swmansion.reanimated.keyboard;
public enum KeyboardState {
UNKNOWN(0),
OPENING(1),
OPEN(2),
CLOSING(3),
CLOSED(4);
private final int mValue;
KeyboardState(int value) {
mValue = value;
}
public int asInt() {
return mValue;
}
}

View File

@@ -0,0 +1,16 @@
package com.swmansion.reanimated.keyboard;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
@DoNotStrip
public class KeyboardWorkletWrapper {
@DoNotStrip private final HybridData mHybridData;
@DoNotStrip
private KeyboardWorkletWrapper(HybridData hybridData) {
mHybridData = hybridData;
}
public native void invoke(int keyboardState, int height);
}

View File

@@ -0,0 +1,98 @@
package com.swmansion.reanimated.keyboard;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.Window;
import android.widget.FrameLayout;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import java.lang.ref.WeakReference;
public class WindowsInsetsManager {
private boolean mIsStatusBarTranslucent = false;
private final WeakReference<ReactApplicationContext> mReactContext;
private final Keyboard mKeyboard;
private final NotifyAboutKeyboardChangeFunction mNotifyAboutKeyboardChange;
public WindowsInsetsManager(
WeakReference<ReactApplicationContext> reactContext,
Keyboard keyboard,
NotifyAboutKeyboardChangeFunction notifyAboutKeyboardChange) {
mReactContext = reactContext;
mKeyboard = keyboard;
mNotifyAboutKeyboardChange = notifyAboutKeyboardChange;
}
private Window getWindow() {
return mReactContext.get().getCurrentActivity().getWindow();
}
private View getRootView() {
return getWindow().getDecorView();
}
public void startObservingChanges(
KeyboardAnimationCallback keyboardAnimationCallback, boolean isStatusBarTranslucent) {
mIsStatusBarTranslucent = isStatusBarTranslucent;
updateWindowDecor(false);
ViewCompat.setOnApplyWindowInsetsListener(getRootView(), this::onApplyWindowInsetsListener);
ViewCompat.setWindowInsetsAnimationCallback(getRootView(), keyboardAnimationCallback);
}
public void stopObservingChanges() {
updateWindowDecor(!mIsStatusBarTranslucent);
updateInsets(0, 0);
View rootView = getRootView();
ViewCompat.setWindowInsetsAnimationCallback(rootView, null);
ViewCompat.setOnApplyWindowInsetsListener(rootView, null);
}
private void updateWindowDecor(boolean decorFitsSystemWindow) {
new Handler(Looper.getMainLooper())
.post(() -> WindowCompat.setDecorFitsSystemWindows(getWindow(), decorFitsSystemWindow));
}
private WindowInsetsCompat onApplyWindowInsetsListener(View view, WindowInsetsCompat insets) {
WindowInsetsCompat defaultInsets = ViewCompat.onApplyWindowInsets(view, insets);
if (mKeyboard.getState() == KeyboardState.OPEN) {
mKeyboard.updateHeight(insets);
mNotifyAboutKeyboardChange.call();
}
setWindowInsets(defaultInsets);
return defaultInsets;
}
private void setWindowInsets(WindowInsetsCompat insets) {
int systemBarsTypeMask = WindowInsetsCompat.Type.systemBars();
int paddingTop = insets.getInsets(systemBarsTypeMask).top;
int paddingBottom = insets.getInsets(systemBarsTypeMask).bottom;
updateInsets(paddingTop, paddingBottom);
}
private void updateInsets(int paddingTop, int paddingBottom) {
new Handler(Looper.getMainLooper())
.post(
() -> {
FrameLayout.LayoutParams params = getLayoutParams(paddingTop, paddingBottom);
int actionBarId = androidx.appcompat.R.id.action_bar_root;
View actionBarRootView = getRootView().findViewById(actionBarId);
actionBarRootView.setLayoutParams(params);
});
}
private FrameLayout.LayoutParams getLayoutParams(int paddingTop, int paddingBottom) {
int matchParentFlag = FrameLayout.LayoutParams.MATCH_PARENT;
FrameLayout.LayoutParams params =
new FrameLayout.LayoutParams(matchParentFlag, matchParentFlag);
if (mIsStatusBarTranslucent) {
params.setMargins(0, 0, 0, paddingBottom);
} else {
params.setMargins(0, paddingTop, 0, paddingBottom);
}
return params;
}
}

View File

@@ -0,0 +1,740 @@
package com.swmansion.reanimated.layoutReanimation;
import android.app.Activity;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.facebook.react.BuildConfig;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableNativeArray;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.IViewManagerWithChildren;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.RootView;
import com.facebook.react.uimanager.ViewManager;
import com.swmansion.reanimated.AndroidUIScheduler;
import com.swmansion.reanimated.Utils;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import javax.annotation.Nullable;
public class AnimationsManager implements ViewHierarchyObserver {
private WeakReference<AndroidUIScheduler> mWeakAndroidUIScheduler;
private ReactContext mContext;
private UIManager mUIManager;
private NativeMethodsHolder mNativeMethodsHolder;
private HashSet<Integer> mEnteringViews = new HashSet<>();
private HashMap<Integer, Rect> mEnteringViewTargetValues = new HashMap<>();
private HashMap<Integer, View> mExitingViews = new HashMap<>();
private HashMap<Integer, Integer> mExitingSubviewCountMap = new HashMap<>();
private HashSet<Integer> mAncestorsToRemove = new HashSet<>();
private HashMap<Integer, Runnable> mCallbacks = new HashMap<>();
private ReanimatedNativeHierarchyManager mReanimatedNativeHierarchyManager;
private boolean isInvalidated;
private SharedTransitionManager mSharedTransitionManager;
public void setReanimatedNativeHierarchyManager(
ReanimatedNativeHierarchyManager reanimatedNativeHierarchyManager) {
this.mReanimatedNativeHierarchyManager = reanimatedNativeHierarchyManager;
}
public ReanimatedNativeHierarchyManager getReanimatedNativeHierarchyManager() {
return mReanimatedNativeHierarchyManager;
}
public void setAndroidUIScheduler(AndroidUIScheduler androidUIScheduler) {
mWeakAndroidUIScheduler = new WeakReference<>(androidUIScheduler);
}
public AnimationsManager(ReactContext context, UIManager uiManager) {
mContext = context;
mUIManager = uiManager;
isInvalidated = false;
mSharedTransitionManager = new SharedTransitionManager(this);
}
public void invalidate() {
isInvalidated = true;
mNativeMethodsHolder = null;
mContext = null;
mUIManager = null;
mExitingViews = null;
mExitingSubviewCountMap = null;
mAncestorsToRemove = null;
mCallbacks = null;
}
@Override
public void onViewRemoval(View view, ViewGroup parent, Runnable callback) {
if (isInvalidated) {
return;
}
Integer tag = view.getId();
mCallbacks.put(tag, callback);
if (!removeOrAnimateExitRecursive(view, true, true)) {
removeView(view, parent);
}
}
@Override
public void onViewCreate(View view, ViewGroup parent, Snapshot after) {
if (isInvalidated) {
return;
}
maybeRegisterSharedView(view);
if (!hasAnimationForTag(view.getId(), LayoutAnimations.Types.ENTERING)) {
return;
}
AndroidUIScheduler androidUIScheduler = mWeakAndroidUIScheduler.get();
if (androidUIScheduler != null) {
androidUIScheduler.triggerUI();
}
int tag = view.getId();
HashMap<String, Object> targetValues = after.toTargetMap();
if (targetValues != null) {
HashMap<String, Object> preparedValues = prepareDataForAnimationWorklet(targetValues, true);
mNativeMethodsHolder.startAnimation(tag, LayoutAnimations.Types.ENTERING, preparedValues);
mEnteringViews.add(tag);
}
}
@Override
public void onViewUpdate(View view, Snapshot before, Snapshot after) {
if (isInvalidated) {
return;
}
int tag = view.getId();
if (!hasAnimationForTag(tag, LayoutAnimations.Types.LAYOUT)) {
if (mEnteringViews.contains(tag)) {
// store values to restore after `entering` finishes
mEnteringViewTargetValues.put(
tag,
new Rect(
after.originX,
after.originY,
after.originX + after.width,
after.originY + after.height));
// restore layout before the update, so the `entering`
// can continue from its current location
view.layout(
before.originX,
before.originY,
before.originX + before.width,
before.originY + before.height);
}
return;
}
HashMap<String, Object> startValues = before.toCurrentMap();
HashMap<String, Object> targetValues = after.toTargetMap();
// If startValues are equal to targetValues it means that there was no UI Operation changing
// layout of the View. So dirtiness of that View is false positive
boolean doNotStartLayout = true;
for (int i = 0; i < Snapshot.targetKeysToTransform.size(); ++i) {
double startV =
((Number) startValues.get(Snapshot.currentKeysToTransform.get(i))).doubleValue();
double targetV =
((Number) targetValues.get(Snapshot.targetKeysToTransform.get(i))).doubleValue();
if (startV != targetV) {
doNotStartLayout = false;
}
}
if (doNotStartLayout) {
return;
}
// View must be in Layout state
HashMap<String, Object> preparedStartValues =
prepareDataForAnimationWorklet(startValues, false);
HashMap<String, Object> preparedTargetValues =
prepareDataForAnimationWorklet(targetValues, true);
HashMap<String, Object> preparedValues = new HashMap<>(preparedTargetValues);
for (String key : preparedStartValues.keySet()) {
preparedValues.put(key, preparedStartValues.get(key));
}
mNativeMethodsHolder.startAnimation(tag, LayoutAnimations.Types.LAYOUT, preparedValues);
}
public void maybeRegisterSharedView(View view) {
if (hasAnimationForTag(view.getId(), LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION)) {
mSharedTransitionManager.notifyAboutNewView(view);
}
if (BuildConfig.DEBUG) {
checkDuplicateSharedTag(view);
}
}
private void checkDuplicateSharedTag(View view) {
int viewTag = view.getId();
ViewParent parent = view.getParent();
while (parent != null) {
if (parent.getClass().getSimpleName().equals("Screen")) {
break;
}
parent = (ViewParent) parent.getParent();
}
if (parent != null) {
int screenTag = ((View) parent).getId();
mNativeMethodsHolder.checkDuplicateSharedTag(viewTag, screenTag);
}
}
public void progressLayoutAnimation(
int tag, Map<String, Object> newStyle, boolean isSharedTransition) {
View view = resolveView(tag);
if (view == null) {
return;
}
ViewGroup parent = (ViewGroup) view.getParent();
if (parent == null) {
return;
}
ViewManager viewManager = resolveViewManager(tag);
ViewManager parentViewManager = resolveViewManager(parent.getId());
if (viewManager == null) {
return;
}
setNewProps(newStyle, view, viewManager, parentViewManager, parent.getId(), isSharedTransition);
}
public void endLayoutAnimation(int tag, boolean removeView) {
View view = resolveView(tag);
if (view == null) {
return;
}
Rect target = mEnteringViewTargetValues.get(tag);
if (!removeView && mEnteringViews.contains(tag) && target != null) {
view.layout(target.left, target.top, target.right, target.bottom);
}
mEnteringViews.remove(tag);
mEnteringViewTargetValues.remove(tag);
if (removeView) {
if (view instanceof ViewGroup && mAncestorsToRemove.contains(tag)) {
cancelAnimationsInSubviews((ViewGroup) view);
}
mExitingViews.remove(tag);
maybeDropAncestors(view);
ViewGroup parent = (ViewGroup) view.getParent();
removeView(view, parent);
}
mSharedTransitionManager.finishSharedAnimation(tag);
}
public void printSubTree(View view, int level) {
if (level == 0) {
Log.v("rea", "----------------------");
}
if (view == null) {
return;
}
StringBuilder out = new StringBuilder();
for (int i = 0; i < level; ++i) {
out.append("+");
}
out.append(" TAG:");
out.append(view.getId());
out.append(" CLASS:");
out.append(view.getClass().getSimpleName());
Log.v("rea", out.toString());
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); ++i) {
printSubTree(((ViewGroup) view).getChildAt(i), level + 1);
}
}
}
public HashMap<String, Object> prepareDataForAnimationWorklet(
HashMap<String, Object> values, boolean isTargetValues) {
return prepareDataForAnimationWorklet(values, isTargetValues, false);
}
public HashMap<String, Object> prepareDataForAnimationWorklet(
HashMap<String, Object> values, boolean isTargetValues, boolean addTransform) {
HashMap<String, Object> preparedValues = new HashMap<>();
ArrayList<String> keys;
if (isTargetValues) {
keys = Snapshot.targetKeysToTransform;
} else {
keys = Snapshot.currentKeysToTransform;
}
for (String key : keys) {
Object value = values.get(key);
float pixelsValue = Utils.convertToFloat(value);
float dipValue = PixelUtil.toDIPFromPixel(pixelsValue);
preparedValues.put(key, dipValue);
}
if (addTransform) {
String key =
isTargetValues ? Snapshot.TARGET_TRANSFORM_MATRIX : Snapshot.CURRENT_TRANSFORM_MATRIX;
preparedValues.put(key, values.get(key));
}
DisplayMetrics displayMetrics = new DisplayMetrics();
Activity currentActivity = mContext.getCurrentActivity();
if (currentActivity != null) {
currentActivity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
int width = displayMetrics.widthPixels;
preparedValues.put("windowWidth", PixelUtil.toDIPFromPixel(width));
preparedValues.put("windowHeight", PixelUtil.toDIPFromPixel(height));
} else {
preparedValues.put("windowWidth", PixelUtil.toDIPFromPixel(0));
preparedValues.put("windowHeight", PixelUtil.toDIPFromPixel(0));
}
return preparedValues;
}
public void setNativeMethods(NativeMethodsHolder nativeMethods) {
mNativeMethodsHolder = nativeMethods;
mSharedTransitionManager.setNativeMethods(nativeMethods);
}
public void setNewProps(
Map<String, Object> props,
View view,
ViewManager viewManager,
ViewManager parentViewManager,
Integer parentTag,
boolean isPositionAbsolute) {
float x =
(props.get(Snapshot.ORIGIN_X) != null)
? ((Double) props.get(Snapshot.ORIGIN_X)).floatValue()
: PixelUtil.toDIPFromPixel(view.getLeft());
float y =
(props.get(Snapshot.ORIGIN_Y) != null)
? ((Double) props.get(Snapshot.ORIGIN_Y)).floatValue()
: PixelUtil.toDIPFromPixel(view.getTop());
float width =
(props.get(Snapshot.WIDTH) != null)
? ((Double) props.get(Snapshot.WIDTH)).floatValue()
: PixelUtil.toDIPFromPixel(view.getWidth());
float height =
(props.get(Snapshot.HEIGHT) != null)
? ((Double) props.get(Snapshot.HEIGHT)).floatValue()
: PixelUtil.toDIPFromPixel(view.getHeight());
if (props.containsKey(Snapshot.TRANSFORM_MATRIX)) {
float[] matrixValues = new float[9];
if (props.get(Snapshot.TRANSFORM_MATRIX) instanceof ReadableNativeArray) {
// this array comes from JavaScript
ReadableNativeArray matrixArray =
(ReadableNativeArray) props.get(Snapshot.TRANSFORM_MATRIX);
for (int i = 0; i < 9; i++) {
matrixValues[i] = ((Double) matrixArray.getDouble(i)).floatValue();
}
} else {
// this array comes from Java
ArrayList<Float> casted = (ArrayList<Float>) props.get(Snapshot.TRANSFORM_MATRIX);
for (int i = 0; i < 9; i++) {
matrixValues[i] = casted.get(i);
}
}
view.setScaleX(matrixValues[0]);
view.setScaleY(matrixValues[4]);
// as far, let's support only scale and translation. Rotation maybe the future feature
// http://eecs.qmul.ac.uk/~gslabaugh/publications/euler.pdf
props.remove(Snapshot.TRANSFORM_MATRIX);
}
updateLayout(view, parentViewManager, parentTag, x, y, width, height, isPositionAbsolute);
props.remove(Snapshot.ORIGIN_X);
props.remove(Snapshot.ORIGIN_Y);
props.remove(Snapshot.GLOBAL_ORIGIN_X);
props.remove(Snapshot.GLOBAL_ORIGIN_Y);
props.remove(Snapshot.WIDTH);
props.remove(Snapshot.HEIGHT);
if (props.size() == 0) {
return;
}
JavaOnlyMap javaOnlyMap = new JavaOnlyMap();
for (String key : props.keySet()) {
addProp(javaOnlyMap, key, props.get(key));
}
viewManager.updateProperties(view, new ReactStylesDiffMap(javaOnlyMap));
}
private static void addProp(WritableMap propMap, String key, Object value) {
if (value == null) {
propMap.putNull(key);
} else if (value instanceof Double) {
propMap.putDouble(key, (Double) value);
} else if (value instanceof Integer) {
propMap.putInt(key, (Integer) value);
} else if (value instanceof Number) {
propMap.putDouble(key, ((Number) value).doubleValue());
} else if (value instanceof Boolean) {
propMap.putBoolean(key, (Boolean) value);
} else if (value instanceof String) {
propMap.putString(key, (String) value);
} else if (value instanceof ReadableArray) {
propMap.putArray(key, (ReadableArray) value);
} else if (value instanceof ReadableMap) {
propMap.putMap(key, (ReadableMap) value);
} else {
throw new IllegalStateException(
"[Reanimated] Unknown type of animated value for Layout Animations.");
}
}
public void updateLayout(
View viewToUpdate,
ViewManager parentViewManager,
int parentTag,
float xf,
float yf,
float widthf,
float heightf,
boolean isPositionAbsolute) {
int x = Math.round(PixelUtil.toPixelFromDIP(xf));
int y = Math.round(PixelUtil.toPixelFromDIP(yf));
int width = Math.round(PixelUtil.toPixelFromDIP(widthf));
int height = Math.round(PixelUtil.toPixelFromDIP(heightf));
// Even though we have exact dimensions, we still call measure because some platform views
// (e.g.
// Switch) assume that method will always be called before onLayout and onDraw. They use it to
// calculate and cache information used in the draw pass. For most views, onMeasure can be
// stubbed out to only call setMeasuredDimensions. For ViewGroups, onLayout should be stubbed
// out to not recursively call layout on its children: React Native already handles doing
// that.
//
// Also, note measure and layout need to be called *after* all View properties have been
// updated
// because of caching and calculation that may occur in onMeasure and onLayout. Layout
// operations should also follow the native view hierarchy and go top to bottom for
// consistency
// with standard layout passes (some views may depend on this).
viewToUpdate.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
// We update the layout of the ReactRootView when there is a change in the layout of its
// child.
// This is required to re-measure the size of the native View container (usually a
// FrameLayout) that is configured with layout_height = WRAP_CONTENT or layout_width =
// WRAP_CONTENT
//
// This code is going to be executed ONLY when there is a change in the size of the Root
// View defined in the js side. Changes in the layout of inner views will not trigger an
// update
// on the layout of the Root View.
ViewParent parent = viewToUpdate.getParent();
if (parent instanceof RootView) {
parent.requestLayout();
}
// Check if the parent of the view has to layout the view, or the child has to lay itself out.
if (parentTag % 10 == 1 && parentViewManager != null) { // parentTag % 10 == 1 - ParentIsARoot
IViewManagerWithChildren parentViewManagerWithChildren;
if (parentViewManager instanceof IViewManagerWithChildren) {
parentViewManagerWithChildren = (IViewManagerWithChildren) parentViewManager;
} else {
throw new IllegalViewOperationException(
"[Reanimated] Trying to use view with tag "
+ parentTag
+ " as a parent, but its Manager doesn't implement IViewManagerWithChildren.");
}
if (!parentViewManagerWithChildren.needsCustomLayoutForChildren()) {
viewToUpdate.layout(x, y, x + width, y + height);
}
} else {
if (isPositionAbsolute) {
Point newPoint = new Point(x, y);
View viewToUpdateParent = (View) viewToUpdate.getParent();
Point convertedPoint = convertScreenLocationToViewCoordinates(newPoint, viewToUpdateParent);
x = convertedPoint.x;
y = convertedPoint.y;
}
viewToUpdate.layout(x, y, x + width, y + height);
}
}
public boolean shouldAnimateExiting(int tag, boolean shouldAnimate) {
return mNativeMethodsHolder.shouldAnimateExiting(tag, shouldAnimate);
}
public boolean hasAnimationForTag(int tag, int type) {
return mNativeMethodsHolder.hasAnimation(tag, type);
}
public boolean isLayoutAnimationEnabled() {
return mNativeMethodsHolder != null && mNativeMethodsHolder.isLayoutAnimationEnabled();
}
private boolean removeOrAnimateExitRecursive(
View view, boolean shouldRemove, boolean shouldAnimate) {
int tag = view.getId();
ViewManager viewManager = resolveViewManager(tag);
if (viewManager != null) {
String viewManagerName = viewManager.getName();
if (viewManagerName.equals("RCTModalHostView")
|| viewManagerName.equals("RNSScreen")
|| viewManagerName.equals("RNSScreenStack")) {
// don't run exiting animation when ScreenStack, Screen, or Modal are removing
cancelAnimationsRecursive(view);
return false;
}
}
shouldAnimate = shouldAnimateExiting(tag, shouldAnimate);
boolean hasExitAnimation =
shouldAnimate
&& (hasAnimationForTag(tag, LayoutAnimations.Types.EXITING)
|| mExitingViews.containsKey(tag));
boolean hasAnimatedChildren = false;
shouldRemove = shouldRemove && !hasExitAnimation;
if (hasAnimationForTag(tag, LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION)) {
mSharedTransitionManager.notifyAboutRemovedView(view);
mSharedTransitionManager.makeSnapshot(view);
}
ArrayList<View> toBeRemoved = new ArrayList<>();
// we might want to keep this view around
// because one of the (children's) children
// has an exiting animation
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
View child = viewGroup.getChildAt(i);
if (removeOrAnimateExitRecursive(child, shouldRemove, shouldAnimate)) {
hasAnimatedChildren = true;
} else if (shouldRemove && child.getId() != -1) {
toBeRemoved.add(child);
}
}
}
boolean wantAnimateExit = hasExitAnimation || hasAnimatedChildren;
if (hasExitAnimation) {
Snapshot before = new Snapshot(view, mReanimatedNativeHierarchyManager);
HashMap<String, Object> currentValues = before.toCurrentMap();
HashMap<String, Object> preparedValues = prepareDataForAnimationWorklet(currentValues, false);
if (!mExitingViews.containsKey(tag)) {
mExitingViews.put(tag, view);
registerExitingAncestors(view);
mNativeMethodsHolder.startAnimation(tag, LayoutAnimations.Types.EXITING, preparedValues);
}
}
mNativeMethodsHolder.clearAnimationConfig(tag);
if (!wantAnimateExit) {
return false;
}
if (hasAnimatedChildren) {
if (tag == -1) {
// View tags are used to identify react views, therefore native-only views
// don't have any view tag and view.getId returns -1
// We shouldn't manage lifetime of non-react components.
cancelAnimationsRecursive(view);
return false;
}
mAncestorsToRemove.add(tag);
}
for (View child : toBeRemoved) {
removeView(child, (ViewGroup) view);
}
return true;
}
public void clearAnimationConfigRecursive(View view) {
mNativeMethodsHolder.clearAnimationConfig(view.getId());
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
clearAnimationConfigRecursive(viewGroup.getChildAt(i));
}
}
}
public void cancelAnimationsInSubviews(View view) {
cancelAnimationsRecursive(view);
clearAnimationConfigRecursive(view);
}
private void registerExitingAncestors(View view) {
View parent = (View) view.getParent();
while (parent != null && !(parent instanceof RootView)) {
int tag = parent.getId();
Integer previousValue = mExitingSubviewCountMap.get(tag);
int newValue = previousValue != null ? previousValue + 1 : 1;
mExitingSubviewCountMap.put(tag, newValue);
parent = (View) parent.getParent();
}
}
private void maybeDropAncestors(View exitingView) {
if (!(exitingView.getParent() instanceof View)) {
return;
}
View parent = (View) exitingView.getParent();
while (parent != null && !(parent instanceof RootView)) {
View view = parent;
parent = (View) view.getParent();
int tag = view.getId();
Integer ancestorOfCount = mExitingSubviewCountMap.get(tag);
ancestorOfCount = ancestorOfCount != null ? ancestorOfCount - 1 : 0;
if (ancestorOfCount <= 0) {
if (mAncestorsToRemove.contains(tag)) {
mAncestorsToRemove.remove(tag);
if (!mExitingViews.containsKey(tag)) {
removeView(view, (ViewGroup) parent);
}
}
mExitingSubviewCountMap.remove(tag);
} else {
mExitingSubviewCountMap.put(tag, ancestorOfCount);
}
}
}
private void removeView(View view, @Nullable ViewGroup parent) {
int tag = view.getId();
if (mCallbacks.containsKey(tag)) {
Runnable callback = mCallbacks.get(tag);
mCallbacks.remove(tag);
if (callback != null) {
callback.run();
}
} else {
mReanimatedNativeHierarchyManager.publicDropView(view);
}
// this removal might be redundant, however we decided to keep it for now to avoid introducing
// breaking changes
if (parent != null && parent.indexOfChild(view) != -1) {
parent.removeView(view);
}
}
public void cancelAnimationsRecursive(View view) {
if (mExitingViews.containsKey(view.getId())) {
endLayoutAnimation(view.getId(), true);
} else if (view instanceof ViewGroup && mExitingSubviewCountMap.containsKey(view.getId())) {
cancelAnimationsInSubviews((ViewGroup) view);
}
}
private void cancelAnimationsInSubviews(ViewGroup view) {
for (int i = view.getChildCount() - 1; i >= 0; i--) {
View child = view.getChildAt(i);
if (child == null) {
continue;
}
if (mExitingViews.containsKey(child.getId())) {
endLayoutAnimation(child.getId(), true);
} else if (child instanceof ViewGroup && mExitingSubviewCountMap.containsKey(child.getId())) {
cancelAnimationsInSubviews((ViewGroup) child);
}
}
}
private View resolveView(int tag) {
if (mExitingViews.containsKey(tag)) {
return mExitingViews.get(tag);
} else {
View view = mSharedTransitionManager.getTransitioningView(tag);
if (view != null) {
return view;
}
}
try {
return mUIManager.resolveView(tag);
} catch (IllegalViewOperationException e) {
// view has been removed already
return null;
}
}
private ViewManager resolveViewManager(int tag) {
try {
return mReanimatedNativeHierarchyManager.resolveViewManager(tag);
} catch (Exception e) {
return null;
}
}
private static Point convertScreenLocationToViewCoordinates(Point fromPoint, View parentView) {
int[] toPoint = {0, 0};
if (parentView != null) {
parentView.getLocationOnScreen(toPoint);
}
return new Point(fromPoint.x - toPoint[0], fromPoint.y - toPoint[1]);
}
public void screenDidLayout(View view) {
mSharedTransitionManager.screenDidLayout(view);
}
public void viewDidLayout(View view) {
mSharedTransitionManager.viewDidLayout(view);
}
public void notifyAboutViewsRemoval(int[] tagsToDelete) {
mSharedTransitionManager.onViewsRemoval(tagsToDelete);
}
public void notifyAboutScreenWillDisappear() {
mSharedTransitionManager.onScreenWillDisappear();
}
public void makeSnapshotOfTopScreenViews(ViewGroup stack) {
mSharedTransitionManager.doSnapshotForTopScreenViews(stack);
}
protected ReactContext getContext() {
return mContext;
}
}

View File

@@ -0,0 +1,89 @@
package com.swmansion.reanimated.layoutReanimation;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.soloader.SoLoader;
import com.swmansion.reanimated.ReanimatedModule;
import java.lang.ref.WeakReference;
import java.util.Map;
public class LayoutAnimations {
public static class Types {
static final int ENTERING = 1;
static final int EXITING = 2;
static final int LAYOUT = 3;
static final int SHARED_ELEMENT_TRANSITION = 4;
static final int SHARED_ELEMENT_TRANSITION_PROGRESS = 5;
}
static {
SoLoader.loadLibrary("reanimated");
}
@DoNotStrip
@SuppressWarnings("unused")
private final HybridData mHybridData;
private final WeakReference<ReactApplicationContext> mContext;
private WeakReference<AnimationsManager> mWeakAnimationsManager = new WeakReference<>(null);
public LayoutAnimations(ReactApplicationContext context) {
mContext = new WeakReference<>(context);
mHybridData = initHybrid();
}
private native HybridData initHybrid();
// LayoutReanimation
public native void startAnimationForTag(int tag, int type, Map<String, String> values);
public native boolean hasAnimationForTag(int tag, int type);
public native boolean shouldAnimateExiting(int tag, boolean shouldAnimate);
public native void checkDuplicateSharedTag(int viewTag, int screenTag);
public native void clearAnimationConfigForTag(int tag);
public native void cancelAnimationForTag(int tag);
public native boolean isLayoutAnimationEnabled();
public native int findPrecedingViewTagForTransition(int tag);
private void endLayoutAnimation(int tag, boolean removeView) {
AnimationsManager animationsManager = getAnimationsManager();
if (animationsManager == null) {
return;
}
animationsManager.endLayoutAnimation(tag, removeView);
}
private void progressLayoutAnimation(
int tag, Map<String, Object> newStyle, boolean isSharedTransition) {
AnimationsManager animationsManager = getAnimationsManager();
if (animationsManager == null) {
return;
}
animationsManager.progressLayoutAnimation(tag, newStyle, isSharedTransition);
}
private AnimationsManager getAnimationsManager() {
AnimationsManager animationsManager = mWeakAnimationsManager.get();
if (animationsManager != null) {
return mWeakAnimationsManager.get();
}
ReactApplicationContext context = mContext.get();
if (context == null) {
return null;
}
animationsManager =
context.getNativeModule(ReanimatedModule.class).getNodesManager().getAnimationsManager();
mWeakAnimationsManager = new WeakReference<>(animationsManager);
return animationsManager;
}
}

View File

@@ -0,0 +1,21 @@
package com.swmansion.reanimated.layoutReanimation;
import java.util.HashMap;
public interface NativeMethodsHolder {
void startAnimation(int tag, int type, HashMap<String, Object> values);
boolean shouldAnimateExiting(int tag, boolean shouldAnimate);
boolean hasAnimation(int tag, int type);
void clearAnimationConfig(int tag);
void cancelAnimation(int tag);
boolean isLayoutAnimationEnabled();
int findPrecedingViewTagForTransition(int tag);
void checkDuplicateSharedTag(int viewTag, int screenTag);
}

View File

@@ -0,0 +1,441 @@
package com.swmansion.reanimated.layoutReanimation;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.ViewAtIndex;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.ViewManagerRegistry;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController;
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationListener;
import com.swmansion.reanimated.ReanimatedModule;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
class ReaLayoutAnimator extends LayoutAnimationController {
private AnimationsManager mAnimationsManager = null;
private volatile boolean mInitialized = false;
private final ReactApplicationContext mContext;
private final WeakReference<NativeViewHierarchyManager> mWeakNativeViewHierarchyManager;
private final ArrayList<View> viewsToSnapshot = new ArrayList<>();
ReaLayoutAnimator(
ReactApplicationContext context, NativeViewHierarchyManager nativeViewHierarchyManager) {
mContext = context;
mWeakNativeViewHierarchyManager = new WeakReference<>(nativeViewHierarchyManager);
}
public void maybeInit() {
if (!mInitialized) {
mInitialized = true;
ReanimatedModule reanimatedModule = mContext.getNativeModule(ReanimatedModule.class);
mAnimationsManager = reanimatedModule.getNodesManager().getAnimationsManager();
mAnimationsManager.setReanimatedNativeHierarchyManager(
(ReanimatedNativeHierarchyManager) mWeakNativeViewHierarchyManager.get());
}
}
public boolean shouldAnimateLayout(View viewToAnimate) {
if (!isLayoutAnimationEnabled()) {
return super.shouldAnimateLayout(viewToAnimate);
}
// if view parent is null, skip animation: view have been clipped, we don't want animation to
// resume when view is re-attached to parent, which is the standard android animation behavior.
// If there's a layout handling animation going on, it should be animated nonetheless since the
// ongoing animation needs to be updated.
if (viewToAnimate == null) {
return false;
}
return (viewToAnimate.getParent() != null);
}
@Override
public void reset() {
super.reset();
// we have to make snapshots of the views after all of them have updated layouts
// to have correct global coordinates in the snapshots
// we do it here because React calls reset() method after all views have updated layouts
// and there is no semantically valid place to do it
for (View view : viewsToSnapshot) {
mAnimationsManager.onViewCreate(
view,
(ViewGroup) view.getParent(),
new Snapshot(view, mWeakNativeViewHierarchyManager.get()));
}
viewsToSnapshot.clear();
}
/**
* Update layout of given view, via immediate update or animation depending on the current batch
* layout animation configuration supplied during initialization. Handles create and update
* animations.
*
* @param view the view to update layout of
* @param x the new X position for the view
* @param y the new Y position for the view
* @param width the new width value for the view
* @param height the new height value for the view
*/
public void applyLayoutUpdate(View view, int x, int y, int width, int height) {
if (!isLayoutAnimationEnabled()) {
super.applyLayoutUpdate(view, x, y, width, height);
return;
}
UiThreadUtil.assertOnUiThread();
maybeInit();
// Determine which animation to use : if view is initially invisible, use create animation,
// otherwise use update animation. This approach is easier than maintaining a list of tags
// for recently created views.
if (view.getWidth() == 0 || view.getHeight() == 0) {
if (!mAnimationsManager.hasAnimationForTag(view.getId(), LayoutAnimations.Types.ENTERING)) {
super.applyLayoutUpdate(view, x, y, width, height);
mAnimationsManager.maybeRegisterSharedView(view);
return;
}
view.layout(x, y, x + width, y + height);
if (view.getId() != -1) {
viewsToSnapshot.add(view);
}
return;
}
Snapshot before = new Snapshot(view, mWeakNativeViewHierarchyManager.get());
view.layout(x, y, x + width, y + height);
Snapshot after = new Snapshot(view, mWeakNativeViewHierarchyManager.get());
mAnimationsManager.onViewUpdate(view, before, after);
}
/**
* Animate a view deletion using the layout animation configuration supplied during
* initialization.
*
* @param view The view to animate.
* @param listener Called once the animation is finished, should be used to completely remove the
* view.
*/
public void deleteView(final View view, final LayoutAnimationListener listener) {
if (!isLayoutAnimationEnabled()) {
super.deleteView(view, listener);
return;
}
UiThreadUtil.assertOnUiThread();
NativeViewHierarchyManager nativeViewHierarchyManager = mWeakNativeViewHierarchyManager.get();
ViewManager viewManager;
try {
viewManager = nativeViewHierarchyManager.resolveViewManager(view.getId());
} catch (IllegalViewOperationException e) {
// (IllegalViewOperationException) == (vm == null)
e.printStackTrace();
mAnimationsManager.cancelAnimationsInSubviews(view);
super.deleteView(view, listener);
return;
}
// we don't want layout animations in native-stack since it is currently buggy there
// so we check if it is a (grand)child of ScreenStack
if (viewManager.getName().equals("RNSScreen")
&& view.getParent() != null
&& view.getParent().getParent() instanceof View) {
// we check grandparent of Screen since the parent is a ScreenStackFragment
View screenParentView = (View) view.getParent().getParent();
ViewManager screenParentViewManager;
try {
screenParentViewManager =
nativeViewHierarchyManager.resolveViewManager(screenParentView.getId());
} catch (IllegalViewOperationException e) {
// (IllegalViewOperationException) == (vm == null)
e.printStackTrace();
mAnimationsManager.cancelAnimationsInSubviews(view);
super.deleteView(view, listener);
return;
}
String parentName = screenParentViewManager.getName();
if (parentName.equals("RNSScreenStack")) {
mAnimationsManager.cancelAnimationsInSubviews(view);
super.deleteView(view, listener);
EventDispatcher eventDispatcher =
UIManagerHelper.getEventDispatcherForReactTag(
(ReactContext) view.getContext(), view.getId());
if (eventDispatcher != null) {
eventDispatcher.addListener(
event -> {
// we schedule the start of transition for the ScreenWilDisappear event, so that the
// layout of the target screen is already calculated
// this allows us to make snapshots on the go, so that they are always up-to-date
if (event.getEventName().equals("topWillDisappear")) {
getAnimationsManager().notifyAboutScreenWillDisappear();
}
});
}
return;
}
}
maybeInit();
mAnimationsManager.onViewRemoval(view, (ViewGroup) view.getParent(), listener::onAnimationEnd);
}
public boolean isLayoutAnimationEnabled() {
// In case the user rapidly reloads the app, there is a possibility that the active instance may
// not be available.
// However, the code will still attempt to trigger the layout animation of views that are going
// to be dropped.
// This is required as without it, the `mAnimationsManager.isLayoutAnimationEnabled`
// would crash when trying to get the uiManager from the context.
if (!mContext.hasActiveReactInstance()) {
return false;
}
maybeInit();
return mAnimationsManager.isLayoutAnimationEnabled();
}
public AnimationsManager getAnimationsManager() {
return mAnimationsManager;
}
}
public class ReanimatedNativeHierarchyManager extends NativeViewHierarchyManager {
private final HashMap<Integer, ArrayList<View>> toBeRemoved = new HashMap<>();
private final HashMap<Integer, Runnable> cleanerCallback = new HashMap<>();
private final ReaLayoutAnimator mReaLayoutAnimator;
private final HashMap<Integer, Set<Integer>> mPendingDeletionsForTag = new HashMap<>();
private boolean initOk = true;
public ReanimatedNativeHierarchyManager(
ViewManagerRegistry viewManagers, ReactApplicationContext reactContext) {
super(viewManagers);
mReaLayoutAnimator = new ReaLayoutAnimator(reactContext, this);
Class<?> clazz = this.getClass().getSuperclass();
if (clazz == null) {
Log.e("reanimated", "unable to resolve super class of ReanimatedNativeHierarchyManager");
return;
}
try {
Field layoutAnimatorField = clazz.getDeclaredField("mLayoutAnimator");
layoutAnimatorField.setAccessible(true);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
// accessFlags is supported only by API >=23
Field modifiersField = Field.class.getDeclaredField("accessFlags");
modifiersField.setAccessible(true);
modifiersField.setInt(
layoutAnimatorField, layoutAnimatorField.getModifiers() & ~Modifier.FINAL);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
layoutAnimatorField.set(this, mReaLayoutAnimator);
} catch (NoSuchFieldException | IllegalAccessException e) {
initOk = false;
e.printStackTrace();
}
try {
Field pendingTagsField = clazz.getDeclaredField("mPendingDeletionsForTag");
pendingTagsField.setAccessible(true);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
// accessFlags is supported only by API >=23
Field pendingTagsFieldModifiers = Field.class.getDeclaredField("accessFlags");
pendingTagsFieldModifiers.setAccessible(true);
pendingTagsFieldModifiers.setInt(
pendingTagsField, pendingTagsField.getModifiers() & ~Modifier.FINAL);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
pendingTagsField.set(this, mPendingDeletionsForTag);
} catch (NoSuchFieldException | IllegalAccessException e) {
initOk = false;
e.printStackTrace();
}
if (initOk) {
setLayoutAnimationEnabled(true);
}
}
private boolean isLayoutAnimationDisabled() {
return !initOk || !mReaLayoutAnimator.isLayoutAnimationEnabled();
}
public synchronized void updateLayout(
int parentTag, int tag, int x, int y, int width, int height) {
super.updateLayout(parentTag, tag, x, y, width, height);
if (isLayoutAnimationDisabled()) {
return;
}
try {
ViewManager viewManager = resolveViewManager(tag);
String viewManagerName = viewManager.getName();
View container = resolveView(parentTag);
if (container != null && viewManagerName.equals("RNSScreen") && mReaLayoutAnimator != null) {
boolean hasHeader = checkIfTopScreenHasHeader((ViewGroup) container);
if (!hasHeader || !container.isLayoutRequested()) {
mReaLayoutAnimator.getAnimationsManager().screenDidLayout(container);
}
}
View view = resolveView(tag);
if (view != null && mReaLayoutAnimator != null) {
mReaLayoutAnimator.getAnimationsManager().viewDidLayout(view);
}
} catch (IllegalViewOperationException e) {
// (IllegalViewOperationException) == (vm == null)
e.printStackTrace();
}
}
private boolean checkIfTopScreenHasHeader(ViewGroup screenStack) {
try {
ViewGroup fragment = (ViewGroup) screenStack.getChildAt(0);
ViewGroup screen = (ViewGroup) fragment.getChildAt(0);
View headerConfig = screen.getChildAt(0);
Field field = headerConfig.getClass().getDeclaredField("mIsHidden");
field.setAccessible(true);
return !field.getBoolean(headerConfig);
} catch (NullPointerException | NoSuchFieldException | IllegalAccessException e) {
return false;
}
}
@Override
public synchronized void manageChildren(
int tag,
@Nullable int[] indicesToRemove,
@Nullable ViewAtIndex[] viewsToAdd,
@Nullable int[] tagsToDelete) {
if (isLayoutAnimationDisabled()) {
super.manageChildren(tag, indicesToRemove, viewsToAdd, tagsToDelete);
return;
}
ViewGroup viewGroup;
ViewGroupManager viewGroupManager;
try {
viewGroup = (ViewGroup) resolveView(tag);
viewGroupManager = (ViewGroupManager) resolveViewManager(tag);
} catch (IllegalViewOperationException e) {
// (IllegalViewOperationException) == (vm == null)
e.printStackTrace();
super.manageChildren(tag, indicesToRemove, viewsToAdd, tagsToDelete);
return;
}
// we don't want layout animations in native-stack since it is currently buggy there
AnimationsManager animationsManager = mReaLayoutAnimator.getAnimationsManager();
if (viewGroupManager.getName().equals("RNSScreenStack")) {
if (tagsToDelete == null) {
animationsManager.makeSnapshotOfTopScreenViews(viewGroup);
} else {
animationsManager.notifyAboutViewsRemoval(tagsToDelete);
}
if (indicesToRemove != null && mReaLayoutAnimator instanceof ReaLayoutAnimator) {
for (int index : indicesToRemove) {
View child = viewGroupManager.getChildAt(viewGroup, index);
mReaLayoutAnimator.getAnimationsManager().cancelAnimationsInSubviews(child);
}
}
super.manageChildren(tag, indicesToRemove, viewsToAdd, tagsToDelete);
return;
}
if (toBeRemoved.containsKey(tag)) {
ArrayList<View> childrenToBeRemoved = toBeRemoved.get(tag);
HashSet<Integer> tagsToRemove = new HashSet<>();
for (View childToRemove : childrenToBeRemoved) {
tagsToRemove.add(childToRemove.getId());
}
while (viewGroupManager.getChildCount(viewGroup) != 0) {
View child =
viewGroupManager.getChildAt(viewGroup, viewGroupManager.getChildCount(viewGroup) - 1);
if (tagsToRemove.contains(child.getId())) {
viewGroupManager.removeViewAt(viewGroup, viewGroupManager.getChildCount(viewGroup) - 1);
} else {
break;
}
}
}
if (tagsToDelete != null) {
if (!toBeRemoved.containsKey(tag)) {
toBeRemoved.put(tag, new ArrayList<>());
}
ArrayList<View> toBeRemovedChildren = toBeRemoved.get(tag);
for (Integer childTag : tagsToDelete) {
View view;
try {
view = resolveView(childTag);
} catch (IllegalViewOperationException e) {
// (IllegalViewOperationException) == (vm == null)
e.printStackTrace();
continue;
}
toBeRemovedChildren.add(view);
// It's far from optimal but let's leave it as it is for now
cleanerCallback.put(
view.getId(),
() -> {
toBeRemovedChildren.remove(view);
viewGroupManager.removeView(viewGroup, view);
});
}
}
// mPendingDeletionsForTag is modify by React
if (mPendingDeletionsForTag != null) {
Set<Integer> pendingTags = mPendingDeletionsForTag.get(tag);
if (pendingTags != null) {
pendingTags.clear();
}
}
animationsManager.notifyAboutViewsRemoval(tagsToDelete);
super.manageChildren(tag, indicesToRemove, viewsToAdd, null);
if (toBeRemoved.containsKey(tag)) {
ArrayList<View> childrenToBeRemoved = toBeRemoved.get(tag);
for (View child : childrenToBeRemoved) {
viewGroupManager.addView(viewGroup, child, viewGroupManager.getChildCount(viewGroup));
}
}
super.manageChildren(tag, null, null, tagsToDelete);
}
public void publicDropView(View view) {
dropView(view);
}
@Override
protected synchronized void dropView(View view) {
if (isLayoutAnimationDisabled()) {
super.dropView(view);
return;
}
if (toBeRemoved.containsKey(view.getId())) {
toBeRemoved.remove(view.getId());
}
if (cleanerCallback.containsKey(view.getId())) {
Runnable runnable = cleanerCallback.get(view.getId());
cleanerCallback.remove(view.getId());
runnable.run();
}
// childrens' callbacks should be cleaned by former publicDropView calls as Animation Manager
// stripes views from bottom to top
super.dropView(view);
}
}

View File

@@ -0,0 +1,68 @@
package com.facebook.react.uimanager;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.swmansion.reanimated.layoutReanimation.ReanimatedNativeHierarchyManager;
import java.util.List;
public class ReanimatedUIImplementation extends UIImplementation {
public ReanimatedUIImplementation(
ReactApplicationContext reactContext,
ViewManagerResolver viewManagerResolver,
EventDispatcher eventDispatcher,
int minTimeLeftInFrameForNonBatchedOperationMs) {
this(
reactContext,
new ViewManagerRegistry(viewManagerResolver),
eventDispatcher,
minTimeLeftInFrameForNonBatchedOperationMs);
}
public ReanimatedUIImplementation(
ReactApplicationContext reactContext,
List<ViewManager> viewManagerList,
EventDispatcher eventDispatcher,
int minTimeLeftInFrameForNonBatchedOperationMs) {
this(
reactContext,
new ViewManagerRegistry(viewManagerList),
eventDispatcher,
minTimeLeftInFrameForNonBatchedOperationMs);
}
public ReanimatedUIImplementation(
ReactApplicationContext reactContext,
ViewManagerRegistry viewManagerRegistry,
EventDispatcher eventDispatcher,
int minTimeLeftInFrameForNonBatchedOperationMs) {
super(
reactContext,
viewManagerRegistry,
new UIViewOperationQueue(
reactContext,
new ReanimatedNativeHierarchyManager(viewManagerRegistry, reactContext),
minTimeLeftInFrameForNonBatchedOperationMs),
eventDispatcher);
}
/**
* Invoked when there is a mutation in a node tree.
*
* @param tag react tag of the node we want to manage
* @param indicesToRemove ordered (asc) list of indicies at which view should be removed
* @param viewsToAdd ordered (asc based on mIndex property) list of tag-index pairs that represent
* a view which should be added at the specified index
* @param tagsToDelete list of tags corresponding to views that should be removed
*/
public void manageChildren(
int viewTag,
@Nullable ReadableArray moveFrom,
@Nullable ReadableArray moveTo,
@Nullable ReadableArray addChildTags,
@Nullable ReadableArray addAtIndices,
@Nullable ReadableArray removeFrom) {
super.manageChildren(viewTag, moveFrom, moveTo, addChildTags, addAtIndices, removeFrom);
}
}

View File

@@ -0,0 +1,19 @@
package com.swmansion.reanimated.layoutReanimation;
import android.view.View;
public class SharedElement {
public View sourceView;
public Snapshot sourceViewSnapshot;
public View targetView;
public Snapshot targetViewSnapshot;
public SharedElement(
View sourceView, Snapshot sourceViewSnapshot, View targetView, Snapshot targetViewSnapshot) {
this.sourceView = sourceView;
this.sourceViewSnapshot = sourceViewSnapshot;
this.targetView = targetView;
this.targetViewSnapshot = targetViewSnapshot;
}
}

View File

@@ -0,0 +1,696 @@
package com.swmansion.reanimated.layoutReanimation;
import android.app.Activity;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.EventDispatcherListener;
import com.facebook.react.views.view.ReactViewGroup;
import com.swmansion.reanimated.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.Nullable;
public class SharedTransitionManager {
private final AnimationsManager mAnimationsManager;
private NativeMethodsHolder mNativeMethodsHolder;
private final List<View> mAddedSharedViews = new ArrayList<>();
private final Map<Integer, View> mSharedTransitionParent = new HashMap<>();
private final Map<Integer, Integer> mSharedTransitionInParentIndex = new HashMap<>();
private final Map<Integer, Snapshot> mSnapshotRegistry = new HashMap<>();
private final Map<Integer, View> mCurrentSharedTransitionViews = new HashMap<>();
private final Map<Integer, SortedSet<Integer>> mSharedViewChildrenIndices = new HashMap<>();
private View mTransitionContainer;
private final List<View> mRemovedSharedViews = new ArrayList<>();
private final Set<Integer> mViewTagsToHide = new HashSet<>();
private final Map<Integer, Integer> mDisableCleaningForViewTag = new HashMap<>();
private List<SharedElement> mSharedElements = new ArrayList<>();
private final Map<Integer, SharedElement> mSharedElementsLookup = new HashMap<>();
private final List<SharedElement> mSharedElementsWithProgress = new ArrayList<>();
private final List<SharedElement> mSharedElementsWithAnimation = new ArrayList<>();
private final Set<View> mReattachedViews = new HashSet<>();
private boolean mIsTransitionPrepared = false;
private final Set<Integer> mTagsToCleanup = new HashSet<>();
class TopWillAppearListener implements EventDispatcherListener {
private final EventDispatcher mEventDispatcher;
public TopWillAppearListener(EventDispatcher eventDispatcher) {
mEventDispatcher = eventDispatcher;
}
@Override
public void onEventDispatch(Event event) {
if (event.getEventName().equals("topWillAppear")) {
tryStartSharedTransitionForViews(mAddedSharedViews, true);
mAddedSharedViews.clear();
mEventDispatcher.removeListener(this);
}
}
}
public SharedTransitionManager(AnimationsManager animationsManager) {
mAnimationsManager = animationsManager;
}
protected void notifyAboutNewView(View view) {
mAddedSharedViews.add(view);
}
protected void notifyAboutRemovedView(View view) {
mRemovedSharedViews.add(view);
}
@Nullable
protected View getTransitioningView(int tag) {
return mCurrentSharedTransitionViews.get(tag);
}
protected void screenDidLayout(View view) {
if (mAddedSharedViews.isEmpty()) {
return;
}
EventDispatcher eventDispatcher =
UIManagerHelper.getEventDispatcherForReactTag(
(ReactContext) view.getContext(), view.getId());
if (eventDispatcher != null) {
eventDispatcher.addListener(new TopWillAppearListener(eventDispatcher));
}
}
protected void viewDidLayout(View view) {
// this was causing problems when I moved the start of transition to the willAppear event
// handler
// maybeRestartAnimationWithNewLayout(view);
}
protected void onViewsRemoval(int[] tagsToDelete) {
if (tagsToDelete == null) {
return;
}
visitTreeForTags(tagsToDelete, new SnapshotTreeVisitor());
if (mRemovedSharedViews.size() > 0) {
// this happens when navigation goes back
mIsTransitionPrepared = prepareSharedTransition(mRemovedSharedViews, false);
if (!mIsTransitionPrepared) {
mRemovedSharedViews.clear();
}
visitTreeForTags(tagsToDelete, new PrepareConfigCleanupTreeVisitor());
}
}
protected void doSnapshotForTopScreenViews(ViewGroup stack) {
int screensCount = stack.getChildCount();
if (screensCount > 0) {
View firstStackChild = stack.getChildAt(0);
if (firstStackChild instanceof ViewGroup) {
View screen = ((ViewGroup) firstStackChild).getChildAt(0);
visitNativeTreeAndMakeSnapshot(screen);
} else {
Log.e("[Reanimated]", "Unable to recognize screen on stack.");
}
}
}
protected void setNativeMethods(NativeMethodsHolder nativeMethods) {
mNativeMethodsHolder = nativeMethods;
}
private void maybeRestartAnimationWithNewLayout(View view) {
View sharedView = mCurrentSharedTransitionViews.get(view.getId());
if (sharedView == null) {
return;
}
List<SharedElement> sharedElementsToRestart = new ArrayList<>();
for (SharedElement sharedElement : mSharedElements) {
if (sharedElement.targetView == sharedView) {
sharedElementsToRestart.add(sharedElement);
View sourceView = sharedElement.sourceView;
View targetView = sharedElement.targetView;
Snapshot newSourceViewSnapshot = new Snapshot(sourceView);
Snapshot currentTargetViewSnapshot = mSnapshotRegistry.get(targetView.getId());
Snapshot newTargetViewSnapshot = new Snapshot(targetView);
int newOriginX =
currentTargetViewSnapshot.originX
- currentTargetViewSnapshot.originXByParent
+ newTargetViewSnapshot.originX;
int newOriginY =
currentTargetViewSnapshot.originY
- currentTargetViewSnapshot.originYByParent
+ newTargetViewSnapshot.originY;
currentTargetViewSnapshot.originX = newOriginX;
currentTargetViewSnapshot.originY = newOriginY;
currentTargetViewSnapshot.globalOriginX = newOriginX;
currentTargetViewSnapshot.globalOriginY = newOriginY;
currentTargetViewSnapshot.originXByParent = newTargetViewSnapshot.originXByParent;
currentTargetViewSnapshot.originYByParent = newTargetViewSnapshot.originYByParent;
currentTargetViewSnapshot.height = newTargetViewSnapshot.height;
currentTargetViewSnapshot.width = newTargetViewSnapshot.width;
sharedElement.sourceViewSnapshot = newSourceViewSnapshot;
sharedElement.targetViewSnapshot = currentTargetViewSnapshot;
disableCleaningForViewTag(sourceView.getId());
disableCleaningForViewTag(targetView.getId());
}
}
startSharedTransition(
sharedElementsToRestart, LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION);
}
protected boolean prepareSharedTransition(List<View> sharedViews, boolean withNewElements) {
if (sharedViews.isEmpty()) {
return false;
}
sortViewsByTags(sharedViews);
List<SharedElement> sharedElements =
getSharedElementsForCurrentTransition(sharedViews, withNewElements);
if (sharedElements.isEmpty()) {
return false;
}
setupTransitionContainer();
reparentSharedViewsForCurrentTransition(sharedElements);
orderByAnimationTypes(sharedElements);
return true;
}
protected void onScreenWillDisappear() {
if (!mIsTransitionPrepared) {
return;
}
mIsTransitionPrepared = false;
for (SharedElement sharedElement : mSharedElementsWithAnimation) {
sharedElement.targetViewSnapshot = new Snapshot(sharedElement.targetView);
}
for (SharedElement sharedElement : mSharedElementsWithProgress) {
sharedElement.targetViewSnapshot = new Snapshot(sharedElement.targetView);
}
startPreparedTransitions();
for (Integer tag : mTagsToCleanup) {
mNativeMethodsHolder.clearAnimationConfig(tag);
}
mTagsToCleanup.clear();
}
private boolean tryStartSharedTransitionForViews(
List<View> sharedViews, boolean withNewElements) {
if (!prepareSharedTransition(sharedViews, withNewElements)) {
return false;
}
startPreparedTransitions();
return true;
}
private void startPreparedTransitions() {
startSharedTransition(
mSharedElementsWithAnimation, LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION);
startSharedTransition(
mSharedElementsWithProgress, LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION_PROGRESS);
}
private void sortViewsByTags(List<View> views) {
/*
All shared views during the transition have the same parent. It is problematic if parent
view and their children are in the same transition. To keep the valid order in the z-axis,
we need to sort views by tags. Parent tag is lower than children tags.
*/
Collections.sort(views, (v1, v2) -> Integer.compare(v2.getId(), v1.getId()));
}
private List<SharedElement> getSharedElementsForCurrentTransition(
List<View> sharedViews, boolean addedNewScreen) {
// ignore removed views if it is transition restart
boolean isTransitionRestart = mReattachedViews.size() > 0;
List<View> newTransitionViews = new ArrayList<>();
Set<Integer> viewTags = new HashSet<>();
if (!addedNewScreen) {
for (View view : sharedViews) {
viewTags.add(view.getId());
}
}
List<SharedElement> sharedElements = new ArrayList<>();
ReanimatedNativeHierarchyManager reanimatedNativeHierarchyManager =
mAnimationsManager.getReanimatedNativeHierarchyManager();
Set<Integer> removedViewsTags = new HashSet<>();
for (View view : mRemovedSharedViews) {
removedViewsTags.add(view.getId());
}
for (View sharedView : sharedViews) {
int targetViewTag =
mNativeMethodsHolder.findPrecedingViewTagForTransition(sharedView.getId());
if (isTransitionRestart) {
while (removedViewsTags.contains(targetViewTag)) {
mNativeMethodsHolder.clearAnimationConfig(targetViewTag);
targetViewTag =
mNativeMethodsHolder.findPrecedingViewTagForTransition(sharedView.getId());
}
}
boolean bothAreRemoved = !addedNewScreen && viewTags.contains(targetViewTag);
if (targetViewTag < 0) {
continue;
}
View viewSource, viewTarget;
if (addedNewScreen) {
viewSource = reanimatedNativeHierarchyManager.resolveView(targetViewTag);
viewTarget = sharedView;
} else {
viewSource = sharedView;
viewTarget = reanimatedNativeHierarchyManager.resolveView(targetViewTag);
}
if (bothAreRemoved) {
// case for nested stack
clearAllSharedConfigsForView(viewSource);
clearAllSharedConfigsForView(viewTarget);
continue;
}
boolean isSourceViewInTransition =
mCurrentSharedTransitionViews.containsKey(viewSource.getId());
if (!isSourceViewInTransition) {
View viewSourceScreen = findScreen(viewSource);
View viewTargetScreen = findScreen(viewTarget);
if (viewSourceScreen == null || viewTargetScreen == null) {
continue;
}
ViewGroup stack = (ViewGroup) findStack(viewSourceScreen);
if (stack == null) {
continue;
}
ViewGroupManager stackViewGroupManager =
(ViewGroupManager) reanimatedNativeHierarchyManager.resolveViewManager(stack.getId());
int screensCount = stackViewGroupManager.getChildCount(stack);
if (screensCount < 2) {
continue;
}
View topScreen = stackViewGroupManager.getChildAt(stack, screensCount - 1);
View secondScreen = stackViewGroupManager.getChildAt(stack, screensCount - 2);
boolean isValidConfiguration;
if (addedNewScreen) {
isValidConfiguration =
secondScreen.getId() == viewSourceScreen.getId()
&& topScreen.getId() == viewTargetScreen.getId();
} else {
isValidConfiguration =
topScreen.getId() == viewSourceScreen.getId()
&& secondScreen.getId() == viewTargetScreen.getId();
}
if (!isValidConfiguration) {
continue;
}
}
Snapshot sourceViewSnapshot = null;
if (addedNewScreen) {
mViewTagsToHide.add(viewSource.getId());
if (isSourceViewInTransition) {
sourceViewSnapshot = new Snapshot(viewSource);
} else {
makeSnapshot(viewSource);
}
makeSnapshot(viewTarget);
} else if (isSourceViewInTransition) {
makeSnapshot(viewSource);
}
if (sourceViewSnapshot == null) {
sourceViewSnapshot = mSnapshotRegistry.get(viewSource.getId());
}
Snapshot targetViewSnapshot = mSnapshotRegistry.get(viewTarget.getId());
if (targetViewSnapshot == null) {
continue;
}
newTransitionViews.add(viewSource);
newTransitionViews.add(viewTarget);
SharedElement sharedElement =
new SharedElement(viewSource, sourceViewSnapshot, viewTarget, targetViewSnapshot);
sharedElements.add(sharedElement);
}
if (!newTransitionViews.isEmpty()) {
List<View> currentSourceViews = new ArrayList<>();
for (SharedElement sharedElement : mSharedElements) {
currentSourceViews.add(sharedElement.sourceView);
}
Set<View> newSourceViews = new HashSet<>();
for (SharedElement sharedElement : sharedElements) {
newSourceViews.add(sharedElement.sourceView);
}
for (View view : currentSourceViews) {
if (!newSourceViews.contains(view)) {
mViewTagsToHide.remove(view.getId());
view.setVisibility(View.VISIBLE);
}
}
mCurrentSharedTransitionViews.clear();
for (View view : newTransitionViews) {
mCurrentSharedTransitionViews.put(view.getId(), view);
}
}
mSharedElements = sharedElements;
for (SharedElement sharedElement : sharedElements) {
mSharedElementsLookup.put(sharedElement.sourceView.getId(), sharedElement);
}
return sharedElements;
}
private void setupTransitionContainer() {
if (mTransitionContainer == null) {
ReactContext context = mAnimationsManager.getContext();
mTransitionContainer = new ReactViewGroup(context);
}
if (mTransitionContainer.getParent() == null) {
ReactContext context = mAnimationsManager.getContext();
Activity currentActivity = context.getCurrentActivity();
if (currentActivity == null) {
return;
}
ViewGroup rootView = (ViewGroup) currentActivity.getWindow().getDecorView().getRootView();
rootView.addView(mTransitionContainer);
mTransitionContainer.bringToFront();
}
}
private void reparentSharedViewsForCurrentTransition(List<SharedElement> sharedElements) {
for (SharedElement sharedElement : sharedElements) {
View viewSource = sharedElement.sourceView;
if (!mSharedTransitionParent.containsKey(viewSource.getId())) {
ViewGroup parent = (ViewGroup) viewSource.getParent();
int parentTag = parent.getId();
int childIndex = parent.indexOfChild(viewSource);
mSharedTransitionParent.put(viewSource.getId(), (View) viewSource.getParent());
mSharedTransitionInParentIndex.put(viewSource.getId(), childIndex);
SortedSet<Integer> childrenIndicesSet = mSharedViewChildrenIndices.get(parentTag);
if (childrenIndicesSet == null) {
mSharedViewChildrenIndices.put(
parentTag, new TreeSet<>(Collections.singleton(childIndex)));
} else {
childrenIndicesSet.add(childIndex);
}
}
}
for (SharedElement sharedElement : sharedElements) {
View viewSource = sharedElement.sourceView;
((ViewGroup) viewSource.getParent()).removeView(viewSource);
((ViewGroup) mTransitionContainer).addView(viewSource);
mReattachedViews.add(viewSource);
}
}
private void startSharedTransition(List<SharedElement> sharedElements, int type) {
for (SharedElement sharedElement : sharedElements) {
View sourceView = sharedElement.sourceView;
sourceView.setVisibility(View.VISIBLE);
startSharedAnimationForView(
sourceView, sharedElement.sourceViewSnapshot, sharedElement.targetViewSnapshot, type);
sharedElement.targetView.setVisibility(View.INVISIBLE);
}
}
private void startSharedAnimationForView(View view, Snapshot before, Snapshot after, int type) {
HashMap<String, Object> targetValues = after.toTargetMap();
HashMap<String, Object> startValues = before.toCurrentMap();
HashMap<String, Object> preparedStartValues =
mAnimationsManager.prepareDataForAnimationWorklet(startValues, false, true);
HashMap<String, Object> preparedTargetValues =
mAnimationsManager.prepareDataForAnimationWorklet(targetValues, true, true);
HashMap<String, Object> preparedValues = new HashMap<>(preparedTargetValues);
preparedValues.putAll(preparedStartValues);
mNativeMethodsHolder.startAnimation(view.getId(), type, preparedValues);
}
protected void finishSharedAnimation(int tag) {
if (mDisableCleaningForViewTag.containsKey(tag)) {
enableCleaningForViewTag(tag);
return;
}
SharedElement sharedElement = mSharedElementsLookup.get(tag);
if (sharedElement == null) {
return;
}
mSharedElementsLookup.remove(tag);
View view = sharedElement.sourceView;
if (mReattachedViews.contains(view)) {
mReattachedViews.remove(view);
int viewTag = view.getId();
((ViewGroup) mTransitionContainer).removeView(view);
View parentView = mSharedTransitionParent.get(viewTag);
int childIndex = mSharedTransitionInParentIndex.get(viewTag);
ViewGroup parentViewGroup = ((ViewGroup) parentView);
int parentTag = parentViewGroup.getId();
SortedSet<Integer> childIndicesSet = mSharedViewChildrenIndices.get(parentTag);
// here we calculate how many children with smaller indices have not been reinserted yet
int childIndexOffset = childIndicesSet.headSet(childIndex).size();
childIndicesSet.remove(childIndex);
if (childIndicesSet.isEmpty()) {
mSharedViewChildrenIndices.remove(parentTag);
}
childIndex -= childIndexOffset;
if (childIndex <= parentViewGroup.getChildCount()) {
parentViewGroup.addView(view, childIndex);
} else {
parentViewGroup.addView(view);
}
Snapshot viewSourcePreviousSnapshot = mSnapshotRegistry.get(viewTag);
if (viewSourcePreviousSnapshot != null) {
int originY = viewSourcePreviousSnapshot.originY;
if (findStack(view) == null) {
viewSourcePreviousSnapshot.originY = viewSourcePreviousSnapshot.originYByParent;
}
Map<String, Object> snapshotMap = viewSourcePreviousSnapshot.toBasicMap();
Map<String, Object> preparedValues = new HashMap<>();
for (String key : snapshotMap.keySet()) {
Object value = snapshotMap.get(key);
if (key.equals(Snapshot.TRANSFORM_MATRIX)) {
preparedValues.put(key, value);
} else {
float pixelsValue = Utils.convertToFloat(value);
double dipValue = PixelUtil.toDIPFromPixel(pixelsValue);
preparedValues.put(key, dipValue);
}
}
mAnimationsManager.progressLayoutAnimation(viewTag, preparedValues, true);
viewSourcePreviousSnapshot.originY = originY;
}
if (mViewTagsToHide.contains(tag)) {
view.setVisibility(View.INVISIBLE);
}
View targetView = sharedElement.targetView;
int targetViewTag = targetView.getId();
mCurrentSharedTransitionViews.remove(targetViewTag);
mCurrentSharedTransitionViews.remove(viewTag);
mSharedTransitionParent.remove(viewTag);
mSharedTransitionInParentIndex.remove(viewTag);
}
sharedElement.targetView.setVisibility(View.VISIBLE);
if (mRemovedSharedViews.contains(view)) {
mRemovedSharedViews.remove(view);
mSnapshotRegistry.remove(view.getId());
mNativeMethodsHolder.clearAnimationConfig(view.getId());
}
if (mReattachedViews.isEmpty()) {
if (mTransitionContainer != null) {
ViewParent transitionContainerParent = mTransitionContainer.getParent();
if (transitionContainerParent != null) {
// To prevent modifications of the views tree while Android is iterating
// over them, we can schedule the modification for the next frame. This
// approach is safe. The transparent transition container will remain on
// the screen for one additional frame before being removed.
mTransitionContainer.post(
() -> {
if (mReattachedViews.size() > 0) {
return;
}
((ViewGroup) transitionContainerParent).removeView(mTransitionContainer);
});
}
}
mSharedElements.clear();
mSharedElementsWithProgress.clear();
mSharedElementsWithAnimation.clear();
mViewTagsToHide.clear();
}
}
@Nullable
private View findScreen(View view) {
ViewParent parent = view.getParent();
while (parent != null) {
if (parent.getClass().getSimpleName().equals("Screen")) {
return (View) parent;
}
parent = parent.getParent();
}
return null;
}
@Nullable
private View findStack(View view) {
ViewParent parent = view.getParent();
while (parent != null) {
if (parent.getClass().getSimpleName().equals("ScreenStack")) {
return (View) parent;
}
parent = parent.getParent();
}
return null;
}
protected void makeSnapshot(View view) {
Snapshot snapshot = new Snapshot(view);
mSnapshotRegistry.put(view.getId(), snapshot);
}
interface TreeVisitor {
void run(View view);
}
class SnapshotTreeVisitor implements TreeVisitor {
public void run(View view) {
if (mAnimationsManager.hasAnimationForTag(
view.getId(), LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION)) {
mRemovedSharedViews.add(view);
makeSnapshot(view);
}
}
}
class PrepareConfigCleanupTreeVisitor implements TreeVisitor {
public void run(View view) {
mTagsToCleanup.add(view.getId());
}
}
protected void visitTreeForTags(int[] viewTags, TreeVisitor treeVisitor) {
if (viewTags == null) {
return;
}
ReanimatedNativeHierarchyManager reanimatedNativeHierarchyManager =
mAnimationsManager.getReanimatedNativeHierarchyManager();
for (int viewTag : viewTags) {
View view = reanimatedNativeHierarchyManager.resolveView(viewTag);
visitTree(view, treeVisitor);
}
}
private void visitTree(View view, TreeVisitor treeVisitor) {
int tag = view.getId();
if (tag == -1) {
return;
}
ViewGroup viewGroup;
ViewGroupManager<ViewGroup> viewGroupManager = null;
ReanimatedNativeHierarchyManager reanimatedNativeHierarchyManager =
mAnimationsManager.getReanimatedNativeHierarchyManager();
try {
treeVisitor.run(view);
if (!(view instanceof ViewGroup)) {
return;
}
viewGroup = (ViewGroup) view;
ViewManager viewManager = reanimatedNativeHierarchyManager.resolveViewManager(tag);
if (viewManager instanceof ViewGroupManager) {
viewGroupManager = (ViewGroupManager<ViewGroup>) viewManager;
}
} catch (IllegalViewOperationException e) {
return;
}
if (viewGroupManager == null) {
return;
}
for (int i = 0; i < viewGroupManager.getChildCount(viewGroup); i++) {
View child = viewGroupManager.getChildAt(viewGroup, i);
visitTree(child, treeVisitor);
}
}
void visitNativeTreeAndMakeSnapshot(View view) {
if (!(view instanceof ViewGroup)) {
return;
}
ViewGroup viewGroup = (ViewGroup) view;
if (mAnimationsManager.hasAnimationForTag(
view.getId(), LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION)) {
makeSnapshot(view);
}
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
visitNativeTreeAndMakeSnapshot(child);
}
}
private void clearAllSharedConfigsForView(View view) {
int viewTag = view.getId();
mSnapshotRegistry.remove(viewTag);
mNativeMethodsHolder.clearAnimationConfig(viewTag);
}
private void disableCleaningForViewTag(int viewTag) {
Integer counter = mDisableCleaningForViewTag.get(viewTag);
if (counter != null) {
mDisableCleaningForViewTag.put(viewTag, counter + 1);
} else {
mDisableCleaningForViewTag.put(viewTag, 1);
}
}
private void enableCleaningForViewTag(int viewTag) {
Integer counter = mDisableCleaningForViewTag.get(viewTag);
if (counter == null) {
return;
}
if (counter == 1) {
mDisableCleaningForViewTag.remove(viewTag);
} else {
mDisableCleaningForViewTag.put(viewTag, counter - 1);
}
}
void orderByAnimationTypes(List<SharedElement> sharedElements) {
mSharedElementsWithProgress.clear();
mSharedElementsWithAnimation.clear();
for (SharedElement sharedElement : sharedElements) {
int viewTag = sharedElement.sourceView.getId();
boolean viewHasProgressAnimation =
mAnimationsManager.hasAnimationForTag(
viewTag, LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION_PROGRESS);
if (viewHasProgressAnimation) {
mSharedElementsWithProgress.add(sharedElement);
} else {
mSharedElementsWithAnimation.add(sharedElement);
}
}
}
}

View File

@@ -0,0 +1,202 @@
package com.swmansion.reanimated.layoutReanimation;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.ViewManager;
import com.swmansion.reanimated.ReactNativeUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
public class Snapshot {
public static final String WIDTH = "width";
public static final String HEIGHT = "height";
public static final String ORIGIN_X = "originX";
public static final String ORIGIN_Y = "originY";
public static final String TRANSFORM_MATRIX = "transformMatrix";
public static final String GLOBAL_ORIGIN_X = "globalOriginX";
public static final String GLOBAL_ORIGIN_Y = "globalOriginY";
public static final String BORDER_RADIUS = "borderRadius";
public static final String CURRENT_WIDTH = "currentWidth";
public static final String CURRENT_HEIGHT = "currentHeight";
public static final String CURRENT_ORIGIN_X = "currentOriginX";
public static final String CURRENT_ORIGIN_Y = "currentOriginY";
public static final String CURRENT_TRANSFORM_MATRIX = "currentTransformMatrix";
public static final String CURRENT_GLOBAL_ORIGIN_X = "currentGlobalOriginX";
public static final String CURRENT_GLOBAL_ORIGIN_Y = "currentGlobalOriginY";
public static final String CURRENT_BORDER_RADIUS = "currentBorderRadius";
public static final String TARGET_WIDTH = "targetWidth";
public static final String TARGET_HEIGHT = "targetHeight";
public static final String TARGET_ORIGIN_X = "targetOriginX";
public static final String TARGET_ORIGIN_Y = "targetOriginY";
public static final String TARGET_TRANSFORM_MATRIX = "targetTransformMatrix";
public static final String TARGET_GLOBAL_ORIGIN_X = "targetGlobalOriginX";
public static final String TARGET_GLOBAL_ORIGIN_Y = "targetGlobalOriginY";
public static final String TARGET_BORDER_RADIUS = "targetBorderRadius";
public View view;
public ViewGroup parent;
public ViewManager viewManager;
public ViewManager parentViewManager;
public int width;
public int height;
public int originX;
public int originY;
public int globalOriginX;
public int globalOriginY;
public List<Float> transformMatrix =
new ArrayList<>(Arrays.asList(1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f));
public int originXByParent;
public int originYByParent;
public float borderRadius;
private float[] identityMatrix = {1, 0, 0, 0, 1, 0, 0, 0, 1};
public static ArrayList<String> targetKeysToTransform =
new ArrayList<>(
Arrays.asList(
Snapshot.TARGET_WIDTH,
Snapshot.TARGET_HEIGHT,
Snapshot.TARGET_ORIGIN_X,
Snapshot.TARGET_ORIGIN_Y,
Snapshot.TARGET_GLOBAL_ORIGIN_X,
Snapshot.TARGET_GLOBAL_ORIGIN_Y,
Snapshot.TARGET_BORDER_RADIUS));
public static ArrayList<String> currentKeysToTransform =
new ArrayList<>(
Arrays.asList(
Snapshot.CURRENT_WIDTH,
Snapshot.CURRENT_HEIGHT,
Snapshot.CURRENT_ORIGIN_X,
Snapshot.CURRENT_ORIGIN_Y,
Snapshot.CURRENT_GLOBAL_ORIGIN_X,
Snapshot.CURRENT_GLOBAL_ORIGIN_Y,
Snapshot.CURRENT_BORDER_RADIUS));
Snapshot(View view, NativeViewHierarchyManager viewHierarchyManager) {
parent = (ViewGroup) view.getParent();
try {
viewManager = viewHierarchyManager.resolveViewManager(view.getId());
parentViewManager = viewHierarchyManager.resolveViewManager(parent.getId());
} catch (IllegalViewOperationException | NullPointerException e) {
// do nothing
}
width = view.getWidth();
height = view.getHeight();
originX = view.getLeft();
originY = view.getTop();
this.view = view;
int[] location = new int[2];
view.getLocationOnScreen(location);
globalOriginX = location[0];
globalOriginY = location[1];
}
public Snapshot(View view) {
int[] location = new int[2];
view.getLocationOnScreen(location);
originX = location[0];
originY = location[1];
width = view.getWidth();
height = view.getHeight();
View transformedView = findTransformedView(view);
if (transformedView != null) {
float[] transformMatrixArray = new float[9];
transformedView.getMatrix().getValues(transformMatrixArray);
transformMatrix = new ArrayList<>();
for (int i = 0; i < 9; i++) {
transformMatrix.add(transformMatrixArray[i]);
}
transformMatrix.set(0, transformedView.getScaleX());
transformMatrix.set(4, transformedView.getScaleY());
transformMatrix.set(2, transformedView.getTranslationX());
transformMatrix.set(5, transformedView.getTranslationY());
originX -= (width - width * transformedView.getScaleX()) / 2;
originY -= (height - height * transformedView.getScaleY()) / 2;
}
originXByParent = view.getLeft();
originYByParent = view.getTop();
borderRadius = ReactNativeUtils.getBorderRadius(view);
}
private void addTargetConfig(HashMap<String, Object> data) {
data.put(Snapshot.TARGET_ORIGIN_Y, originY);
data.put(Snapshot.TARGET_ORIGIN_X, originX);
data.put(Snapshot.TARGET_GLOBAL_ORIGIN_Y, globalOriginY);
data.put(Snapshot.TARGET_GLOBAL_ORIGIN_X, globalOriginX);
data.put(Snapshot.TARGET_HEIGHT, height);
data.put(Snapshot.TARGET_WIDTH, width);
data.put(Snapshot.TARGET_TRANSFORM_MATRIX, transformMatrix);
data.put(Snapshot.TARGET_BORDER_RADIUS, borderRadius);
}
private void addCurrentConfig(HashMap<String, Object> data) {
data.put(Snapshot.CURRENT_ORIGIN_Y, originY);
data.put(Snapshot.CURRENT_ORIGIN_X, originX);
data.put(Snapshot.CURRENT_GLOBAL_ORIGIN_Y, globalOriginY);
data.put(Snapshot.CURRENT_GLOBAL_ORIGIN_X, globalOriginX);
data.put(Snapshot.CURRENT_HEIGHT, height);
data.put(Snapshot.CURRENT_WIDTH, width);
data.put(Snapshot.CURRENT_TRANSFORM_MATRIX, transformMatrix);
data.put(Snapshot.CURRENT_BORDER_RADIUS, borderRadius);
}
private void addBasicConfig(HashMap<String, Object> data) {
data.put(Snapshot.ORIGIN_Y, originY);
data.put(Snapshot.ORIGIN_X, originX);
data.put(Snapshot.GLOBAL_ORIGIN_Y, globalOriginY);
data.put(Snapshot.GLOBAL_ORIGIN_X, globalOriginX);
data.put(Snapshot.HEIGHT, height);
data.put(Snapshot.WIDTH, width);
data.put(Snapshot.TRANSFORM_MATRIX, transformMatrix);
data.put(Snapshot.BORDER_RADIUS, borderRadius);
}
public HashMap<String, Object> toTargetMap() {
HashMap<String, Object> data = new HashMap<>();
addTargetConfig(data);
return data;
}
public HashMap<String, Object> toCurrentMap() {
HashMap<String, Object> data = new HashMap<>();
addCurrentConfig(data);
return data;
}
public HashMap<String, Object> toBasicMap() {
HashMap<String, Object> data = new HashMap<>();
addBasicConfig(data);
return data;
}
private View findTransformedView(View view) {
View transformedView = null;
boolean isTransformed = false;
do {
if (transformedView == null) {
transformedView = view;
} else {
if (!(transformedView.getParent() instanceof View)) {
break;
}
transformedView = (View) transformedView.getParent();
}
if (transformedView == null) {
break;
}
float[] transformArray = new float[9];
transformedView.getMatrix().getValues(transformArray);
isTransformed = !Arrays.equals(transformArray, identityMatrix);
} while (!isTransformed
&& transformedView != null
&& !transformedView.getClass().getSimpleName().equals("Screen"));
return (isTransformed && transformedView != null) ? transformedView : null;
}
}

View File

@@ -0,0 +1,12 @@
package com.swmansion.reanimated.layoutReanimation;
import android.view.View;
import android.view.ViewGroup;
public interface ViewHierarchyObserver {
void onViewRemoval(View view, ViewGroup parent, Runnable callback);
void onViewCreate(View view, ViewGroup parent, Snapshot after);
void onViewUpdate(View view, Snapshot before, Snapshot after);
}

View File

@@ -0,0 +1,19 @@
package com.swmansion.reanimated.nativeProxy;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.swmansion.reanimated.NodesManager;
@DoNotStrip
public class AnimationFrameCallback implements NodesManager.OnAnimationFrame {
@DoNotStrip private final HybridData mHybridData;
@DoNotStrip
private AnimationFrameCallback(HybridData hybridData) {
mHybridData = hybridData;
}
@Override
public native void onAnimationFrame(double timestampMs);
}

View File

@@ -0,0 +1,36 @@
package com.swmansion.reanimated.nativeProxy;
import androidx.annotation.Nullable;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.RCTEventEmitter;
@DoNotStrip
public class EventHandler implements RCTEventEmitter {
@DoNotStrip private final HybridData mHybridData;
UIManagerModule.CustomEventNamesResolver mCustomEventNamesResolver;
@DoNotStrip
private EventHandler(HybridData hybridData) {
mHybridData = hybridData;
}
@Override
public void receiveEvent(int emitterReactTag, String eventName, @Nullable WritableMap event) {
String resolvedEventName = mCustomEventNamesResolver.resolveCustomEventName(eventName);
receiveEvent(resolvedEventName, emitterReactTag, event);
}
public native void receiveEvent(
String eventName, int emitterReactTag, @Nullable WritableMap event);
@Override
public void receiveTouches(
String eventName, WritableArray touches, WritableArray changedIndices) {
// not interested in processing touch events this way, we process raw events only
}
}

View File

@@ -0,0 +1,265 @@
package com.swmansion.reanimated.nativeProxy;
import android.content.ContentResolver;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.ReactApplication;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableNativeArray;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.soloader.SoLoader;
import com.swmansion.common.GestureHandlerStateManager;
import com.swmansion.reanimated.AndroidUIScheduler;
import com.swmansion.reanimated.BuildConfig;
import com.swmansion.reanimated.NativeProxy;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.ReanimatedModule;
import com.swmansion.reanimated.Utils;
import com.swmansion.reanimated.keyboard.KeyboardAnimationManager;
import com.swmansion.reanimated.keyboard.KeyboardWorkletWrapper;
import com.swmansion.reanimated.layoutReanimation.AnimationsManager;
import com.swmansion.reanimated.layoutReanimation.LayoutAnimations;
import com.swmansion.reanimated.sensor.ReanimatedSensorContainer;
import com.swmansion.reanimated.sensor.ReanimatedSensorType;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public abstract class NativeProxyCommon {
static {
SoLoader.loadLibrary("reanimated");
}
protected NodesManager mNodesManager;
protected final WeakReference<ReactApplicationContext> mContext;
protected AndroidUIScheduler mAndroidUIScheduler;
private ReanimatedSensorContainer reanimatedSensorContainer;
private final GestureHandlerStateManager gestureHandlerStateManager;
private KeyboardAnimationManager keyboardAnimationManager;
private Long firstUptime = SystemClock.uptimeMillis();
private boolean slowAnimationsEnabled = false;
protected String cppVersion = null;
protected NativeProxyCommon(ReactApplicationContext context) {
mAndroidUIScheduler = new AndroidUIScheduler(context);
mContext = new WeakReference<>(context);
reanimatedSensorContainer = new ReanimatedSensorContainer(mContext);
keyboardAnimationManager = new KeyboardAnimationManager(mContext);
addDevMenuOption();
GestureHandlerStateManager tempHandlerStateManager;
try {
Class<NativeModule> gestureHandlerModuleClass =
(Class<NativeModule>)
Class.forName("com.swmansion.gesturehandler.react.RNGestureHandlerModule");
tempHandlerStateManager =
(GestureHandlerStateManager) context.getNativeModule(gestureHandlerModuleClass);
} catch (ClassCastException | ClassNotFoundException e) {
tempHandlerStateManager = null;
}
gestureHandlerStateManager = tempHandlerStateManager;
}
protected native void installJSIBindings();
public AndroidUIScheduler getAndroidUIScheduler() {
return mAndroidUIScheduler;
}
private void toggleSlowAnimations() {
slowAnimationsEnabled = !slowAnimationsEnabled;
if (slowAnimationsEnabled) {
firstUptime = SystemClock.uptimeMillis();
}
}
private void addDevMenuOption() {
// In Expo, `ApplicationContext` is not an instance of `ReactApplication`
if (mContext.get().getApplicationContext() instanceof ReactApplication) {
final DevSupportManager devSupportManager =
((ReactApplication) mContext.get().getApplicationContext())
.getReactNativeHost()
.getReactInstanceManager()
.getDevSupportManager();
devSupportManager.addCustomDevOption(
"Toggle slow animations (Reanimated)", this::toggleSlowAnimations);
}
}
@DoNotStrip
public void requestRender(AnimationFrameCallback callback) {
mNodesManager.postOnAnimation(callback);
}
@DoNotStrip
public String getReanimatedJavaVersion() {
return BuildConfig.REANIMATED_VERSION_JAVA;
}
@DoNotStrip
@SuppressWarnings("unused")
// It turns out it's pretty difficult to set a member of a class
// instance through JNI so we decided to use a setter instead.
protected void setCppVersion(String version) {
cppVersion = version;
}
protected void checkCppVersion() {
if (cppVersion == null) {
throw new RuntimeException(
"[Reanimated] Java side failed to resolve C++ code version. "
+ "See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#java-side-failed-to-resolve-c-code-version for more information.");
}
String javaVersion = getReanimatedJavaVersion();
if (!cppVersion.equals(javaVersion)) {
throw new RuntimeException(
"[Reanimated] Mismatch between Java code version and C++ code version ("
+ javaVersion
+ " vs. "
+ cppVersion
+ " respectively). See "
+ "https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-java-code-version-and-c-code-version for more information.");
}
}
@DoNotStrip
public void updateProps(int viewTag, Map<String, Object> props) {
mNodesManager.updateProps(viewTag, props);
}
@DoNotStrip
public void synchronouslyUpdateUIProps(int viewTag, ReadableMap uiProps) {
mNodesManager.synchronouslyUpdateUIProps(viewTag, uiProps);
}
@DoNotStrip
public String obtainProp(int viewTag, String propName) {
return mNodesManager.obtainProp(viewTag, propName);
}
@DoNotStrip
public void scrollTo(int viewTag, double x, double y, boolean animated) {
mNodesManager.scrollTo(viewTag, x, y, animated);
}
@DoNotStrip
public void dispatchCommand(int viewTag, String commandId, ReadableArray commandArgs) {
mNodesManager.dispatchCommand(viewTag, commandId, commandArgs);
}
@DoNotStrip
public void setGestureState(int handlerTag, int newState) {
if (gestureHandlerStateManager != null) {
gestureHandlerStateManager.setGestureHandlerState(handlerTag, newState);
}
}
@DoNotStrip
public long getAnimationTimestamp() {
if (slowAnimationsEnabled) {
final long ANIMATIONS_DRAG_FACTOR = 10;
return this.firstUptime
+ (SystemClock.uptimeMillis() - this.firstUptime) / ANIMATIONS_DRAG_FACTOR;
} else {
return SystemClock.uptimeMillis();
}
}
@DoNotStrip
public float[] measure(int viewTag) {
return mNodesManager.measure(viewTag);
}
@DoNotStrip
public void configureProps(ReadableNativeArray uiProps, ReadableNativeArray nativeProps) {
Set<String> uiPropsSet = convertProps(uiProps);
Set<String> nativePropsSet = convertProps(nativeProps);
mNodesManager.configureProps(uiPropsSet, nativePropsSet);
}
private Set<String> convertProps(ReadableNativeArray props) {
Set<String> propsSet = new HashSet<>();
ArrayList<Object> propsList = props.toArrayList();
for (int i = 0; i < propsList.size(); i++) {
propsSet.add((String) propsList.get(i));
}
return propsSet;
}
@DoNotStrip
public void registerEventHandler(EventHandler handler) {
handler.mCustomEventNamesResolver = mNodesManager.getEventNameResolver();
mNodesManager.registerEventHandler(handler);
}
@DoNotStrip
public int registerSensor(int sensorType, int interval, SensorSetter setter) {
return reanimatedSensorContainer.registerSensor(
ReanimatedSensorType.getInstanceById(sensorType), interval, setter);
}
@DoNotStrip
public void unregisterSensor(int sensorId) {
reanimatedSensorContainer.unregisterSensor(sensorId);
}
@DoNotStrip
public int subscribeForKeyboardEvents(
KeyboardWorkletWrapper keyboardWorkletWrapper, boolean isStatusBarTranslucent) {
return keyboardAnimationManager.subscribeForKeyboardUpdates(
keyboardWorkletWrapper, isStatusBarTranslucent);
}
@DoNotStrip
public void unsubscribeFromKeyboardEvents(int listenerId) {
keyboardAnimationManager.unsubscribeFromKeyboardUpdates(listenerId);
}
protected abstract HybridData getHybridData();
public void invalidate() {
mAndroidUIScheduler.deactivate();
}
public void prepareLayoutAnimations(LayoutAnimations layoutAnimations) {
if (Utils.isChromeDebugger) {
Log.w("[REANIMATED]", "You can not use LayoutAnimation with enabled Chrome Debugger");
return;
}
mNodesManager = mContext.get().getNativeModule(ReanimatedModule.class).getNodesManager();
AnimationsManager animationsManager =
mContext
.get()
.getNativeModule(ReanimatedModule.class)
.getNodesManager()
.getAnimationsManager();
animationsManager.setNativeMethods(NativeProxy.createNativeMethodsHolder(layoutAnimations));
}
@DoNotStrip
public boolean getIsReducedMotion() {
ContentResolver mContentResolver = mContext.get().getContentResolver();
String rawValue =
Settings.Global.getString(mContentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE);
float parsedValue = rawValue != null ? Float.parseFloat(rawValue) : 1f;
return parsedValue == 0f;
}
@DoNotStrip
void maybeFlushUIUpdatesQueue() {
if (!mNodesManager.isAnimationRunning()) {
mNodesManager.performOperations();
}
}
}

View File

@@ -0,0 +1,19 @@
package com.swmansion.reanimated.nativeProxy;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class NoopEventHandler implements RCTEventEmitter {
@Override
public void receiveEvent(int targetTag, String eventName, @Nullable WritableMap event) {
// NOOP
}
@Override
public void receiveTouches(
String eventName, WritableArray touches, WritableArray changedIndices) {
// NOOP
}
}

View File

@@ -0,0 +1,17 @@
package com.swmansion.reanimated.nativeProxy;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
@DoNotStrip
public class SensorSetter {
@DoNotStrip private final HybridData mHybridData;
@DoNotStrip
private SensorSetter(HybridData hybridData) {
mHybridData = hybridData;
}
public native void sensorSetter(float[] value, int orientationDegrees);
}

View File

@@ -0,0 +1,52 @@
package com.swmansion.reanimated.sensor;
import static android.content.Context.WINDOW_SERVICE;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.view.Display;
import android.view.WindowManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.swmansion.reanimated.nativeProxy.SensorSetter;
import java.lang.ref.WeakReference;
public class ReanimatedSensor {
ReanimatedSensorListener listener;
SensorManager sensorManager;
Sensor sensor;
ReanimatedSensorType sensorType;
int interval;
private static final int DEFAULT_INTERVAL = 8;
ReanimatedSensor(
WeakReference<ReactApplicationContext> reactContext,
ReanimatedSensorType sensorType,
int interval,
SensorSetter setter) {
WindowManager wm = (WindowManager) reactContext.get().getSystemService(WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
listener = new ReanimatedSensorListener(setter, interval, display);
sensorManager =
(SensorManager) reactContext.get().getSystemService(reactContext.get().SENSOR_SERVICE);
this.sensorType = sensorType;
if (interval == -1) {
this.interval = DEFAULT_INTERVAL;
} else {
this.interval = interval;
}
}
boolean initialize() {
sensor = sensorManager.getDefaultSensor(sensorType.getType());
if (sensor != null) {
sensorManager.registerListener(listener, sensor, interval * 1000);
return true;
}
return false;
}
void cancel() {
sensorManager.unregisterListener(listener, sensor);
}
}

View File

@@ -0,0 +1,39 @@
package com.swmansion.reanimated.sensor;
import android.util.Log;
import com.facebook.react.bridge.ReactApplicationContext;
import com.swmansion.reanimated.nativeProxy.SensorSetter;
import java.lang.ref.WeakReference;
import java.util.HashMap;
public class ReanimatedSensorContainer {
private int nextSensorId = 0;
private final WeakReference<ReactApplicationContext> reactContext;
private final HashMap<Integer, ReanimatedSensor> sensors = new HashMap<>();
public ReanimatedSensorContainer(WeakReference<ReactApplicationContext> reactContext) {
this.reactContext = reactContext;
}
public int registerSensor(ReanimatedSensorType sensorType, int interval, SensorSetter setter) {
ReanimatedSensor sensor = new ReanimatedSensor(reactContext, sensorType, interval, setter);
int sensorId = -1;
if (sensor.initialize()) {
sensorId = nextSensorId;
nextSensorId++;
sensors.put(sensorId, sensor);
}
return sensorId;
}
public void unregisterSensor(int sensorId) {
ReanimatedSensor sensor = sensors.get(sensorId);
if (sensor != null) {
sensor.cancel();
sensors.remove(sensorId);
} else {
Log.e("Reanimated", "Tried to unregister nonexistent sensor");
}
}
}

View File

@@ -0,0 +1,96 @@
package com.swmansion.reanimated.sensor;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.view.Display;
import android.view.Surface;
import com.swmansion.reanimated.nativeProxy.SensorSetter;
public class ReanimatedSensorListener implements SensorEventListener {
private SensorSetter setter;
private double lastRead = (double) System.currentTimeMillis();
private final double interval;
private float[] rotation = new float[9];
private float[] orientation = new float[3];
private float[] quaternion = new float[4];
private final Display display;
ReanimatedSensorListener(SensorSetter setter, double interval, Display display) {
this.setter = setter;
this.interval = interval;
this.display = display;
}
@Override
public void onSensorChanged(SensorEvent event) {
double current = (double) System.currentTimeMillis();
if (current - lastRead < interval) {
return;
}
int sensorType = event.sensor.getType();
lastRead = current;
int orientationDegrees;
switch (display.getRotation()) {
case Surface.ROTATION_90:
orientationDegrees = 90;
break;
case Surface.ROTATION_180:
orientationDegrees = 180;
break;
case Surface.ROTATION_270:
orientationDegrees = 270;
break;
default:
orientationDegrees = 0;
break;
}
switch (sensorType) {
case Sensor.TYPE_ROTATION_VECTOR:
{
SensorManager.getQuaternionFromVector(quaternion, event.values);
SensorManager.getRotationMatrixFromVector(rotation, event.values);
SensorManager.getOrientation(rotation, orientation);
float[] data =
new float[] {
quaternion[1], // qx
quaternion[3], // qy -> we set qz to match iOS
-quaternion[2], // qz -> we set -qy to match iOS
quaternion[0], // qw
// make Android consistent with iOS, which is better documented here:
// https://developer.apple.com/documentation/coremotion/getting_processed_device-motion_data/
-orientation[0], // yaw
-orientation[1], // pitch
orientation[2] // roll
};
setter.sensorSetter(data, orientationDegrees);
break;
}
case Sensor.TYPE_GYROSCOPE:
case Sensor.TYPE_MAGNETIC_FIELD:
{
float[] data = new float[] {event.values[0], event.values[1], event.values[2]};
setter.sensorSetter(data, orientationDegrees);
break;
}
case Sensor.TYPE_GRAVITY:
case Sensor.TYPE_LINEAR_ACCELERATION:
{
float[] data = new float[] {-event.values[0], -event.values[1], -event.values[2]};
setter.sensorSetter(data, orientationDegrees);
break;
}
default:
throw new IllegalArgumentException("[Reanimated] Unknown sensor type.");
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
}

View File

@@ -0,0 +1,37 @@
package com.swmansion.reanimated.sensor;
import android.hardware.Sensor;
public enum ReanimatedSensorType {
ACCELEROMETER(Sensor.TYPE_LINEAR_ACCELERATION),
GYROSCOPE(Sensor.TYPE_GYROSCOPE),
GRAVITY(Sensor.TYPE_GRAVITY),
MAGNETIC_FIELD(Sensor.TYPE_MAGNETIC_FIELD),
ROTATION_VECTOR(Sensor.TYPE_ROTATION_VECTOR);
private final int type;
ReanimatedSensorType(int type) {
this.type = type;
}
public int getType() {
return type;
}
public static ReanimatedSensorType getInstanceById(int typeId) {
switch (typeId) {
case 1:
return ReanimatedSensorType.ACCELEROMETER;
case 2:
return ReanimatedSensorType.GYROSCOPE;
case 3:
return ReanimatedSensorType.GRAVITY;
case 4:
return ReanimatedSensorType.MAGNETIC_FIELD;
case 5:
return ReanimatedSensorType.ROTATION_VECTOR;
}
throw new IllegalArgumentException("[Reanimated] Unknown sensor type.");
}
}

View File

@@ -0,0 +1,140 @@
package com.swmansion.reanimated;
import static com.swmansion.reanimated.Utils.simplifyStringNumbersList;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl;
import com.swmansion.reanimated.layoutReanimation.LayoutAnimations;
import com.swmansion.reanimated.layoutReanimation.NativeMethodsHolder;
import com.swmansion.reanimated.nativeProxy.NativeProxyCommon;
import java.lang.ref.WeakReference;
import java.util.HashMap;
public class NativeProxy extends NativeProxyCommon {
@DoNotStrip
@SuppressWarnings("unused")
private final HybridData mHybridData;
public NativeProxy(ReactApplicationContext context, String valueUnpackerCode) {
super(context);
CallInvokerHolderImpl holder =
(CallInvokerHolderImpl) context.getCatalystInstance().getJSCallInvokerHolder();
LayoutAnimations LayoutAnimations = new LayoutAnimations(context);
ReanimatedMessageQueueThread messageQueueThread = new ReanimatedMessageQueueThread();
mHybridData =
initHybrid(
context.getJavaScriptContextHolder().get(),
holder,
mAndroidUIScheduler,
LayoutAnimations,
messageQueueThread,
valueUnpackerCode);
prepareLayoutAnimations(LayoutAnimations);
installJSIBindings();
if (BuildConfig.DEBUG) {
checkCppVersion();
}
}
private native HybridData initHybrid(
long jsContext,
CallInvokerHolderImpl jsCallInvokerHolder,
AndroidUIScheduler androidUIScheduler,
LayoutAnimations LayoutAnimations,
MessageQueueThread messageQueueThread,
String valueUnpackerCode);
public native boolean isAnyHandlerWaitingForEvent(String eventName, int emitterReactTag);
public native void performOperations();
@Override
protected HybridData getHybridData() {
return mHybridData;
}
public static NativeMethodsHolder createNativeMethodsHolder(LayoutAnimations layoutAnimations) {
WeakReference<LayoutAnimations> weakLayoutAnimations = new WeakReference<>(layoutAnimations);
return new NativeMethodsHolder() {
@Override
public void startAnimation(int tag, int type, HashMap<String, Object> values) {
LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
if (layoutAnimations != null) {
HashMap<String, String> preparedValues = new HashMap<>();
for (String key : values.keySet()) {
String stringValue = values.get(key).toString();
if (key.endsWith("TransformMatrix")) {
preparedValues.put(key, simplifyStringNumbersList(stringValue));
} else {
preparedValues.put(key, stringValue);
}
}
layoutAnimations.startAnimationForTag(tag, type, preparedValues);
}
}
@Override
public boolean shouldAnimateExiting(int tag, boolean shouldAnimate) {
LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
if (layoutAnimations != null) {
return layoutAnimations.shouldAnimateExiting(tag, shouldAnimate);
}
return false;
}
@Override
public boolean isLayoutAnimationEnabled() {
LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
if (layoutAnimations != null) {
return layoutAnimations.isLayoutAnimationEnabled();
}
return false;
}
@Override
public boolean hasAnimation(int tag, int type) {
LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
if (layoutAnimations != null) {
return layoutAnimations.hasAnimationForTag(tag, type);
}
return false;
}
@Override
public void clearAnimationConfig(int tag) {
LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
if (layoutAnimations != null) {
layoutAnimations.clearAnimationConfigForTag(tag);
}
}
@Override
public void cancelAnimation(int tag) {
LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
if (layoutAnimations != null) {
layoutAnimations.cancelAnimationForTag(tag);
}
}
@Override
public int findPrecedingViewTagForTransition(int tag) {
LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
if (layoutAnimations != null) {
return layoutAnimations.findPrecedingViewTagForTransition(tag);
}
return -1;
}
public void checkDuplicateSharedTag(int viewTag, int screenTag) {
LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
if (layoutAnimations != null) {
layoutAnimations.checkDuplicateSharedTag(viewTag, screenTag);
}
}
};
}
}

View File

@@ -0,0 +1,37 @@
/**
* This code was copied from android/build/generated/source/codegen/java/com/swmansion/reanimated/NativeReanimatedModuleSpec.java
* which was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GenerateModuleJavaSpec.js
*
* @nolint
*/
package com.swmansion.reanimated;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import javax.annotation.Nonnull;
public abstract class NativeReanimatedModuleSpec extends ReactContextBaseJavaModule implements TurboModule {
public static final String NAME = "ReanimatedModule";
public NativeReanimatedModuleSpec(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public @Nonnull String getName() {
return NAME;
}
@ReactMethod(isBlockingSynchronousMethod = true)
@DoNotStrip
public abstract boolean installTurboModule(String valueUnpackerCode);
}

View File

@@ -0,0 +1,17 @@
package com.swmansion.reanimated;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.NodesManager;
class ReaCompatibility {
public ReaCompatibility(ReactApplicationContext reactApplicationContext) {
}
public void registerFabricEventListener(NodesManager nodesManager) {
}
public void synchronouslyUpdateUIProps(int viewTag, ReadableMap uiProps) {
}
}

View File

@@ -0,0 +1,9 @@
package com.swmansion.reanimated;
public class ReactFeatureFlagsWrapper {
public static void enableMountHooks() {
// no-op
}
}

View File

@@ -0,0 +1,11 @@
package com.swmansion.reanimated;
import com.facebook.react.config.ReactFeatureFlags;
public class ReactFeatureFlagsWrapper {
public static void enableMountHooks() {
ReactFeatureFlags.enableMountHooks = true;
}
}

View File

@@ -0,0 +1,127 @@
package com.swmansion.reanimated;
import android.util.Log;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIManagerModuleListener;
import java.util.ArrayList;
import javax.annotation.Nullable;
@ReactModule(name = ReanimatedModule.NAME)
public class ReanimatedModule extends NativeReanimatedModuleSpec
implements LifecycleEventListener, UIManagerModuleListener {
public static final String NAME = "ReanimatedModule";
private interface UIThreadOperation {
void execute(NodesManager nodesManager);
}
private ArrayList<UIThreadOperation> mOperations = new ArrayList<>();
private @Nullable NodesManager mNodesManager;
public ReanimatedModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public void initialize() {
ReactApplicationContext reactCtx = getReactApplicationContext();
UIManagerModule uiManager = reactCtx.getNativeModule(UIManagerModule.class);
reactCtx.addLifecycleEventListener(this);
uiManager.addUIManagerListener(this);
}
@Override
public void onHostPause() {
if (mNodesManager != null) {
mNodesManager.onHostPause();
}
}
@Override
public void onHostResume() {
if (mNodesManager != null) {
mNodesManager.onHostResume();
}
}
@Override
public void onHostDestroy() {
// do nothing
}
@Override
public void willDispatchViewUpdates(final UIManagerModule uiManager) {
if (mOperations.isEmpty()) {
return;
}
final ArrayList<UIThreadOperation> operations = mOperations;
mOperations = new ArrayList<>();
uiManager.addUIBlock(
new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
NodesManager nodesManager = getNodesManager();
for (UIThreadOperation operation : operations) {
operation.execute(nodesManager);
}
}
});
}
@Override
public String getName() {
return NAME;
}
/*package*/
public NodesManager getNodesManager() {
if (mNodesManager == null) {
mNodesManager = new NodesManager(getReactApplicationContext());
}
return mNodesManager;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public boolean installTurboModule(String valueUnpackerCode) {
// When debugging in chrome the JS context is not available.
// https://github.com/facebook/react-native/blob/v0.67.0-rc.6/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobCollector.java#L25
Utils.isChromeDebugger = getReactApplicationContext().getJavaScriptContextHolder().get() == 0;
if (!Utils.isChromeDebugger) {
this.getNodesManager().initWithContext(getReactApplicationContext(), valueUnpackerCode);
return true;
} else {
Log.w(
"[REANIMATED]",
"Unable to create Reanimated Native Module. You can ignore this message if you are using Chrome Debugger now.");
return false;
}
}
@ReactMethod
public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}
@ReactMethod
public void removeListeners(Integer count) {
// Keep: Required for RN built in Event Emitter Calls.
}
@Override
public void invalidate() {
super.invalidate();
if (mNodesManager != null) {
mNodesManager.invalidate();
}
}
}

View File

@@ -0,0 +1,271 @@
package com.facebook.react.uimanager;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import java.util.List;
@ReactModule(name = UIManagerModule.NAME)
public class ReanimatedUIManager extends UIManagerModule {
public ReanimatedUIManager(
ReactApplicationContext reactContext,
List<ViewManager> viewManagersList,
int minTimeLeftInFrameForNonBatchedOperationMs) {
super(reactContext, viewManagersList, minTimeLeftInFrameForNonBatchedOperationMs);
}
public void onBatchComplete() {
super.onBatchComplete();
}
@Override
public boolean canOverrideExistingModule() {
return true;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public @Nullable WritableMap getConstantsForViewManager(@Nullable String viewManagerName) {
return super.getConstantsForViewManager(viewManagerName);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap getDefaultEventTypes() {
return super.getDefaultEventTypes();
}
/** Unregisters a new root view. */
@ReactMethod
public void removeRootView(int rootViewTag) {
super.removeRootView(rootViewTag);
}
@ReactMethod
public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
super.createView(tag, className, rootViewTag, props);
}
@ReactMethod
public void updateView(final int tag, final String className, final ReadableMap props) {
super.updateView(tag, className, props);
}
/**
* Interface for adding/removing/moving views within a parent view from JS.
*
* @param viewTag the view tag of the parent view
* @param moveFrom a list of indices in the parent view to move views from
* @param moveTo parallel to moveFrom, a list of indices in the parent view to move views to
* @param addChildTags a list of tags of views to add to the parent
* @param addAtIndices parallel to addChildTags, a list of indices to insert those children at
* @param removeFrom a list of indices of views to permanently remove. The memory for the
* corresponding views and data structures should be reclaimed.
*/
@ReactMethod
public void manageChildren(
int viewTag,
@Nullable ReadableArray moveFrom,
@Nullable ReadableArray moveTo,
@Nullable ReadableArray addChildTags,
@Nullable ReadableArray addAtIndices,
@Nullable ReadableArray removeFrom) {
super.manageChildren(viewTag, moveFrom, moveTo, addChildTags, addAtIndices, removeFrom);
}
/**
* Interface for fast tracking the initial adding of views. Children view tags are assumed to be
* in order
*
* @param viewTag the view tag of the parent view
* @param childrenTags An array of tags to add to the parent in order
*/
@ReactMethod
public void setChildren(int viewTag, ReadableArray childrenTags) {
super.setChildren(viewTag, childrenTags);
}
/**
* Replaces the View specified by oldTag with the View specified by newTag within oldTag's parent.
* This resolves to a simple {@link #manageChildren} call, but React doesn't have enough info in
* JS to formulate it itself.
*
* @deprecated This method will not be available in Fabric UIManager class.
*/
@ReactMethod
@Deprecated
public void replaceExistingNonRootView(int oldTag, int newTag) {
super.replaceExistingNonRootView(oldTag, newTag);
}
/**
* Method which takes a container tag and then releases all subviews for that container upon
* receipt.
*
* @param containerTag the tag of the container for which the subviews must be removed
* @deprecated This method will not be available in Fabric UIManager class.
*/
@ReactMethod
@Deprecated
public void removeSubviewsFromContainerWithID(int containerTag) {
super.removeSubviewsFromContainerWithID(containerTag);
}
/**
* Determines the location on screen, width, and height of the given view and returns the values
* via an async callback.
*/
@ReactMethod
public void measure(int reactTag, Callback callback) {
super.measure(reactTag, callback);
}
/**
* Determines the location on screen, width, and height of the given view relative to the device
* screen and returns the values via an async callback. This is the absolute position including
* things like the status bar
*/
@ReactMethod
public void measureInWindow(int reactTag, Callback callback) {
super.measureInWindow(reactTag, callback);
}
/**
* Measures the view specified by tag relative to the given ancestorTag. This means that the
* returned x, y are relative to the origin x, y of the ancestor view. Results are stored in the
* given outputBuffer. We allow ancestor view and measured view to be the same, in which case the
* position always will be (0, 0) and method will only measure the view dimensions.
*
* <p>NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
* window which can cause unexpected results when measuring relative to things like ScrollViews
* that can have offset content on the screen.
*/
@ReactMethod
public void measureLayout(
int tag, int ancestorTag, Callback errorCallback, Callback successCallback) {
super.measureLayout(tag, ancestorTag, errorCallback, successCallback);
}
/**
* Like {@link #measure} and {@link #measureLayout} but measures relative to the immediate parent.
*
* <p>NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
* window which can cause unexpected results when measuring relative to things like ScrollViews
* that can have offset content on the screen.
*
* @deprecated this method will not be available in FabricUIManager class.
*/
@ReactMethod
@Deprecated
public void measureLayoutRelativeToParent(
int tag, Callback errorCallback, Callback successCallback) {
super.measureLayoutRelativeToParent(tag, errorCallback, successCallback);
}
/**
* Find the touch target child native view in the supplied root view hierarchy, given a react
* target location.
*
* <p>This method is currently used only by Element Inspector DevTool.
*
* @param reactTag the tag of the root view to traverse
* @param point an array containing both X and Y target location
* @param callback will be called if with the identified child view react ID, and measurement
* info. If no view was found, callback will be invoked with no data.
*/
@ReactMethod
public void findSubviewIn(
final int reactTag, final ReadableArray point, final Callback callback) {
super.findSubviewIn(reactTag, point, callback);
}
/**
* Check if the first shadow node is the descendant of the second shadow node
*
* @deprecated this method will not be available in FabricUIManager class.
*/
@ReactMethod
@Deprecated
public void viewIsDescendantOf(
final int reactTag, final int ancestorReactTag, final Callback callback) {
super.viewIsDescendantOf(reactTag, ancestorReactTag, callback);
}
@ReactMethod
public void setJSResponder(int reactTag, boolean blockNativeResponder) {
super.setJSResponder(reactTag, blockNativeResponder);
}
@ReactMethod
public void clearJSResponder() {
super.clearJSResponder();
}
@ReactMethod
public void dispatchViewManagerCommand(
int reactTag, Dynamic commandId, @Nullable ReadableArray commandArgs) {
super.dispatchViewManagerCommand(reactTag, commandId, commandArgs);
}
/**
* Show a PopupMenu.
*
* @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this
* needs to be the tag of a native view (shadow views can not be anchors)
* @param items the menu items as an array of strings
* @param error will be called if there is an error displaying the menu
* @param success will be called with the position of the selected item as the first argument, or
* no arguments if the menu is dismissed
*/
@ReactMethod
public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Callback success) {
super.showPopupMenu(reactTag, items, error, success);
}
@ReactMethod
public void dismissPopupMenu() {
super.dismissPopupMenu();
}
/**
* LayoutAnimation API on Android is currently experimental. Therefore, it needs to be enabled
* explicitly in order to avoid regression in existing application written for iOS using this API.
*
* <p>Warning : This method will be removed in future version of React Native, and layout
* animation will be enabled by default, so always check for its existence before invoking it.
*
* <p>TODO(9139831) : remove this method once layout animation is fully stable.
*
* @param enabled whether layout animation is enabled or not
*/
@ReactMethod
public void setLayoutAnimationEnabledExperimental(boolean enabled) {
super.setLayoutAnimationEnabledExperimental(enabled);
}
/**
* Configure an animation to be used for the native layout changes, and native views creation. The
* animation will only apply during the current batch operations.
*
* <p>TODO(7728153) : animating view deletion is currently not supported.
*
* @param config the configuration of the animation for view addition/removal/update.
* @param success will be called when the animation completes, or when the animation get
* interrupted. In this case, callback parameter will be false.
* @param error will be called if there was an error processing the animation
*/
@ReactMethod
public void configureNextLayoutAnimation(ReadableMap config, Callback success, Callback error) {
super.configureNextLayoutAnimation(config, success, error);
}
@ReactMethod
public void sendAccessibilityEvent(int tag, int eventType) {
super.sendAccessibilityEvent(tag, eventType);
}
}

View File

@@ -0,0 +1,176 @@
package com.swmansion.reanimated;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.bridge.UIManagerListener;
import com.facebook.react.fabric.FabricUIManager;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIManagerModuleListener;
import java.util.ArrayList;
import javax.annotation.Nullable;
@ReactModule(name = ReanimatedModule.NAME)
public class ReanimatedModule extends NativeReanimatedModuleSpec
implements LifecycleEventListener, UIManagerModuleListener,
UIManagerListener {
public static final String NAME = "ReanimatedModule";
public void didDispatchMountItems(@NonNull UIManager uiManager) {
// Keep: Required for UIManagerListener
}
public void didMountItems(@NonNull UIManager uiManager) {
// Keep: Required for UIManagerListener
}
public void didScheduleMountItems(@NonNull UIManager uiManager) {
// Keep: Required for UIManagerListener
}
public void willDispatchViewUpdates(@NonNull UIManager uiManager) {
// This method is called for the interface of UIManagerListener on Fabric.
// The below function with the same name won't be called.
if (mOperations.isEmpty()) {
return;
}
final ArrayList<UIThreadOperation> operations = mOperations;
mOperations = new ArrayList<>();
if (uiManager instanceof FabricUIManager) {
((FabricUIManager)uiManager).addUIBlock(uiBlockViewResolver -> {
NodesManager nodesManager = getNodesManager();
for (UIThreadOperation operation : operations) {
operation.execute(nodesManager);
}
});
} else {
throw new RuntimeException(
"[Reanimated] Failed to obtain instance of FabricUIManager.");
}
}
public void willMountItems(@NonNull UIManager uiManager) {}
private interface UIThreadOperation {
void execute(NodesManager nodesManager);
}
private ArrayList<UIThreadOperation> mOperations = new ArrayList<>();
private @Nullable NodesManager mNodesManager;
public ReanimatedModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public void initialize() {
ReactApplicationContext reactCtx = getReactApplicationContext();
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
UIManager uiManager = reactCtx.getFabricUIManager();
if (uiManager instanceof FabricUIManager) {
((FabricUIManager)uiManager).addUIManagerEventListener(this);
} else {
throw new RuntimeException(
"[Reanimated] Failed to obtain instance of FabricUIManager.");
}
} else {
UIManagerModule uiManager =
reactCtx.getNativeModule(UIManagerModule.class);
uiManager.addUIManagerListener(this);
}
reactCtx.addLifecycleEventListener(this);
}
@Override
public void onHostPause() {
if (mNodesManager != null) {
mNodesManager.onHostPause();
}
}
@Override
public void onHostResume() {
if (mNodesManager != null) {
mNodesManager.onHostResume();
}
}
@Override
public void onHostDestroy() {
// do nothing
}
@Override
public void willDispatchViewUpdates(final UIManagerModule uiManager) {
// This method is called for the interface of UIManagerModuleListener on
// Paper. The below function with the same name won't be called.
if (mOperations.isEmpty()) {
return;
}
final ArrayList<UIThreadOperation> operations = mOperations;
mOperations = new ArrayList<>();
uiManager.addUIBlock(nativeViewHierarchyManager -> {
NodesManager nodesManager = getNodesManager();
for (UIThreadOperation operation : operations) {
operation.execute(nodesManager);
}
});
}
@Override
public String getName() {
return NAME;
}
/*package*/
public NodesManager getNodesManager() {
if (mNodesManager == null) {
mNodesManager = new NodesManager(getReactApplicationContext());
}
return mNodesManager;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public boolean installTurboModule(String valueUnpackerCode) {
// When debugging in chrome the JS context is not available.
// https://github.com/facebook/react-native/blob/v0.67.0-rc.6/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobCollector.java#L25
Utils.isChromeDebugger =
getReactApplicationContext().getJavaScriptContextHolder().get() == 0;
if (!Utils.isChromeDebugger) {
this.getNodesManager().initWithContext(
getReactApplicationContext(), valueUnpackerCode);
return true;
} else {
Log.w(
"[REANIMATED]",
"Unable to create Reanimated Native Module. You can ignore this message if you are using Chrome Debugger now.");
return false;
}
}
@ReactMethod
public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}
@ReactMethod
public void removeListeners(Integer count) {
// Keep: Required for RN built in Event Emitter Calls.
}
@Override
public void invalidate() {
super.invalidate();
if (mNodesManager != null) {
mNodesManager.invalidate();
}
}
}

View File

@@ -0,0 +1,245 @@
package com.facebook.react.uimanager;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import java.util.List;
@ReactModule(name = UIManagerModule.NAME)
public class ReanimatedUIManager extends UIManagerModule {
public ReanimatedUIManager(
ReactApplicationContext reactContext,
List<ViewManager> viewManagersList,
int minTimeLeftInFrameForNonBatchedOperationMs) {
super(reactContext, viewManagersList, minTimeLeftInFrameForNonBatchedOperationMs);
}
public void onBatchComplete() {
super.onBatchComplete();
}
@Override
public boolean canOverrideExistingModule() {
return true;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public @Nullable WritableMap getConstantsForViewManager(@Nullable String viewManagerName) {
return super.getConstantsForViewManager(viewManagerName);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap getDefaultEventTypes() {
return super.getDefaultEventTypes();
}
/** Unregisters a new root view. */
@ReactMethod
public void removeRootView(int rootViewTag) {
super.removeRootView(rootViewTag);
}
@ReactMethod
public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
super.createView(tag, className, rootViewTag, props);
}
@ReactMethod
public void updateView(final int tag, final String className, final ReadableMap props) {
super.updateView(tag, className, props);
}
/**
* Interface for adding/removing/moving views within a parent view from JS.
*
* @param viewTag the view tag of the parent view
* @param moveFrom a list of indices in the parent view to move views from
* @param moveTo parallel to moveFrom, a list of indices in the parent view to move views to
* @param addChildTags a list of tags of views to add to the parent
* @param addAtIndices parallel to addChildTags, a list of indices to insert those children at
* @param removeFrom a list of indices of views to permanently remove. The memory for the
* corresponding views and data structures should be reclaimed.
*/
@ReactMethod
public void manageChildren(
int viewTag,
@Nullable ReadableArray moveFrom,
@Nullable ReadableArray moveTo,
@Nullable ReadableArray addChildTags,
@Nullable ReadableArray addAtIndices,
@Nullable ReadableArray removeFrom) {
super.manageChildren(viewTag, moveFrom, moveTo, addChildTags, addAtIndices, removeFrom);
}
/**
* Interface for fast tracking the initial adding of views. Children view tags are assumed to be
* in order
*
* @param viewTag the view tag of the parent view
* @param childrenTags An array of tags to add to the parent in order
*/
@ReactMethod
public void setChildren(int viewTag, ReadableArray childrenTags) {
super.setChildren(viewTag, childrenTags);
}
/**
* Determines the location on screen, width, and height of the given view and returns the values
* via an async callback.
*/
@ReactMethod
public void measure(int reactTag, Callback callback) {
super.measure(reactTag, callback);
}
/**
* Determines the location on screen, width, and height of the given view relative to the device
* screen and returns the values via an async callback. This is the absolute position including
* things like the status bar
*/
@ReactMethod
public void measureInWindow(int reactTag, Callback callback) {
super.measureInWindow(reactTag, callback);
}
/**
* Measures the view specified by tag relative to the given ancestorTag. This means that the
* returned x, y are relative to the origin x, y of the ancestor view. Results are stored in the
* given outputBuffer. We allow ancestor view and measured view to be the same, in which case the
* position always will be (0, 0) and method will only measure the view dimensions.
*
* <p>NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
* window which can cause unexpected results when measuring relative to things like ScrollViews
* that can have offset content on the screen.
*/
@ReactMethod
public void measureLayout(
int tag, int ancestorTag, Callback errorCallback, Callback successCallback) {
super.measureLayout(tag, ancestorTag, errorCallback, successCallback);
}
/**
* Like {@link #measure} and {@link #measureLayout} but measures relative to the immediate parent.
*
* <p>NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
* window which can cause unexpected results when measuring relative to things like ScrollViews
* that can have offset content on the screen.
*
* @deprecated this method will not be available in FabricUIManager class.
*/
@ReactMethod
@Deprecated
public void measureLayoutRelativeToParent(
int tag, Callback errorCallback, Callback successCallback) {
super.measureLayoutRelativeToParent(tag, errorCallback, successCallback);
}
/**
* Find the touch target child native view in the supplied root view hierarchy, given a react
* target location.
*
* <p>This method is currently used only by Element Inspector DevTool.
*
* @param reactTag the tag of the root view to traverse
* @param point an array containing both X and Y target location
* @param callback will be called if with the identified child view react ID, and measurement
* info. If no view was found, callback will be invoked with no data.
*/
@ReactMethod
public void findSubviewIn(
final int reactTag, final ReadableArray point, final Callback callback) {
super.findSubviewIn(reactTag, point, callback);
}
/**
* Check if the first shadow node is the descendant of the second shadow node
*
* @deprecated this method will not be available in FabricUIManager class.
*/
@ReactMethod
@Deprecated
public void viewIsDescendantOf(
final int reactTag, final int ancestorReactTag, final Callback callback) {
super.viewIsDescendantOf(reactTag, ancestorReactTag, callback);
}
@ReactMethod
public void setJSResponder(int reactTag, boolean blockNativeResponder) {
super.setJSResponder(reactTag, blockNativeResponder);
}
@ReactMethod
public void clearJSResponder() {
super.clearJSResponder();
}
@ReactMethod
public void dispatchViewManagerCommand(
int reactTag, Dynamic commandId, @Nullable ReadableArray commandArgs) {
super.dispatchViewManagerCommand(reactTag, commandId, commandArgs);
}
/**
* Show a PopupMenu.
*
* @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this
* needs to be the tag of a native view (shadow views can not be anchors)
* @param items the menu items as an array of strings
* @param error will be called if there is an error displaying the menu
* @param success will be called with the position of the selected item as the first argument, or
* no arguments if the menu is dismissed
*/
@ReactMethod
public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Callback success) {
super.showPopupMenu(reactTag, items, error, success);
}
@ReactMethod
public void dismissPopupMenu() {
super.dismissPopupMenu();
}
/**
* LayoutAnimation API on Android is currently experimental. Therefore, it needs to be enabled
* explicitly in order to avoid regression in existing application written for iOS using this API.
*
* <p>Warning : This method will be removed in future version of React Native, and layout
* animation will be enabled by default, so always check for its existence before invoking it.
*
* <p>TODO(9139831) : remove this method once layout animation is fully stable.
*
* @param enabled whether layout animation is enabled or not
*/
@ReactMethod
public void setLayoutAnimationEnabledExperimental(boolean enabled) {
super.setLayoutAnimationEnabledExperimental(enabled);
}
/**
* Configure an animation to be used for the native layout changes, and native views creation. The
* animation will only apply during the current batch operations.
*
* <p>TODO(7728153) : animating view deletion is currently not supported.
*
* @param config the configuration of the animation for view addition/removal/update.
* @param success will be called when the animation completes, or when the animation get
* interrupted. In this case, callback parameter will be false.
* @param error will be called if there was an error processing the animation
*/
@ReactMethod
public void configureNextLayoutAnimation(ReadableMap config, Callback success, Callback error) {
super.configureNextLayoutAnimation(config, success, error);
}
@ReactMethod
public void sendAccessibilityEvent(int tag, int eventType) {
super.sendAccessibilityEvent(tag, eventType);
}
}

View File

@@ -0,0 +1,116 @@
package com.swmansion.reanimated;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.fabric.FabricUIManager;
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.common.UIManagerType;
import com.swmansion.reanimated.layoutReanimation.LayoutAnimations;
import com.swmansion.reanimated.layoutReanimation.NativeMethodsHolder;
import com.swmansion.reanimated.nativeProxy.NativeProxyCommon;
import java.util.HashMap;
public class NativeProxy extends NativeProxyCommon {
@DoNotStrip
@SuppressWarnings("unused")
private final HybridData mHybridData;
public NativeProxy(ReactApplicationContext context, String valueUnpackerCode) {
super(context);
ReactFeatureFlagsWrapper.enableMountHooks();
CallInvokerHolderImpl holder =
(CallInvokerHolderImpl) context.getCatalystInstance().getJSCallInvokerHolder();
FabricUIManager fabricUIManager =
(FabricUIManager) UIManagerHelper.getUIManager(context, UIManagerType.FABRIC);
LayoutAnimations LayoutAnimations = new LayoutAnimations(context);
ReanimatedMessageQueueThread messageQueueThread = new ReanimatedMessageQueueThread();
mHybridData =
initHybrid(
context.getJavaScriptContextHolder().get(),
holder,
mAndroidUIScheduler,
LayoutAnimations,
messageQueueThread,
fabricUIManager,
valueUnpackerCode);
prepareLayoutAnimations(LayoutAnimations);
installJSIBindings();
if (BuildConfig.DEBUG) {
checkCppVersion();
}
}
private native HybridData initHybrid(
long jsContext,
CallInvokerHolderImpl jsCallInvokerHolder,
AndroidUIScheduler androidUIScheduler,
LayoutAnimations LayoutAnimations,
MessageQueueThread messageQueueThread,
FabricUIManager fabricUIManager,
String valueUnpackerCode);
public native boolean isAnyHandlerWaitingForEvent(String eventName, int emitterReactTag);
public native void performOperations();
@Override
protected HybridData getHybridData() {
return mHybridData;
}
public static NativeMethodsHolder createNativeMethodsHolder(LayoutAnimations layoutAnimations) {
return new NativeMethodsHolder() {
@Override
public void startAnimation(int tag, int type, HashMap<String, Object> values) {
// NOT IMPLEMENTED
}
@Override
public boolean isLayoutAnimationEnabled() {
// NOT IMPLEMENTED
return false;
}
@Override
public int findPrecedingViewTagForTransition(int tag) {
// NOT IMPLEMENTED
return -1;
}
@Override
public boolean shouldAnimateExiting(int tag, boolean shouldAnimate) {
// NOT IMPLEMENTED
return false;
}
@Override
public boolean hasAnimation(int tag, int type) {
// NOT IMPLEMENTED
return false;
}
@Override
public void clearAnimationConfig(int tag) {
// NOT IMPLEMENTED
}
@Override
public void cancelAnimation(int tag) {
// NOT IMPLEMENTED
}
@Override
public void checkDuplicateSharedTag(int viewTag, int screenTag) {
// NOT IMPLEMENTED
}
};
}
}

View File

@@ -0,0 +1,143 @@
package com.swmansion.reanimated;
import androidx.annotation.OptIn;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.RuntimeExecutor;
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.common.annotations.FrameworkAPI;
import com.facebook.react.fabric.FabricUIManager;
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.common.UIManagerType;
import com.swmansion.reanimated.layoutReanimation.LayoutAnimations;
import com.swmansion.reanimated.layoutReanimation.NativeMethodsHolder;
import com.swmansion.reanimated.nativeProxy.NativeProxyCommon;
import java.util.HashMap;
import java.util.Objects;
public class NativeProxy extends NativeProxyCommon {
@DoNotStrip
@SuppressWarnings("unused")
private final HybridData mHybridData;
public @OptIn(markerClass = FrameworkAPI.class) NativeProxy(ReactApplicationContext context, String valueUnpackerCode) {
super(context);
ReactFeatureFlagsWrapper.enableMountHooks();
FabricUIManager fabricUIManager =
(FabricUIManager) UIManagerHelper.getUIManager(context, UIManagerType.FABRIC);
LayoutAnimations LayoutAnimations = new LayoutAnimations(context);
ReanimatedMessageQueueThread messageQueueThread = new ReanimatedMessageQueueThread();
if (context.isBridgeless()) {
RuntimeExecutor runtimeExecutor = context.getRuntimeExecutor();
mHybridData = initHybridBridgeless(
Objects.requireNonNull(context.getJavaScriptContextHolder()).get(),
runtimeExecutor,
mAndroidUIScheduler,
LayoutAnimations,
messageQueueThread,
fabricUIManager,
valueUnpackerCode
);
} else {
CallInvokerHolderImpl callInvokerHolder = (CallInvokerHolderImpl) context.getCatalystInstance().getJSCallInvokerHolder();
mHybridData =
initHybrid(
Objects.requireNonNull(context.getJavaScriptContextHolder()).get(),
callInvokerHolder,
mAndroidUIScheduler,
LayoutAnimations,
messageQueueThread,
fabricUIManager,
valueUnpackerCode);
}
prepareLayoutAnimations(LayoutAnimations);
installJSIBindings();
if (BuildConfig.DEBUG) {
checkCppVersion();
}
}
private native HybridData initHybrid(
long jsContext,
CallInvokerHolderImpl jsCallInvokerHolder,
AndroidUIScheduler androidUIScheduler,
LayoutAnimations LayoutAnimations,
MessageQueueThread messageQueueThread,
FabricUIManager fabricUIManager,
String valueUnpackerCode);
private native HybridData initHybridBridgeless(
long jsContext,
RuntimeExecutor runtimeExecutor,
AndroidUIScheduler androidUIScheduler,
LayoutAnimations LayoutAnimations,
MessageQueueThread messageQueueThread,
FabricUIManager fabricUIManager,
String valueUnpackerCode);
public native boolean isAnyHandlerWaitingForEvent(String eventName, int emitterReactTag);
public native void performOperations();
@Override
protected HybridData getHybridData() {
return mHybridData;
}
public static NativeMethodsHolder createNativeMethodsHolder(LayoutAnimations layoutAnimations) {
return new NativeMethodsHolder() {
@Override
public void startAnimation(int tag, int type, HashMap<String, Object> values) {
// NOT IMPLEMENTED
}
@Override
public boolean isLayoutAnimationEnabled() {
// NOT IMPLEMENTED
return false;
}
@Override
public int findPrecedingViewTagForTransition(int tag) {
// NOT IMPLEMENTED
return -1;
}
@Override
public boolean shouldAnimateExiting(int tag, boolean shouldAnimate) {
// NOT IMPLEMENTED
return false;
}
@Override
public boolean hasAnimation(int tag, int type) {
// NOT IMPLEMENTED
return false;
}
@Override
public void clearAnimationConfig(int tag) {
// NOT IMPLEMENTED
}
@Override
public void cancelAnimation(int tag) {
// NOT IMPLEMENTED
}
@Override
public void checkDuplicateSharedTag(int viewTag, int screenTag) {
// NOT IMPLEMENTED
}
};
}
}

View File

@@ -0,0 +1,12 @@
package com.swmansion.reanimated;
import com.facebook.proguard.annotations.DoNotStrip;
import com.swmansion.reanimated.ReanimatedMessageQueueThreadBase;
@DoNotStrip
public class ReanimatedMessageQueueThread extends ReanimatedMessageQueueThreadBase {
@Override
public boolean runOnQueue(Runnable runnable) {
return messageQueueThread.runOnQueue(runnable);
}
}

View File

@@ -0,0 +1,17 @@
package com.swmansion.reanimated;
import com.facebook.proguard.annotations.DoNotStrip;
import com.swmansion.reanimated.ReanimatedMessageQueueThreadBase;
@DoNotStrip
public class ReanimatedMessageQueueThread extends ReanimatedMessageQueueThreadBase {
@Override
public boolean runOnQueue(Runnable runnable) {
return messageQueueThread.runOnQueue(runnable);
}
@Override
public boolean isIdle() {
return messageQueueThread.isIdle();
}
}