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