diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetBehaviorController.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetBehaviorController.kt index 9d535205f6..5e5046cc2a 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetBehaviorController.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetBehaviorController.kt @@ -1,13 +1,45 @@ package com.swmansion.rnscreens.gamma.modals.formsheet +import android.view.View import android.widget.FrameLayout import com.google.android.material.bottomsheet.BottomSheetBehavior internal class FormSheetBehaviorController( private val sheetView: FrameLayout, + private val onDetentChanged: ((index: Int) -> Unit)? = null, ) { private val behavior = BottomSheetBehavior.from(sheetView) + private var currentDetentsCount: Int = 1 + private var lastEmittedDetentIndex: Int = -1 + + private val bottomSheetCallback = + object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged( + bottomSheet: View, + newState: Int, + ) { + val index = mapStateToDetentIndex(newState) + if (index != -1 && index != lastEmittedDetentIndex) { + lastEmittedDetentIndex = index + onDetentChanged?.invoke(index) + } + } + + override fun onSlide( + bottomSheet: View, + slideOffset: Float, + ) = Unit + } + + internal fun setup() { + behavior.addBottomSheetCallback(bottomSheetCallback) + } + + internal fun destroy() { + behavior.removeBottomSheetCallback(bottomSheetCallback) + } + /** * @param detents - parsed detents configuration. * @param sheetAvailableSpace - the full window height that detent fractions are measured against. @@ -25,6 +57,8 @@ internal class FormSheetBehaviorController( contentHeightForFitToContents: Int = 0, nativeContainerPaddingBottom: Int = 0, ) { + currentDetentsCount = detents.count + if (sheetAvailableSpace <= 0) { return } @@ -95,4 +129,23 @@ internal class FormSheetBehaviorController( // TODO: @t0maboro - in v4 impl the state was passed as a param, consider the same approach state = BottomSheetBehavior.STATE_COLLAPSED } + + private fun mapStateToDetentIndex(state: Int): Int = + when (currentDetentsCount) { + 1 -> if (state == BottomSheetBehavior.STATE_EXPANDED) 0 else -1 + 2 -> + when (state) { + BottomSheetBehavior.STATE_COLLAPSED -> 0 + BottomSheetBehavior.STATE_EXPANDED -> 1 + else -> -1 + } + 3 -> + when (state) { + BottomSheetBehavior.STATE_COLLAPSED -> 0 + BottomSheetBehavior.STATE_HALF_EXPANDED -> 1 + BottomSheetBehavior.STATE_EXPANDED -> 2 + else -> -1 + } + else -> -1 + } } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDetentChangedEvent.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDetentChangedEvent.kt new file mode 100644 index 0000000000..ca1d8dff29 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDetentChangedEvent.kt @@ -0,0 +1,33 @@ +package com.swmansion.rnscreens.gamma.modals.formsheet + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.swmansion.rnscreens.gamma.common.event.NamingAwareEventType + +class FormSheetDetentChangedEvent( + surfaceId: Int, + viewId: Int, + private val index: Int, +) : Event(surfaceId, viewId), + NamingAwareEventType { + override fun getEventName(): String = EVENT_NAME + + override fun getEventRegistrationName() = EVENT_REGISTRATION_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putInt("index", index) + } + + override fun canCoalesce(): Boolean = false + + companion object : NamingAwareEventType { + const val EVENT_NAME = "topDetentChanged" + const val EVENT_REGISTRATION_NAME = "onDetentChanged" + + override fun getEventName() = EVENT_NAME + + override fun getEventRegistrationName() = EVENT_REGISTRATION_NAME + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDialogEventEmitter.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDialogEventEmitter.kt new file mode 100644 index 0000000000..29d9c3e373 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDialogEventEmitter.kt @@ -0,0 +1,9 @@ +package com.swmansion.rnscreens.gamma.modals.formsheet + +import com.swmansion.rnscreens.gamma.common.event.ViewAppearanceEventEmitter + +internal interface FormSheetDialogEventEmitter : ViewAppearanceEventEmitter { + fun emitOnNativeDismissEvent() + + fun emitOnDetentChanged(index: Int) +} diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDialogManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDialogManager.kt index d5fd725a06..818d78a117 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDialogManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetDialogManager.kt @@ -5,13 +5,12 @@ import android.util.Log import android.view.ContextThemeWrapper import android.view.View import android.widget.FrameLayout -import com.swmansion.rnscreens.gamma.common.event.ViewAppearanceEventEmitter import com.swmansion.rnscreens.gamma.modals.dimmingview.DimmingViewManager +import kotlin.properties.Delegates class FormSheetDialogManager( context: Context, private val contentView: View, - private val onDismissRequest: () -> Unit, ) { private var formSheetConfig = FormSheetConfig() @@ -34,7 +33,9 @@ class FormSheetDialogManager( private val bottomSheetView = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) private val behaviorController = - bottomSheetView?.let { FormSheetBehaviorController(it) } + bottomSheetView?.let { + FormSheetBehaviorController(it) { index -> eventEmitter?.emitOnDetentChanged(index) } + } private val dimmingManager = DimmingViewManager(context, dialog) @@ -51,10 +52,12 @@ class FormSheetDialogManager( dialog = dialog, bottomSheetView = bottomSheetView, dimmingManager = dimmingManager, - onNativeDismiss = onDismissRequest, + onNativeDismiss = { eventEmitter?.emitOnNativeDismissEvent() }, ) - internal var appearanceEventEmitter: ViewAppearanceEventEmitter? by presentationManager::appearanceEventEmitter + internal var eventEmitter: FormSheetDialogEventEmitter? by Delegates.observable(null) { _, _, newValue -> + presentationManager.appearanceEventEmitter = newValue + } internal val contentSizeChangeDelegate: FormSheetContentSizeChangeDelegate get() = dimensionsCoordinator @@ -62,6 +65,7 @@ class FormSheetDialogManager( init { presentationManager.setup() dimensionsCoordinator.setup() + behaviorController?.setup() } internal fun applyConfig(newConfig: FormSheetConfig) { @@ -104,6 +108,7 @@ class FormSheetDialogManager( } internal fun destroy() { + behaviorController?.destroy() presentationManager.destroy() dimensionsCoordinator.destroy() dialog.dismiss() diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHost.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHost.kt index 3f7903feaf..11c1746d00 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHost.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHost.kt @@ -32,7 +32,6 @@ class FormSheetHost( FormSheetDialogManager( context = context, contentView = sheetContentView, - onDismissRequest = { eventEmitter.emitOnNativeDismissEvent() }, ) init { @@ -78,7 +77,7 @@ class FormSheetHost( internal fun onViewManagerAddEventEmitters() { check(id != NO_ID) { "[RNScreens] FormSheetHost must have its tag set when registering event emitters" } eventEmitter = FormSheetHostEventEmitter(reactContext, id) - dialogManager.appearanceEventEmitter = eventEmitter + dialogManager.eventEmitter = eventEmitter } internal fun onAfterUpdateTransaction() { diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHostEventEmitter.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHostEventEmitter.kt index 32d454c17f..d9b7b4b00e 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHostEventEmitter.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHostEventEmitter.kt @@ -2,14 +2,13 @@ package com.swmansion.rnscreens.gamma.modals.formsheet import com.facebook.react.bridge.ReactContext import com.swmansion.rnscreens.gamma.common.event.BaseEventEmitter -import com.swmansion.rnscreens.gamma.common.event.ViewAppearanceEventEmitter internal class FormSheetHostEventEmitter( reactContext: ReactContext, viewTag: Int, ) : BaseEventEmitter(reactContext, viewTag), - ViewAppearanceEventEmitter { - fun emitOnNativeDismissEvent() { + FormSheetDialogEventEmitter { + override fun emitOnNativeDismissEvent() { reactEventDispatcher.dispatchEvent( FormSheetNativeDismissEvent(surfaceId, viewTag), ) @@ -44,4 +43,10 @@ internal class FormSheetHostEventEmitter( FormSheetDidDisappearEvent(surfaceId, viewTag), ) } + + override fun emitOnDetentChanged(index: Int) { + reactEventDispatcher.dispatchEvent( + FormSheetDetentChangedEvent(surfaceId, viewTag, index), + ) + } } diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHostViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHostViewManager.kt index 7313d382a8..4a7ccffbee 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHostViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/modals/formsheet/FormSheetHostViewManager.kt @@ -132,6 +132,7 @@ class FormSheetHostViewManager : makeEventRegistrationInfo(FormSheetDidAppearEvent), makeEventRegistrationInfo(FormSheetWillDisappearEvent), makeEventRegistrationInfo(FormSheetDidDisappearEvent), + makeEventRegistrationInfo(FormSheetDetentChangedEvent), ) override fun updateState( diff --git a/apps/src/tests/single-feature-tests/form-sheet/index.ts b/apps/src/tests/single-feature-tests/form-sheet/index.ts index b047fe2e04..f509ccc688 100644 --- a/apps/src/tests/single-feature-tests/form-sheet/index.ts +++ b/apps/src/tests/single-feature-tests/form-sheet/index.ts @@ -11,7 +11,7 @@ import TestFormSheetInitialDetentIndex from './test-form-sheet-initial-detent-in import TestFormSheetLargestUndimmedDetentIndex from './test-form-sheet-largest-undimmed-detent-index-ios'; import TestFormSheetLifecycleEvents from './test-form-sheet-lifecycle-events'; import TestFormSheetNativeContainerStyle from './test-form-sheet-native-container-style-ios'; -import TestFormSheetOnDetentChanged from './test-form-sheet-on-detent-changed-ios'; +import TestFormSheetOnDetentChanged from './test-form-sheet-on-detent-changed'; import TestFormSheetPreferredCornerRadius from './test-form-sheet-preferred-corner-radius-ios'; import TestFormSheetPresentationState from './test-form-sheet-presentation-state'; import TestFormSheetPreventNativeDismiss from './test-form-sheet-prevent-native-dismiss-ios'; @@ -28,7 +28,7 @@ export { default as TestFormSheetInitialDetentIndex } from './test-form-sheet-in export { default as TestFormSheetLargestUndimmedDetentIndex } from './test-form-sheet-largest-undimmed-detent-index-ios'; export { default as TestFormSheetLifecycleEvents } from './test-form-sheet-lifecycle-events'; export { default as TestFormSheetNativeContainerStyle } from './test-form-sheet-native-container-style-ios'; -export { default as TestFormSheetOnDetentChanged } from './test-form-sheet-on-detent-changed-ios'; +export { default as TestFormSheetOnDetentChanged } from './test-form-sheet-on-detent-changed'; export { default as TestFormSheetPreferredCornerRadius } from './test-form-sheet-preferred-corner-radius-ios'; export { default as TestFormSheetPresentationState } from './test-form-sheet-presentation-state'; export { default as TestFormSheetPreventNativeDismiss } from './test-form-sheet-prevent-native-dismiss-ios'; diff --git a/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed-ios/index.tsx b/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed/index.tsx similarity index 86% rename from apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed-ios/index.tsx rename to apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed/index.tsx index 392aab6d9b..4bf9cf0a40 100644 --- a/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed-ios/index.tsx +++ b/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed/index.tsx @@ -1,17 +1,9 @@ import React, { useState } from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; import { FormSheet } from 'react-native-screens/experimental'; -import type { ScenarioDescription } from '@apps/tests/shared/helpers'; import { createScenario } from '@apps/tests/shared/helpers'; import { Colors } from '@apps/shared/styling'; - -const scenarioDescription: ScenarioDescription = { - name: 'OnDetentChanged', - key: 'test-form-sheet-on-detent-changed-ios', - details: - 'Allows testing the onDetentChanged event, verifying that the correct detent index is reported when swiping.', - platforms: ['ios'], -}; +import { scenarioDescription } from './scenario-description'; function TestFormSheetOnDetentChanged() { const [isOpen, setIsOpen] = useState(false); @@ -77,7 +69,6 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: Colors.background, padding: 24, - justifyContent: 'center', alignItems: 'center', }, sheetTitle: { diff --git a/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed/scenario-description.ts b/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed/scenario-description.ts new file mode 100644 index 0000000000..6514d387a2 --- /dev/null +++ b/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed/scenario-description.ts @@ -0,0 +1,11 @@ +import { ScenarioDescription } from '@apps/tests/shared/helpers'; + +export const scenarioDescription: ScenarioDescription = { + name: 'OnDetentChanged', + key: 'test-form-sheet-on-detent-changed', + details: + 'Allows testing the onDetentChanged event, verifying that the correct detent index is reported when swiping.', + platforms: ['android', 'ios'], + e2eCoverage: 'tbd', + smokeTest: false, +}; diff --git a/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed-ios/scenario.md b/apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed/scenario.md similarity index 100% rename from apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed-ios/scenario.md rename to apps/src/tests/single-feature-tests/form-sheet/test-form-sheet-on-detent-changed/scenario.md diff --git a/src/components/gamma/modals/form-sheet/FormSheet.types.ts b/src/components/gamma/modals/form-sheet/FormSheet.types.ts index 4422106326..f3f444c4d0 100644 --- a/src/components/gamma/modals/form-sheet/FormSheet.types.ts +++ b/src/components/gamma/modals/form-sheet/FormSheet.types.ts @@ -196,7 +196,7 @@ export interface FormSheetProps { * * Provides the `index` of the newly selected detent from the `detents` array. * - * @platform ios + * @platform android, ios */ onDetentChanged?: | FormSheetEventHandler