add fullHeight prop, fix Android bottom sheet behavior, keyboard handling
Native module:
- Add `fullHeight` prop to BottomSheetView on Android and iOS
- iOS: fullHeight sets detent to .large, skips content observation
- Android: fullHeight uses isFitToContents=false with expandedOffset=statusBarHeight
Android bottom sheet behavior overhaul:
- Switch normal sheets to isFitToContents=false with expandedOffset, enabling
proper 3-state snapping (half-expanded ↔ expanded ↔ hidden). Previously
isFitToContents=true prevented settling at half-expanded during gestures,
making it impossible to swipe back down from expanded.
- preventExpansion sheets keep isFitToContents=true with maxHeight cap, plus
a state callback safety net that bounces EXPANDED→HALF_EXPANDED. This
catches an edge case where rapid content resizes during opening can invert
the expanded/half-expanded offsets, causing the sheet to dismiss.
- Add requestLayout() after maxHeight changes in updateLayout() so the
FrameLayout actually re-measures (fixes keyboard padding not resizing sheet)
- Set backgroundTint=transparent in the theme to remove Material3 surface
tint that was visible in gaps between sheet and screen edge
Keyboard handling:
- Remove native keyboard expansion logic (insets listener, isKeyboardVisible
tracking, manual STATE_EXPANDED on keyboard show)
- Replace with JS-side keyboard padding: ScrollableInner listens for RN
Keyboard events and adds bottom padding equal to keyboard height
- Generalize useOnKeyboardDidShow → useOnKeyboard(eventName, cb)
Callers:
- Replace minHeight: screenHeight hack with fullHeight: true in EditProfile,
ChangeHandle, AddAppPassword, ImageAltText, GifAltText, LanguageSelect,
FollowDialog, GifSelect, StarterPack, Select, NewChat, ShareViaChat,
DraftsList, WizardEditList, ListAddRemoveUsers, CreateOrEditList
- Remove unused useWindowDimensions imports
- ServerInput: simplify to just preventExpansion (was platform-specific)
- ImageAltTextDialog: remove old {height: 300} keyboard spacer hack
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
@@ -25,6 +25,10 @@ class BottomSheetModule : Module() {
|
||||
view.dismiss()
|
||||
}
|
||||
|
||||
Prop("fullHeight") { view: BottomSheetView, prop: Boolean ->
|
||||
view.fullHeight = prop
|
||||
}
|
||||
|
||||
Prop("disableDrag") { view: BottomSheetView, prop: Boolean ->
|
||||
view.disableDrag = prop
|
||||
}
|
||||
|
||||
+36
-48
@@ -8,8 +8,6 @@ import android.view.ViewStructure
|
||||
import android.view.Window
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.facebook.react.bridge.LifecycleEventListener
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
@@ -33,7 +31,6 @@ class BottomSheetView(
|
||||
|
||||
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
||||
private var eventDispatcher: EventDispatcher? = null
|
||||
private var isKeyboardVisible: Boolean = false
|
||||
|
||||
// Native content height observation (eliminates JS bridge round-trip)
|
||||
private var contentLayoutListener: View.OnLayoutChangeListener? = null
|
||||
@@ -71,6 +68,8 @@ class BottomSheetView(
|
||||
this.dialog?.setCancelable(!value)
|
||||
}
|
||||
|
||||
var fullHeight = false
|
||||
|
||||
var preventExpansion = false
|
||||
|
||||
var minHeight = 0f
|
||||
@@ -202,28 +201,36 @@ class BottomSheetView(
|
||||
|
||||
val behavior = BottomSheetBehavior.from(it)
|
||||
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
behavior.isFitToContents = true
|
||||
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
||||
behavior.skipCollapsed = true
|
||||
behavior.isDraggable = true
|
||||
behavior.isHideable = true
|
||||
|
||||
if (preventExpansion) {
|
||||
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
|
||||
} else {
|
||||
behavior.maxHeight = (screenHeight - getStatusBarHeight()).toInt()
|
||||
}
|
||||
|
||||
val targetHeight = this.getTargetHeight()
|
||||
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
|
||||
val shouldBeExpanded = targetHeight >= availableHeight
|
||||
|
||||
if (shouldBeExpanded) {
|
||||
if (fullHeight) {
|
||||
behavior.isFitToContents = false
|
||||
behavior.expandedOffset = getStatusBarHeight()
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
this.selectedSnapPoint = 2
|
||||
} else {
|
||||
} else if (preventExpansion) {
|
||||
behavior.isFitToContents = true
|
||||
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
||||
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
|
||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||
this.selectedSnapPoint = 1
|
||||
} else {
|
||||
behavior.isFitToContents = false
|
||||
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
||||
behavior.expandedOffset = getStatusBarHeight()
|
||||
|
||||
val targetHeight = this.getTargetHeight()
|
||||
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
|
||||
val shouldBeExpanded = targetHeight >= availableHeight
|
||||
|
||||
if (shouldBeExpanded) {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
this.selectedSnapPoint = 2
|
||||
} else {
|
||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||
this.selectedSnapPoint = 1
|
||||
}
|
||||
}
|
||||
|
||||
behavior.addBottomSheetCallback(
|
||||
@@ -232,6 +239,10 @@ class BottomSheetView(
|
||||
bottomSheet: View,
|
||||
newState: Int,
|
||||
) {
|
||||
if (newState == BottomSheetBehavior.STATE_EXPANDED && preventExpansion) {
|
||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||
return
|
||||
}
|
||||
when (newState) {
|
||||
BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1
|
||||
@@ -258,34 +269,14 @@ class BottomSheetView(
|
||||
this.isOpening = true
|
||||
dialog.show()
|
||||
this.dialog = dialog
|
||||
this.startObservingContentHeight()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets ->
|
||||
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
|
||||
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
||||
val behavior = bottomSheet?.let { BottomSheetBehavior.from(it) }
|
||||
|
||||
val wasKeyboardVisible = isKeyboardVisible
|
||||
isKeyboardVisible = imeVisible
|
||||
|
||||
if (imeVisible && behavior != null && bottomSheet != null && behavior.state != BottomSheetBehavior.STATE_EXPANDED && behavior.state != BottomSheetBehavior.STATE_HIDDEN) {
|
||||
if (preventExpansion) {
|
||||
behavior.maxHeight = (screenHeight - getStatusBarHeight()).toInt()
|
||||
bottomSheet.requestLayout()
|
||||
bottomSheet.post {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
} else {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
} else if (!imeVisible && wasKeyboardVisible) {
|
||||
updateLayout()
|
||||
}
|
||||
insets
|
||||
if (!fullHeight) {
|
||||
this.startObservingContentHeight()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun updateLayout() {
|
||||
if (fullHeight) return
|
||||
val dialog = this.dialog ?: return
|
||||
val contentHeight = this.getContentHeight()
|
||||
|
||||
@@ -300,6 +291,7 @@ class BottomSheetView(
|
||||
|
||||
if (preventExpansion) {
|
||||
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
|
||||
it.requestLayout()
|
||||
}
|
||||
|
||||
val targetHeight = this.getTargetHeight()
|
||||
@@ -315,11 +307,7 @@ class BottomSheetView(
|
||||
return
|
||||
}
|
||||
|
||||
if (isKeyboardVisible) {
|
||||
if (behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
} else if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) {
|
||||
if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
} else if (!shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||
@@ -330,7 +318,7 @@ class BottomSheetView(
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
this.dialog?.dismiss()
|
||||
this.dialog?.cancel()
|
||||
}
|
||||
|
||||
// Observe each direct child of innerView via OnLayoutChangeListener so that
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
<item name="paddingLeftSystemWindowInsets">true</item>
|
||||
<item name="paddingRightSystemWindowInsets">true</item>
|
||||
<item name="paddingTopSystemWindowInsets">false</item>
|
||||
<item name="backgroundTint">@android:color/transparent</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -19,6 +19,10 @@ public class BottomSheetModule: Module {
|
||||
view.dismiss()
|
||||
}
|
||||
|
||||
Prop("fullHeight") { (view: SheetView, prop: Bool) in
|
||||
view.fullHeight = prop
|
||||
}
|
||||
|
||||
Prop("cornerRadius") { (view: SheetView, prop: Float) in
|
||||
view.cornerRadius = CGFloat(prop)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||
}
|
||||
|
||||
// React view props
|
||||
var fullHeight = false
|
||||
var preventDismiss = false
|
||||
var preventExpansion = false
|
||||
var cornerRadius: CGFloat?
|
||||
@@ -132,7 +133,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||
}
|
||||
|
||||
let sheetVc = SheetViewController()
|
||||
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
|
||||
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion, fullHeight: self.fullHeight)
|
||||
if let sheet = sheetVc.sheetPresentationController {
|
||||
sheet.delegate = self
|
||||
sheet.preferredCornerRadius = self.cornerRadius
|
||||
@@ -151,7 +152,9 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||
|
||||
self.sheetVc = sheetVc
|
||||
self.isOpening = true
|
||||
self.startObservingContentHeight()
|
||||
if !self.fullHeight {
|
||||
self.startObservingContentHeight()
|
||||
}
|
||||
|
||||
rvc.present(sheetVc, animated: true) { [weak self] in
|
||||
self?.isOpening = false
|
||||
|
||||
@@ -20,13 +20,19 @@ class SheetViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||
func setDetents(contentHeight: CGFloat, preventExpansion: Bool, fullHeight: Bool = false) {
|
||||
guard let sheet = self.sheetPresentationController,
|
||||
let screenHeight = Util.getScreenHeight()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if fullHeight {
|
||||
sheet.detents = [.large()]
|
||||
sheet.selectedDetentIdentifier = .large
|
||||
return
|
||||
}
|
||||
|
||||
// On iOS 26, the floaty sheet presentation adds the device bottom safe area
|
||||
// on top of the custom detent value, creating visible padding inside the pill.
|
||||
// Subtract it so the pill height matches our actual content.
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface BottomSheetViewProps {
|
||||
disableDrag?: boolean
|
||||
sourceViewTag?: number
|
||||
|
||||
fullHeight?: boolean
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
|
||||
|
||||
Reference in New Issue
Block a user