diff --git a/RNScreens.podspec b/RNScreens.podspec index 57d240f9ec..be7cc24a33 100644 --- a/RNScreens.podspec +++ b/RNScreens.podspec @@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json"))) new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1' platform = new_arch_enabled ? "11.0" : "9.0" -source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/**/*.{cpp,h}"] +source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/RNScreensTurboModule.cpp", "cpp/RNScreensTurboModule.h"] Pod::Spec.new do |s| s.name = "RNScreens" diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 8d549009af..1745fb576b 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -2,11 +2,22 @@ cmake_minimum_required(VERSION 3.9.0) project(rnscreens) +if(${RNS_NEW_ARCH_ENABLED}) add_library(rnscreens SHARED ../cpp/RNScreensTurboModule.cpp + ../cpp/RNSScreenRemovalListener.cpp ./src/main/cpp/jni-adapter.cpp + ./src/main/cpp/NativeProxy.cpp + ./src/main/cpp/OnLoad.cpp ) +else() +add_library(rnscreens + SHARED + ../cpp/RNScreensTurboModule.cpp + ./src/main/cpp/jni-adapter.cpp +) +endif() include_directories( ../cpp @@ -19,9 +30,42 @@ set_target_properties(rnscreens PROPERTIES POSITION_INDEPENDENT_CODE ON ) +target_compile_definitions( + rnscreens + PRIVATE + -DFOLLY_NO_CONFIG=1 +) + find_package(ReactAndroid REQUIRED CONFIG) -target_link_libraries(rnscreens - ReactAndroid::jsi - android -) +if(${RNS_NEW_ARCH_ENABLED}) + find_package(fbjni REQUIRED CONFIG) + + target_link_libraries( + rnscreens + ReactAndroid::jsi + ReactAndroid::react_nativemodule_core + ReactAndroid::react_utils + ReactAndroid::reactnativejni + ReactAndroid::fabricjni + ReactAndroid::react_debug + ReactAndroid::react_render_core + ReactAndroid::runtimeexecutor + ReactAndroid::react_render_graphics + ReactAndroid::rrc_view + ReactAndroid::yoga + ReactAndroid::rrc_text + ReactAndroid::glog + ReactAndroid::react_render_componentregistry + ReactAndroid::react_render_consistency + ReactAndroid::react_performance_timeline + ReactAndroid::react_render_observers_events + fbjni::fbjni + android + ) +else() + target_link_libraries(rnscreens + ReactAndroid::jsi + android + ) +endif() diff --git a/android/build.gradle b/android/build.gradle index a61c220f9e..a4142011aa 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -81,6 +81,7 @@ 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 IS_NEW_ARCHITECTURE_ENABLED = isNewArchitectureEnabled() android { compileSdkVersion safeExtGet('compileSdkVersion', rnsDefaultCompileSdkVersion) @@ -106,13 +107,14 @@ android { targetSdkVersion safeExtGet('targetSdkVersion', rnsDefaultTargetSdkVersion) versionCode 1 versionName "1.0" - buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", IS_NEW_ARCHITECTURE_ENABLED.toString() ndk { abiFilters (*reactNativeArchitectures()) } externalNativeBuild { cmake { - arguments "-DANDROID_STL=c++_shared" + arguments "-DANDROID_STL=c++_shared", + "-DRNS_NEW_ARCH_ENABLED=${IS_NEW_ARCHITECTURE_ENABLED}" } } } @@ -143,13 +145,15 @@ android { "META-INF/**", "**/libjsi.so", "**/libc++_shared.so", - "**/libreact_render*.so" + "**/libreact_render*.so", + "**/libreactnativejni.so", + "**/libreact_performance_timeline.so" ] } sourceSets.main { ext.androidResDir = "src/main/res" java { - if (isNewArchitectureEnabled()) { + if (IS_NEW_ARCHITECTURE_ENABLED) { srcDirs += [ "src/fabric/java", ] diff --git a/android/src/fabric/java/com/swmansion/rnscreens/NativeProxy.kt b/android/src/fabric/java/com/swmansion/rnscreens/NativeProxy.kt new file mode 100644 index 0000000000..49f205e67f --- /dev/null +++ b/android/src/fabric/java/com/swmansion/rnscreens/NativeProxy.kt @@ -0,0 +1,53 @@ +package com.swmansion.rnscreens + +import android.util.Log +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.react.fabric.FabricUIManager +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap + +class NativeProxy { + @DoNotStrip + @Suppress("unused") + private val mHybridData: HybridData + + init { + mHybridData = initHybrid() + } + + private external fun initHybrid(): HybridData + + external fun nativeAddMutationsListener(fabricUIManager: FabricUIManager) + + companion object { + // we use ConcurrentHashMap here since it will be read on the JS thread, + // and written to on the UI thread. + private val viewsMap = ConcurrentHashMap>() + + fun addScreenToMap( + tag: Int, + view: Screen, + ) { + viewsMap[tag] = WeakReference(view) + } + + fun removeScreenFromMap(tag: Int) { + viewsMap.remove(tag) + } + + fun clearMapOnInvalidate() { + viewsMap.clear() + } + } + + @DoNotStrip + public fun notifyScreenRemoved(screenTag: Int) { + val screen = viewsMap[screenTag]?.get() + if (screen is Screen) { + screen.startRemovalTransition() + } else { + Log.w("[RNScreens]", "Did not find view with tag $screenTag.") + } + } +} diff --git a/android/src/main/cpp/NativeProxy.cpp b/android/src/main/cpp/NativeProxy.cpp new file mode 100644 index 0000000000..62cb98f38b --- /dev/null +++ b/android/src/main/cpp/NativeProxy.cpp @@ -0,0 +1,51 @@ +#include +#include +#include + +#include + +#include "NativeProxy.h" + +using namespace facebook; +using namespace react; + +namespace rnscreens { + +NativeProxy::NativeProxy(jni::alias_ref jThis) + : javaPart_(jni::make_global(jThis)) {} + +NativeProxy::~NativeProxy() {} + +void NativeProxy::registerNatives() { + registerHybrid( + {makeNativeMethod("initHybrid", NativeProxy::initHybrid), + makeNativeMethod( + "nativeAddMutationsListener", + NativeProxy::nativeAddMutationsListener)}); +} + +void NativeProxy::nativeAddMutationsListener( + jni::alias_ref + fabricUIManager) { + auto uiManager = + fabricUIManager->getBinding()->getScheduler()->getUIManager(); + screenRemovalListener_ = + std::make_shared([this](int tag) { + static const auto method = + javaPart_->getClass()->getMethod("notifyScreenRemoved"); + method(javaPart_, tag); + }); + + uiManager->getShadowTreeRegistry().enumerate( + [this](const facebook::react::ShadowTree &shadowTree, bool &stop) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + screenRemovalListener_); + }); +} + +jni::local_ref NativeProxy::initHybrid( + jni::alias_ref jThis) { + return makeCxxInstance(jThis); +} + +} // namespace rnscreens diff --git a/android/src/main/cpp/NativeProxy.h b/android/src/main/cpp/NativeProxy.h new file mode 100644 index 0000000000..80aa57ceb6 --- /dev/null +++ b/android/src/main/cpp/NativeProxy.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include "RNSScreenRemovalListener.h" + +#include + +namespace rnscreens { +using namespace facebook; +using namespace facebook::jni; + +class NativeProxy : public jni::HybridClass { + public: + std::shared_ptr screenRemovalListener_; + static auto constexpr kJavaDescriptor = + "Lcom/swmansion/rnscreens/NativeProxy;"; + static jni::local_ref initHybrid( + jni::alias_ref jThis); + static void registerNatives(); + + ~NativeProxy(); + + private: + friend HybridBase; + jni::global_ref javaPart_; + + explicit NativeProxy(jni::alias_ref jThis); + + void nativeAddMutationsListener( + jni::alias_ref + fabricUIManager); +}; + +} // namespace rnscreens diff --git a/android/src/main/cpp/OnLoad.cpp b/android/src/main/cpp/OnLoad.cpp new file mode 100644 index 0000000000..258432e237 --- /dev/null +++ b/android/src/main/cpp/OnLoad.cpp @@ -0,0 +1,8 @@ +#include + +#include "NativeProxy.h" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + return facebook::jni::initialize( + vm, [] { rnscreens::NativeProxy::registerNatives(); }); +} diff --git a/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/android/src/main/java/com/swmansion/rnscreens/Screen.kt index 5d867e75de..fb4260fc3b 100644 --- a/android/src/main/java/com/swmansion/rnscreens/Screen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/Screen.kt @@ -6,6 +6,7 @@ import android.graphics.Paint import android.os.Parcelable import android.util.SparseArray import android.util.TypedValue +import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.webkit.WebView @@ -37,6 +38,7 @@ class Screen( var screenOrientation: Int? = null private set var isStatusBarAnimated: Boolean? = null + var isBeingRemoved = false init { // we set layout params as WindowManager.LayoutParams to workaround the issue with TextInputs @@ -280,6 +282,40 @@ class Screen( var nativeBackButtonDismissalEnabled: Boolean = true + fun startRemovalTransition() { + if (!isBeingRemoved) { + isBeingRemoved = true + startTransitionRecursive(this) + } + } + + private fun startTransitionRecursive(parent: ViewGroup?) { + parent?.let { + for (i in 0 until it.childCount) { + val child = it.getChildAt(i) + if (child.javaClass.simpleName.equals("CircleImageView")) { + // SwipeRefreshLayout class which has CircleImageView as a child, + // does not handle `startViewTransition` properly. + // It has a custom `getChildDrawingOrder` method which returns + // wrong index if we called `startViewTransition` on the views on new arch. + // We add a simple View to bump the number of children to make it work. + // TODO: find a better way to handle this scenario + it.addView(View(context), i) + } else { + child?.let { view -> it.startViewTransition(view) } + } + if (child is ScreenStackHeaderConfig) { + // we want to start transition on children of the toolbar too, + // which is not a child of ScreenStackHeaderConfig + startTransitionRecursive(child.toolbar) + } + if (child is ViewGroup) { + startTransitionRecursive(child) + } + } + } + } + private fun calculateHeaderHeight(): Pair { val actionBarTv = TypedValue() val resolvedActionBarSize = diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt index 9cea3fe971..e1fc8cecc3 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt @@ -304,7 +304,7 @@ class ScreenStackHeaderConfig( } private fun maybeUpdate() { - if (parent != null && !isDestroyed) { + if (parent != null && !isDestroyed && screen?.isBeingRemoved == false) { onUpdate() } } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt index 735d6f5dcc..b93fe8a9a4 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt @@ -203,7 +203,7 @@ class ScreenStackHeaderConfigViewManager : // TODO: Find better way to handle platform specific props private fun logNotAvailable(propName: String) { - Log.w("RN SCREENS", "$propName prop is not available on Android") + Log.w("[RNScreens]", "$propName prop is not available on Android") } override fun setBackTitle( diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt index c3dd2c880d..f9db0beb6a 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt @@ -1,7 +1,6 @@ package com.swmansion.rnscreens import android.view.View -import android.view.ViewGroup import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.LayoutShadowNode @@ -32,6 +31,7 @@ class ScreenStackViewManager : index: Int, ) { require(child is Screen) { "Attempt attach child that is not of type RNScreen" } + NativeProxy.addScreenToMap(child.id, child) parent.addScreen(child, index) } @@ -39,29 +39,19 @@ class ScreenStackViewManager : parent: ScreenStack, index: Int, ) { - prepareOutTransition(parent.getScreenAt(index)) + val screen = parent.getScreenAt(index) + prepareOutTransition(screen) parent.removeScreenAt(index) + NativeProxy.removeScreenFromMap(screen.id) } private fun prepareOutTransition(screen: Screen?) { - startTransitionRecursive(screen) + screen?.startRemovalTransition() } - private fun startTransitionRecursive(parent: ViewGroup?) { - parent?.let { - for (i in 0 until it.childCount) { - val child = it.getChildAt(i) - child?.let { view -> it.startViewTransition(view) } - if (child is ScreenStackHeaderConfig) { - // we want to start transition on children of the toolbar too, - // which is not a child of ScreenStackHeaderConfig - startTransitionRecursive(child.toolbar) - } - if (child is ViewGroup) { - startTransitionRecursive(child) - } - } - } + override fun invalidate() { + super.invalidate() + NativeProxy.clearMapOnInvalidate() } override fun getChildCount(parent: ScreenStack) = parent.screenCount diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreensModule.kt b/android/src/main/java/com/swmansion/rnscreens/ScreensModule.kt index 047a14acc8..756c8e45c2 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreensModule.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreensModule.kt @@ -4,8 +4,10 @@ import android.util.Log import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.fabric.FabricUIManager import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.common.UIManagerType import com.swmansion.rnscreens.events.ScreenTransitionProgressEvent import java.util.concurrent.atomic.AtomicBoolean @@ -15,6 +17,7 @@ class ScreensModule( ) : NativeScreensModuleSpec(reactContext) { private var topScreenId: Int = -1 private val isActiveTransition = AtomicBoolean(false) + private var proxy: NativeProxy? = null init { try { @@ -32,6 +35,18 @@ class ScreensModule( private external fun nativeInstall(jsiPtr: Long) + override fun initialize() { + super.initialize() + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + val fabricUIManager = + UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC) as FabricUIManager + proxy = + NativeProxy().apply { + nativeAddMutationsListener(fabricUIManager) + } + } + } + override fun getName(): String = NAME @DoNotStrip diff --git a/android/src/main/java/com/swmansion/rnscreens/SearchBarManager.kt b/android/src/main/java/com/swmansion/rnscreens/SearchBarManager.kt index 2d14ccbdbc..07287be923 100644 --- a/android/src/main/java/com/swmansion/rnscreens/SearchBarManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/SearchBarManager.kt @@ -159,7 +159,7 @@ class SearchBarManager : } private fun logNotAvailable(propName: String) { - Log.w("RN SCREENS", "$propName prop is not available on Android") + Log.w("[RNScreens]", "$propName prop is not available on Android") } // NativeCommands diff --git a/android/src/paper/java/com/swmansion/rnscreens/NativeProxy.kt b/android/src/paper/java/com/swmansion/rnscreens/NativeProxy.kt new file mode 100644 index 0000000000..5c71566926 --- /dev/null +++ b/android/src/paper/java/com/swmansion/rnscreens/NativeProxy.kt @@ -0,0 +1,19 @@ +package com.swmansion.rnscreens + +import com.facebook.react.fabric.FabricUIManager + +class NativeProxy { + // do nothing on Paper + fun nativeAddMutationsListener(fabricUIManager: FabricUIManager) = Unit + + companion object { + fun addScreenToMap( + tag: Int, + view: Screen, + ) = Unit + + fun removeScreenFromMap(tag: Int) = Unit + + fun clearMapOnInvalidate() = Unit + } +} diff --git a/cpp/RNSScreenRemovalListener.cpp b/cpp/RNSScreenRemovalListener.cpp new file mode 100644 index 0000000000..e634a46483 --- /dev/null +++ b/cpp/RNSScreenRemovalListener.cpp @@ -0,0 +1,25 @@ +#include "RNSScreenRemovalListener.h" +#include +using namespace facebook::react; + +std::optional RNSScreenRemovalListener::pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number transactionNumber, + const TransactionTelemetry &telemetry, + ShadowViewMutationList mutations) const { + for (const ShadowViewMutation &mutation : mutations) { + if (mutation.type == ShadowViewMutation::Type::Remove && + mutation.oldChildShadowView.componentName != nullptr && + strcmp(mutation.parentShadowView.componentName, "RNSScreenStack") == + 0) { + listenerFunction_(mutation.oldChildShadowView.tag); + } + } + + return MountingTransaction{ + surfaceId, transactionNumber, std::move(mutations), telemetry}; +} + +bool RNSScreenRemovalListener::shouldOverridePullTransaction() const { + return true; +} diff --git a/cpp/RNSScreenRemovalListener.h b/cpp/RNSScreenRemovalListener.h new file mode 100644 index 0000000000..5bf6629581 --- /dev/null +++ b/cpp/RNSScreenRemovalListener.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +using namespace facebook::react; + +struct RNSScreenRemovalListener : public MountingOverrideDelegate { + std::function listenerFunction_; + RNSScreenRemovalListener(std::function &&listenerFunction_) + : listenerFunction_(std::move(listenerFunction_)) {} + + bool shouldOverridePullTransaction() const override; + std::optional pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + const TransactionTelemetry &telemetry, + ShadowViewMutationList mutations) const override; +}; diff --git a/ios/RNSModule.mm b/ios/RNSModule.mm index be05de43c7..f3f14d3437 100644 --- a/ios/RNSModule.mm +++ b/ios/RNSModule.mm @@ -116,7 +116,7 @@ - (RNSScreenStackView *)getStackView:(NSNumber *)stackTag { RNSScreenStackView *view = [self getScreenStackView:stackTag]; if (view != nil && ![view isKindOfClass:[RNSScreenStackView class]]) { - RCTLogError(@"Invalid view type, expecting RNSScreenStackView, got: %@", view); + RCTLogError(@"[RNScreens] Invalid view type, expecting RNSScreenStackView, got: %@", view); return nil; } return view; diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index 2c21edade6..28202a81d2 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -40,7 +40,7 @@ namespace react = facebook::react; - (void)notifyFinishTransitioning; - (RNSScreenView *)screenView; #ifdef RCT_NEW_ARCH_ENABLED -- (void)setViewToSnapshot:(UIView *)snapshot; +- (void)setViewToSnapshot; - (CGFloat)calculateHeaderHeightIsModal:(BOOL)isModal; #endif diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index 053e59adb5..b673407e31 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -1391,12 +1391,16 @@ - (void)hideHeaderIfNecessary #ifdef RCT_NEW_ARCH_ENABLED #pragma mark - Fabric specific -- (void)setViewToSnapshot:(UIView *)snapshot +- (void)setViewToSnapshot { UIView *superView = self.view.superview; - [self.view removeFromSuperview]; - self.view = snapshot; - [superView addSubview:self.view]; + // if we dismissed the view natively, it will already be detached from view hierarchy + if (self.view.window != nil) { + UIView *snapshot = [self.view snapshotViewAfterScreenUpdates:NO]; + [self.view removeFromSuperview]; + self.view = snapshot; + [superView addSubview:snapshot]; + } } #else diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index 428e3ca6cc..ea27b03d5f 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -120,9 +120,6 @@ @implementation RNSScreenStackView { UIPercentDrivenInteractiveTransition *_interactionController; __weak RNSScreenStackManager *_manager; BOOL _updateScheduled; -#ifdef RCT_NEW_ARCH_ENABLED - UIView *_snapshot; -#endif } #ifdef RCT_NEW_ARCH_ENABLED @@ -1132,8 +1129,7 @@ - (void)unmountChildComponentView:(UIView *)childCompo if (screenChildComponent.window != nil && ((screenChildComponent == _controller.visibleViewController.view && _presentedModals.count < 2) || screenChildComponent == [_presentedModals.lastObject view])) { - [self takeSnapshot]; - [screenChildComponent.controller setViewToSnapshot:_snapshot]; + [screenChildComponent.controller setViewToSnapshot]; } RCTAssert( @@ -1155,15 +1151,6 @@ - (void)unmountChildComponentView:(UIView *)childCompo [screenChildComponent removeFromSuperview]; } -- (void)takeSnapshot -{ - if (_presentedModals.count < 2) { - _snapshot = [_controller.visibleViewController.view snapshotViewAfterScreenUpdates:NO]; - } else { - _snapshot = [[_presentedModals.lastObject view] snapshotViewAfterScreenUpdates:NO]; - } -} - - (void)mountingTransactionDidMount:(const facebook::react::MountingTransaction &)transaction withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry {