You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(Android, Tabs): safe area component for Android (#3215)
## Description
Adds implementation for handling safe area on Android for bottom tabs.
Implementation has been adapted from
[`react-native-safe-area-context`](https://github.com/AppAndFlow/react-native-safe-area-context).
TODO:
- [x] change native layout so that TabScreen renders under the tab bar
(to allow using transparent tab bar)
- [x] check implementation for older Android APIs (I checked APIs: 25,
28, 29, 30, 36 and 24 on Paper)
- [x] add simple API (for all edges) to control which types of insets
are used by `SafeAreaView` component (system and/or interface bars)
- [ ] [Separate PR] add per-edge API to control which types of insets
are used by `SafeAreaView` component (system and/or interface bars)
(software-mansion/react-native-screens-labs#434)
- [ ] [[Separate
PR](#3240)]
fix interaction with Stack v4 (`CustomToolbar`)
(software-mansion/react-native-screens-labs#435)
- [ ] [Separate PR] There were no problems with using margins for now so
I left it as it was. We can change this later to padding or add a prop
to switch between padding and margins on both Android and iOS.
(software-mansion/react-native-screens-labs#436)
https://github.com/user-attachments/assets/c59af116-a653-40e3-9345-fe8d3ba170ee
### Transparent tab bar
| `top: false, bottom: false` | `top: true, bottom: false` | `top: true,
bottom: true` |
| --- | --- | --- |
| <img width="1280" height="2856" alt="Screenshot_20250916_091258"
src="https://github.com/user-attachments/assets/03afdf11-b4d8-4ee9-b32f-d893074208b3"
/> | <img width="1280" height="2856" alt="Screenshot_20250916_091303"
src="https://github.com/user-attachments/assets/7d70b007-63ae-4d62-884b-59d04f96158e"
/> | <img width="1280" height="2856" alt="Screenshot_20250916_091311"
src="https://github.com/user-attachments/assets/0ad3d293-0f1b-4a58-8139-dfd797bfc98b"
/> |
### Changes to TabsHost's layout
#### SafeAreaView
After internal discussion about approach to `SafeAreaView`, we had
following conclusions:
- as edge-to-edge becomes desirable (and is the default for apps
targeting Android SDK 35 or above), and to simplify layout handling, we
want the `Screen`s of our navigation containers (e.g. `StackScreen`,
`BottomTabsScreen`) to have **full dimensions of their parents, even if
it means that they will be laid out behind navigation bars** (header in
Stack, tab bar),
- `SafeAreaView` will provide unified way to handle the safe area,
- on Android, we want to control which insets we want to handle:
- **system insets** (received from `onApplyWindowInsets`), e.g.
`systemBars`, `displayCutout`
- **interface insets** - custom insets from navigation bars, e.g.
`bottomNavigationView`
#### Before this PR
Prior to this PR, we were using `LinearLayout` for `TabsHost`:
```kotlin
class TabsHost(
val reactContext: ThemedReactContext,
) : LinearLayout(reactContext),
TabScreenDelegate {
// ...
private val bottomNavigationView: BottomNavigationView =
BottomNavigationView(wrappedContext).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}
private val contentView: FrameLayout =
FrameLayout(reactContext).apply {
layoutParams =
LinearLayout
.LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT,
).apply {
weight = 1f
}
id = ViewIdGenerator.generateViewId()
}
// ...
}
```
<img width="946" height="817" alt="Screenshot 2025-09-24 at 17 23 20"
src="https://github.com/user-attachments/assets/784a769a-d98a-4d4e-a9aa-0413a8d85fed"
/>
This approach had following problems:
- `contentView` did not have dimensions of its parent, which is not what
we wanted,
- Yoga wasn't aware of `contentView`'s height - all content inside
`TabScreen` was laid out as if the screen had full height of its parent
-> this meant that `contentView`'s dimensions and actual content
dimensions were not in sync,
- it did not support using translucent tab bar - screen's content was
cut off outside of `contentView`'s bounds.
<img width="1280" height="2856" alt="3215_before_transparent"
src="https://github.com/user-attachments/assets/9af6b066-7f2f-4927-ac28-00277e4077ce"
/>
#### Approach in this PR
In this PR, we change `TabsHost`'s layout to `FrameLayout` which allows
multiple views placed on top of each other - this is what we want to
achieve (tab bar floating over content, attached to the bottom).
To attach `bottomNavigationView` to *the bottom*, we use
`Gravity.BOTTOM`.
```kotlin
class TabsHost(
val reactContext: ThemedReactContext,
) : FrameLayout(reactContext),
TabScreenDelegate,
SafeAreaProvider,
View.OnLayoutChangeListener {
// ...
private val bottomNavigationView: BottomNavigationView =
BottomNavigationView(wrappedContext).apply {
layoutParams =
LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT,
Gravity.BOTTOM,
)
}
private val contentView: FrameLayout =
FrameLayout(reactContext).apply {
layoutParams =
LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
)
id = ViewIdGenerator.generateViewId()
}
// ...
}
```
<img width="447" height="826" alt="Screenshot 2025-09-24 at 17 23 29"
src="https://github.com/user-attachments/assets/e3b67014-a193-4bb8-ba19-5b7c8f7816f8"
/>
Now, we can:
1. use opaque or translucent `bottomNavigationView`,
2. use `SafeAreaView` to control how much space can the actual content
take (do we allow it to render under `bottomNavigationView`).
<img width="1280" height="2856" alt="3215_after_transparent"
src="https://github.com/user-attachments/assets/2f2944f9-1984-4c01-b335-53051c58cbd3"
/>
### Support for older Android versions
On Android versions prior to R, insets dispatch is broken (children of
ViewGroup receive insets from previous child; they should all receive
the same insets). That's why we need to override
`dispatchApplyWindowInsets` implementation in `TabsHost`. In
`ViewGroup`'s implementation of this method, `View`'s implementation is
used (via `super`) - we can't access this directly. Unfortunately,
`View`'s implementation sets some private flags which are used by
default `onApplyWindowInsets` implementation. If we try to use
`onApplyWindowInsets` on API 28 without setting the private flag,
application goes into infinite loop (`fitSystemWindows` calls
`dispatchApplyWindowInsets` -> we might want to investigate this in more
detail). As we don't use insets in `TabsHost`, I decided not to call
`onApplyWindowInsets` in `TabsHost` at all. I haven't found any problems
with it yet.
## Changes
- add implementation for `SafeAreaView` and related classes for both
architectures
- add `SafeAreaProvider` interface and implement it for `TabsHost`
- change `TabsHost` to use `FrameLayout`:
- make `TabsScreen` layout behind tab bar (take full available height to
match JS screen)
- override `dispatchApplyWindowInsets` in `TabsHost` in order to fix
insets for older Android versions
- add `insetType` prop to control what kind of insets should
SafeAreaView respect (all, only system, only interface)
## Test code and steps to reproduce
Run `TestBottomTabs` with uncommented SafeAreaView. You can change which
edges are enabled and add `insetType` prop.
## Checklist
- [x] Included code example that can be used to test this change
- [ ] Updated TS types
- [ ] Updated documentation: <!-- For adding new props to native-stack
-->
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx
- [ ]
https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx
- [x] Ensured that CI passes
0 commit comments