Observe content view layout natively on Android too

Same approach as the iOS KVO change: use View.OnLayoutChangeListener on
the content view to detect height changes purely on the native side.
This eliminates the setTimeout → updateLayout() JS bridge hack that was
a known workaround for async timing issues on Android.

The onLayout callback in JS is now only used for the iOS 15 fallback
height measurement. All other height observation is native.

https://claude.ai/code/session_01Rp7ef1h3fKh6fhStcjqLJ5
This commit is contained in:
Claude
2026-02-28 23:59:24 +00:00
committed by Samuel Newman
parent f83662275d
commit 00e990b276
2 changed files with 46 additions and 18 deletions
@@ -36,6 +36,10 @@ class BottomSheetView(
private var eventDispatcher: EventDispatcher? = null private var eventDispatcher: EventDispatcher? = null
private var isKeyboardVisible: Boolean = false private var isKeyboardVisible: Boolean = false
// Native content height observation (eliminates JS bridge round-trip)
private var contentLayoutListener: View.OnLayoutChangeListener? = null
private var observedContentView: View? = null
private val screenHeight = private val screenHeight =
context.resources.displayMetrics.heightPixels context.resources.displayMetrics.heightPixels
.toFloat() .toFloat()
@@ -129,6 +133,7 @@ class BottomSheetView(
} }
private fun destroy() { private fun destroy() {
this.stopObservingContentHeight()
this.isClosing = false this.isClosing = false
this.isOpen = false this.isOpen = false
this.dialog = null this.dialog = null
@@ -245,6 +250,7 @@ class BottomSheetView(
this.isOpening = true this.isOpening = true
dialog.show() dialog.show()
this.dialog = dialog this.dialog = dialog
this.startObservingContentHeight()
ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets -> ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets ->
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
@@ -310,6 +316,35 @@ class BottomSheetView(
this.dialog?.dismiss() this.dialog?.dismiss()
} }
// Observe the content view's layout changes so that height updates are detected
// purely on the native side, without a JS bridge round-trip through onLayout.
private fun startObservingContentHeight() {
stopObservingContentHeight()
val innerViewGroup = this.innerView as? ViewGroup ?: return
val contentView = innerViewGroup.getChildAt(0) ?: return
val listener = View.OnLayoutChangeListener { _, _, top, _, bottom, _, _, oldTop, _, oldBottom ->
val newHeight = bottom - top
val oldHeight = oldBottom - oldTop
if (newHeight != oldHeight && newHeight > 0 && (isOpen || isOpening) && !isClosing) {
updateLayout()
}
}
contentView.addOnLayoutChangeListener(listener)
this.contentLayoutListener = listener
this.observedContentView = contentView
}
private fun stopObservingContentHeight() {
contentLayoutListener?.let { listener ->
observedContentView?.removeOnLayoutChangeListener(listener)
}
contentLayoutListener = null
observedContentView = null
}
// Util // Util
private fun getContentHeight(): Float { private fun getContentHeight(): Float {
@@ -113,23 +113,16 @@ export class BottomSheetNativeComponent extends React.Component<
nativeViewRef={this.ref} nativeViewRef={this.ref}
onStateChange={this.onStateChange} onStateChange={this.onStateChange}
extraStyles={extraStyles} extraStyles={extraStyles}
onLayout={e => { onLayout={
if (IS_IOS15) { IS_IOS15
const {height} = e.nativeEvent.layout ? e => {
this.setState({viewHeight: height}) const {height} = e.nativeEvent.layout
} this.setState({viewHeight: height})
if (Platform.OS === 'android') { }
// TEMP HACKFIX: I had to timebox this, but this is Bad. : undefined
// On Android, if you run updateLayout() immediately, // Content height changes are observed natively on both platforms:
// it will take ages to actually run on the native side. // iOS uses KVO on content view bounds, Android uses OnLayoutChangeListener.
// However, adding literally any delay will fix this, including }
// a console.log() - just sending the log to the CLI is enough.
// TODO: Get to the bottom of this and fix it properly! -sfn
setTimeout(() => this.updateLayout())
}
// iOS: content height changes are observed natively via KVO on
// the content view's bounds, so no JS bridge round-trip needed.
}}
/> />
</Portal> </Portal>
) )
@@ -150,7 +143,7 @@ function BottomSheetNativeComponentInner({
event: NativeSyntheticEvent<{state: BottomSheetState}>, event: NativeSyntheticEvent<{state: BottomSheetState}>,
) => void ) => void
nativeViewRef: React.RefObject<View> nativeViewRef: React.RefObject<View>
onLayout: (event: LayoutChangeEvent) => void onLayout?: (event: LayoutChangeEvent) => void
}) { }) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const cornerRadius = rest.cornerRadius ?? 0 const cornerRadius = rest.cornerRadius ?? 0