Get sheet padding working consistently (#7798)

* tweak height/padding of iOS

* tweak android ratio calculation

* add a bit of extra padding to full height iOS to account for the bit below the safe area
This commit is contained in:
Samuel Newman
2025-02-21 10:59:08 -08:00
committed by GitHub
parent 798bf47827
commit 5d30111b78
4 changed files with 116 additions and 58 deletions
@@ -31,9 +31,13 @@ class BottomSheetView(
private lateinit var dialogRootViewGroup: DialogRootViewGroup private lateinit var dialogRootViewGroup: DialogRootViewGroup
private var eventDispatcher: EventDispatcher? = null private var eventDispatcher: EventDispatcher? = null
private val screenHeight = private val rawScreenHeight = context.resources.displayMetrics.heightPixels.toFloat()
context.resources.displayMetrics.heightPixels private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat()
.toFloat()
private fun getNavigationBarHeight(): Int {
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
}
private val onAttemptDismiss by EventDispatcher() private val onAttemptDismiss by EventDispatcher()
private val onSnapPointChange by EventDispatcher() private val onSnapPointChange by EventDispatcher()
@@ -63,12 +67,12 @@ class BottomSheetView(
} }
} }
var maxHeight = this.screenHeight var maxHeight = this.safeScreenHeight
set(value) { set(value) {
val px = dpToPx(value) val px = dpToPx(value)
field = field =
if (px > this.screenHeight) { if (px > this.safeScreenHeight) {
this.screenHeight this.safeScreenHeight
} else { } else {
px px
} }
@@ -153,6 +157,19 @@ class BottomSheetView(
// Presentation // Presentation
private fun getHalfExpandedRatio(contentHeight: Float): Float {
return when {
// Full height sheets
contentHeight >= safeScreenHeight -> 0.99f
// Medium height sheets (>50% but <100%)
contentHeight >= safeScreenHeight / 2 ->
this.clampRatio(this.getTargetHeight() / safeScreenHeight)
// Small height sheets (<50%)
else ->
this.clampRatio(this.getTargetHeight() / rawScreenHeight)
}
}
private fun present() { private fun present() {
if (this.isOpen || this.isOpening || this.isClosing) return if (this.isOpen || this.isOpening || this.isClosing) return
@@ -172,12 +189,12 @@ class BottomSheetView(
val behavior = BottomSheetBehavior.from(it) val behavior = BottomSheetBehavior.from(it)
behavior.state = BottomSheetBehavior.STATE_HIDDEN behavior.state = BottomSheetBehavior.STATE_HIDDEN
behavior.isFitToContents = true behavior.isFitToContents = true
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight) behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.skipCollapsed = true behavior.skipCollapsed = true
behavior.isDraggable = true behavior.isDraggable = true
behavior.isHideable = true behavior.isHideable = true
if (contentHeight >= this.screenHeight || this.minHeight >= this.screenHeight) { if (contentHeight >= this.safeScreenHeight || this.minHeight >= this.safeScreenHeight) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
this.selectedSnapPoint = 2 this.selectedSnapPoint = 2
} else { } else {
@@ -227,11 +244,11 @@ class BottomSheetView(
bottomSheet?.let { bottomSheet?.let {
val behavior = BottomSheetBehavior.from(it) val behavior = BottomSheetBehavior.from(it)
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight) behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { } else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
} }
} }
@@ -1,14 +1,17 @@
import * as React from 'react' import * as React from 'react'
import { import {
Dimensions, Dimensions,
LayoutChangeEvent,
NativeSyntheticEvent, NativeSyntheticEvent,
Platform, Platform,
StyleProp, StyleProp,
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {isIOS} from '#/platform/detection'
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types' import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
import {BottomSheetPortalProvider} from './BottomSheetPortal' import {BottomSheetPortalProvider} from './BottomSheetPortal'
import {Context as PortalContext} from './BottomSheetPortal' import {Context as PortalContext} from './BottomSheetPortal'
@@ -81,12 +84,10 @@ export class BottomSheetNativeComponent extends React.Component<
return null return null
} }
const {children, backgroundColor, ...rest} = this.props
const cornerRadius = rest.cornerRadius ?? 0
let extraStyles let extraStyles
if (isIOS15 && this.state.viewHeight) { if (isIOS15 && this.state.viewHeight) {
const {viewHeight} = this.state const {viewHeight} = this.state
const cornerRadius = this.props.cornerRadius ?? 0
if (viewHeight < screenHeight / 2) { if (viewHeight < screenHeight / 2) {
extraStyles = { extraStyles = {
height: viewHeight, height: viewHeight,
@@ -99,39 +100,70 @@ export class BottomSheetNativeComponent extends React.Component<
return ( return (
<Portal> <Portal>
<NativeView <BottomSheetNativeComponentInner
{...rest} {...this.props}
nativeViewRef={this.ref}
onStateChange={this.onStateChange} onStateChange={this.onStateChange}
ref={this.ref} extraStyles={extraStyles}
style={{ onLayout={e => {
position: 'absolute', const {height} = e.nativeEvent.layout
height: screenHeight, this.setState({viewHeight: height})
width: '100%', this.updateLayout()
}} }}
containerBackgroundColor={backgroundColor}> />
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
extraStyles,
]}>
<View
onLayout={e => {
const {height} = e.nativeEvent.layout
this.setState({viewHeight: height})
this.updateLayout()
}}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
</View>
</NativeView>
</Portal> </Portal>
) )
} }
} }
function BottomSheetNativeComponentInner({
children,
backgroundColor,
onLayout,
onStateChange,
nativeViewRef,
extraStyles,
...rest
}: BottomSheetViewProps & {
extraStyles?: StyleProp<ViewStyle>
onStateChange: (
event: NativeSyntheticEvent<{state: BottomSheetState}>,
) => void
nativeViewRef: React.RefObject<View>
onLayout: (event: LayoutChangeEvent) => void
}) {
const insets = useSafeAreaInsets()
const cornerRadius = rest.cornerRadius ?? 0
const sheetHeight = isIOS ? screenHeight - insets.top : screenHeight
return (
<NativeView
{...rest}
onStateChange={onStateChange}
ref={nativeViewRef}
style={{
position: 'absolute',
height: sheetHeight,
width: '100%',
}}
containerBackgroundColor={backgroundColor}>
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
extraStyles,
]}>
<View onLayout={onLayout}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
</View>
</NativeView>
)
}
+22 -13
View File
@@ -26,7 +26,7 @@ import {isAndroid, isIOS} from '#/platform/detection'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
import {List, ListMethods, ListProps} from '#/view/com/util/List' import {List, ListMethods, ListProps} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, tokens, useTheme} from '#/alf'
import {useThemeName} from '#/alf/util/useColorModeTheme' import {useThemeName} from '#/alf/util/useColorModeTheme'
import {Context, useDialogContext} from '#/components/Dialog/context' import {Context, useDialogContext} from '#/components/Dialog/context'
import { import {
@@ -46,7 +46,7 @@ export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/shared' export * from '#/components/Dialog/shared'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils' export * from '#/components/Dialog/utils'
// @ts-ignore
export const Input = createInput(TextInput) export const Input = createInput(TextInput)
export function Outer({ export function Outer({
@@ -168,7 +168,9 @@ export function Outer({
onStateChange={onStateChange} onStateChange={onStateChange}
disableDrag={disableDrag}> disableDrag={disableDrag}>
<Context.Provider value={context}> <Context.Provider value={context}>
<View testID={testID}>{children}</View> <View testID={testID} style={[a.relative]}>
{children}
</View>
</Context.Provider> </Context.Provider>
</BottomSheet> </BottomSheet>
) )
@@ -196,7 +198,7 @@ export function Inner({children, style, header}: DialogInnerProps) {
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
function ScrollableInner( function ScrollableInner(
{children, style, contentContainerStyle, header, ...props}, {children, contentContainerStyle, header, ...props},
ref, ref,
) { ) {
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
@@ -216,13 +218,21 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
[], [],
) )
const basePading = let paddingBottom = 0
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight) if (isIOS) {
const fullPaddingBase = insets.bottom + insets.top + basePading paddingBottom += keyboardHeight / 4
const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50 if (nativeSnapPoint === BottomSheetSnapPoint.Full) {
paddingBottom += insets.bottom + tokens.space.md
const paddingBottom = }
nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading paddingBottom = Math.max(paddingBottom, tokens.space._2xl)
} else {
paddingBottom += keyboardHeight
if (nativeSnapPoint === BottomSheetSnapPoint.Full) {
paddingBottom += insets.top
}
paddingBottom +=
Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl
}
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!isAndroid) { if (!isAndroid) {
@@ -238,7 +248,6 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
return ( return (
<KeyboardAwareScrollView <KeyboardAwareScrollView
style={[style]}
contentContainerStyle={[ contentContainerStyle={[
a.pt_2xl, a.pt_2xl,
a.px_xl, a.px_xl,
@@ -316,7 +325,7 @@ export function Handle() {
style={[ style={[
a.rounded_sm, a.rounded_sm,
{ {
top: 10, top: tokens.space._2xl / 2 - 2.5,
width: 35, width: 35,
height: 5, height: 5,
alignSelf: 'center', alignSelf: 'center',
+1 -1
View File
@@ -100,7 +100,7 @@ export function Outer({
<Dialog.Handle /> <Dialog.Handle />
{/* Re-wrap with context since Dialogs are portal-ed to root */} {/* Re-wrap with context since Dialogs are portal-ed to root */}
<Context.Provider value={context}> <Context.Provider value={context}>
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}> <Dialog.ScrollableInner label={_(msg`Menu`)}>
<View style={[a.gap_lg]}> <View style={[a.gap_lg]}>
{children} {children}
{isNative && showCancel && <Cancel />} {isNative && showCancel && <Cancel />}