From cdf1817121d21e8d2be8377fe349b3c4ef2f2ee0 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Tue, 8 Oct 2024 10:42:06 +0200 Subject: [PATCH] fix(Android)!: overflowing text in native header (#2325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description > [!caution] This PR includes **BREAKING CHANGES** Corresponding PR in `react-navigation`: * https://github.com/react-navigation/react-navigation/pull/12125 Fixes #1946 This PR refactors the header config component to use flex-box model instead of absolute positioning in Yoga layer. This is required so that the Yoga layouts children of header config with respect to one another (not absolutely), so that the title can be properly truncated. In the end, the subviews are laid out by system anyway, thus we send additional information, such as padding / margins from native side to the shadow tree, so that we can adjust for these in Yoga layout. > [!important] This PR introduces a bug in very first few frames - before the information from HostTree is propagated to ShadowTree the header title might not be truncated properly. This is known issue and based on the same mechanism and "jumping content" due to not including header dimensions in first Yoga layout. ## Changes ## Test code and steps to reproduce I've tried to test these changes carefully, however there still might be some bugs. It would be nice if we could get rid of them during beta-phase of 4.0. Below I paste my test matrix I conducted on `Test1649` on both platforms and architectures. ![image](https://github.com/user-attachments/assets/504fad12-dbd4-4648-871d-3c65215758ce) ## Checklist - [x] Included code example that can be used to test this change - [ ] Ensured that CI passes --- Example/android/settings.gradle | 3 +- Example/ios/Podfile.lock | 10 +- .../ScreensExample.xcodeproj/project.pbxproj | 8 + .../FabricExample.xcodeproj/project.pbxproj | 10 +- FabricExample/ios/Podfile.lock | 12 +- .../FabricEnabledHeaderConfigViewGroup.kt | 61 +++++++ .../com/swmansion/rnscreens/CustomToolbar.kt | 14 ++ .../rnscreens/ScreenStackHeaderConfig.kt | 3 +- .../ScreenStackHeaderConfigShadowNode.kt | 25 +++ .../ScreenStackHeaderConfigViewManager.kt | 18 ++ .../rnscreens/utils/PaddingBundle.kt | 8 + android/src/main/jni/rnscreens.h | 2 + .../FabricEnabledHeaderConfigViewGroup.kt | 39 ++++ apps/src/tests/TestHeaderTitle.tsx | 170 ++++++++++++++++++ apps/src/tests/index.ts | 1 + ...reenStackHeaderConfigComponentDescriptor.h | 44 +++++ .../RNSScreenStackHeaderConfigShadowNode.cpp | 8 + .../RNSScreenStackHeaderConfigShadowNode.h | 32 ++++ .../RNSScreenStackHeaderConfigState.cpp | 23 +++ .../RNSScreenStackHeaderConfigState.h | 50 ++++++ ...eenStackHeaderSubviewComponentDescriptor.h | 27 +++ .../RNSScreenStackHeaderSubviewShadowNode.cpp | 8 + .../RNSScreenStackHeaderSubviewShadowNode.h | 32 ++++ .../RNSScreenStackHeaderSubviewState.cpp | 15 ++ .../RNSScreenStackHeaderSubviewState.h | 40 +++++ ios/RNSScreenStack.mm | 32 ++++ ios/RNSScreenStackHeaderConfig.h | 63 +++++++ ios/RNSScreenStackHeaderConfig.mm | 124 ++++++++++++- ios/RNSScreenStackHeaderSubview.mm | 2 + ios/utils/UINavigationBar+RNSUtility.h | 37 ++++ ios/utils/UINavigationBar+RNSUtility.mm | 44 +++++ ios/utils/UIView+RNSUtility.mm | 1 - react-navigation | 2 +- scripts/codegen-utils.js | 1 + src/components/ScreenStackHeaderConfig.tsx | 81 ++++++--- .../ScreenStackHeaderConfigNativeComponent.ts | 4 +- ...ScreenStackHeaderSubviewNativeComponent.ts | 4 +- 37 files changed, 1006 insertions(+), 52 deletions(-) create mode 100644 android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt create mode 100644 android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigShadowNode.kt create mode 100644 android/src/main/java/com/swmansion/rnscreens/utils/PaddingBundle.kt create mode 100644 android/src/paper/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt create mode 100644 apps/src/tests/TestHeaderTitle.tsx create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.cpp create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigShadowNode.h create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigState.cpp create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigState.h create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewShadowNode.cpp create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewShadowNode.h create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewState.cpp create mode 100644 common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewState.h create mode 100644 ios/utils/UINavigationBar+RNSUtility.h create mode 100644 ios/utils/UINavigationBar+RNSUtility.mm diff --git a/Example/android/settings.gradle b/Example/android/settings.gradle index 79c1fd61bc..7320dfe94a 100644 --- a/Example/android/settings.gradle +++ b/Example/android/settings.gradle @@ -1,6 +1,7 @@ pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } plugins { id("com.facebook.react.settings") } -extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } +extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> ex.autolinkLibrariesFromCommand() } rootProject.name = 'ScreensExample' include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') + diff --git a/Example/ios/Podfile.lock b/Example/ios/Podfile.lock index 2dc578cf86..877b527ae9 100644 --- a/Example/ios/Podfile.lock +++ b/Example/ios/Podfile.lock @@ -1543,7 +1543,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (3.32.0): + - RNScreens (4.0.0-beta.5): - DoubleConversion - glog - hermes-engine @@ -1787,12 +1787,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 4cb898d0bf20404aab1850c656dcea009429d6c1 - DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 FBLazyVector: d08b51db67e61e1adaed7aefdb43b43f247ee46a fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 - glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + glog: 69ef571f3de08433d766d614c73a9838a06bf7eb hermes-engine: b205fccb3c7b52031e5bdb458a40f85f806bb7e8 - RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47 + RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 RCTDeprecation: 8c3d64b4ab77cf28adefa261e04fd205c2715607 RCTRequired: 70f9b55e176be07e234e2efe43b31de14d7cd5ba RCTTypeSafety: 570d25d58d8795b1a146f5dee4965a05b6fdf8ac @@ -1851,7 +1851,7 @@ SPEC CHECKSUMS: ReactCommon: dcc6f8545034e6f3d82f9555b39a2c03c2ccd005 RNGestureHandler: 044a81d99e5ad7a67b4c23d9f8ea4c6c30fd4bca RNReanimated: 7892f7ef3a5b9c941b3145aa3398effac3d7809d - RNScreens: ca47818a6197d1b14782f0aa3bb4157a512e96dd + RNScreens: 201abaf5f7fe97ccc761d32ef1a53b2fc7f70b32 RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1e170d028257c3ceb6e652dd62b2698dbc108a4b diff --git a/Example/ios/ScreensExample.xcodeproj/project.pbxproj b/Example/ios/ScreensExample.xcodeproj/project.pbxproj index 097475abbe..a94c85ae8a 100644 --- a/Example/ios/ScreensExample.xcodeproj/project.pbxproj +++ b/Example/ios/ScreensExample.xcodeproj/project.pbxproj @@ -408,6 +408,7 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ScreensExample-ScreensExampleTests/Pods-ScreensExample-ScreensExampleTests-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf", @@ -427,9 +428,11 @@ "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf", @@ -449,6 +452,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -462,6 +466,7 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ScreensExample/Pods-ScreensExample-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf", @@ -481,9 +486,11 @@ "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf", @@ -503,6 +510,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/FabricExample/ios/FabricExample.xcodeproj/project.pbxproj b/FabricExample/ios/FabricExample.xcodeproj/project.pbxproj index b76468a33d..446fbbf767 100644 --- a/FabricExample/ios/FabricExample.xcodeproj/project.pbxproj +++ b/FabricExample/ios/FabricExample.xcodeproj/project.pbxproj @@ -594,7 +594,10 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -667,7 +670,10 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/FabricExample/ios/Podfile.lock b/FabricExample/ios/Podfile.lock index ba5262082e..ff45d03ff8 100644 --- a/FabricExample/ios/Podfile.lock +++ b/FabricExample/ios/Podfile.lock @@ -1607,7 +1607,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (3.32.0): + - RNScreens (4.0.0-beta.5): - DoubleConversion - glog - hermes-engine @@ -1628,9 +1628,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 3.32.0) + - RNScreens/common (= 4.0.0-beta.5) - Yoga - - RNScreens/common (3.32.0): + - RNScreens/common (4.0.0-beta.5): - DoubleConversion - glog - hermes-engine @@ -1877,9 +1877,9 @@ SPEC CHECKSUMS: DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 FBLazyVector: d08b51db67e61e1adaed7aefdb43b43f247ee46a fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 - glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 + glog: 69ef571f3de08433d766d614c73a9838a06bf7eb hermes-engine: b205fccb3c7b52031e5bdb458a40f85f806bb7e8 - RCT-Folly: 045d6ecaa59d826c5736dfba0b2f4083ff8d79df + RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 RCTDeprecation: 8c3d64b4ab77cf28adefa261e04fd205c2715607 RCTRequired: 70f9b55e176be07e234e2efe43b31de14d7cd5ba RCTTypeSafety: 570d25d58d8795b1a146f5dee4965a05b6fdf8ac @@ -1938,7 +1938,7 @@ SPEC CHECKSUMS: ReactCommon: dcc6f8545034e6f3d82f9555b39a2c03c2ccd005 RNGestureHandler: f6a669a7d4ed470acebf8637d347eb52ae07d401 RNReanimated: bb5b1c59b5fc19a4e83c942cfe4ea49c5d959dd2 - RNScreens: 83aa5357fbb09aa87130fbea02325b53b7260fd6 + RNScreens: eb3b3d35da3333bc272b9e2d1b9afaa34d26d107 RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1e170d028257c3ceb6e652dd62b2698dbc108a4b diff --git a/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt b/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt new file mode 100644 index 0000000000..cf1a2aa387 --- /dev/null +++ b/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt @@ -0,0 +1,61 @@ +package com.swmansion.rnscreens + +import android.content.Context +import android.view.ViewGroup +import androidx.annotation.UiThread +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.StateWrapper +import kotlin.math.abs + +abstract class FabricEnabledHeaderConfigViewGroup( + context: Context?, +) : ViewGroup(context) { + private var mStateWrapper: StateWrapper? = null + + private var lastPaddingStart = 0f + private var lastPaddingEnd = 0f + + fun setStateWrapper(wrapper: StateWrapper?) { + mStateWrapper = wrapper + } + + fun updatePaddingsFabric( + paddingStart: Int, + paddingEnd: Int, + ) { + updateState(paddingStart, paddingEnd) + } + + @UiThread + fun updateState( + paddingStart: Int, + paddingEnd: Int, + ) { + val paddingStartDip: Float = PixelUtil.toDIPFromPixel(paddingStart.toFloat()) + val paddingEndDip: Float = PixelUtil.toDIPFromPixel(paddingEnd.toFloat()) + + // Check incoming state values. If they're already the correct value, return early to prevent + // infinite UpdateState/SetState loop. + if (abs(lastPaddingStart - paddingStart) < DELTA && + abs(lastPaddingEnd - paddingEnd) < DELTA + ) { + return + } + + lastPaddingStart = paddingStartDip + lastPaddingEnd = paddingEndDip + + val map: WritableMap = + WritableNativeMap().apply { + putDouble("paddingStart", paddingStartDip.toDouble()) + putDouble("paddingEnd", paddingEndDip.toDouble()) + } + mStateWrapper?.updateState(map) + } + + companion object { + private const val DELTA = 0.9f + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt b/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt index 0682ebc063..1cff34143c 100644 --- a/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +++ b/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt @@ -48,4 +48,18 @@ open class CustomToolbar( } } } + + override fun onLayout( + changed: Boolean, + l: Int, + t: Int, + r: Int, + b: Int, + ) { + super.onLayout(changed, l, t, r, b) + + // our children are already laid out + val contentInsetStart = if (navigationIcon != null) contentInsetStartWithNavigation else contentInsetStart + config.updatePaddingsFabric(contentInsetStart, contentInsetEnd) + } } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt index dba26bdefa..def872e9e0 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt @@ -8,7 +8,6 @@ import android.text.TextUtils import android.util.TypedValue import android.view.Gravity import android.view.View.OnClickListener -import android.view.ViewGroup import android.view.WindowInsets import android.widget.ImageView import android.widget.TextView @@ -25,7 +24,7 @@ import com.swmansion.rnscreens.events.HeaderDetachedEvent class ScreenStackHeaderConfig( context: Context, -) : ViewGroup(context) { +) : FabricEnabledHeaderConfigViewGroup(context) { private val configSubviews = ArrayList(3) val toolbar: CustomToolbar var isHeaderHidden = false // named this way to avoid conflict with platform's isHidden diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigShadowNode.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigShadowNode.kt new file mode 100644 index 0000000000..48a864ff5a --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigShadowNode.kt @@ -0,0 +1,25 @@ +package com.swmansion.rnscreens + +import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.LayoutShadowNode +import com.facebook.react.uimanager.Spacing +import com.swmansion.rnscreens.utils.PaddingBundle + +internal class ScreenStackHeaderConfigShadowNode( + private var context: ReactContext, +) : LayoutShadowNode() { + var paddingStart: Float = 0f + var paddingEnd: Float = 0f + + override fun setLocalData(data: Any?) { + if (data is PaddingBundle) { + paddingStart = data.paddingStart + paddingEnd = data.paddingEnd + + setPadding(Spacing.START, paddingStart) + setPadding(Spacing.END, paddingEnd) + } else { + super.setLocalData(data) + } + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt index 7c1893eedc..194bec2e98 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt @@ -3,8 +3,12 @@ package com.swmansion.rnscreens import android.util.Log import android.view.View import com.facebook.react.bridge.JSApplicationCausedNativeException +import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.common.MapBuilder import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.LayoutShadowNode +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewManagerDelegate @@ -29,6 +33,9 @@ class ScreenStackHeaderConfigViewManager : override fun createViewInstance(reactContext: ThemedReactContext) = ScreenStackHeaderConfig(reactContext) + // This works only on Paper. On Fabric the shadow node is implemented in C++ layer. + override fun createShadowNodeInstance(context: ReactApplicationContext): LayoutShadowNode = ScreenStackHeaderConfigShadowNode(context) + override fun addView( parent: ScreenStackHeaderConfig, child: View, @@ -42,6 +49,17 @@ class ScreenStackHeaderConfigViewManager : parent.addConfigSubview(child, index) } + override fun updateState( + view: ScreenStackHeaderConfig, + props: ReactStylesDiffMap?, + stateWrapper: StateWrapper?, + ): Any? { + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + view.setStateWrapper(stateWrapper) + } + return super.updateState(view, props, stateWrapper) + } + override fun onDropViewInstance( @Nonnull view: ScreenStackHeaderConfig, ) { diff --git a/android/src/main/java/com/swmansion/rnscreens/utils/PaddingBundle.kt b/android/src/main/java/com/swmansion/rnscreens/utils/PaddingBundle.kt new file mode 100644 index 0000000000..7fb209e400 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/utils/PaddingBundle.kt @@ -0,0 +1,8 @@ +package com.swmansion.rnscreens.utils + +// Used only on Paper together with `setLocalData` mechanism to pass +// the information on header paddings to shadow node. +data class PaddingBundle( + val paddingStart: Float, + val paddingEnd: Float, +) diff --git a/android/src/main/jni/rnscreens.h b/android/src/main/jni/rnscreens.h index a8dbde1289..e57cd2fcab 100644 --- a/android/src/main/jni/rnscreens.h +++ b/android/src/main/jni/rnscreens.h @@ -18,6 +18,8 @@ */ #include #include +#include +#include namespace facebook { namespace react { diff --git a/android/src/paper/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt b/android/src/paper/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt new file mode 100644 index 0000000000..f1abaf546a --- /dev/null +++ b/android/src/paper/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt @@ -0,0 +1,39 @@ +package com.swmansion.rnscreens + +import android.content.Context +import android.view.ViewGroup +import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.StateWrapper +import com.facebook.react.uimanager.UIManagerModule +import com.swmansion.rnscreens.utils.PaddingBundle +import kotlin.math.abs + +abstract class FabricEnabledHeaderConfigViewGroup( + context: Context, +) : ViewGroup(context) { + private var lastPaddingStart = 0 + private var lastPaddingEnd = 0 + + fun setStateWrapper(wrapper: StateWrapper?) = Unit + + fun updatePaddingsFabric( + paddingStart: Int, + paddingEnd: Int, + ) { + // Note that on Paper we do not convert these props from px to dip. This is done internally by RN. + if (abs(lastPaddingStart - paddingStart) < DELTA && abs(lastPaddingEnd - paddingEnd) < DELTA) { + return + } + + lastPaddingStart = paddingStart + lastPaddingEnd = paddingEnd + + val reactContext = context as? ReactContext + val uiManagerModule = reactContext?.getNativeModule(UIManagerModule::class.java) + uiManagerModule?.setViewLocalData(this.id, PaddingBundle(paddingStart.toFloat(), paddingEnd.toFloat())) + } + + companion object { + private const val DELTA = 0.9 + } +} diff --git a/apps/src/tests/TestHeaderTitle.tsx b/apps/src/tests/TestHeaderTitle.tsx new file mode 100644 index 0000000000..06fa450644 --- /dev/null +++ b/apps/src/tests/TestHeaderTitle.tsx @@ -0,0 +1,170 @@ +import * as React from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { View, Text, StyleSheet, Pressable, Button } from 'react-native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; + + +const Stack = createNativeStackNavigator(); + +const baseTitle = 'Ab'; +const baseTitle2 = 'Ac'; + +// Toggle these two for short / long string on first screen +// const homeScreenTitle = "Screen"; +const homeScreenTitle = baseTitle.repeat(24); + +// Toggle these two for short / long string on second screen +// const secondScreenTitle = "Details"; +const secondScreenTitle = baseTitle2.repeat(24); + +const searchBarScreenTitle = "SearchBarScreen"; + +const headerOptions = { + headerLeft: () => { + return ( + + + ) + }, + // headerRight: () => ( + // + // + // ), + // headerRight: () => { + // return ( + // + // + // + // + // ); + // }, + // headerRight: () => ( + // + // + // + // + // ), + // headerTitle: baseTitle.repeat(20), + // headerTitle: () => ( + // {baseTitle.repeat(5)} + // ), + headerRight: () => { + return ( + + + + + ); + }, + headerTitle: () => ( + + {baseTitle.repeat(24)} + + ), + // title: baseTitle.repeat(4), + headerTitleAlign: 'left', +} + +function App() { + return ( + + + + + + + + ); +} + +function Screen({ navigation }: any) { + + // Just a reference contents, mimicking the setup with header config & subviews + + return ( + + + + + + + {baseTitle.repeat(5)} + + + + + +