Skip to content

Commit

Permalink
fix(Android)!: overflowing text in native header (#2325)
Browse files Browse the repository at this point in the history
## Description

> [!caution]
This PR includes **BREAKING CHANGES**

Corresponding PR in `react-navigation`:

* react-navigation/react-navigation#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.

<!--
### Before / After

 `headerTitleAlign: 'left'`, long title as string ⬇️

| Before ✅ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/ec7dd405-7244-4c18-b96d-5ffd79be78b8"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/7b0086b5-3554-4b76-99d5-302d03cab96e"></img>
| |

Works fine, cause `title` is passed as regular string to native side,
where it is simply assigned to `toolbar.title` & `AppCompatTextView` is
used by Android.

`headerTitleAlign: 'left'`, `headerLeft`, long title as string ⬇️ 

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/228f64b9-4ca8-4c65-8128-a5097bbae595"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/e1426702-a020-4113-a271-588c62e385db"></img>
| |


Title despite being a plain string is wrapped inside `HeaderTitle`
component & rendered as a `Text` inside `SubviewLeft` together with
`HeaderLeft`. The text overflows, because subview is positioned as
`absolute`: it does not respect native toolbar's content inset (left
padding), as Yoga has no information of it & the text view does measure
itself with width of whole containing box (which is the `HeaderConfig`
which has width of full screen).

What's also important to notice is that in this configuration
`backButtonIcon` is disabled.

`headerTitleAlign: 'left'`, `headerRight`, long title as string ⬇️ 

| Before ✅  | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/2af7a845-4c7c-419a-a9bc-5e44525fb935"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/7afaa643-7a00-42a4-8cb2-3d4fa9a9781b"></img>
| |


Works ok, because text is assigned to `toolbar.title`, thus
`AppCompatTextView` is in use & native layout takes care of truncating
the text & respecting the `HeaderRight`. We good here.

`headerTitleAlign: 'left'`, `headerLeft`, `headerRight`, long title as
string ⬇️

| Before ❌  | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/186b8534-00e0-4dc6-b4b0-45a12e9a84c5"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/3bfc1bd6-3e9f-4ede-a375-7fa329e6a4f0"></img>
| |

Text is put together with `HeaderLeft` into `SubviewLeft` & rendered
inside `Text`. Subviews are positioned as `absolute` & Yoga measures
text with full width of the screen & does not take into account
`absolute` siblings.

Notice that in the second row of screenshots `backButtonIcon` is also
not present.


`headerTitleAlign: 'left'`, long title as `Text` ⬇️

| Before ✅ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/ec7dd405-7244-4c18-b96d-5ffd79be78b8"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/7b0086b5-3554-4b76-99d5-302d03cab96e"></img>
| |


`headerTitleAlign: 'left'`, `headerLeft`, long title as `Text` ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/34046637-886e-44e7-8588-2143cc5ec7bc"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/b57d9beb-0c5e-405d-b920-393bee801a07"></img>
| |


`headerTitleAlign: 'left'`, `headerRight`, long title as `Text` ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/83afc32a-b8f5-4ffc-9570-8b9034e4abb9"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/04d58470-53c3-45bb-a335-f6265b34c1e6"></img>
| |


`headerTitleAlign: 'left'`, `headerLeft`, `headerRight`, long title as
`Text` ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/30dcc02e-4fa4-4a89-9add-b925e1290bb2"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/716de207-aeec-4766-8cab-09f535f2fac2"></img>
| |


`headerTitleAlign: 'center'`, long title as string ⬇️

| Before ✅ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/58194a20-9373-4352-a2b6-ada2982c2646"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/6c31121d-09d9-40f5-8bf8-688cd4d1a28e"></img>
| |



`headerTitleAlign: 'center'`, `headerLeft`, long title as string ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/c8c2507a-fb23-4265-b257-e2a469e0cbae"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/cdc6efaa-83f8-4e7d-9aba-68ad0b276bbb"></img>
| |

`headerTitleAlign: 'center'`, `headerRight`, long title as string ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/c83cf46a-0b5a-49cf-96b2-c602df090a83"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/e33623ae-0a32-46d6-9355-2ce353c60a66"></img>
| |


