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,319 @@
project(Reanimated)
cmake_minimum_required(VERSION 3.8)
set (CMAKE_VERBOSE_MAKEFILE ON)
if(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 73)
set (CMAKE_CXX_STANDARD 20)
else()
set (CMAKE_CXX_STANDARD 17)
endif()
# default CMAKE_CXX_FLAGS: "-g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fstack-protector-all"
if(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 71)
include("${REACT_NATIVE_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake")
add_compile_options(${folly_FLAGS})
else()
set(folly_FLAGS "-DFOLLY_NO_CONFIG=1 -DFOLLY_HAVE_CLOCK_GETTIME=1 -DFOLLY_HAVE_MEMRCHR=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_MOBILE=1 -DFOLLY_HAVE_RECVMMSG=1 -DFOLLY_HAVE_PTHREAD=1")
string(APPEND CMAKE_CXX_FLAGS " ${folly_FLAGS}")
endif()
string(APPEND CMAKE_CXX_FLAGS " -DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION} -DREANIMATED_VERSION=${REANIMATED_VERSION} -DHERMES_ENABLE_DEBUGGER=${HERMES_ENABLE_DEBUGGER}")
string(APPEND CMAKE_CXX_FLAGS " -fexceptions -fno-omit-frame-pointer -frtti -fstack-protector-all -std=c++${CMAKE_CXX_STANDARD} -Wall -Werror")
if(${IS_NEW_ARCHITECTURE_ENABLED})
string(APPEND CMAKE_CXX_FLAGS " -DRCT_NEW_ARCH_ENABLED")
endif()
if(${IS_REANIMATED_EXAMPLE_APP})
string(APPEND CMAKE_CXX_FLAGS " -DIS_REANIMATED_EXAMPLE_APP")
endif()
if(NOT ${CMAKE_BUILD_TYPE} MATCHES "Debug")
string(APPEND CMAKE_CXX_FLAGS " -DNDEBUG")
endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "")
set (PACKAGE_NAME "reanimated")
set (BUILD_DIR ${CMAKE_SOURCE_DIR}/build)
set (SRC_DIR ${CMAKE_SOURCE_DIR}/src)
set (COMMON_SRC_DIR "${CMAKE_SOURCE_DIR}/../Common")
file(GLOB_RECURSE SOURCES_COMMON CONFIGURE_DEPENDS "${COMMON_SRC_DIR}/cpp/**.cpp")
file(GLOB_RECURSE SOURCES_ANDROID CONFIGURE_DEPENDS "${SRC_DIR}/main/cpp/**.cpp")
if(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 71)
# Consume shared libraries and headers from prefabs
find_package(fbjni REQUIRED CONFIG)
find_package(ReactAndroid REQUIRED CONFIG)
if(${JS_RUNTIME} STREQUAL "hermes")
find_package(hermes-engine REQUIRED CONFIG)
endif()
else()
# Consume shared libraries from found .so files
if(${IS_NEW_ARCHITECTURE_ENABLED})
message(FATAL_ERROR "not supported")
else()
set (RN_SO_DIR "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/first-party/react/jni")
set (FBJNI_HEADERS_DIR "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/first-party/fbjni/headers")
endif()
file (GLOB LIBRN_DIR "${RN_SO_DIR}/${ANDROID_ABI}")
endif()
add_library(
${PACKAGE_NAME}
SHARED
${SOURCES_COMMON}
${SOURCES_ANDROID}
${INCLUDE_JSI_CPP}
${INCLUDE_JSIDYNAMIC_CPP}
)
# includes
target_include_directories(
${PACKAGE_NAME}
PRIVATE
"${COMMON_SRC_DIR}/cpp/AnimatedSensor"
"${COMMON_SRC_DIR}/cpp/Fabric"
"${COMMON_SRC_DIR}/cpp/hidden_headers"
"${COMMON_SRC_DIR}/cpp/LayoutAnimations"
"${COMMON_SRC_DIR}/cpp/NativeModules"
"${COMMON_SRC_DIR}/cpp/ReanimatedRuntime"
"${COMMON_SRC_DIR}/cpp/Registries"
"${COMMON_SRC_DIR}/cpp/SharedItems"
"${COMMON_SRC_DIR}/cpp/Tools"
"${SRC_DIR}/main/cpp"
)
if(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 71)
target_include_directories(
${PACKAGE_NAME}
PRIVATE
"${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule"
"${REACT_NATIVE_DIR}/ReactCommon"
"${REACT_NATIVE_DIR}/ReactCommon/callinvoker"
"${REACT_NATIVE_DIR}/ReactCommon/react/renderer/graphics/platform/cxx"
"${REACT_NATIVE_DIR}/ReactCommon/runtimeexecutor"
"${REACT_NATIVE_DIR}/ReactCommon/yoga"
)
else()
file(GLOB LIBFBJNI_INCLUDE_DIR ${FBJNI_HEADERS_DIR})
target_include_directories(
${PACKAGE_NAME}
PRIVATE
"${LIBFBJNI_INCLUDE_DIR}"
"${BUILD_DIR}/third-party-ndk/boost/boost_${BOOST_VERSION}"
"${BUILD_DIR}/third-party-ndk/double-conversion"
"${BUILD_DIR}/third-party-ndk/folly"
"${BUILD_DIR}/third-party-ndk/glog/exported"
"${REACT_NATIVE_DIR}/React"
"${REACT_NATIVE_DIR}/React/Base"
"${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni"
"${REACT_NATIVE_DIR}/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni"
"${REACT_NATIVE_DIR}/ReactAndroid/src/main/java/com/facebook/react/fabric/jni"
"${REACT_NATIVE_DIR}/ReactCommon"
"${REACT_NATIVE_DIR}/ReactCommon/callinvoker"
"${REACT_NATIVE_DIR}/ReactCommon/jsi"
"${REACT_NATIVE_DIR}/ReactCommon/hermes"
"${REACT_NATIVE_DIR}/ReactCommon/react/renderer/graphics/platform/cxx"
"${REACT_NATIVE_DIR}/ReactCommon/runtimeexecutor"
"${REACT_NATIVE_DIR}/ReactCommon/turbomodule/core"
"${REACT_NATIVE_DIR}/ReactCommon/turbomodule"
"${REACT_NATIVE_DIR}/ReactCommon/yoga"
)
endif()
# build shared lib
set_target_properties(${PACKAGE_NAME} PROPERTIES LINKER_LANGUAGE CXX)
target_link_libraries(
${PACKAGE_NAME}
log
android
)
if(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 71)
target_link_libraries(
${PACKAGE_NAME}
ReactAndroid::folly_runtime
ReactAndroid::glog
ReactAndroid::jsi
ReactAndroid::reactnativejni
fbjni::fbjni
)
else()
find_library(
JSI_LIB
jsi
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
find_library(
GLOG_LIB
glog
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
find_library(
FBJNI_LIB
fbjni
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
if(${REACT_NATIVE_MINOR_VERSION} LESS 69)
find_library(
FOLLY_LIB
folly_json
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
else()
find_library(
FOLLY_LIB
folly_runtime
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
endif()
find_library(
REACTNATIVEJNI_LIB
reactnativejni
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
target_link_libraries(
${PACKAGE_NAME}
${JSI_LIB}
${GLOG_LIB}
${FBJNI_LIB}
${FOLLY_LIB}
${REACTNATIVEJNI_LIB}
)
endif()
if(${JS_RUNTIME} STREQUAL "hermes")
string(APPEND CMAKE_CXX_FLAGS " -DJS_RUNTIME_HERMES=1")
if(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 71)
# From prefab from module `com.facebook.react:hermes-android`
set(HERMES_LIB hermes-engine::libhermes)
elseif(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 69)
# Bundled Hermes from module `com.facebook.react:hermes-engine` or project `:ReactAndroid:hermes-engine`
target_include_directories(
${PACKAGE_NAME}
PRIVATE
"${JS_RUNTIME_DIR}/API"
"${JS_RUNTIME_DIR}/public"
)
set(HERMES_LIB "${BUILD_DIR}/third-party-ndk/hermes/jni/${ANDROID_ABI}/libhermes.so")
else()
# From `hermes-engine` npm package
target_include_directories(
${PACKAGE_NAME}
PRIVATE
"${JS_RUNTIME_DIR}/android/include"
)
set(HERMES_LIB "${BUILD_DIR}/third-party-ndk/hermes/jni/${ANDROID_ABI}/libhermes.so")
endif()
target_link_libraries(
${PACKAGE_NAME}
${HERMES_LIB}
)
if (${HERMES_ENABLE_DEBUGGER})
if(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 71)
set(HERMES_EXECUTOR_LIB ReactAndroid::hermes_executor)
else()
find_library(
HERMES_EXECUTOR_LIB
hermes-executor-debug
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
endif()
if(${REACT_NATIVE_MINOR_VERSION} LESS_EQUAL 67)
find_library(
HERMES_INSPECTOR_LIB
hermes-inspector
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
target_link_libraries(
${PACKAGE_NAME}
${HERMES_INSPECTOR_LIB}
)
endif()
target_link_libraries(
${PACKAGE_NAME}
${HERMES_EXECUTOR_LIB}
)
endif()
elseif(${JS_RUNTIME} STREQUAL "v8")
string(APPEND CMAKE_CXX_FLAGS " -DJS_RUNTIME_V8=1")
target_include_directories(
${PACKAGE_NAME}
PRIVATE
"${JS_RUNTIME_DIR}/src"
)
file (GLOB V8_SO_DIR "${JS_RUNTIME_DIR}/android/build/intermediates/library_jni/*/jni/${ANDROID_ABI}")
find_library(
V8EXECUTOR_LIB
v8executor
PATHS ${V8_SO_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
target_link_libraries(
${PACKAGE_NAME}
${V8EXECUTOR_LIB}
)
elseif(${JS_RUNTIME} STREQUAL "jsc")
string(APPEND CMAKE_CXX_FLAGS " -DJS_RUNTIME_JSC=1")
if(${REACT_NATIVE_MINOR_VERSION} GREATER_EQUAL 71)
set(JSEXECUTOR_LIB ReactAndroid::jscexecutor)
else()
find_library(
JSEXECUTOR_LIB
jscexecutor
PATHS ${LIBRN_DIR}
NO_DEFAULT_PATH
NO_CMAKE_FIND_ROOT_PATH
)
endif()
target_link_libraries(${PACKAGE_NAME} ${JSEXECUTOR_LIB})
else()
message(FATAL_ERROR "Unknown JS runtime ${JS_RUNTIME}.")
endif()
if(${IS_NEW_ARCHITECTURE_ENABLED})
target_link_libraries(
${PACKAGE_NAME}
ReactAndroid::fabricjni
ReactAndroid::react_debug
ReactAndroid::react_render_core
ReactAndroid::react_render_mounting
ReactAndroid::react_render_scheduler
ReactAndroid::react_render_uimanager
ReactAndroid::rrc_view
)
endif()
# Resolves "CMake Warning: Manually-specified variables were not used by the project"
# when any of the following variables is not used in some build configuration.
set (ignoreMe "${JS_RUNTIME_DIR}")
set (ignoreMe "${BOOST_VERSION}")

View File

@@ -0,0 +1,15 @@
README
======
If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm:
1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed
2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK
```
ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/{username}/Library/Android/sdk
```
3. Delete the `maven` folder
4. Run `sudo ./gradlew installArchives`
5. Verify that latest set of generated files is in the maven folder with the correct version number

View File

@@ -0,0 +1,921 @@
import com.android.Version
import org.apache.tools.ant.filters.ReplaceTokens
import org.apache.tools.ant.taskdefs.condition.Os
import groovy.json.JsonSlurper
import javax.inject.Inject
import java.nio.file.Files
import java.nio.file.Paths
/**
* Finds the path of the installed npm package with the given name using Node's
* module resolution algorithm, which searches "node_modules" directories up to
* the file system root. This handles various cases, including:
*
* - Working in the open-source RN repo:
* Gradle: /path/to/react-native/ReactAndroid
* Node module: /path/to/react-native/node_modules/[package]
*
* - Installing RN as a dependency of an app and searching for hoisted
* dependencies:
* Gradle: /path/to/app/node_modules/react-native/ReactAndroid
* Node module: /path/to/app/node_modules/[package]
*
* - Working in a larger repo (e.g., Facebook) that contains RN:
* Gradle: /path/to/repo/path/to/react-native/ReactAndroid
* Node module: /path/to/repo/node_modules/[package]
*
* The search begins at the given base directory (a File object). The returned
* path is a string.
*/
static def findNodeModulePath(baseDir, packageName) {
def basePath = baseDir.toPath().normalize()
// Node's module resolution algorithm searches up to the root directory,
// after which the base path will be null
while (basePath) {
def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
if (candidatePath.toFile().exists()) {
return candidatePath.toString()
}
basePath = basePath.getParent()
}
return null
}
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
def safeAppExtGet(prop, fallback) {
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
appProject?.ext?.has(prop) ? appProject.ext.get(prop) : fallback
}
def resolveBuildType() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests()['args'].toString()
return tskReqStr.contains('Release') ? 'release' : 'debug'
}
def isReanimatedExampleApp() {
return safeAppExtGet("isReanimatedExampleApp", false)
}
def isNewArchitectureEnabled() {
// To opt-in for the New Architecture, you can either:
// - Set `newArchEnabled` to true inside the `gradle.properties` file
// - Invoke gradle with `-newArchEnabled=true`
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}
def resolveReactNativeDirectory() {
def reactNativeLocation = safeAppExtGet("REACT_NATIVE_NODE_MODULES_DIR", null)
if (reactNativeLocation != null) {
return file(reactNativeLocation)
}
if (isReanimatedExampleApp()) {
return file("$projectDir/../${getPlaygroundAppName()}/node_modules/react-native")
}
// monorepo workaround
// react-native can be hoisted or in project's own node_modules
def reactNativeFromProjectNodeModules = file("${rootProject.projectDir}/../node_modules/react-native")
if (reactNativeFromProjectNodeModules.exists()) {
return reactNativeFromProjectNodeModules
}
def reactNativeFromNodeModulesWithReanimated = file("${projectDir}/../../react-native")
if (reactNativeFromNodeModulesWithReanimated.exists()) {
return reactNativeFromNodeModulesWithReanimated
}
throw new GradleException(
"[Reanimated] Unable to resolve react-native location in node_modules. You should project extension property (in `app/build.gradle`) `REACT_NATIVE_NODE_MODULES_DIR` with path to react-native."
)
}
def getPlaygroundAppName() { // only for the development
String playgroundAppName = ""
try {
rootProject.getSubprojects().forEach({project ->
if (project.plugins.hasPlugin("com.android.application")) {
var projectCatalogAbsolutePath = project.projectDir.toString().replace("/android/app", "")
var slashPosition = projectCatalogAbsolutePath.lastIndexOf("/")
playgroundAppName = projectCatalogAbsolutePath.substring(slashPosition + 1)
}
})
} catch (_) {
throw new GradleException("[Reanimated] Couldn't determine playground app name.")
}
return playgroundAppName
}
def getReanimatedVersion() {
def inputFile = file(projectDir.path + '/../package.json')
def json = new JsonSlurper().parseText(inputFile.text)
return json.version
}
def getReanimatedMajorVersion() {
def (major, minor, patch) = getReanimatedVersion().tokenize('.')
return major.toInteger()
}
def toPlatformFileString(String path) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
path = path.replace(File.separatorChar, '/' as char)
}
return path
}
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}
def reactNativeRootDir = resolveReactNativeDirectory()
def reactProperties = new Properties()
file("$reactNativeRootDir/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) }
def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME")
def REACT_NATIVE_MINOR_VERSION = REACT_NATIVE_VERSION.startsWith("0.0.0-") ? 1000 : REACT_NATIVE_VERSION.split("\\.")[1].toInteger()
def REANIMATED_VERSION = getReanimatedVersion()
def REANIMATED_MAJOR_VERSION = getReanimatedMajorVersion()
def IS_NEW_ARCHITECTURE_ENABLED = isNewArchitectureEnabled()
// for React Native <= 0.70
def BOOST_VERSION = reactProperties.getProperty("BOOST_VERSION")
def DOUBLE_CONVERSION_VERSION = reactProperties.getProperty("DOUBLE_CONVERSION_VERSION")
def FOLLY_VERSION = reactProperties.getProperty("FOLLY_VERSION")
def GLOG_VERSION = reactProperties.getProperty("GLOG_VERSION")
def FBJNI_VERSION = "0.3.0"
// We download various C++ open-source dependencies into downloads.
// We then copy both the downloaded code and our custom makefiles and headers into third-party-ndk.
// After that we build native code from src/main/jni with module path pointing at third-party-ndk.
def customDownloadsDir = System.getenv("REACT_NATIVE_DOWNLOADS_DIR")
def downloadsDir = customDownloadsDir ? new File(customDownloadsDir) : new File("$buildDir/downloads")
def thirdPartyNdkDir = new File("$buildDir/third-party-ndk")
def reactNativeThirdParty = new File("$reactNativeRootDir/ReactAndroid/src/main/jni/third-party")
def reactNativeAndroidDownloadDir = new File("$reactNativeRootDir/ReactAndroid/build/downloads")
def prefabHeadersDir = project.file("$buildDir/prefab-headers/reanimated")
def JS_RUNTIME = {
// Override JS runtime with environment variable
if (System.getenv("JS_RUNTIME")) {
return System.getenv("JS_RUNTIME")
}
// Enable V8 runtime if react-native-v8 is installed
def v8Project = rootProject.getSubprojects().find { project -> project.name == "react-native-v8" }
if (v8Project != null) {
return "v8"
}
// Check if Hermes is enabled in app setup
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
if ((REACT_NATIVE_MINOR_VERSION >= 71 && appProject?.hermesEnabled?.toBoolean()) || appProject?.ext?.react?.enableHermes?.toBoolean()) {
return "hermes"
}
// Use JavaScriptCore (JSC) by default
return "jsc"
}.call()
def jsRuntimeDir = {
if (JS_RUNTIME == "hermes") {
if (REACT_NATIVE_MINOR_VERSION >= 69) {
return Paths.get(reactNativeRootDir.path, "sdks", "hermes")
} else {
return Paths.get(reactNativeRootDir.path, "..", "hermes-engine")
}
} else if (JS_RUNTIME == "v8") {
return findProject(":react-native-v8").getProjectDir().getParent()
} else {
return Paths.get(reactNativeRootDir.path, "ReactCommon", "jsi")
}
}.call()
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
buildscript {
repositories {
google()
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "com.android.tools.build:gradle:7.3.1"
classpath "de.undercouch:gradle-download-task:5.0.1"
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.11.0"
}
}
if (project == rootProject) {
apply from: "spotless.gradle"
}
apply plugin: "com.android.library"
apply plugin: "maven-publish"
apply plugin: "de.undercouch.download"
android {
compileSdkVersion safeExtGet("compileSdkVersion", 30)
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() >= 7) {
namespace "com.swmansion.reanimated"
}
if (rootProject.hasProperty("ndkPath")) {
ndkPath rootProject.ext.ndkPath
}
if (rootProject.hasProperty("ndkVersion")) {
ndkVersion rootProject.ext.ndkVersion
}
buildFeatures {
if (REACT_NATIVE_MINOR_VERSION > 68) {
prefab true
prefabPublishing true
buildConfig true
}
}
prefab {
reanimated {
headers prefabHeadersDir.absolutePath
}
}
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 16)
targetSdkVersion safeExtGet("targetSdkVersion", 30)
versionCode 1
versionName "1.0"
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", IS_NEW_ARCHITECTURE_ENABLED.toString())
buildConfigField("String", "REANIMATED_VERSION_JAVA", "\"${REANIMATED_VERSION}\"")
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared",
"-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}",
"-DANDROID_TOOLCHAIN=clang",
REACT_NATIVE_MINOR_VERSION < 71 ? "-DBOOST_VERSION=${BOOST_VERSION}" : "-DBOOST_VERSION=",
"-DREACT_NATIVE_DIR=${toPlatformFileString(reactNativeRootDir.path)}",
"-DJS_RUNTIME=${JS_RUNTIME}",
"-DJS_RUNTIME_DIR=${jsRuntimeDir}",
"-DIS_NEW_ARCHITECTURE_ENABLED=${IS_NEW_ARCHITECTURE_ENABLED}",
"-DIS_REANIMATED_EXAMPLE_APP=${isReanimatedExampleApp()}",
"-DREANIMATED_VERSION=${REANIMATED_VERSION}"
abiFilters (*reactNativeArchitectures())
}
}
buildConfigField("boolean", "IS_INTERNAL_BUILD", "false")
buildConfigField("int", "EXOPACKAGE_FLAGS", "0")
buildConfigField("int", "REACT_NATIVE_MINOR_VERSION", REACT_NATIVE_MINOR_VERSION.toString())
consumerProguardFiles 'proguard-rules.pro'
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
buildTypes {
debug {
externalNativeBuild {
cmake {
if (JS_RUNTIME == "hermes") {
arguments "-DHERMES_ENABLE_DEBUGGER=1"
} else {
arguments "-DHERMES_ENABLE_DEBUGGER=0"
}
}
}
}
release {
externalNativeBuild {
cmake {
arguments "-DHERMES_ENABLE_DEBUGGER=0"
}
}
}
}
lintOptions {
abortOnError false
}
packagingOptions {
doNotStrip resolveBuildType() == 'debug' ? "**/**/*.so" : ''
excludes = [
"META-INF",
"META-INF/**",
"**/libc++_shared.so",
"**/libfbjni.so",
"**/libjsi.so",
"**/libfolly_json.so",
"**/libfolly_runtime.so",
"**/libglog.so",
"**/libhermes.so",
"**/libhermes-executor-debug.so",
"**/libhermes_executor.so",
"**/libreactnativejni.so",
"**/libturbomodulejsijni.so",
"**/libreact_nativemodule_core.so",
"**/libjscexecutor.so",
"**/libv8executor.so",
]
}
tasks.withType(JavaCompile) {
compileTask ->
compileTask.dependsOn(packageNdkLibs)
}
configurations {
extractHeaders
extractSO
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
// For some reason gradle only complains about the duplicated version of librrc_root and libreact_render libraries
// while there are more libraries copied in intermediates folder of the lib build directory, we exclude
// only the ones that make the build fail (ideally we should only include libreanimated but we
// are only allowed to specify exlude patterns)
exclude "**/libreact_render*.so"
exclude "**/librrc_root.so"
}
sourceSets.main {
java {
if (IS_NEW_ARCHITECTURE_ENABLED) {
srcDirs += "src/fabric/java"
} else {
srcDirs += "src/paper/java"
}
// messageQueueThread
if (REANIMATED_MAJOR_VERSION > 2) {
if (REACT_NATIVE_MINOR_VERSION <= 72) {
srcDirs += "src/reactNativeVersionPatch/messageQueueThread/72"
} else {
srcDirs += "src/reactNativeVersionPatch/messageQueueThread/latest"
}
}
// ReanimatedUIManager & ReanimatedUIImplementation
if (REACT_NATIVE_MINOR_VERSION <= 73) {
srcDirs += "src/reactNativeVersionPatch/ReanimatedUIManager/73"
} else {
srcDirs += "src/reactNativeVersionPatch/ReanimatedUIManager/latest"
}
// ReactFeatureFlags
if (IS_NEW_ARCHITECTURE_ENABLED) {
if (REACT_NATIVE_MINOR_VERSION <= 72) {
srcDirs += "src/reactNativeVersionPatch/ReactFeatureFlagsWrapper/72"
} else {
srcDirs += "src/reactNativeVersionPatch/ReactFeatureFlagsWrapper/latest"
}
}
// RuntimeExecutor
if (IS_NEW_ARCHITECTURE_ENABLED) {
if (REACT_NATIVE_MINOR_VERSION <= 73) {
srcDirs += "src/reactNativeVersionPatch/RuntimeExecutor/73"
} else {
srcDirs += "src/reactNativeVersionPatch/RuntimeExecutor/latest"
}
}
}
}
}
def assertLatestReactNativeWithNewArchitecture = task assertLatestReactNativeWithNewArchitectureTask {
onlyIf { IS_NEW_ARCHITECTURE_ENABLED && REANIMATED_MAJOR_VERSION == 3 && REACT_NATIVE_MINOR_VERSION < 72 }
doFirst {
// If you change the minimal React Native version remember to update Compatibility Table in docs
throw new GradleException(
"[Reanimated] Outdated version of React Native for New Architecture. Reanimated " + REANIMATED_VERSION + " supports the New Architecture on React Native 0.72.0+. See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#outdated-version-of-react-native-for-new-architecture for more information."
)
}
}
def assertMinimalReactNativeVersion = task assertMinimalReactNativeVersionTask {
onlyIf { REACT_NATIVE_MINOR_VERSION < 66 }
doFirst {
// If you change the minimal React Native version remember to update Compatibility Table in docs
throw new GradleException("[Reanimated] Unsupported React Native version. Please use 0.66 or newer.")
}
}
task prepareHeadersForPrefab(type: Copy) {
from("$projectDir/src/main/cpp")
from("$projectDir/../Common/cpp/AnimatedSensor")
from("$projectDir/../Common/cpp/Fabric")
from("$projectDir/../Common/cpp/LayoutAnimations")
from("$projectDir/../Common/cpp/NativeModules")
from("$projectDir/../Common/cpp/ReanimatedRuntime")
from("$projectDir/../Common/cpp/Registries")
from("$projectDir/../Common/cpp/SharedItems")
from("$projectDir/../Common/cpp/Tools")
include("*.h")
into(prefabHeadersDir)
}
tasks.preBuild {
dependsOn assertLatestReactNativeWithNewArchitecture, assertMinimalReactNativeVersion
}
task cleanCmakeCache() {
tasks.getByName("clean").dependsOn(cleanCmakeCache)
doFirst {
delete "${projectDir}/.cxx"
}
}
task printVersions {
println "Android gradle plugin: ${Version.ANDROID_GRADLE_PLUGIN_VERSION}"
println "Gradle: ${project.gradle.gradleVersion}"
}
task createNativeDepsDirectories() {
downloadsDir.mkdirs()
thirdPartyNdkDir.mkdirs()
prefabHeadersDir.mkdirs()
}
def resolveTaskFactory(String taskName, String artifactLocalName, File reactNativeAndroidDownloadDir, File reanimatedDownloadDir) {
return tasks.create(name: taskName, dependsOn: createNativeDepsDirectories, type: Copy) {
from reactNativeAndroidDownloadDir
include artifactLocalName
into reanimatedDownloadDir
onlyIf {
// First we check whether the file is already in our download directory
if (file("$reanimatedDownloadDir/$artifactLocalName").isFile()) {
return false
}
// If it is not the case we check whether it was downloaded by ReactAndroid project
if (file("$reactNativeAndroidDownloadDir/$artifactLocalName").isFile()) {
return true
}
return false
}
}
}
/*
Reanimated includes "hermes/hermes.h" header file in `NativeProxy.cpp`.
Previously, we used header files from `hermes-engine` package in `node_modules`.
In React Native 0.69 and 0.70, Hermes is no longer distributed as package on NPM.
On the new architecture, Hermes is downloaded from GitHub and then compiled from sources.
However, on the old architecture, we need to download Hermes header files on our own
as well as unzip Hermes AAR in order to obtain `libhermes.so` shared library.
For more details, see https://reactnative.dev/architecture/bundled-hermes
or https://github.com/reactwg/react-native-new-architecture/discussions/4
*/
if (REACT_NATIVE_MINOR_VERSION in [69, 70] && !IS_NEW_ARCHITECTURE_ENABLED) {
// copied from `react-native/ReactAndroid/hermes-engine/build.gradle`
def downloadDir = customDownloadsDir ? new File(customDownloadsDir) : new File(reactNativeRootDir, "sdks/download")
// By default we are going to download and unzip hermes inside the /sdks/hermes folder
// but you can provide an override for where the hermes source code is located.
def hermesDir = System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR") ?: new File(reactNativeRootDir, "sdks/hermes")
def hermesVersion = "main"
def hermesVersionFile = new File(reactNativeRootDir, "sdks/.hermesversion")
if (hermesVersionFile.exists()) {
hermesVersion = hermesVersionFile.text
}
task downloadHermes(type: Download) {
src("https://github.com/facebook/hermes/tarball/${hermesVersion}")
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadDir, "hermes.tar.gz"))
}
task unzipHermes(dependsOn: downloadHermes, type: Copy) {
from(tarTree(downloadHermes.dest)) {
eachFile { file ->
// We flatten the unzip as the tarball contains a `facebook-hermes-<SHA>`
// folder at the top level.
if (file.relativePath.segments.size() > 1) {
file.relativePath = new RelativePath(!file.isDirectory(), file.relativePath.segments.drop(1))
}
}
}
into(hermesDir)
}
}
if (REACT_NATIVE_MINOR_VERSION < 71) {
// You need to have following folders in this directory:
// - boost_1_63_0
// - double-conversion-1.1.6
// - folly-deprecate-dynamic-initializer
// - glog-0.3.5
def dependenciesPath = System.getenv("REACT_NATIVE_DEPENDENCIES")
// The Boost library is a very large download (>100MB).
// If Boost is already present on your system, define the REACT_NATIVE_BOOST_PATH env variable
// and the build will use that.
def boostPath = dependenciesPath ?: System.getenv("REACT_NATIVE_BOOST_PATH")
def follyReplaceContent = '''
ssize_t r;
do {
r = open(name, flags, mode);
} while (r == -1 && errno == EINTR);
return r;
'''
Task resolveBoost = resolveTaskFactory("resolveBoost", "boost_${BOOST_VERSION}.tar.gz", reactNativeAndroidDownloadDir, downloadsDir)
Task resolveDoubleConversion = resolveTaskFactory(
"resolveDoubleConversion",
"double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz",
reactNativeAndroidDownloadDir,
downloadsDir
)
Task resolveFolly = resolveTaskFactory("resolveFolly", "folly-${FOLLY_VERSION}.tar.gz", reactNativeAndroidDownloadDir, downloadsDir)
Task resolveGlog = resolveTaskFactory("resolveGlog", "glog-${GLOG_VERSION}.tar.gz", reactNativeAndroidDownloadDir, downloadsDir)
if (IS_NEW_ARCHITECTURE_ENABLED) {
def reactNativeAndroidProject = findProject(":ReactAndroid")
if (reactNativeAndroidProject != null) {
reactNativeAndroidProject.afterEvaluate {
def resolveTasks = [resolveBoost, resolveGlog, resolveDoubleConversion, resolveFolly]
resolveTasks.forEach({ task ->
String reactAndroidDownloadTaskName = "download" + task.name.replace("resolve", "")
def reactAndroidDownloadTask = reactNativeAndroidProject.getTasks().findByName(reactAndroidDownloadTaskName)
if (reactAndroidDownloadTask != null) {
task.dependsOn(reactAndroidDownloadTask)
} else {
logger.warn("[Reanimated] Failed to find task named `$reactAndroidDownloadTaskName` in `:ReactAndroid` project." +
" Explicit dependency between it and $task.name task can not be set.")
}
})
}
} else {
throw new GradleException("[Reanimated] Failed to find `:ReactAndroid` project. Explicit dependency between download tasks can not be set.")
}
}
task downloadBoost(dependsOn: resolveBoost, type: Download) {
def transformedVersion = BOOST_VERSION.replace("_", ".")
def artifactLocalName = "boost_${BOOST_VERSION}.tar.gz"
def srcUrl = "https://archives.boost.io/release/${transformedVersion}/source/${artifactLocalName}"
if (REACT_NATIVE_MINOR_VERSION < 69) {
srcUrl = "https://github.com/react-native-community/boost-for-react-native/releases/download/v${transformedVersion}-0/${artifactLocalName}"
}
src(srcUrl)
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, artifactLocalName))
}
task prepareBoost(dependsOn: boostPath ? [] : [downloadBoost], type: Copy) {
from(boostPath ?: tarTree(resources.gzip(downloadBoost.dest)))
from("$reactNativeThirdParty/boost/Android.mk")
include("Android.mk", "boost_${BOOST_VERSION}/boost/**/*.hpp", "boost/boost/**/*.hpp")
includeEmptyDirs = false
into("$thirdPartyNdkDir/boost")
doLast {
file("$thirdPartyNdkDir/boost/boost").renameTo("$thirdPartyNdkDir/boost/boost_${BOOST_VERSION}")
}
}
task downloadDoubleConversion(dependsOn: resolveDoubleConversion, type: Download) {
src("https://github.com/google/double-conversion/archive/v${DOUBLE_CONVERSION_VERSION}.tar.gz")
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, "double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz"))
}
task prepareDoubleConversion(dependsOn: dependenciesPath ? [] : [downloadDoubleConversion], type: Copy) {
from(dependenciesPath ?: tarTree(downloadDoubleConversion.dest))
from("$reactNativeThirdParty/double-conversion/Android.mk")
include("double-conversion-${DOUBLE_CONVERSION_VERSION}/src/**/*", "Android.mk")
filesMatching("*/src/**/*", { fname -> fname.path = "double-conversion/${fname.name}" })
includeEmptyDirs = false
into("$thirdPartyNdkDir/double-conversion")
}
task downloadFolly(dependsOn: resolveFolly, type: Download) {
src("https://github.com/facebook/folly/archive/v${FOLLY_VERSION}.tar.gz")
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, "folly-${FOLLY_VERSION}.tar.gz"))
}
task prepareFolly(dependsOn: dependenciesPath ? [] : [downloadFolly], type: Copy) {
from(dependenciesPath ?: tarTree(downloadFolly.dest))
from("$reactNativeThirdParty/folly/Android.mk")
include("folly-${FOLLY_VERSION}/folly/**/*", "Android.mk")
eachFile { fname -> fname.path = (fname.path - "folly-${FOLLY_VERSION}/") }
// Fixes problem with Folly failing to build on certain systems. See
// https://github.com/software-mansion/react-native-reanimated/issues/1024
filter { line -> line.replaceAll("return int\\(wrapNoInt\\(open, name, flags, mode\\)\\);", follyReplaceContent) }
includeEmptyDirs = false
into("$thirdPartyNdkDir/folly")
}
task downloadGlog(dependsOn: resolveGlog, type: Download) {
src("https://github.com/google/glog/archive/v${GLOG_VERSION}.tar.gz")
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, "glog-${GLOG_VERSION}.tar.gz"))
}
// Prepare glog sources to be compiled, this task will perform steps that normally should've been
// executed by automake. This way we can avoid dependencies on make/automake
task prepareGlog(dependsOn: dependenciesPath ? [] : [downloadGlog], type: Copy) {
duplicatesStrategy = "include"
from(dependenciesPath ?: tarTree(downloadGlog.dest))
from("$reactNativeThirdParty/glog/")
include("glog-${GLOG_VERSION}/src/**/*", "Android.mk", "config.h")
includeEmptyDirs = false
filesMatching("**/*.h.in") {
filter(ReplaceTokens, tokens: [
ac_cv_have_unistd_h : "1",
ac_cv_have_stdint_h : "1",
ac_cv_have_systypes_h : "1",
ac_cv_have_inttypes_h : "1",
ac_cv_have_libgflags : "0",
ac_google_start_namespace : "namespace google {",
ac_cv_have_uint16_t : "1",
ac_cv_have_u_int16_t : "1",
ac_cv_have___uint16 : "0",
ac_google_end_namespace : "}",
ac_cv_have___builtin_expect : "1",
ac_google_namespace : "google",
ac_cv___attribute___noinline : "__attribute__ ((noinline))",
ac_cv___attribute___noreturn : "__attribute__ ((noreturn))",
ac_cv___attribute___printf_4_5: "__attribute__((__format__ (__printf__, 4, 5)))"
])
it.path = (it.name - ".in")
}
into("$thirdPartyNdkDir/glog")
doLast {
copy {
from(fileTree(dir: "$thirdPartyNdkDir/glog", includes: ["stl_logging.h", "logging.h", "raw_logging.h", "vlog_is_on.h", "**/src/glog/log_severity.h"]).files)
includeEmptyDirs = false
into("$thirdPartyNdkDir/glog/exported/glog")
}
}
}
task prepareHermes {
if (REACT_NATIVE_MINOR_VERSION >= 69) {
if (!IS_NEW_ARCHITECTURE_ENABLED) {
dependsOn(unzipHermes)
}
doLast {
// e.g. hermes-engine-0.70.0-rc.1-debug.aar
def hermesAAR = file(
"$reactNativeRootDir/android/com/facebook/react/hermes-engine/" +
"${REACT_NATIVE_VERSION}/hermes-engine-${REACT_NATIVE_VERSION}-" +
"${resolveBuildType()}.aar"
)
if (!hermesAAR.exists()) {
throw new GradleException("[Reanimated] Could not find hermes-engine AAR.", null)
}
def soFiles = zipTree(hermesAAR).matching({ it.include "**/*.so" })
copy {
from soFiles
from "$reactNativeRootDir/ReactAndroid/src/main/jni/first-party/hermes/Android.mk"
into "$thirdPartyNdkDir/hermes"
}
}
} else {
doLast {
def hermesPackagePath = findNodeModulePath(projectDir, "hermes-engine")
if (!hermesPackagePath) {
throw new GradleException("[Reanimated] Could not find the hermes-engine npm package.", null)
}
def hermesAAR = file("$hermesPackagePath/android/hermes-${resolveBuildType()}.aar") // e.g. hermes-debug.aar
if (!hermesAAR.exists()) {
throw new GradleException("[Reanimated] The hermes-engine npm package is missing \"android/hermes-${resolveBuildType()}.aar\".", null)
}
def soFiles = zipTree(hermesAAR).matching({ it.include "**/*.so" })
copy {
from soFiles
from "$reactNativeRootDir/ReactAndroid/src/main/jni/first-party/hermes/Android.mk"
into "$thirdPartyNdkDir/hermes"
}
}
}
}
task prepareJSC {
if (REACT_NATIVE_MINOR_VERSION >= 71) {
// do nothing
} else {
doLast {
def jscPackagePath = findNodeModulePath(projectDir, "jsc-android")
if (!jscPackagePath) {
throw new GradleException("[Reanimated] Could not find the jsc-android npm package.", null)
}
def jscDist = file("$jscPackagePath/dist")
if (!jscDist.exists()) {
throw new GradleException("[Reanimated] The jsc-android npm package is missing its \"dist\" directory.", null)
}
def jscAAR = fileTree(jscDist).matching({ it.include "**/android-jsc/**/*.aar" }).singleFile
def soFiles = zipTree(jscAAR).matching({ it.include "**/*.so" })
def headerFiles = fileTree(jscDist).matching({ it.include "**/include/*.h" })
copy {
from(soFiles)
from(headerFiles)
from("$reactNativeRootDir/ReactAndroid/src/main/jni/third-party/jsc/Android.mk")
filesMatching("**/*.h", { it.path = "JavaScriptCore/${it.name}" })
includeEmptyDirs(false)
into("$thirdPartyNdkDir/jsc")
}
}
}
}
task extractAARHeaders {
doLast {
configurations.extractHeaders.files.each {
def file = it.absoluteFile
def packageName = file.name.tokenize('-')[0]
copy {
from zipTree(file)
into "$reactNativeRootDir/ReactAndroid/src/main/jni/first-party/$packageName/headers"
include "**/*.h"
}
}
}
}
task extractSOFiles {
doLast {
configurations.extractSO.files.each {
def file = it.absoluteFile
def packageName = file.name.tokenize('-')[0]
copy {
from zipTree(file)
into "$reactNativeRootDir/ReactAndroid/src/main/jni/first-party/$packageName/"
include "jni/**/*.so"
}
}
}
}
task unpackReactNativeAAR {
def buildType = resolveBuildType()
def rnAarMatcher = "**/react-native/**/*${buildType}.aar"
if (REACT_NATIVE_MINOR_VERSION < 69) {
rnAarMatcher = "**/**/*.aar"
}
def rnAAR = fileTree("$reactNativeRootDir/android").matching({ it.include rnAarMatcher }).singleFile
def file = rnAAR.absoluteFile
def packageName = file.name.tokenize('-')[0]
copy {
from zipTree(file)
into "$reactNativeRootDir/ReactAndroid/src/main/jni/first-party/$packageName/"
include "jni/**/*.so"
}
}
task downloadNdkBuildDependencies {
if (!boostPath) {
dependsOn(downloadBoost)
}
dependsOn(downloadDoubleConversion)
dependsOn(downloadFolly)
dependsOn(downloadGlog)
}
task prepareThirdPartyNdkHeaders(dependsOn:[
downloadNdkBuildDependencies,
prepareBoost,
prepareDoubleConversion,
prepareFolly,
prepareGlog,
unpackReactNativeAAR]
) {}
}
task packageNdkLibs(type: Copy) {
from("$buildDir/reanimated-ndk/all")
include("**/libreanimated.so")
into("$projectDir/src/main/jniLibs")
}
repositories {
mavenCentral()
mavenLocal()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$reactNativeRootDir/android"
}
maven {
// Android JSC is installed from npm
url "$reactNativeRootDir/../jsc-android/dist"
}
google()
}
dependencies {
implementation "com.facebook.yoga:proguard-annotations:1.19.0"
implementation "androidx.transition:transition:1.1.0"
implementation "androidx.core:core:1.6.0"
if (REACT_NATIVE_MINOR_VERSION >= 71) {
implementation "com.facebook.react:react-android" // version substituted by RNGP
if (JS_RUNTIME == "hermes") {
implementation "com.facebook.react:hermes-android" // version substituted by RNGP
}
} else {
// noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "com.facebook.fbjni:fbjni-java-only:" + FBJNI_VERSION
extractHeaders("com.facebook.fbjni:fbjni:" + FBJNI_VERSION + ":headers")
extractSO("com.facebook.fbjni:fbjni:" + FBJNI_VERSION)
def jscAAR = fileTree("$reactNativeRootDir/../jsc-android/dist/org/webkit/android-jsc").matching({ it.include "**/**/*.aar" }).singleFile
extractSO(files(jscAAR))
}
}
def nativeBuildDependsOn(dependsOnTask) {
def buildTasks = tasks.findAll({ task -> (
!task.name.contains("Clean")
&& (task.name.contains("externalNative")
|| task.name.contains("CMake")
|| task.name.contains("generateJsonModel")
)
) })
buildTasks.forEach { task -> task.dependsOn(dependsOnTask) }
}
afterEvaluate {
if (REACT_NATIVE_MINOR_VERSION < 71) {
extractAARHeaders.dependsOn(prepareThirdPartyNdkHeaders)
extractSOFiles.dependsOn(prepareThirdPartyNdkHeaders)
nativeBuildDependsOn(prepareThirdPartyNdkHeaders)
nativeBuildDependsOn(extractAARHeaders)
nativeBuildDependsOn(extractSOFiles)
}
preBuild.dependsOn(prepareHeadersForPrefab)
tasks.forEach({ task ->
if (task.name.contains("JniLibFolders")) {
task.dependsOn(packageNdkLibs)
}
})
if (JS_RUNTIME == "hermes") {
if (REACT_NATIVE_MINOR_VERSION < 71) {
extractAARHeaders.dependsOn(prepareHermes)
extractSOFiles.dependsOn(prepareHermes)
}
} else if (JS_RUNTIME == "v8") {
def buildTasks = tasks.findAll({ task ->
!task.name.contains("Clean") && (task.name.contains("externalNative") || task.name.contains("CMake") || task.name.startsWith("generateJsonModel")) })
buildTasks.forEach { task ->
def buildType = task.name.endsWith('Debug') ? 'Debug' : 'Release'
task.dependsOn(":react-native-v8:copy${buildType}JniLibsProjectOnly")
}
} else if (JS_RUNTIME == "jsc") {
if (REACT_NATIVE_MINOR_VERSION < 71) {
extractAARHeaders.dependsOn(prepareJSC)
extractSOFiles.dependsOn(prepareJSC)
}
} else {
throw GradleScriptException("[Reanimated] Unknown JS runtime ${JS_RUNTIME}.")
}
}

View File

@@ -0,0 +1 @@
// required for compilation purpose

View File

@@ -0,0 +1,25 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8A --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,3 @@
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
-keep class com.facebook.react.fabric.** { *; }

View File

@@ -0,0 +1 @@
include 'lib'

View File

@@ -0,0 +1,9 @@
// formatter & linter configuration for java
apply plugin: 'com.diffplug.spotless'
spotless {
java {
target 'src/main/java/**/*.java'
googleJavaFormat()
}
}

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();
}
}