diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProvider.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProvider.kt index 1b77db80..409c6492 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProvider.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProvider.kt @@ -24,12 +24,17 @@ class SafeAreaProvider(context: Context?) : private var mLastFrame: Rect? = null private var mLastCorners: List? = 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) { @@ -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() diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProviderManager.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProviderManager.kt index d440bc46..46036d71 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProviderManager.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProviderManager.kt @@ -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 @@ -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") diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt index 17eec587..45a006e1 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt @@ -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(), @@ -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) @@ -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( @@ -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() diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.kt index e5c56eb9..8c6917bf 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.kt @@ -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 diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerDelegate.java index f007c96d..15283079 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerDelegate.java @@ -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); + } } } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerInterface.java index 466e74c0..cc8d60af 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerInterface.java @@ -12,5 +12,5 @@ import android.view.View; public interface RNCSafeAreaProviderManagerInterface { - // No props + void setIgnoreSystemBarsVisibility(T view, boolean value); } diff --git a/docs/docs/api/safe-area-provider.mdx b/docs/docs/api/safe-area-provider.mdx index 485fe733..ef0a3df4 100644 --- a/docs/docs/api/safe-area-provider.mdx +++ b/docs/docs/api/safe-area-provider.mdx @@ -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. diff --git a/example/src/App.tsx b/example/src/App.tsx index 096e0da8..509a74bf 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -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'; @@ -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( @@ -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; @@ -109,6 +117,9 @@ export default function App() { case 'additional-insets': content = ; break; + case 'ignore-system-bars-visibility': + content = ; + break; default: content = ( void; +}) { + const insets = useSafeAreaInsets(); + const [statusBarHidden, setStatusBarHidden] = React.useState(false); + + return ( + <> +