From 00e990b2762b4d716746ec335a4b90e16fe89285 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 23:59:24 +0000 Subject: [PATCH] Observe content view layout natively on Android too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../modules/bottomsheet/BottomSheetView.kt | 35 +++++++++++++++++++ .../src/BottomSheetNativeComponent.tsx | 29 ++++++--------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt index e8e640ea2b..f636d4f8ea 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt @@ -36,6 +36,10 @@ class BottomSheetView( 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 + private var observedContentView: View? = null + private val screenHeight = context.resources.displayMetrics.heightPixels .toFloat() @@ -129,6 +133,7 @@ class BottomSheetView( } private fun destroy() { + this.stopObservingContentHeight() this.isClosing = false this.isOpen = false this.dialog = null @@ -245,6 +250,7 @@ class BottomSheetView( this.isOpening = true dialog.show() this.dialog = dialog + this.startObservingContentHeight() ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets -> val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) @@ -310,6 +316,35 @@ class BottomSheetView( 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 private fun getContentHeight(): Float { diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx index 788abf9d9d..0cba53f3b7 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -113,23 +113,16 @@ export class BottomSheetNativeComponent extends React.Component< nativeViewRef={this.ref} onStateChange={this.onStateChange} extraStyles={extraStyles} - onLayout={e => { - if (IS_IOS15) { - const {height} = e.nativeEvent.layout - this.setState({viewHeight: height}) - } - if (Platform.OS === 'android') { - // TEMP HACKFIX: I had to timebox this, but this is Bad. - // On Android, if you run updateLayout() immediately, - // it will take ages to actually run on the native side. - // 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. - }} + onLayout={ + IS_IOS15 + ? e => { + const {height} = e.nativeEvent.layout + this.setState({viewHeight: height}) + } + : undefined + // Content height changes are observed natively on both platforms: + // iOS uses KVO on content view bounds, Android uses OnLayoutChangeListener. + } /> ) @@ -150,7 +143,7 @@ function BottomSheetNativeComponentInner({ event: NativeSyntheticEvent<{state: BottomSheetState}>, ) => void nativeViewRef: React.RefObject - onLayout: (event: LayoutChangeEvent) => void + onLayout?: (event: LayoutChangeEvent) => void }) { const insets = useSafeAreaInsets() const cornerRadius = rest.cornerRadius ?? 0