`headerTitleAlign: 'center'`, `headerLeft`, `headerRight`, long title as
string ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/a8c626b8-7b9d-4b91-b2f9-4a94ae10fd4a"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/90dc5994-2aca-4ce2-8360-4baea67734be"></img>
| |


`headerTitleAlign: 'center'`, long title as `Text` ⬇️

| Before ✅ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/b119f64a-4638-4e28-abf8-33b23290d195"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/0f0a92e4-305e-429a-b852-623dc06f43f8"></img>
| |


`headerTitleAlign: 'center'`, `headerLeft`, long title as `Text` ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/1966cca9-1cce-47f7-8a87-a0b687edfe7a"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/5ea1bb3a-1926-4abf-839c-3ddac73c74aa"></img>
| |


`headerTitleAlign: 'center'`, `headerRight`, long title as `Text` ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/c2801a4d-7f63-4e67-acb3-0ce55e09e937"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/789de0b6-f703-45dd-aeb0-8e3082bbf267"></img>
| |


`headerTitleAlign: 'center'`, `headerLeft`, `headerRight`, long title as
`Text` ⬇️

| Before ❌ | After ❓|
|--------|--------|
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/9159990b-845f-4d26-b103-d1a5ecdec656"></img>
| |
| <img width="480" height="200"
src="https://github.com/user-attachments/assets/07cdf821-9503-4f68-a87f-9dc7a96ea032"></img>
| |

-->

## Changes

<!--
Please describe things you've changed here, make a **high level**
overview, if change is simple you can omit this section.

For example:

- Updated `about.md` docs

-->

<!--

## Screenshots / GIFs

Here you can add screenshots / GIFs documenting your change.

You can add before / after section if you're changing some behavior.

### Before

### After

-->

## 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
  • Loading branch information
kkafar authored Oct 8, 2024
1 parent 91d89c4 commit 501f6cb
Show file tree
Hide file tree
Showing 37 changed files with 1,006 additions and 52 deletions.
3 changes: 2 additions & 1 deletion Example/android/settings.gradle
Original file line number Diff line number Diff line change
@@ -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')

10 changes: 5 additions & 5 deletions Example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1851,7 +1851,7 @@ SPEC CHECKSUMS:
ReactCommon: dcc6f8545034e6f3d82f9555b39a2c03c2ccd005
RNGestureHandler: 044a81d99e5ad7a67b4c23d9f8ea4c6c30fd4bca
RNReanimated: 7892f7ef3a5b9c941b3145aa3398effac3d7809d
RNScreens: ca47818a6197d1b14782f0aa3bb4157a512e96dd
RNScreens: 201abaf5f7fe97ccc761d32ef1a53b2fc7f70b32
RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: 1e170d028257c3ceb6e652dd62b2698dbc108a4b
Expand Down
8 changes: 8 additions & 0 deletions Example/ios/ScreensExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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;
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions FabricExample/ios/FabricExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions FabricExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1938,7 +1938,7 @@ SPEC CHECKSUMS:
ReactCommon: dcc6f8545034e6f3d82f9555b39a2c03c2ccd005
RNGestureHandler: f6a669a7d4ed470acebf8637d347eb52ae07d401
RNReanimated: bb5b1c59b5fc19a4e83c942cfe4ea49c5d959dd2
RNScreens: 83aa5357fbb09aa87130fbea02325b53b7260fd6
RNScreens: eb3b3d35da3333bc272b9e2d1b9afaa34d26d107
RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: 1e170d028257c3ceb6e652dd62b2698dbc108a4b
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
14 changes: 14 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,7 +24,7 @@ import com.swmansion.rnscreens.events.HeaderDetachedEvent

class ScreenStackHeaderConfig(
context: Context,
) : ViewGroup(context) {
) : FabricEnabledHeaderConfigViewGroup(context) {
private val configSubviews = ArrayList<ScreenStackHeaderSubview>(3)
val toolbar: CustomToolbar
var isHeaderHidden = false // named this way to avoid conflict with platform's isHidden
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
2 changes: 2 additions & 0 deletions android/src/main/jni/rnscreens.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
#include <react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h>
#include <react/renderer/components/rnscreens/RNSModalScreenComponentDescriptor.h>
#include <react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h>
#include <react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h>

namespace facebook {
namespace react {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 501f6cb

Please sign in to comment.