Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ class SafeAreaProvider(context: Context?) :
private var mLastFrame: Rect? = null
private var mLastCorners: List<RoundedCorner?>? = null
private val mDispatchedInsets = DispatchedWindowInsets()
private var mIgnoreSystemBarsVisibility = false

internal fun getDispatchedWindowInsets(): WindowInsets? = mDispatchedInsets.get(this)

internal fun getIgnoreSystemBarsVisibility(): Boolean = mIgnoreSystemBarsVisibility

private fun maybeUpdateInsets() {
val insetsChangeHandler = mInsetsChangeHandler ?: return
val edgeInsets = getSafeAreaInsets(this, getDispatchedWindowInsets()) ?: return
val edgeInsets =
getSafeAreaInsets(this, getDispatchedWindowInsets(), mIgnoreSystemBarsVisibility)
?: return
val frame = getFrame(rootView as ViewGroup, this) ?: return
val corners = getRoundedCorners(this)
if (mLastInsets != edgeInsets || mLastFrame != frame || mLastCorners != corners) {
Expand Down Expand Up @@ -72,6 +77,15 @@ class SafeAreaProvider(context: Context?) :
return true
}

fun setIgnoreSystemBarsVisibility(ignoreSystemBarsVisibility: Boolean) {
if (mIgnoreSystemBarsVisibility == ignoreSystemBarsVisibility) {
return
}
mIgnoreSystemBarsVisibility = ignoreSystemBarsVisibility
maybeUpdateInsets()
invalidate()
}

fun setOnInsetsChangeHandler(handler: OnInsetsChangeHandler?) {
mInsetsChangeHandler = handler
maybeUpdateInsets()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.RNCSafeAreaProviderManagerDelegate
import com.facebook.react.viewmanagers.RNCSafeAreaProviderManagerInterface

Expand All @@ -19,6 +20,11 @@ class SafeAreaProviderManager :

public override fun createViewInstance(context: ThemedReactContext) = SafeAreaProvider(context)

@ReactProp(name = "ignoreSystemBarsVisibility")
override fun setIgnoreSystemBarsVisibility(view: SafeAreaProvider, value: Boolean) {
view.setIgnoreSystemBarsVisibility(value)
}

override fun getExportedCustomDirectEventTypeConstants() =
mutableMapOf(
InsetsChangeEvent.EVENT_NAME to mutableMapOf("registrationName" to "onInsetsChange")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ import kotlin.math.max
import kotlin.math.min

@RequiresApi(Build.VERSION_CODES.R)
private fun getRootWindowInsetsCompatR(windowInsets: WindowInsets?): EdgeInsets? {
private fun getRootWindowInsetsCompatR(
windowInsets: WindowInsets?,
ignoreSystemBarsVisibility: Boolean,
): EdgeInsets? {
if (windowInsets == null) {
return null
}
val types =
WindowInsets.Type.statusBars() or
WindowInsets.Type.displayCutout() or
WindowInsets.Type.navigationBars() or
WindowInsets.Type.captionBar()
val insets =
windowInsets?.getInsets(
WindowInsets.Type.statusBars() or
WindowInsets.Type.displayCutout() or
WindowInsets.Type.navigationBars() or
WindowInsets.Type.captionBar()
) ?: return null
if (ignoreSystemBarsVisibility) windowInsets.getInsetsIgnoringVisibility(types)
else windowInsets.getInsets(types)
return EdgeInsets(
top = insets.top.toFloat(),
right = insets.right.toFloat(),
Expand All @@ -44,6 +51,19 @@ private fun getRootWindowInsetsCompatM(windowInsets: WindowInsets?): EdgeInsets?
)
}

@RequiresApi(Build.VERSION_CODES.M)
@Suppress("DEPRECATION")
private fun getStableWindowInsetsCompatM(windowInsets: WindowInsets?): EdgeInsets? {
val insets = windowInsets ?: return null
val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) insets.displayCutout else null
return EdgeInsets(
top = max(insets.stableInsetTop, cutout?.safeInsetTop ?: 0).toFloat(),
right = max(insets.stableInsetRight, cutout?.safeInsetRight ?: 0).toFloat(),
bottom = max(insets.stableInsetBottom, cutout?.safeInsetBottom ?: 0).toFloat(),
left = max(insets.stableInsetLeft, cutout?.safeInsetLeft ?: 0).toFloat(),
)
}

private fun getRootWindowInsetsCompatBase(rootView: View): EdgeInsets? {
val visibleRect = android.graphics.Rect()
rootView.getWindowVisibleDisplayFrame(visibleRect)
Expand All @@ -58,12 +78,18 @@ private fun getRootWindowInsetsCompatBase(rootView: View): EdgeInsets? {
private fun getRootWindowInsetsCompat(
rootView: View,
dispatchedInsets: WindowInsets?,
ignoreSystemBarsVisibility: Boolean,
): EdgeInsets? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
maxInsets(
getRootWindowInsetsCompatR(rootView.rootWindowInsets),
getRootWindowInsetsCompatR(dispatchedInsets),
getRootWindowInsetsCompatR(rootView.rootWindowInsets, ignoreSystemBarsVisibility),
getRootWindowInsetsCompatR(dispatchedInsets, ignoreSystemBarsVisibility),
)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ignoreSystemBarsVisibility ->
maxInsets(
getStableWindowInsetsCompatM(rootView.rootWindowInsets),
getStableWindowInsetsCompatM(dispatchedInsets),
)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ->
maxInsets(
Expand Down Expand Up @@ -119,13 +145,19 @@ internal class DispatchedWindowInsets {
}

@JvmOverloads
fun getSafeAreaInsets(view: View, dispatchedInsets: WindowInsets? = null): EdgeInsets? {
fun getSafeAreaInsets(
view: View,
dispatchedInsets: WindowInsets? = null,
ignoreSystemBarsVisibility: Boolean = false,
): EdgeInsets? {
// The view has not been layout yet.
if (view.height == 0) {
return null
}
val rootView = view.rootView
val windowInsets = getRootWindowInsetsCompat(rootView, dispatchedInsets) ?: return null
val windowInsets =
getRootWindowInsetsCompat(rootView, dispatchedInsets, ignoreSystemBarsVisibility)
?: return null

// Calculate the part of the view that overlaps with window insets.
val windowWidth = rootView.width.toFloat()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ class SafeAreaView(context: Context?) :
val dispatchedInsets =
if (providerView is SafeAreaProvider) providerView.getDispatchedWindowInsets()
else mDispatchedInsets.get(this)
val safeAreaInsets = getSafeAreaInsets(providerView, dispatchedInsets) ?: return false
val ignoreSystemBarsVisibility =
providerView is SafeAreaProvider && providerView.getIgnoreSystemBarsVisibility()
val safeAreaInsets =
getSafeAreaInsets(providerView, dispatchedInsets, ignoreSystemBarsVisibility)
?: return false
val edgeInsets: EdgeInsets
if (mCornerAdaptation == CornerAdaptation.NONE) {
edgeInsets = safeAreaInsets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ public RNCSafeAreaProviderManagerDelegate(U viewManager) {

@Override
public void setProperty(T view, String propName, @Nullable Object value) {
super.setProperty(view, propName, value);
switch (propName) {
case "ignoreSystemBarsVisibility":
mViewManager.setIgnoreSystemBarsVisibility(view, value == null ? false : (boolean) value);
break;
default:
super.setProperty(view, propName, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
import android.view.View;

public interface RNCSafeAreaProviderManagerInterface<T extends View> {
// No props
void setIgnoreSystemBarsVisibility(T view, boolean value);
}
8 changes: 8 additions & 0 deletions docs/docs/api/safe-area-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ Optional, defaults to `null`.

Can be used to provide the initial value for frame and insets, this allows rendering immediately. See [optimization](/optimizations) for more information on how to use this prop.

#### `ignoreSystemBarsVisibility`

Optional, defaults to the value of the nearest parent `SafeAreaProvider`, or `false` without one.

When `true`, the insets keep the space of the status bar and the navigation bar while they are hidden, so content does not jump when the bars hide or show. On Android this uses `getInsetsIgnoringVisibility` on API 30+ and the stable insets on API 23-29. On iOS the top inset keeps the last visible status bar height. On web this prop has no effect.

A nested `SafeAreaProvider` without this prop uses the value of its parent. Set it to `false` to turn it off for a subtree.

#### `ref`

Optional ref to the underlying view.
13 changes: 12 additions & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AdditionalInsetsExample from './AdditionalInsetsExample';
import CornerAdaptationExample from './CornerAdaptationExample';
import FormSheetExample from './FormSheetExample';
import HooksExample from './HooksExample';
import IgnoreSystemBarsVisibilityExample from './IgnoreSystemBarsVisibilityExample';
import ListenerExample from './ListenerExample';
import SafeAreaViewExample from './SafeAreaViewExample';
import SafeAreaViewEdgesExample from './SafeAreaViewEdgesExample';
Expand All @@ -22,7 +23,8 @@ type Example =
| 'react-navigation'
| 'native-stack'
| 'form-sheet'
| 'additional-insets';
| 'additional-insets'
| 'ignore-system-bars-visibility';

export default function App() {
const [currentExample, setCurrentExample] = React.useState<Example | null>(
Expand Down Expand Up @@ -78,6 +80,12 @@ export default function App() {
DevSettings.addMenuItem('Show Additional Insets Example', () => {
setCurrentExample('additional-insets');
});
DevSettings.addMenuItem(
'Show Ignore System Bars Visibility Example',
() => {
setCurrentExample('ignore-system-bars-visibility');
},
);
}, []);

let content: React.ReactElement<unknown>;
Expand Down Expand Up @@ -109,6 +117,9 @@ export default function App() {
case 'additional-insets':
content = <AdditionalInsetsExample />;
break;
case 'ignore-system-bars-visibility':
content = <IgnoreSystemBarsVisibilityExample />;
break;
default:
content = (
<View
Expand Down
89 changes: 89 additions & 0 deletions example/src/IgnoreSystemBarsVisibilityExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as React from 'react';
import {
Button,
ScrollView,
StatusBar,
Switch,
Text,
View,
} from 'react-native';

import {
SafeAreaProvider,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
import { DataView } from './components/DataView';
import { Card } from './components/Card';

function IgnoreSystemBarsVisibilityScreen({
ignoreSystemBarsVisibility,
onIgnoreSystemBarsVisibilityChange,
}: {
ignoreSystemBarsVisibility: boolean;
onIgnoreSystemBarsVisibilityChange: (value: boolean) => void;
}) {
const insets = useSafeAreaInsets();
const [statusBarHidden, setStatusBarHidden] = React.useState(false);

return (
<>
<StatusBar
hidden={statusBarHidden}
barStyle="dark-content"
backgroundColor="transparent"
translucent
/>
<View
style={{
flex: 1,
backgroundColor: 'red',
paddingTop: insets.top,
paddingLeft: insets.left,
paddingBottom: insets.bottom,
paddingRight: insets.right,
}}
>
<ScrollView style={{ flex: 1, backgroundColor: '#eee' }}>
<Card title="ignoreSystemBarsVisibility">
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Text>{String(ignoreSystemBarsVisibility)}</Text>
<Switch
value={ignoreSystemBarsVisibility}
onValueChange={onIgnoreSystemBarsVisibilityChange}
/>
</View>
</Card>
<Card title="Status bar">
<Button
title={statusBarHidden ? 'Show status bar' : 'Hide status bar'}
onPress={() => setStatusBarHidden((hidden) => !hidden)}
/>
</Card>
<Card title="Provider insets">
<DataView data={insets} />
</Card>
</ScrollView>
</View>
</>
);
}

export default function IgnoreSystemBarsVisibilityExample() {
const [ignoreSystemBarsVisibility, setIgnoreSystemBarsVisibility] =
React.useState(true);

return (
<SafeAreaProvider ignoreSystemBarsVisibility={ignoreSystemBarsVisibility}>
<IgnoreSystemBarsVisibilityScreen
ignoreSystemBarsVisibility={ignoreSystemBarsVisibility}
onIgnoreSystemBarsVisibilityChange={setIgnoreSystemBarsVisibility}
/>
</SafeAreaProvider>
);
}
2 changes: 2 additions & 0 deletions ios/Fabric/RNCSafeAreaProviderComponentView.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ NS_ASSUME_NONNULL_BEGIN

@interface RNCSafeAreaProviderComponentView : RCTViewComponentView

@property (nonatomic, readonly) BOOL ignoreSystemBarsVisibility;

extern NSString *const RNCSafeAreaDidChange;

@end
Expand Down
21 changes: 21 additions & 0 deletions ios/Fabric/RNCSafeAreaProviderComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ - (void)invalidateSafeAreaInsetsMeasuringCorners:(BOOL)measureCorners
}
UIEdgeInsets verticalCornerAdaptedInsets = _measuredVerticalCornerAdaptedInsets;
UIEdgeInsets horizontalCornerAdaptedInsets = _measuredHorizontalCornerAdaptedInsets;
if (_ignoreSystemBarsVisibility) {
CGFloat statusBarTopInset = RNCSafeAreaStatusBarTopInset(self);
safeAreaInsets.top = MAX(safeAreaInsets.top, statusBarTopInset);
verticalCornerAdaptedInsets.top = MAX(verticalCornerAdaptedInsets.top, statusBarTopInset);
horizontalCornerAdaptedInsets.top = MAX(horizontalCornerAdaptedInsets.top, statusBarTopInset);
}
#elif TARGET_OS_OSX
UIEdgeInsets verticalCornerAdaptedInsets = safeAreaInsets;
UIEdgeInsets horizontalCornerAdaptedInsets = safeAreaInsets;
Expand Down Expand Up @@ -185,9 +191,24 @@ + (ComponentDescriptorProvider)componentDescriptorProvider
return concreteComponentDescriptorProvider<RNCSafeAreaProviderComponentDescriptor>();
}

- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &newProps = *std::static_pointer_cast<const RNCSafeAreaProviderProps>(props);
BOOL ignoreSystemBarsVisibility = newProps.ignoreSystemBarsVisibility;
BOOL ignoreSystemBarsVisibilityChanged = ignoreSystemBarsVisibility != _ignoreSystemBarsVisibility;
_ignoreSystemBarsVisibility = ignoreSystemBarsVisibility;

[super updateProps:props oldProps:oldProps];

if (ignoreSystemBarsVisibilityChanged) {
[self invalidateSafeAreaInsets];
}
}

- (void)prepareForRecycle
{
[super prepareForRecycle];
_ignoreSystemBarsVisibility = NO;
_currentSafeAreaInsets = UIEdgeInsetsZero;
_currentVerticalCornerAdaptedInsets = UIEdgeInsetsZero;
_currentHorizontalCornerAdaptedInsets = UIEdgeInsetsZero;
Expand Down
4 changes: 4 additions & 0 deletions ios/Fabric/RNCSafeAreaViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ - (void)updateStateIfNecessaryMeasuringCorners:(BOOL)measureCorners
}
#if TARGET_OS_IPHONE
UIEdgeInsets safeAreaInsets = [self providerSafeAreaInsetsMeasuringCorners:measureCorners];
if ([_providerView isKindOfClass:RNCSafeAreaProviderComponentView.class] &&
((RNCSafeAreaProviderComponentView *)_providerView).ignoreSystemBarsVisibility) {
safeAreaInsets.top = MAX(safeAreaInsets.top, RNCSafeAreaStatusBarTopInset(_providerView));
}

if (UIEdgeInsetsEqualToEdgeInsetsWithThreshold(safeAreaInsets, _currentSafeAreaInsets, 1.0 / RCTScreenScale())) {
return;
Expand Down
2 changes: 2 additions & 0 deletions ios/RNCSafeAreaProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN
// need to be coalesced before sending, for performance reasons.
@property (nonatomic, copy) RCTBubblingEventBlock onInsetsChange;

@property (nonatomic, assign) BOOL ignoreSystemBarsVisibility;

@end

NS_ASSUME_NONNULL_END
Loading