@lodev09/react-native-true-sheet 4.0.0-beta.0 → 4.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +42 -31
- package/RNTrueSheet.podspec +10 -2
- package/android/build.gradle +2 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +174 -49
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +371 -18
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +14 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +156 -21
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +14 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +28 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +84 -24
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +5 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekView.kt +72 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekViewManager.kt +33 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +526 -231
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +1224 -603
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +136 -29
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +63 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +36 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +255 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +303 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +277 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +162 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +180 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +207 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +220 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetDragEvents.kt +71 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetFocusEvents.kt +65 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +112 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetStateEvents.kt +56 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +84 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +93 -67
- package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +30 -0
- package/android/src/main/jni/CMakeLists.txt +63 -0
- package/android/src/main/jni/TrueSheetSpec.h +19 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +37 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +56 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +61 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +45 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +23 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +52 -0
- package/ios/TrueSheetContainerView.h +81 -6
- package/ios/TrueSheetContainerView.mm +275 -21
- package/ios/TrueSheetContentView.h +38 -1
- package/ios/TrueSheetContentView.mm +413 -43
- package/ios/TrueSheetFooterView.h +27 -2
- package/ios/TrueSheetFooterView.mm +141 -35
- package/ios/{events/OnDidDismissEvent.h → TrueSheetHeaderView.h} +10 -7
- package/ios/TrueSheetHeaderView.mm +64 -0
- package/ios/TrueSheetModule.mm +92 -5
- package/ios/TrueSheetPeekView.h +32 -0
- package/ios/TrueSheetPeekView.mm +99 -0
- package/ios/TrueSheetView.h +7 -15
- package/ios/TrueSheetView.mm +633 -215
- package/ios/TrueSheetViewController.h +84 -16
- package/ios/TrueSheetViewController.mm +1133 -333
- package/ios/core/RNScreensEventObserver.h +46 -0
- package/ios/core/RNScreensEventObserver.mm +280 -0
- package/ios/core/TrueSheetBlurView.h +27 -0
- package/ios/core/TrueSheetBlurView.mm +82 -0
- package/ios/core/TrueSheetDetentCalculator.h +107 -0
- package/ios/core/TrueSheetDetentCalculator.mm +197 -0
- package/ios/core/TrueSheetGrabberView.h +72 -0
- package/ios/core/TrueSheetGrabberView.mm +178 -0
- package/ios/core/TrueSheetKeyboardObserver.h +39 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +112 -0
- package/ios/events/TrueSheetDragEvents.h +39 -0
- package/ios/events/TrueSheetDragEvents.mm +62 -0
- package/ios/events/{OnPositionChangeEvent.h → TrueSheetFocusEvents.h} +8 -5
- package/ios/events/TrueSheetFocusEvents.mm +49 -0
- package/ios/events/TrueSheetLifecycleEvents.h +40 -0
- package/ios/events/TrueSheetLifecycleEvents.mm +71 -0
- package/ios/events/TrueSheetStateEvents.h +35 -0
- package/ios/events/TrueSheetStateEvents.mm +49 -0
- package/ios/tvos/TrueSheetStubs.mm +154 -0
- package/ios/{events/OnMountEvent.h → utils/BlurUtil.h} +5 -7
- package/ios/utils/BlurUtil.mm +69 -0
- package/ios/utils/GestureUtil.h +7 -0
- package/ios/utils/GestureUtil.mm +12 -0
- package/ios/utils/PlatformUtil.h +15 -0
- package/ios/utils/UIView+FirstResponder.h +15 -0
- package/ios/utils/UIView+FirstResponder.mm +26 -0
- package/ios/{events/OnWillDismissEvent.h → utils/UIView+ScrollEdgeInteraction.h} +6 -7
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +62 -0
- package/ios/utils/WindowUtil.mm +17 -1
- package/lib/module/TrueSheet.js +230 -64
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +1203 -0
- package/lib/module/TrueSheet.web.js.map +1 -0
- package/lib/module/TrueSheetPeek.js +15 -0
- package/lib/module/TrueSheetPeek.js.map +1 -0
- package/lib/module/TrueSheetPeek.web.js +55 -0
- package/lib/module/TrueSheetPeek.web.js.map +1 -0
- package/lib/module/TrueSheetProvider.js +28 -0
- package/lib/module/TrueSheetProvider.js.map +1 -0
- package/lib/module/TrueSheetProvider.web.js +157 -0
- package/lib/module/TrueSheetProvider.web.js.map +1 -0
- package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +105 -18
- package/lib/module/index.js +3 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/mocks/index.js +93 -0
- package/lib/module/mocks/index.js.map +1 -0
- package/lib/module/mocks/navigation.js +95 -0
- package/lib/module/mocks/navigation.js.map +1 -0
- package/lib/module/mocks/navigationExpoRouter.js +21 -0
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
- package/lib/module/mocks/reanimated.js +91 -0
- package/lib/module/mocks/reanimated.js.map +1 -0
- package/lib/module/navigation/TrueSheetNavigatorContent.js +73 -0
- package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
- package/lib/module/navigation/actions.js +50 -0
- package/lib/module/navigation/actions.js.map +1 -0
- package/lib/module/navigation/createTrueSheetRouter.js +166 -0
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +48 -0
- package/lib/module/navigation/expo-router/index.js.map +1 -0
- package/lib/module/navigation/index.js +47 -0
- package/lib/module/navigation/index.js.map +1 -0
- package/lib/module/navigation/navigator.js +10 -0
- package/lib/module/navigation/navigator.js.map +1 -0
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +56 -0
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -0
- package/lib/module/navigation/screen/TrueSheetScreen.js +49 -0
- package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -0
- package/lib/module/navigation/screen/index.js +5 -0
- package/lib/module/navigation/screen/index.js.map +1 -0
- package/lib/module/navigation/screen/types.js +4 -0
- package/lib/module/navigation/screen/types.js.map +1 -0
- package/lib/module/navigation/screen/useSheetScreenState.js +68 -0
- package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -0
- package/lib/module/navigation/types.js +4 -0
- package/lib/module/navigation/types.js.map +1 -0
- package/lib/module/navigation/useTrueSheetNavigation.js +24 -0
- package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -0
- package/lib/module/reanimated/ReanimatedTrueSheet.js +15 -9
- package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js +81 -0
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -0
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +4 -2
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -1
- package/lib/module/reanimated/index.js +2 -2
- package/lib/module/reanimated/index.js.map +1 -1
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js +20 -0
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js.map +1 -0
- package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
- package/lib/module/web/constants.js +16 -0
- package/lib/module/web/constants.js.map +1 -0
- package/lib/module/web/dom.js +10 -0
- package/lib/module/web/dom.js.map +1 -0
- package/lib/module/web/vaul/README.md +9 -0
- package/lib/module/web/vaul/browser.js +30 -0
- package/lib/module/web/vaul/browser.js.map +1 -0
- package/lib/module/web/vaul/constants.js +15 -0
- package/lib/module/web/vaul/constants.js.map +1 -0
- package/lib/module/web/vaul/context.js +59 -0
- package/lib/module/web/vaul/context.js.map +1 -0
- package/lib/module/web/vaul/helpers.js +99 -0
- package/lib/module/web/vaul/helpers.js.map +1 -0
- package/lib/module/web/vaul/index.js +1309 -0
- package/lib/module/web/vaul/index.js.map +1 -0
- package/lib/module/web/vaul/style.css +287 -0
- package/lib/module/web/vaul/style.css.d.js +2 -0
- package/lib/module/web/vaul/style.css.d.js.map +1 -0
- package/lib/module/web/vaul/types.js +2 -0
- package/lib/module/web/vaul/types.js.map +1 -0
- package/lib/module/web/vaul/use-composed-refs.js +34 -0
- package/lib/module/web/vaul/use-composed-refs.js.map +1 -0
- package/lib/module/web/vaul/use-controllable-state.js +54 -0
- package/lib/module/web/vaul/use-controllable-state.js.map +1 -0
- package/lib/module/web/vaul/use-position-fixed.js +123 -0
- package/lib/module/web/vaul/use-position-fixed.js.map +1 -0
- package/lib/module/web/vaul/use-prevent-scroll.js +258 -0
- package/lib/module/web/vaul/use-prevent-scroll.js.map +1 -0
- package/lib/module/web/vaul/use-scale-background.js +57 -0
- package/lib/module/web/vaul/use-scale-background.js.map +1 -0
- package/lib/module/web/vaul/use-snap-points.js +305 -0
- package/lib/module/web/vaul/use-snap-points.js.map +1 -0
- package/lib/typescript/src/TrueSheet.d.ts +51 -14
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +463 -54
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts +5 -0
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.d.ts +9 -0
- package/lib/typescript/src/TrueSheetPeek.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts +28 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetProvider.d.ts +18 -0
- package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +27 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +67 -16
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +43 -0
- package/lib/typescript/src/mocks/index.d.ts.map +1 -0
- package/lib/typescript/src/mocks/navigation.d.ts +70 -0
- package/lib/typescript/src/mocks/navigation.d.ts.map +1 -0
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
- package/lib/typescript/src/mocks/reanimated.d.ts +48 -0
- package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -0
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
- package/lib/typescript/src/navigation/actions.d.ts +71 -0
- package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +14 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +46 -0
- package/lib/typescript/src/navigation/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/navigator.d.ts +7 -0
- package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +3 -0
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +3 -0
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/index.d.ts +4 -0
- package/lib/typescript/src/navigation/screen/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/types.d.ts +14 -0
- package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +33 -0
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -0
- package/lib/typescript/src/navigation/types.d.ts +177 -0
- package/lib/typescript/src/navigation/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +23 -0
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts +41 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +8 -2
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts +15 -0
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +25 -3
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
- package/lib/typescript/src/web/constants.d.ts +14 -0
- package/lib/typescript/src/web/constants.d.ts.map +1 -0
- package/lib/typescript/src/web/dom.d.ts +7 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/browser.d.ts +8 -0
- package/lib/typescript/src/web/vaul/browser.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts +13 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/context.d.ts +53 -0
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts +17 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/index.d.ts +228 -0
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/types.d.ts +7 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-composed-refs.d.ts +14 -0
- package/lib/typescript/src/web/vaul/use-composed-refs.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-controllable-state.d.ts +9 -0
- package/lib/typescript/src/web/vaul/use-controllable-state.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-position-fixed.d.ts +19 -0
- package/lib/typescript/src/web/vaul/use-position-fixed.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts +18 -0
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-scale-background.d.ts +2 -0
- package/lib/typescript/src/web/vaul/use-scale-background.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +38 -0
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -0
- package/package.json +95 -36
- package/react-native.config.js +9 -2
- package/src/TrueSheet.tsx +281 -62
- package/src/TrueSheet.types.ts +525 -71
- package/src/TrueSheet.web.tsx +1362 -0
- package/src/TrueSheetPeek.tsx +11 -0
- package/src/TrueSheetPeek.web.tsx +64 -0
- package/src/TrueSheetProvider.tsx +33 -0
- package/src/TrueSheetProvider.web.tsx +220 -0
- package/src/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +105 -18
- package/src/index.ts +2 -2
- package/src/mocks/index.ts +89 -0
- package/src/mocks/navigation.ts +79 -0
- package/src/mocks/navigationExpoRouter.ts +40 -0
- package/src/mocks/reanimated.ts +100 -0
- package/src/navigation/TrueSheetNavigatorContent.tsx +106 -0
- package/src/navigation/actions.ts +78 -0
- package/src/navigation/createTrueSheetRouter.ts +222 -0
- package/src/navigation/expo-router/index.ts +89 -0
- package/src/navigation/index.ts +81 -0
- package/src/navigation/navigator.ts +18 -0
- package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +61 -0
- package/src/navigation/screen/TrueSheetScreen.tsx +54 -0
- package/src/navigation/screen/index.ts +3 -0
- package/src/navigation/screen/types.ts +19 -0
- package/src/navigation/screen/useSheetScreenState.ts +100 -0
- package/src/navigation/types.ts +251 -0
- package/src/navigation/useTrueSheetNavigation.ts +26 -0
- package/src/reanimated/ReanimatedTrueSheet.tsx +12 -7
- package/src/reanimated/ReanimatedTrueSheet.web.tsx +75 -0
- package/src/reanimated/ReanimatedTrueSheetProvider.tsx +12 -8
- package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +31 -0
- package/src/specs/NativeTrueSheetModule.ts +28 -3
- package/src/web/constants.ts +13 -0
- package/src/web/dom.ts +9 -0
- package/src/web/vaul/README.md +9 -0
- package/src/web/vaul/browser.ts +38 -0
- package/src/web/vaul/constants.ts +20 -0
- package/src/web/vaul/context.ts +101 -0
- package/src/web/vaul/helpers.ts +131 -0
- package/src/web/vaul/index.tsx +1740 -0
- package/src/web/vaul/style.css +287 -0
- package/src/web/vaul/style.css.d.ts +1 -0
- package/src/web/vaul/types.ts +7 -0
- package/src/web/vaul/use-composed-refs.ts +35 -0
- package/src/web/vaul/use-controllable-state.ts +66 -0
- package/src/web/vaul/use-position-fixed.ts +147 -0
- package/src/web/vaul/use-prevent-scroll.ts +309 -0
- package/src/web/vaul/use-scale-background.ts +64 -0
- package/src/web/vaul/use-snap-points.ts +455 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +0 -32
- package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +0 -27
- package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +0 -26
- package/android/src/main/res/values/styles.xml +0 -8
- package/ios/events/OnDetentChangeEvent.h +0 -28
- package/ios/events/OnDetentChangeEvent.mm +0 -30
- package/ios/events/OnDidDismissEvent.mm +0 -25
- package/ios/events/OnDidPresentEvent.h +0 -28
- package/ios/events/OnDidPresentEvent.mm +0 -30
- package/ios/events/OnDragBeginEvent.h +0 -28
- package/ios/events/OnDragBeginEvent.mm +0 -30
- package/ios/events/OnDragChangeEvent.h +0 -28
- package/ios/events/OnDragChangeEvent.mm +0 -30
- package/ios/events/OnDragEndEvent.h +0 -28
- package/ios/events/OnDragEndEvent.mm +0 -30
- package/ios/events/OnMountEvent.mm +0 -25
- package/ios/events/OnPositionChangeEvent.mm +0 -32
- package/ios/events/OnSizeChangeEvent.h +0 -28
- package/ios/events/OnSizeChangeEvent.mm +0 -30
- package/ios/events/OnWillDismissEvent.mm +0 -25
- package/ios/events/OnWillPresentEvent.h +0 -28
- package/ios/events/OnWillPresentEvent.mm +0 -30
- package/ios/utils/LayoutUtil.h +0 -44
- package/ios/utils/LayoutUtil.mm +0 -50
- package/lib/module/TrueSheetGrabber.js +0 -51
- package/lib/module/TrueSheetGrabber.js.map +0 -1
- package/lib/typescript/src/TrueSheetGrabber.d.ts +0 -39
- package/lib/typescript/src/TrueSheetGrabber.d.ts.map +0 -1
- package/src/TrueSheetGrabber.tsx +0 -82
- package/src/__mocks__/index.js +0 -67
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
-
import android.graphics.
|
|
5
|
-
import android.
|
|
6
|
-
import android.graphics.drawable.shapes.RoundRectShape
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.os.Build
|
|
7
6
|
import android.view.MotionEvent
|
|
8
7
|
import android.view.View
|
|
9
|
-
import android.view.
|
|
8
|
+
import android.view.ViewGroup
|
|
9
|
+
import android.view.accessibility.AccessibilityEvent
|
|
10
10
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
11
|
-
import android.widget.
|
|
11
|
+
import android.widget.ImageView
|
|
12
|
+
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
13
|
+
import androidx.core.graphics.createBitmap
|
|
12
14
|
import androidx.core.view.isNotEmpty
|
|
13
15
|
import com.facebook.react.R
|
|
14
|
-
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
15
16
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
16
17
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
17
18
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
@@ -19,837 +20,1457 @@ import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
|
19
20
|
import com.facebook.react.uimanager.RootView
|
|
20
21
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
21
22
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
23
|
+
import com.facebook.react.util.RNLog
|
|
22
24
|
import com.facebook.react.views.view.ReactViewGroup
|
|
23
25
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
24
|
-
import com.
|
|
26
|
+
import com.lodev09.truesheet.core.AccessibilityOptions
|
|
27
|
+
import com.lodev09.truesheet.core.GrabberOptions
|
|
28
|
+
import com.lodev09.truesheet.core.TrueSheetBottomSheetBehavior
|
|
29
|
+
import com.lodev09.truesheet.core.TrueSheetBottomSheetView
|
|
30
|
+
import com.lodev09.truesheet.core.TrueSheetBottomSheetViewDelegate
|
|
31
|
+
import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
|
|
32
|
+
import com.lodev09.truesheet.core.TrueSheetCoordinatorLayoutDelegate
|
|
33
|
+
import com.lodev09.truesheet.core.TrueSheetDetentCalculator
|
|
34
|
+
import com.lodev09.truesheet.core.TrueSheetDetentCalculatorDelegate
|
|
35
|
+
import com.lodev09.truesheet.core.TrueSheetDimView
|
|
36
|
+
import com.lodev09.truesheet.core.TrueSheetDimViewDelegate
|
|
37
|
+
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
38
|
+
import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
39
|
+
import com.lodev09.truesheet.core.TrueSheetStackManager
|
|
40
|
+
import com.lodev09.truesheet.utils.KeyboardUtils
|
|
25
41
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
42
|
+
import com.lodev09.truesheet.utils.TouchEventDeduper
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// MARK: - Data Types & Delegate Protocol
|
|
46
|
+
// =============================================================================
|
|
26
47
|
|
|
27
48
|
data class DetentInfo(val index: Int, val position: Float)
|
|
28
49
|
|
|
29
|
-
/**
|
|
30
|
-
* Delegate protocol for TrueSheetViewController lifecycle and interaction events.
|
|
31
|
-
* Similar to iOS TrueSheetViewControllerDelegate pattern.
|
|
32
|
-
*/
|
|
33
50
|
interface TrueSheetViewControllerDelegate {
|
|
34
|
-
|
|
35
|
-
fun
|
|
51
|
+
val eventDispatcher: EventDispatcher?
|
|
52
|
+
fun findRootContainerView(): ViewGroup?
|
|
53
|
+
fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
|
|
54
|
+
fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
|
|
36
55
|
fun viewControllerWillDismiss()
|
|
37
|
-
fun viewControllerDidDismiss()
|
|
38
|
-
fun viewControllerDidChangeDetent(index: Int, position: Float)
|
|
39
|
-
fun viewControllerDidDragBegin(index: Int, position: Float)
|
|
40
|
-
fun viewControllerDidDragChange(index: Int, position: Float)
|
|
41
|
-
fun viewControllerDidDragEnd(index: Int, position: Float)
|
|
42
|
-
fun viewControllerDidChangePosition(index:
|
|
56
|
+
fun viewControllerDidDismiss(parent: TrueSheetView?)
|
|
57
|
+
fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float)
|
|
58
|
+
fun viewControllerDidDragBegin(index: Int, position: Float, detent: Float)
|
|
59
|
+
fun viewControllerDidDragChange(index: Int, position: Float, detent: Float)
|
|
60
|
+
fun viewControllerDidDragEnd(index: Int, position: Float, detent: Float)
|
|
61
|
+
fun viewControllerDidChangePosition(index: Float, position: Float, detent: Float, realtime: Boolean)
|
|
43
62
|
fun viewControllerDidChangeSize(width: Int, height: Int)
|
|
63
|
+
fun viewControllerWillFocus()
|
|
64
|
+
fun viewControllerDidFocus()
|
|
65
|
+
fun viewControllerWillBlur()
|
|
66
|
+
fun viewControllerDidBlur()
|
|
67
|
+
fun viewControllerDidChangeVisibility(visible: Boolean)
|
|
44
68
|
}
|
|
45
69
|
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// MARK: - TrueSheetViewController
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
46
74
|
/**
|
|
47
|
-
*
|
|
48
|
-
* Similar to iOS TrueSheetViewController pattern.
|
|
75
|
+
* Controls the presentation and behavior of a bottom sheet.
|
|
49
76
|
*
|
|
50
|
-
*
|
|
77
|
+
* Uses CoordinatorLayout with BottomSheetBehavior to manage the sheet within the activity window,
|
|
78
|
+
* enabling touch pass-through to underlying views. Handles detent configuration, drag interactions,
|
|
79
|
+
* keyboard avoidance, dimmed backgrounds, back button, and lifecycle events for stacked sheets.
|
|
51
80
|
*/
|
|
81
|
+
enum class TrueSheetAnchor {
|
|
82
|
+
LEFT,
|
|
83
|
+
CENTER,
|
|
84
|
+
RIGHT;
|
|
85
|
+
|
|
86
|
+
companion object {
|
|
87
|
+
fun fromString(value: String?): TrueSheetAnchor =
|
|
88
|
+
when (value) {
|
|
89
|
+
"left" -> LEFT
|
|
90
|
+
"right" -> RIGHT
|
|
91
|
+
else -> CENTER
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
enum class TrueSheetInsetAdjustment {
|
|
97
|
+
AUTOMATIC,
|
|
98
|
+
NEVER;
|
|
99
|
+
|
|
100
|
+
companion object {
|
|
101
|
+
fun fromString(value: String?): TrueSheetInsetAdjustment =
|
|
102
|
+
when (value) {
|
|
103
|
+
"never" -> NEVER
|
|
104
|
+
else -> AUTOMATIC
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
52
109
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
53
110
|
class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
54
111
|
ReactViewGroup(reactContext),
|
|
55
|
-
RootView
|
|
112
|
+
RootView,
|
|
113
|
+
TrueSheetDetentCalculatorDelegate,
|
|
114
|
+
TrueSheetDimViewDelegate,
|
|
115
|
+
TrueSheetCoordinatorLayoutDelegate,
|
|
116
|
+
TrueSheetBottomSheetViewDelegate {
|
|
56
117
|
|
|
57
118
|
companion object {
|
|
58
|
-
private const val
|
|
119
|
+
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
120
|
+
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
121
|
+
private const val DEFAULT_ANCHOR_OFFSET = 16 // dp
|
|
122
|
+
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
123
|
+
private const val DISMISS_DURATION = 200L
|
|
124
|
+
private const val SCREEN_FADE_DURATION = 150L
|
|
59
125
|
}
|
|
60
126
|
|
|
61
|
-
//
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// MARK: - Types
|
|
129
|
+
// =============================================================================
|
|
62
130
|
|
|
63
|
-
|
|
131
|
+
private sealed class InteractionState {
|
|
132
|
+
data object Idle : InteractionState()
|
|
133
|
+
data class Dragging(val startTop: Int) : InteractionState()
|
|
134
|
+
data object Reconfiguring : InteractionState()
|
|
135
|
+
}
|
|
64
136
|
|
|
65
|
-
|
|
66
|
-
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// MARK: - Properties
|
|
139
|
+
// =============================================================================
|
|
67
140
|
|
|
68
|
-
/**
|
|
69
|
-
* Delegate for handling view controller events
|
|
70
|
-
*/
|
|
71
141
|
var delegate: TrueSheetViewControllerDelegate? = null
|
|
72
142
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
private var
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The sheet behavior from the dialog
|
|
80
|
-
*/
|
|
81
|
-
private val behavior: BottomSheetBehavior<FrameLayout>?
|
|
82
|
-
get() = dialog?.behavior
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The sheet container view from Material BottomSheetDialog (our parent)
|
|
86
|
-
*/
|
|
87
|
-
private val sheetContainer: FrameLayout?
|
|
88
|
-
get() = this.parent as? FrameLayout
|
|
143
|
+
// CoordinatorLayout components (replaces DialogFragment)
|
|
144
|
+
internal var sheetView: TrueSheetBottomSheetView? = null
|
|
145
|
+
internal var coordinatorLayout: TrueSheetCoordinatorLayout? = null
|
|
146
|
+
private var dimView: TrueSheetDimView? = null
|
|
147
|
+
private var parentDimView: TrueSheetDimView? = null
|
|
89
148
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*/
|
|
94
|
-
private val bottomSheetView: FrameLayout?
|
|
95
|
-
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
149
|
+
// Presentation State
|
|
150
|
+
var isPresented = false
|
|
151
|
+
private set
|
|
96
152
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
*/
|
|
100
|
-
private val containerView: TrueSheetContainerView?
|
|
101
|
-
get() = if (this.isNotEmpty()) {
|
|
102
|
-
this.getChildAt(0) as? TrueSheetContainerView
|
|
103
|
-
} else {
|
|
104
|
-
null
|
|
105
|
-
}
|
|
153
|
+
var isSheetVisible = false
|
|
154
|
+
private set
|
|
106
155
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
*/
|
|
110
|
-
private val footerView: TrueSheetFooterView?
|
|
111
|
-
get() = containerView?.footerView
|
|
156
|
+
var currentDetentIndex: Int = -1
|
|
157
|
+
private set
|
|
112
158
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
private var
|
|
159
|
+
// Target detent index during an in-flight resize animation. -1 when idle.
|
|
160
|
+
// Used by setupSheetDetents so a layout-driven reconfigure during the
|
|
161
|
+
// animation doesn't snap the sheet back to the stale currentDetentIndex.
|
|
162
|
+
private var pendingDetentIndex: Int = -1
|
|
117
163
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*/
|
|
121
|
-
var isPresented = false
|
|
164
|
+
private var interactionState: InteractionState = InteractionState.Idle
|
|
165
|
+
internal var isBeingDismissed = false
|
|
122
166
|
private set
|
|
167
|
+
var wasHiddenByScreen = false
|
|
168
|
+
private var shouldAnimatePresent = false
|
|
169
|
+
private var isPresentAnimating = false
|
|
123
170
|
|
|
124
|
-
private
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
128
|
-
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
129
|
-
}
|
|
171
|
+
private var lastStateWidth: Int = 0
|
|
172
|
+
private var lastStateHeight: Int = 0
|
|
173
|
+
private var lastEmittedPositionPx: Int = -1
|
|
130
174
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
var
|
|
175
|
+
// Keyboard State
|
|
176
|
+
private var detentIndexBeforeKeyboard: Int = -1
|
|
177
|
+
private var isKeyboardDismissProgrammatic = false
|
|
178
|
+
private var focusedViewBeforeBlur: View? = null
|
|
135
179
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) ScreenUtils.getStatusBarHeight(reactContext) else 0
|
|
180
|
+
// Promises
|
|
181
|
+
var presentPromise: (() -> Unit)? = null
|
|
182
|
+
var resizePromise: (() -> Unit)? = null
|
|
183
|
+
var dismissPromise: (() -> Unit)? = null
|
|
141
184
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*/
|
|
145
|
-
var currentDetentIndex: Int = -1
|
|
146
|
-
private set
|
|
185
|
+
// For stacked sheets
|
|
186
|
+
var parentSheetView: TrueSheetView? = null
|
|
147
187
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
188
|
+
// Helper Objects
|
|
189
|
+
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
190
|
+
internal val detentCalculator = TrueSheetDetentCalculator(reactContext).apply {
|
|
191
|
+
delegate = this@TrueSheetViewController
|
|
192
|
+
}
|
|
152
193
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
var dismissPromise: (() -> Unit)? = null
|
|
194
|
+
// Touch Dispatchers
|
|
195
|
+
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
196
|
+
private val jsPointerDispatcher = JSPointerDispatcher(this)
|
|
157
197
|
|
|
158
|
-
//
|
|
198
|
+
// True when the active touch stream began inside the footer. Latched on
|
|
199
|
+
// ACTION_DOWN so subsequent MOVE events keep routing to the footer even if
|
|
200
|
+
// the finger drifts outside the footer's rect mid-gesture.
|
|
201
|
+
private var footerOwnsTouchStream = false
|
|
159
202
|
|
|
160
|
-
|
|
161
|
-
* Specify whether the sheet background is dimmed.
|
|
162
|
-
* Set to `false` to allow interaction with the background components.
|
|
163
|
-
*/
|
|
164
|
-
var dimmed = true
|
|
203
|
+
private val touchDeduper = TouchEventDeduper()
|
|
165
204
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
* This is ignored if `dimmed` is set to `false`.
|
|
169
|
-
*/
|
|
170
|
-
var dimmedIndex = 0
|
|
205
|
+
private val eventDispatcher
|
|
206
|
+
get() = delegate?.eventDispatcher
|
|
171
207
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
var
|
|
208
|
+
// Detent Configuration
|
|
209
|
+
override var maxContentHeight: Int? = null
|
|
210
|
+
override var maxContentWidth: Int? = null
|
|
211
|
+
override var anchor: TrueSheetAnchor = TrueSheetAnchor.CENTER
|
|
212
|
+
override var anchorOffset: Int = DEFAULT_ANCHOR_OFFSET.dpToPx().toInt()
|
|
213
|
+
override var detents: MutableList<Double> = mutableListOf(0.5, 1.0)
|
|
176
214
|
|
|
177
|
-
|
|
215
|
+
// Appearance Configuration
|
|
216
|
+
var dimmed = true
|
|
217
|
+
var dimmedDetentIndex = 0
|
|
218
|
+
override var grabber: Boolean = true
|
|
219
|
+
override var grabberOptions: GrabberOptions? = null
|
|
220
|
+
override var accessibilityOptions: AccessibilityOptions? = null
|
|
221
|
+
override var sheetBackgroundColor: Int? = null
|
|
222
|
+
var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
|
|
223
|
+
|
|
224
|
+
var scrollableOptions: ScrollableOptions? = null
|
|
225
|
+
set(value) {
|
|
226
|
+
field = value
|
|
227
|
+
behavior?.scrollingExpandsSheet = value?.scrollingExpandsSheet ?: true
|
|
228
|
+
}
|
|
178
229
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
230
|
+
override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
231
|
+
set(value) {
|
|
232
|
+
field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
|
|
233
|
+
if (isPresented) sheetView?.setupBackground()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
override var sheetElevation: Float = -1f
|
|
237
|
+
set(value) {
|
|
238
|
+
field = value
|
|
239
|
+
if (isPresented) sheetView?.setupElevation()
|
|
240
|
+
}
|
|
184
241
|
|
|
185
242
|
var dismissible: Boolean = true
|
|
186
243
|
set(value) {
|
|
187
244
|
field = value
|
|
188
|
-
|
|
189
|
-
setCanceledOnTouchOutside(value)
|
|
190
|
-
setCancelable(value)
|
|
191
|
-
behavior.isHideable = value
|
|
192
|
-
}
|
|
245
|
+
behavior?.isHideable = value
|
|
193
246
|
}
|
|
194
247
|
|
|
195
|
-
var
|
|
196
|
-
|
|
197
|
-
|
|
248
|
+
override var draggable: Boolean = true
|
|
249
|
+
set(value) {
|
|
250
|
+
field = value
|
|
251
|
+
behavior?.isDraggable = value
|
|
252
|
+
if (isPresented) sheetView?.setupGrabber()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// =============================================================================
|
|
256
|
+
// MARK: - Computed Properties
|
|
257
|
+
// =============================================================================
|
|
258
|
+
|
|
259
|
+
// Behavior
|
|
260
|
+
private val behavior: TrueSheetBottomSheetBehavior<TrueSheetBottomSheetView>?
|
|
261
|
+
get() = sheetView?.behavior
|
|
262
|
+
|
|
263
|
+
internal val containerView: TrueSheetContainerView?
|
|
264
|
+
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
265
|
+
|
|
266
|
+
// Screen Measurements
|
|
267
|
+
override val screenHeight: Int
|
|
268
|
+
get() = ScreenUtils.getScreenHeight(reactContext)
|
|
269
|
+
|
|
270
|
+
val screenWidth: Int
|
|
271
|
+
get() = ScreenUtils.getScreenWidth(reactContext)
|
|
272
|
+
|
|
273
|
+
// Includes system bars for accurate positioning
|
|
274
|
+
override val realScreenHeight: Int
|
|
275
|
+
get() = ScreenUtils.getRealScreenHeight(reactContext)
|
|
276
|
+
|
|
277
|
+
// Content Measurements
|
|
278
|
+
// Cached values used during dismiss when container is unmounted
|
|
279
|
+
private var cachedContentHeight: Int = 0
|
|
280
|
+
private var cachedHeaderHeight: Int = 0
|
|
281
|
+
private var cachedFooterHeight: Int = 0
|
|
282
|
+
private var cachedPeekContentHeight: Int = 0
|
|
283
|
+
|
|
284
|
+
override val contentHeight: Int
|
|
285
|
+
get() = containerView?.contentHeight ?: cachedContentHeight
|
|
286
|
+
|
|
287
|
+
override val headerHeight: Int
|
|
288
|
+
get() = containerView?.headerHeight ?: cachedHeaderHeight
|
|
289
|
+
|
|
290
|
+
override var absoluteHeader: Boolean = false
|
|
291
|
+
|
|
292
|
+
override val footerHeight: Int
|
|
293
|
+
get() = containerView?.footerHeight ?: cachedFooterHeight
|
|
294
|
+
|
|
295
|
+
override var absoluteFooter: Boolean = false
|
|
296
|
+
|
|
297
|
+
override val peekContentHeight: Int
|
|
298
|
+
get() = containerView?.peekContentHeight ?: cachedPeekContentHeight
|
|
299
|
+
|
|
300
|
+
// Insets
|
|
301
|
+
// Target keyboard height used for detent calculations
|
|
302
|
+
override val keyboardInset: Int
|
|
303
|
+
get() = keyboardObserver?.targetHeight ?: 0
|
|
304
|
+
|
|
305
|
+
// Current animated keyboard height for positioning
|
|
306
|
+
private val currentKeyboardInset: Int
|
|
307
|
+
get() = keyboardObserver?.currentHeight ?: 0
|
|
198
308
|
|
|
199
|
-
private
|
|
309
|
+
private val isKeyboardTransitioning: Boolean
|
|
310
|
+
get() = keyboardObserver?.isTransitioning ?: false
|
|
200
311
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
312
|
+
fun isFocusedViewWithinSheet(): Boolean {
|
|
313
|
+
val sheet = sheetView ?: return false
|
|
314
|
+
return keyboardObserver?.isFocusedViewWithinSheet(sheet) ?: false
|
|
204
315
|
}
|
|
205
316
|
|
|
206
|
-
|
|
317
|
+
val bottomInset: Int
|
|
318
|
+
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).bottom else 0
|
|
207
319
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
*/
|
|
211
|
-
fun createDialog() {
|
|
212
|
-
if (dialog != null) return
|
|
320
|
+
override val topInset: Int
|
|
321
|
+
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).top else 0
|
|
213
322
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
323
|
+
override val contentBottomInset: Int
|
|
324
|
+
get() = if (insetAdjustment == TrueSheetInsetAdjustment.AUTOMATIC) bottomInset else 0
|
|
325
|
+
|
|
326
|
+
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
|
327
|
+
private val edgeToEdgeEnabled: Boolean
|
|
328
|
+
get() {
|
|
329
|
+
val defaultEnabled = Build.VERSION.SDK_INT >= 36
|
|
330
|
+
return BuildConfig.EDGE_TO_EDGE_ENABLED || defaultEnabled
|
|
218
331
|
}
|
|
219
332
|
|
|
220
|
-
|
|
221
|
-
|
|
333
|
+
// Sheet State
|
|
334
|
+
val isExpanded: Boolean
|
|
335
|
+
get() {
|
|
336
|
+
val sheetTop = sheetView?.top ?: return false
|
|
337
|
+
return sheetTop <= topInset
|
|
338
|
+
}
|
|
222
339
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
340
|
+
val currentTranslationY: Int
|
|
341
|
+
get() = sheetView?.translationY?.toInt() ?: 0
|
|
342
|
+
|
|
343
|
+
override val isTopmostSheet: Boolean
|
|
344
|
+
get() {
|
|
345
|
+
val hostView = delegate as? TrueSheetView ?: return true
|
|
346
|
+
return TrueSheetStackManager.isTopmostSheet(hostView)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private val dimViews: List<TrueSheetDimView>
|
|
350
|
+
get() = listOfNotNull(dimView, parentDimView)
|
|
228
351
|
|
|
229
|
-
|
|
230
|
-
|
|
352
|
+
val isDimmedAtCurrentDetent: Boolean
|
|
353
|
+
get() = isDimmedAtDetentIndex(currentDetentIndex)
|
|
231
354
|
|
|
232
|
-
|
|
233
|
-
setupBottomSheetBehavior(this)
|
|
355
|
+
fun isDimmedAtDetentIndex(index: Int): Boolean = dimmed && index >= dimmedDetentIndex
|
|
234
356
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
behavior.isHideable = dismissible
|
|
357
|
+
// =============================================================================
|
|
358
|
+
// MARK: - Sheet Creation & Cleanup
|
|
359
|
+
// =============================================================================
|
|
239
360
|
|
|
240
|
-
|
|
241
|
-
|
|
361
|
+
fun createSheet() {
|
|
362
|
+
if (coordinatorLayout != null) return
|
|
363
|
+
|
|
364
|
+
// Create coordinator layout
|
|
365
|
+
coordinatorLayout = TrueSheetCoordinatorLayout(reactContext).apply {
|
|
366
|
+
delegate = this@TrueSheetViewController
|
|
242
367
|
}
|
|
243
|
-
}
|
|
244
368
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
*/
|
|
248
|
-
private fun cleanupDialog() {
|
|
249
|
-
dialog?.apply {
|
|
250
|
-
setOnShowListener(null)
|
|
251
|
-
setOnCancelListener(null)
|
|
252
|
-
setOnDismissListener(null)
|
|
369
|
+
sheetView = TrueSheetBottomSheetView(reactContext).apply {
|
|
370
|
+
delegate = this@TrueSheetViewController
|
|
253
371
|
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private fun cleanupSheet() {
|
|
375
|
+
cleanupKeyboardObserver()
|
|
376
|
+
sheetView?.animate()?.cancel()
|
|
377
|
+
|
|
378
|
+
// Cleanup dim views
|
|
379
|
+
dimView?.detach()
|
|
380
|
+
dimView = null
|
|
381
|
+
parentDimView?.detach()
|
|
382
|
+
parentDimView = null
|
|
254
383
|
|
|
255
|
-
//
|
|
256
|
-
|
|
384
|
+
// Cleanup snapshot
|
|
385
|
+
removeSheetSnapshot()
|
|
257
386
|
|
|
258
|
-
|
|
259
|
-
|
|
387
|
+
// Detach content from sheet
|
|
388
|
+
sheetView?.removeView(this)
|
|
389
|
+
|
|
390
|
+
containerView?.cleanupKeyboardHandler()
|
|
391
|
+
coordinatorLayout = null
|
|
392
|
+
sheetView = null
|
|
393
|
+
|
|
394
|
+
interactionState = InteractionState.Idle
|
|
395
|
+
isBeingDismissed = false
|
|
260
396
|
isPresented = false
|
|
397
|
+
isSheetVisible = false
|
|
398
|
+
wasHiddenByScreen = false
|
|
399
|
+
cachedContentHeight = 0
|
|
400
|
+
cachedHeaderHeight = 0
|
|
401
|
+
isPresentAnimating = false
|
|
402
|
+
lastEmittedPositionPx = -1
|
|
403
|
+
detentIndexBeforeKeyboard = -1
|
|
404
|
+
pendingDetentIndex = -1
|
|
405
|
+
isKeyboardDismissProgrammatic = false
|
|
406
|
+
focusedViewBeforeBlur = null
|
|
407
|
+
shouldAnimatePresent = true
|
|
261
408
|
}
|
|
262
409
|
|
|
263
|
-
|
|
264
|
-
* Setup dialog lifecycle listeners
|
|
265
|
-
*/
|
|
266
|
-
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
267
|
-
// Setup listener when the dialog has been presented
|
|
268
|
-
dialog.setOnShowListener {
|
|
269
|
-
isPresented = true
|
|
270
|
-
|
|
271
|
-
// Re-enable animation
|
|
272
|
-
resetAnimation()
|
|
273
|
-
|
|
274
|
-
// Wait for the sheet to settle before notifying didPresent
|
|
275
|
-
// The sheet animates to its final position after onShow fires
|
|
276
|
-
sheetContainer?.post {
|
|
277
|
-
// Notify delegate with the settled position
|
|
278
|
-
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
279
|
-
delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position)
|
|
280
|
-
|
|
281
|
-
// Emit position change with transitioning=true so Reanimated can animate it
|
|
282
|
-
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = true)
|
|
283
|
-
|
|
284
|
-
// Resolve the present promise
|
|
285
|
-
presentPromise?.let { promise ->
|
|
286
|
-
promise()
|
|
287
|
-
presentPromise = null
|
|
288
|
-
}
|
|
410
|
+
private var snapshotView: View? = null
|
|
289
411
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
412
|
+
fun createSheetSnapshot() {
|
|
413
|
+
if (!isPresented) return
|
|
414
|
+
val sheet = sheetView ?: return
|
|
415
|
+
if (sheet.width <= 0 || sheet.height <= 0) return
|
|
294
416
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
delegate?.viewControllerWillDismiss()
|
|
299
|
-
}
|
|
417
|
+
val bitmap = createBitmap(sheet.width, sheet.height)
|
|
418
|
+
val canvas = Canvas(bitmap)
|
|
419
|
+
sheet.draw(canvas)
|
|
300
420
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
promise()
|
|
306
|
-
dismissPromise = null
|
|
307
|
-
}
|
|
421
|
+
val snapshot = ImageView(reactContext).apply {
|
|
422
|
+
setImageBitmap(bitmap)
|
|
423
|
+
layoutParams = LayoutParams(sheet.width, sheet.height)
|
|
424
|
+
}
|
|
308
425
|
|
|
309
|
-
|
|
310
|
-
|
|
426
|
+
sheet.addView(snapshot, 0)
|
|
427
|
+
snapshotView = snapshot
|
|
428
|
+
}
|
|
311
429
|
|
|
312
|
-
|
|
313
|
-
|
|
430
|
+
fun removeSheetSnapshot() {
|
|
431
|
+
snapshotView?.let {
|
|
432
|
+
(it as? ImageView)?.setImageDrawable(null)
|
|
433
|
+
sheetView?.removeView(it)
|
|
314
434
|
}
|
|
435
|
+
snapshotView = null
|
|
315
436
|
}
|
|
316
437
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
private fun setupBottomSheetBehavior(dialog: BottomSheetDialog) {
|
|
321
|
-
dialog.behavior.addBottomSheetCallback(
|
|
322
|
-
object : BottomSheetBehavior.BottomSheetCallback() {
|
|
323
|
-
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
324
|
-
val behavior = behavior ?: return
|
|
325
|
-
|
|
326
|
-
// Emit position change event continuously during slide
|
|
327
|
-
// Set transitioning=false during drag to get real-time position updates
|
|
328
|
-
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
329
|
-
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = false)
|
|
330
|
-
|
|
331
|
-
when (behavior.state) {
|
|
332
|
-
// For consistency with iOS, we consider SETTLING as dragging change
|
|
333
|
-
BottomSheetBehavior.STATE_DRAGGING,
|
|
334
|
-
BottomSheetBehavior.STATE_SETTLING -> handleDragChange(sheetView)
|
|
335
|
-
|
|
336
|
-
else -> { }
|
|
337
|
-
}
|
|
438
|
+
// =============================================================================
|
|
439
|
+
// MARK: - TrueSheetCoordinatorLayout.Delegate
|
|
440
|
+
// =============================================================================
|
|
338
441
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
442
|
+
override fun coordinatorLayoutDidLayout(changed: Boolean) {
|
|
443
|
+
// Reposition footer when layout changes
|
|
444
|
+
if (isPresented && changed) {
|
|
445
|
+
positionFooter()
|
|
446
|
+
}
|
|
447
|
+
}
|
|
342
448
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// This ensures we can dismiss even if dialog state gets out of sync
|
|
346
|
-
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
|
347
|
-
dismiss()
|
|
348
|
-
return
|
|
349
|
-
}
|
|
449
|
+
override fun coordinatorLayoutDidChangeConfiguration() {
|
|
450
|
+
if (!isPresented) return
|
|
350
451
|
|
|
351
|
-
|
|
452
|
+
sheetView?.updateGravity()
|
|
453
|
+
updateBehaviorMaxWidth()
|
|
454
|
+
updateStateDimensions()
|
|
455
|
+
sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
|
|
456
|
+
}
|
|
352
457
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
|
|
458
|
+
override fun findScrollView(): ViewGroup? = containerView?.contentView?.findScrollView()
|
|
459
|
+
override fun findSheetView(): TrueSheetBottomSheetView? = sheetView
|
|
356
460
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
BottomSheetBehavior.STATE_HALF_EXPANDED -> handleDragEnd(newState)
|
|
461
|
+
// =============================================================================
|
|
462
|
+
// MARK: - TrueSheetDimViewDelegate
|
|
463
|
+
// =============================================================================
|
|
361
464
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
)
|
|
367
|
-
}
|
|
465
|
+
override fun dimViewDidTap() {
|
|
466
|
+
val hostView = delegate as? TrueSheetView ?: return
|
|
368
467
|
|
|
369
|
-
|
|
468
|
+
val children = TrueSheetStackManager.getSheetsAbove(hostView)
|
|
469
|
+
val topmostChild = children.firstOrNull()?.viewController
|
|
370
470
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Dialog not created yet - this shouldn't happen but handle gracefully
|
|
471
|
+
// If topmost child is dimmed, only handle that child
|
|
472
|
+
if (topmostChild?.isDimmedAtCurrentDetent == true) {
|
|
473
|
+
if (topmostChild.dismissible) {
|
|
474
|
+
topmostChild.dismiss(animated = true)
|
|
475
|
+
}
|
|
377
476
|
return
|
|
378
477
|
}
|
|
379
478
|
|
|
380
|
-
|
|
381
|
-
|
|
479
|
+
// Pass through to parent - dismiss all if possible
|
|
480
|
+
val allDismissible = dismissible && children.all { it.viewController.dismissible }
|
|
481
|
+
if (allDismissible) {
|
|
482
|
+
children.forEach { it.viewController.dismiss(animated = true) }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (dismissible) {
|
|
486
|
+
dismiss(animated = true)
|
|
487
|
+
} else if (parentSheetView == null && isDimmedAtCurrentDetent && dimmedDetentIndex > 0) {
|
|
488
|
+
setStateForDetentIndex(dimmedDetentIndex - 1)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
382
491
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
387
|
-
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position)
|
|
492
|
+
// =============================================================================
|
|
493
|
+
// MARK: - TrueSheetBottomSheetViewDelegate - Grabber Tap
|
|
494
|
+
// =============================================================================
|
|
388
495
|
|
|
389
|
-
|
|
390
|
-
|
|
496
|
+
override fun bottomSheetViewDidTapGrabber() {
|
|
497
|
+
val nextIndex = (currentDetentIndex + 1) % detents.size
|
|
498
|
+
if (nextIndex == 0 && detents.size == 1 && dismissible) {
|
|
499
|
+
dismiss(animated = true)
|
|
391
500
|
} else {
|
|
392
|
-
|
|
393
|
-
|
|
501
|
+
setStateForDetentIndex(nextIndex)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
394
504
|
|
|
395
|
-
|
|
396
|
-
|
|
505
|
+
override fun bottomSheetViewDidAccessibilityIncrement() {
|
|
506
|
+
if (currentDetentIndex < detents.size - 1) {
|
|
507
|
+
setStateForDetentIndex(currentDetentIndex + 1)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
397
510
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
511
|
+
override fun bottomSheetViewDidAccessibilityDecrement() {
|
|
512
|
+
if (currentDetentIndex > 0) {
|
|
513
|
+
setStateForDetentIndex(currentDetentIndex - 1)
|
|
514
|
+
} else if (dismissible) {
|
|
515
|
+
dismiss(animated = true)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
401
518
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
519
|
+
// =============================================================================
|
|
520
|
+
// MARK: - BottomSheetCallback
|
|
521
|
+
// =============================================================================
|
|
522
|
+
|
|
523
|
+
private val sheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
|
524
|
+
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
525
|
+
handleStateChanged(sheetView, newState)
|
|
526
|
+
}
|
|
406
527
|
|
|
407
|
-
|
|
528
|
+
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
529
|
+
handleSlide(sheetView, slideOffset)
|
|
408
530
|
}
|
|
409
531
|
}
|
|
410
532
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
delegate?.viewControllerDidChangePosition(currentDetentIndex, offScreenPosition, transitioning = true)
|
|
533
|
+
private fun handleStateChanged(sheetView: View, newState: Int) {
|
|
534
|
+
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
|
535
|
+
if (isBeingDismissed) return
|
|
536
|
+
isBeingDismissed = true
|
|
537
|
+
dismissKeyboard()
|
|
538
|
+
emitWillDismissEvents()
|
|
539
|
+
finishDismiss()
|
|
540
|
+
return
|
|
420
541
|
}
|
|
421
542
|
|
|
422
|
-
|
|
543
|
+
if (!isPresented) return
|
|
544
|
+
|
|
545
|
+
when (newState) {
|
|
546
|
+
BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
|
|
547
|
+
|
|
548
|
+
BottomSheetBehavior.STATE_SETTLING -> handleSettling(sheetView)
|
|
549
|
+
|
|
550
|
+
BottomSheetBehavior.STATE_EXPANDED,
|
|
551
|
+
BottomSheetBehavior.STATE_COLLAPSED,
|
|
552
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED -> handleStateSettled(sheetView, newState)
|
|
553
|
+
|
|
554
|
+
else -> {}
|
|
555
|
+
}
|
|
423
556
|
}
|
|
424
557
|
|
|
425
|
-
|
|
558
|
+
private fun handleSlide(sheetView: View, slideOffset: Float) {
|
|
559
|
+
// Skip during dismiss animation
|
|
560
|
+
if (isBeingDismissed) return
|
|
426
561
|
|
|
427
|
-
|
|
428
|
-
* Setup sheet detents based on the detent preference.
|
|
429
|
-
*/
|
|
430
|
-
fun setupSheetDetents() {
|
|
431
|
-
val behavior = this.behavior ?: return
|
|
562
|
+
val behavior = behavior ?: return
|
|
432
563
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
isFitToContents = true
|
|
564
|
+
when (behavior.state) {
|
|
565
|
+
BottomSheetBehavior.STATE_DRAGGING,
|
|
566
|
+
BottomSheetBehavior.STATE_SETTLING -> handleDragChange(sheetView)
|
|
437
567
|
|
|
438
|
-
|
|
439
|
-
|
|
568
|
+
else -> { }
|
|
569
|
+
}
|
|
440
570
|
|
|
441
|
-
|
|
442
|
-
1 -> {
|
|
443
|
-
val detentHeight = getDetentHeight(detents[0])
|
|
444
|
-
maxHeight = detentHeight
|
|
445
|
-
skipCollapsed = true
|
|
446
|
-
}
|
|
571
|
+
emitChangePositionDelegate(sheetView.top)
|
|
447
572
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
573
|
+
// Position on every slide frame — during keyboard transitions the IME
|
|
574
|
+
// animation callbacks are sparser than the sheet's settle frames, and the
|
|
575
|
+
// footer rides the sheet between them, jittering against the keyboard.
|
|
576
|
+
// positionFooter derives from current state, so double-driving it with
|
|
577
|
+
// keyboardDidChangeHeight is harmless.
|
|
578
|
+
positionFooter(slideOffset)
|
|
579
|
+
|
|
580
|
+
if (!isKeyboardTransitioning) {
|
|
581
|
+
updateDimAmount(sheetView.top)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
454
584
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
isFitToContents = false
|
|
585
|
+
private fun handleStateSettled(sheetView: View, newState: Int) {
|
|
586
|
+
if (interactionState is InteractionState.Reconfiguring) return
|
|
458
587
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
588
|
+
// Sheet has reached a stable state — any in-flight resize target is now stale,
|
|
589
|
+
// whether settled at the target, interrupted by drag, or superseded.
|
|
590
|
+
pendingDetentIndex = -1
|
|
462
591
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
592
|
+
val index = detentCalculator.getDetentIndexForState(newState) ?: return
|
|
593
|
+
val position = getPositionDpForView(sheetView)
|
|
594
|
+
val detentInfo = DetentInfo(index, position)
|
|
595
|
+
|
|
596
|
+
// Handle present animation completion
|
|
597
|
+
if (isPresentAnimating) {
|
|
598
|
+
isPresentAnimating = false
|
|
599
|
+
finishPresent()
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
when (interactionState) {
|
|
604
|
+
is InteractionState.Dragging -> {
|
|
605
|
+
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
606
|
+
delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
|
|
607
|
+
|
|
608
|
+
// Skip detent change if keyboard inset is still active — detent mapping is unreliable.
|
|
609
|
+
// keyboardWillHide will recalculate detents and settle at the correct index.
|
|
610
|
+
if (detentInfo.index != currentDetentIndex && keyboardInset == 0) {
|
|
611
|
+
currentDetentIndex = detentInfo.index
|
|
612
|
+
setupDimmedBackground()
|
|
613
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
614
|
+
this@TrueSheetViewController.sheetView?.updateGrabberAccessibilityValue(detentInfo.index, detents.size)
|
|
467
615
|
}
|
|
616
|
+
|
|
617
|
+
interactionState = InteractionState.Idle
|
|
468
618
|
}
|
|
469
619
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
620
|
+
else -> {
|
|
621
|
+
if (detentInfo.index != currentDetentIndex) {
|
|
622
|
+
currentDetentIndex = detentInfo.index
|
|
623
|
+
if (!isKeyboardTransitioning) {
|
|
624
|
+
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
625
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
626
|
+
}
|
|
627
|
+
this@TrueSheetViewController.sheetView?.updateGrabberAccessibilityValue(detentInfo.index, detents.size)
|
|
477
628
|
}
|
|
478
629
|
}
|
|
479
630
|
}
|
|
631
|
+
|
|
632
|
+
// Settled — resize the container to the settled detent (applies deferred shrinks)
|
|
633
|
+
updateStateDimensions()
|
|
634
|
+
|
|
635
|
+
if (!isKeyboardTransitioning) {
|
|
636
|
+
updateDimAmount(animated = true)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
480
640
|
}
|
|
481
641
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
fun setupBackground() {
|
|
486
|
-
sheetContainer?.apply {
|
|
487
|
-
val outerRadii = floatArrayOf(
|
|
488
|
-
cornerRadius,
|
|
489
|
-
cornerRadius,
|
|
490
|
-
cornerRadius,
|
|
491
|
-
cornerRadius,
|
|
492
|
-
0f,
|
|
493
|
-
0f,
|
|
494
|
-
0f,
|
|
495
|
-
0f
|
|
496
|
-
)
|
|
642
|
+
// =============================================================================
|
|
643
|
+
// MARK: - Screen Visibility
|
|
644
|
+
// =============================================================================
|
|
497
645
|
|
|
498
|
-
|
|
499
|
-
|
|
646
|
+
private fun setSheetVisibility(visible: Boolean) {
|
|
647
|
+
coordinatorLayout?.visibility = if (visible) VISIBLE else GONE
|
|
648
|
+
dimViews.forEach { it.visibility = if (visible) VISIBLE else INVISIBLE }
|
|
649
|
+
}
|
|
500
650
|
|
|
501
|
-
|
|
502
|
-
|
|
651
|
+
internal fun hideForScreen() {
|
|
652
|
+
val sheet = sheetView ?: run {
|
|
653
|
+
RNLog.e(reactContext, "TrueSheet: sheetView is null in hideForScreen")
|
|
654
|
+
return
|
|
503
655
|
}
|
|
656
|
+
|
|
657
|
+
isSheetVisible = false
|
|
658
|
+
wasHiddenByScreen = true
|
|
659
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
660
|
+
delegate?.viewControllerDidChangeVisibility(false)
|
|
661
|
+
dimViews.forEach { it.animate().alpha(0f).setDuration(SCREEN_FADE_DURATION).start() }
|
|
662
|
+
sheet.animate()
|
|
663
|
+
.alpha(0f)
|
|
664
|
+
.setDuration(SCREEN_FADE_DURATION)
|
|
665
|
+
.withEndAction {
|
|
666
|
+
setSheetVisibility(false)
|
|
667
|
+
}
|
|
668
|
+
.start()
|
|
669
|
+
|
|
670
|
+
// This will hide parent sheets first
|
|
671
|
+
parentSheetView?.viewController?.hideForScreen()
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
internal fun showAfterScreen() {
|
|
675
|
+
isSheetVisible = true
|
|
676
|
+
delegate?.viewControllerDidChangeVisibility(true)
|
|
677
|
+
setSheetVisibility(true)
|
|
678
|
+
sheetView?.alpha = 1f
|
|
679
|
+
updateDimAmount(animated = true)
|
|
680
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
504
681
|
}
|
|
505
682
|
|
|
506
683
|
/**
|
|
507
|
-
*
|
|
508
|
-
*
|
|
684
|
+
* Re-applies hidden state after returning from background.
|
|
685
|
+
* Android may restore visibility on activity resume, so we need to hide it again.
|
|
509
686
|
*/
|
|
510
|
-
fun
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (dimmed && detentIndex >= dimmedIndex) {
|
|
516
|
-
// Remove touch listener
|
|
517
|
-
view.setOnTouchListener(null)
|
|
518
|
-
|
|
519
|
-
// Add the dimmed background
|
|
520
|
-
setFlags(
|
|
521
|
-
WindowManager.LayoutParams.FLAG_DIM_BEHIND,
|
|
522
|
-
WindowManager.LayoutParams.FLAG_DIM_BEHIND
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
dialog.setCanceledOnTouchOutside(dismissible)
|
|
526
|
-
} else {
|
|
527
|
-
// Override the background touch and pass it to the components outside
|
|
528
|
-
view.setOnTouchListener { v, event ->
|
|
529
|
-
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
530
|
-
reactContext.currentActivity?.dispatchTouchEvent(event)
|
|
531
|
-
false
|
|
532
|
-
}
|
|
687
|
+
fun reapplyHiddenState() {
|
|
688
|
+
if (!wasHiddenByScreen) return
|
|
689
|
+
setSheetVisibility(false)
|
|
690
|
+
}
|
|
533
691
|
|
|
534
|
-
|
|
535
|
-
|
|
692
|
+
// =============================================================================
|
|
693
|
+
// MARK: - Presentation
|
|
694
|
+
// =============================================================================
|
|
536
695
|
|
|
537
|
-
|
|
696
|
+
fun present(detentIndex: Int, animated: Boolean = true) {
|
|
697
|
+
val coordinator = this.coordinatorLayout ?: return
|
|
698
|
+
val sheet = this.sheetView ?: return
|
|
699
|
+
if (isPresented) return
|
|
700
|
+
|
|
701
|
+
shouldAnimatePresent = animated
|
|
702
|
+
currentDetentIndex = detentIndex
|
|
703
|
+
interactionState = InteractionState.Idle
|
|
704
|
+
|
|
705
|
+
// Setup sheet in coordinator layout
|
|
706
|
+
setupSheetInCoordinator(coordinator, sheet)
|
|
707
|
+
|
|
708
|
+
emitWillPresentEvents()
|
|
709
|
+
|
|
710
|
+
setupSheetDetents()
|
|
711
|
+
setupDimmedBackground()
|
|
712
|
+
setupKeyboardObserver()
|
|
713
|
+
|
|
714
|
+
sheet.setupBackground()
|
|
715
|
+
sheet.setupElevation()
|
|
716
|
+
sheet.setupGrabber()
|
|
717
|
+
sheet.setupAccessibility()
|
|
718
|
+
|
|
719
|
+
if (shouldAnimatePresent) {
|
|
720
|
+
isPresentAnimating = true
|
|
721
|
+
post { setStateForDetentIndex(currentDetentIndex) }
|
|
722
|
+
} else {
|
|
723
|
+
setStateForDetentIndex(currentDetentIndex)
|
|
724
|
+
post {
|
|
725
|
+
emitChangePositionDelegate(detentCalculator.getSheetTopForDetentIndex(currentDetentIndex))
|
|
726
|
+
updateDimAmount()
|
|
727
|
+
finishPresent()
|
|
538
728
|
}
|
|
539
729
|
}
|
|
730
|
+
|
|
731
|
+
isPresented = true
|
|
732
|
+
isSheetVisible = true
|
|
540
733
|
}
|
|
541
734
|
|
|
542
|
-
fun
|
|
543
|
-
|
|
544
|
-
|
|
735
|
+
fun resize(detentIndex: Int) {
|
|
736
|
+
if (!isPresented) return
|
|
737
|
+
|
|
738
|
+
// Commit the new index so keyboardWillHide restores to it instead of the stale one
|
|
739
|
+
if (detentIndexBeforeKeyboard >= 0) {
|
|
740
|
+
detentIndexBeforeKeyboard = detentIndex
|
|
545
741
|
}
|
|
742
|
+
|
|
743
|
+
pendingDetentIndex = detentIndex
|
|
744
|
+
updateStateDimensions(deferShrink = true)
|
|
745
|
+
setupDimmedBackground()
|
|
746
|
+
setStateForDetentIndex(detentIndex)
|
|
747
|
+
resizePromise?.invoke()
|
|
748
|
+
resizePromise = null
|
|
546
749
|
}
|
|
547
750
|
|
|
548
|
-
fun
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
751
|
+
private fun setupSheetInCoordinator(coordinator: TrueSheetCoordinatorLayout, sheet: TrueSheetBottomSheetView) {
|
|
752
|
+
// Add this controller as content to the sheet
|
|
753
|
+
(parent as? ViewGroup)?.removeView(this)
|
|
754
|
+
sheet.addView(this)
|
|
552
755
|
|
|
553
|
-
|
|
756
|
+
// Create layout params with behavior
|
|
757
|
+
val params = sheet.createLayoutParams()
|
|
554
758
|
|
|
555
|
-
|
|
556
|
-
|
|
759
|
+
@Suppress("UNCHECKED_CAST")
|
|
760
|
+
val behavior = params.behavior as TrueSheetBottomSheetBehavior<TrueSheetBottomSheetView>
|
|
557
761
|
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
762
|
+
// Configure behavior
|
|
763
|
+
behavior.isHideable = true
|
|
764
|
+
behavior.isDraggable = draggable
|
|
765
|
+
behavior.scrollingExpandsSheet = scrollableOptions?.scrollingExpandsSheet ?: true
|
|
766
|
+
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
767
|
+
behavior.addBottomSheetCallback(sheetCallback)
|
|
768
|
+
|
|
769
|
+
// Add sheet to coordinator
|
|
770
|
+
coordinator.addView(sheet, params)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
fun dismiss(animated: Boolean = true) {
|
|
774
|
+
if (isBeingDismissed) return
|
|
775
|
+
|
|
776
|
+
isBeingDismissed = true
|
|
777
|
+
dismissKeyboard()
|
|
778
|
+
emitWillDismissEvents()
|
|
779
|
+
|
|
780
|
+
if (animated) {
|
|
781
|
+
animateDismiss()
|
|
782
|
+
} else {
|
|
783
|
+
emitChangePositionDelegate(realScreenHeight)
|
|
784
|
+
finishDismiss()
|
|
561
785
|
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private fun dismissKeyboard() {
|
|
789
|
+
isKeyboardDismissProgrammatic = true
|
|
790
|
+
KeyboardUtils.dismiss(reactContext)
|
|
562
791
|
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
footer.y = minOf(footerY, maxAllowedY)
|
|
792
|
+
// Clear focus from any focused view within the sheet to prevent
|
|
793
|
+
// Android from autofocusing the next focusable view on the main
|
|
794
|
+
// screen when the sheet is removed from the hierarchy.
|
|
795
|
+
sheetView?.findFocus()?.clearFocus()
|
|
568
796
|
}
|
|
569
797
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
behavior?.state = getStateForDetentIndex(index)
|
|
798
|
+
fun handleBackPress() {
|
|
799
|
+
if (dismissible) {
|
|
800
|
+
dismiss(animated = true)
|
|
801
|
+
}
|
|
575
802
|
}
|
|
576
803
|
|
|
577
|
-
fun
|
|
578
|
-
|
|
804
|
+
private fun animateDismiss() {
|
|
805
|
+
val sheet = sheetView ?: run {
|
|
806
|
+
finishDismiss()
|
|
807
|
+
return
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
sheet.animate()
|
|
811
|
+
.y(realScreenHeight.toFloat())
|
|
812
|
+
.setDuration(DISMISS_DURATION)
|
|
813
|
+
.setInterpolator(android.view.animation.AccelerateInterpolator())
|
|
814
|
+
.setUpdateListener { updateSheetVisuals(sheet.y.toInt()) }
|
|
815
|
+
.withEndAction { finishDismiss() }
|
|
816
|
+
.start()
|
|
579
817
|
}
|
|
580
818
|
|
|
581
|
-
|
|
819
|
+
private fun finishPresent() {
|
|
820
|
+
// Restore isHideable to actual value after present animation
|
|
821
|
+
behavior?.isHideable = dismissible
|
|
582
822
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
823
|
+
containerView?.setupKeyboardHandler()
|
|
824
|
+
|
|
825
|
+
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
826
|
+
delegate?.viewControllerDidPresent(index, position, detent)
|
|
827
|
+
parentSheetView?.viewControllerDidBlur()
|
|
828
|
+
delegate?.viewControllerDidFocus()
|
|
829
|
+
sheetView?.updateGrabberAccessibilityValue(index, detents.size)
|
|
830
|
+
|
|
831
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
832
|
+
// Move accessibility focus into the sheet
|
|
833
|
+
sheetView?.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
|
|
834
|
+
|
|
835
|
+
presentPromise?.invoke()
|
|
836
|
+
presentPromise = null
|
|
591
837
|
}
|
|
592
838
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
delegate?.viewControllerDidDragBegin(detentInfo.index, detentInfo.position)
|
|
599
|
-
isDragging = true
|
|
839
|
+
private fun finishDismiss() {
|
|
840
|
+
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
841
|
+
restoreFocusedView()
|
|
842
|
+
emitDidDismissEvents()
|
|
843
|
+
cleanupSheet()
|
|
600
844
|
}
|
|
601
845
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
private fun handleDragChange(sheetView: View) {
|
|
606
|
-
if (!isDragging) return
|
|
846
|
+
// =============================================================================
|
|
847
|
+
// MARK: - Sheet Configuration
|
|
848
|
+
// =============================================================================
|
|
607
849
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
850
|
+
fun setupSheetDetents(applyState: Boolean = true) {
|
|
851
|
+
val behavior = this.behavior ?: run {
|
|
852
|
+
RNLog.e(reactContext, "TrueSheet: behavior is null in setupSheetDetents")
|
|
853
|
+
return
|
|
854
|
+
}
|
|
611
855
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// For consistency with iOS,
|
|
619
|
-
// we only handle state changes after dragging.
|
|
620
|
-
//
|
|
621
|
-
// Changing detent programmatically is handled via the present method.
|
|
622
|
-
val detentInfo = getDetentInfoForState(state)
|
|
623
|
-
detentInfo?.let {
|
|
624
|
-
// Notify delegate of drag end
|
|
625
|
-
delegate?.viewControllerDidDragEnd(it.index, it.position)
|
|
626
|
-
|
|
627
|
-
if (it.index != currentDetentIndex) {
|
|
628
|
-
presentPromise?.let { promise ->
|
|
629
|
-
promise()
|
|
630
|
-
presentPromise = null
|
|
631
|
-
}
|
|
856
|
+
containerView?.let {
|
|
857
|
+
cachedContentHeight = it.contentHeight
|
|
858
|
+
cachedHeaderHeight = it.headerHeight
|
|
859
|
+
cachedFooterHeight = it.footerHeight
|
|
860
|
+
cachedPeekContentHeight = it.peekContentHeight
|
|
861
|
+
}
|
|
632
862
|
|
|
633
|
-
|
|
634
|
-
setupDimmedBackground(it.index)
|
|
863
|
+
interactionState = InteractionState.Reconfiguring
|
|
635
864
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
865
|
+
behavior.isFitToContents = false
|
|
866
|
+
|
|
867
|
+
val maxAvailableHeight = realScreenHeight - topInset
|
|
868
|
+
|
|
869
|
+
val peekHeight = minOf(detentCalculator.getDetentHeight(detents[0]), maxAvailableHeight)
|
|
870
|
+
|
|
871
|
+
val halfExpandedDetentHeight = when (detents.size) {
|
|
872
|
+
1 -> peekHeight
|
|
873
|
+
else -> detentCalculator.getDetentHeight(detents[1])
|
|
639
874
|
}
|
|
640
875
|
|
|
641
|
-
|
|
642
|
-
}
|
|
876
|
+
val maxDetentHeight = minOf(detentCalculator.getDetentHeight(detents.last()), maxAvailableHeight)
|
|
643
877
|
|
|
644
|
-
|
|
878
|
+
val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
|
|
879
|
+
val halfExpandedRatio = (adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat())
|
|
645
880
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
881
|
+
val expandedOffset = realScreenHeight - maxDetentHeight
|
|
882
|
+
|
|
883
|
+
// fitToContents works better with <= 2 detents when no expanded offset
|
|
884
|
+
val fitToContents = detents.size < 3 && expandedOffset == 0
|
|
885
|
+
|
|
886
|
+
configureDetents(
|
|
887
|
+
behavior = behavior,
|
|
888
|
+
peekHeight = peekHeight,
|
|
889
|
+
halfExpandedRatio = halfExpandedRatio,
|
|
890
|
+
expandedOffset = expandedOffset,
|
|
891
|
+
fitToContents = fitToContents,
|
|
892
|
+
animate = isPresented
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
updateStateDimensions()
|
|
896
|
+
|
|
897
|
+
if (isPresented && applyState) {
|
|
898
|
+
// Prefer the pending target while a resize animation is in flight so a
|
|
899
|
+
// layout-driven reconfigure doesn't revert to the stale currentDetentIndex.
|
|
900
|
+
val targetIndex = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
901
|
+
setStateForDetentIndex(targetIndex)
|
|
658
902
|
}
|
|
659
903
|
|
|
660
|
-
|
|
661
|
-
val maxAllowedHeight = maxScreenHeight - sheetTopInset
|
|
662
|
-
val finalHeight = maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
663
|
-
return finalHeight
|
|
904
|
+
interactionState = InteractionState.Idle
|
|
664
905
|
}
|
|
665
906
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
907
|
+
private fun configureDetents(
|
|
908
|
+
behavior: BottomSheetBehavior<TrueSheetBottomSheetView>,
|
|
909
|
+
peekHeight: Int,
|
|
910
|
+
halfExpandedRatio: Float,
|
|
911
|
+
expandedOffset: Int,
|
|
912
|
+
fitToContents: Boolean,
|
|
913
|
+
animate: Boolean
|
|
914
|
+
) {
|
|
915
|
+
behavior.apply {
|
|
916
|
+
isFitToContents = fitToContents
|
|
917
|
+
skipCollapsed = false
|
|
918
|
+
setPeekHeight(peekHeight, animate)
|
|
919
|
+
this.halfExpandedRatio = halfExpandedRatio.coerceIn(0.01f, 0.999f)
|
|
920
|
+
this.expandedOffset = expandedOffset
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
fun setupSheetDetentsForSizeChange() {
|
|
925
|
+
// Skip while dragging — the size change is driven by the drag itself (the
|
|
926
|
+
// container tracks the sheet's visible height), and reconfiguring resets
|
|
927
|
+
// behavior state, killing the gesture.
|
|
928
|
+
if (interactionState is InteractionState.Dragging) return
|
|
929
|
+
|
|
930
|
+
setupSheetDetents()
|
|
931
|
+
positionFooter()
|
|
932
|
+
}
|
|
674
933
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
934
|
+
fun setStateForDetentIndex(index: Int) {
|
|
935
|
+
behavior?.state = detentCalculator.getStateForDetentIndex(index)
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// =============================================================================
|
|
939
|
+
// MARK: - Dimmed Background
|
|
940
|
+
// =============================================================================
|
|
941
|
+
|
|
942
|
+
fun setupDimmedBackground() {
|
|
943
|
+
val coordinator = this.coordinatorLayout ?: run {
|
|
944
|
+
RNLog.e(reactContext, "TrueSheet: coordinatorLayout is null in setupDimmedBackground")
|
|
945
|
+
return
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (dimmed) {
|
|
949
|
+
val parentDimVisible = (parentSheetView?.viewController?.dimView?.alpha ?: 0f) > 0f
|
|
950
|
+
|
|
951
|
+
if (dimView == null) {
|
|
952
|
+
dimView = TrueSheetDimView(reactContext).apply {
|
|
953
|
+
delegate = this@TrueSheetViewController
|
|
680
954
|
}
|
|
681
955
|
}
|
|
956
|
+
if (!parentDimVisible) {
|
|
957
|
+
dimView?.attachToCoordinator(coordinator)
|
|
958
|
+
}
|
|
682
959
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
960
|
+
// Attach dim view to parent sheet if stacked
|
|
961
|
+
val parentController = parentSheetView?.viewController
|
|
962
|
+
val parentBottomSheet = parentController?.sheetView
|
|
963
|
+
if (parentBottomSheet != null) {
|
|
964
|
+
if (parentDimView == null) {
|
|
965
|
+
parentDimView = TrueSheetDimView(reactContext).apply {
|
|
966
|
+
delegate = this@TrueSheetViewController
|
|
967
|
+
}
|
|
689
968
|
}
|
|
969
|
+
parentDimView?.attach(parentBottomSheet, parentController.sheetCornerRadius)
|
|
690
970
|
}
|
|
971
|
+
} else {
|
|
972
|
+
dimView?.detach()
|
|
973
|
+
dimView = null
|
|
974
|
+
parentDimView?.detach()
|
|
975
|
+
parentDimView = null
|
|
976
|
+
}
|
|
977
|
+
}
|
|
691
978
|
|
|
692
|
-
|
|
979
|
+
fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
|
|
980
|
+
if (!dimmed) return
|
|
981
|
+
if (contentHeight == 0) return
|
|
982
|
+
|
|
983
|
+
// While keyboard is active or transitioning, use the target detent position for dim
|
|
984
|
+
val top = if (keyboardInset > 0 || isKeyboardTransitioning) {
|
|
985
|
+
detentCalculator.getSheetTopForDetentIndex(currentDetentIndex)
|
|
986
|
+
} else {
|
|
987
|
+
val keyboardOffset = if (isBeingDismissed) 0 else currentKeyboardInset
|
|
988
|
+
(sheetTop ?: sheetView?.top ?: return) + keyboardOffset
|
|
693
989
|
}
|
|
694
990
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
991
|
+
if (animated) {
|
|
992
|
+
val targetAlpha = dimView?.calculateAlpha(
|
|
993
|
+
top,
|
|
994
|
+
dimmedDetentIndex,
|
|
995
|
+
detentCalculator::getSheetTopForDetentIndex
|
|
996
|
+
) ?: 0f
|
|
997
|
+
dimViews.forEach { it.animate().alpha(targetAlpha).setDuration(200).start() }
|
|
998
|
+
} else {
|
|
999
|
+
dimViews.forEach { it.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex) }
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// =============================================================================
|
|
1004
|
+
// MARK: - Footer Positioning
|
|
1005
|
+
// =============================================================================
|
|
1006
|
+
|
|
1007
|
+
var footerKeyboardOffset: Int = 0
|
|
1008
|
+
|
|
1009
|
+
fun positionFooter(slideOffset: Float? = null) {
|
|
1010
|
+
if (!isPresented) return
|
|
1011
|
+
val footerView = containerView?.footerView ?: return
|
|
1012
|
+
val sheet = sheetView ?: return
|
|
1013
|
+
|
|
1014
|
+
val keyboardShift = if (currentKeyboardInset > 0) maxOf(0, currentKeyboardInset + footerKeyboardOffset) else 0
|
|
1015
|
+
|
|
1016
|
+
// A relative footer is laid out by Yoga (pinned to the container's bottom
|
|
1017
|
+
// edge) and stays in the layout flow behind the keyboard — only an
|
|
1018
|
+
// absolute footer floats above it
|
|
1019
|
+
if (!absoluteFooter) {
|
|
1020
|
+
footerView.translationY = 0f
|
|
1021
|
+
return
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
val footerHeight = footerView.height
|
|
1025
|
+
val sheetHeight = sheet.height
|
|
1026
|
+
val sheetTop = sheet.top
|
|
1027
|
+
|
|
1028
|
+
var footerY = (sheetHeight - sheetTop - footerHeight - keyboardShift).toFloat()
|
|
1029
|
+
|
|
1030
|
+
// Adjust during dismiss animation when slideOffset is negative. Skipped
|
|
1031
|
+
// while the keyboard is open — the detents collapse to the expanded
|
|
1032
|
+
// position, so any downward drag reads as a negative slideOffset and the
|
|
1033
|
+
// adjustment would sink the footer behind the keyboard, snapping back up
|
|
1034
|
+
// when the keyboard detents reset on release.
|
|
1035
|
+
if (slideOffset != null && slideOffset < 0 && keyboardShift == 0) {
|
|
1036
|
+
footerY -= (footerHeight * slideOffset)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Clamp to prevent footer going above safe area
|
|
1040
|
+
val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
|
|
1041
|
+
footerView.y = minOf(footerY, maxAllowedY)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// =============================================================================
|
|
1045
|
+
// MARK: - Keyboard Handling
|
|
1046
|
+
// =============================================================================
|
|
1047
|
+
|
|
1048
|
+
private fun shouldHandleKeyboard(checkFocus: Boolean = true): Boolean {
|
|
1049
|
+
if (wasHiddenByScreen) return false
|
|
1050
|
+
if (!isTopmostSheet) return false
|
|
1051
|
+
if (checkFocus && !isFocusedViewWithinSheet()) return false
|
|
1052
|
+
return true
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
fun saveFocusedView() {
|
|
1056
|
+
focusedViewBeforeBlur = delegate?.findRootContainerView()?.findFocus()
|
|
1057
|
+
?: reactContext.currentActivity?.currentFocus
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
fun restoreFocusedView() {
|
|
1061
|
+
val viewToFocus = focusedViewBeforeBlur ?: return
|
|
1062
|
+
focusedViewBeforeBlur = null
|
|
1063
|
+
|
|
1064
|
+
if (!viewToFocus.isAttachedToWindow) return
|
|
1065
|
+
if (viewToFocus.requestFocus()) {
|
|
1066
|
+
viewToFocus.postDelayed({
|
|
1067
|
+
KeyboardUtils.show(viewToFocus)
|
|
1068
|
+
}, 100)
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
fun setupKeyboardObserver() {
|
|
1073
|
+
val coordinator = coordinatorLayout ?: run {
|
|
1074
|
+
RNLog.e(reactContext, "TrueSheet: coordinatorLayout is null in setupKeyboardObserver")
|
|
1075
|
+
return
|
|
1076
|
+
}
|
|
1077
|
+
cleanupKeyboardObserver()
|
|
1078
|
+
keyboardObserver = TrueSheetKeyboardObserver(coordinator, reactContext).apply {
|
|
1079
|
+
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
1080
|
+
override fun keyboardWillShow(height: Int) {
|
|
1081
|
+
if (!shouldHandleKeyboard()) return
|
|
1082
|
+
// An absolute footer rises above the keyboard, so it drops the bottom
|
|
1083
|
+
// inset padding — before detents are configured against its height
|
|
1084
|
+
if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
|
|
1085
|
+
// If a resize is in flight, restore to its target — not the stale current
|
|
1086
|
+
detentIndexBeforeKeyboard = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
1087
|
+
pendingDetentIndex = -1
|
|
1088
|
+
// Commit the target index first so the container grows before the sheet expands
|
|
1089
|
+
currentDetentIndex = detents.size - 1
|
|
1090
|
+
setupSheetDetents()
|
|
1091
|
+
positionFooter()
|
|
1092
|
+
updateDimAmount(animated = true)
|
|
705
1093
|
}
|
|
706
|
-
}
|
|
707
1094
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1095
|
+
override fun keyboardWillHide() {
|
|
1096
|
+
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1097
|
+
containerView?.footerView?.keyboardVisible = false
|
|
1098
|
+
val restoring = !isBeingDismissed && detentIndexBeforeKeyboard >= 0
|
|
1099
|
+
|
|
1100
|
+
// Skip reconfigure during interactive keyboard dismiss (e.g. keyboardDismissMode="on-drag")
|
|
1101
|
+
// to prevent the sheet from jumping. keyboardDidHide will reconfigure after.
|
|
1102
|
+
if (restoring || isKeyboardDismissProgrammatic) {
|
|
1103
|
+
setupSheetDetents()
|
|
1104
|
+
if (restoring) {
|
|
1105
|
+
currentDetentIndex = detentIndexBeforeKeyboard
|
|
1106
|
+
setStateForDetentIndex(currentDetentIndex)
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Same-frame reposition — the reconfigure above shrinks the container
|
|
1111
|
+
// to the target detent, which yanks the bottom-pinned footer up until
|
|
1112
|
+
// the next keyboard animation frame repositions it.
|
|
1113
|
+
positionFooter()
|
|
1114
|
+
updateDimAmount(
|
|
1115
|
+
sheetTop = detentCalculator.getSheetTopForDetentIndex(currentDetentIndex),
|
|
1116
|
+
animated = true
|
|
1117
|
+
)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
override fun keyboardDidHide() {
|
|
1121
|
+
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1122
|
+
containerView?.footerView?.keyboardVisible = false
|
|
1123
|
+
detentIndexBeforeKeyboard = -1
|
|
1124
|
+
isKeyboardDismissProgrammatic = false
|
|
1125
|
+
setupSheetDetents(applyState = false)
|
|
1126
|
+
positionFooter()
|
|
1127
|
+
updateDimAmount(
|
|
1128
|
+
sheetTop = detentCalculator.getSheetTopForDetentIndex(currentDetentIndex),
|
|
1129
|
+
animated = true
|
|
1130
|
+
)
|
|
713
1131
|
}
|
|
714
|
-
}
|
|
715
1132
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1133
|
+
override fun keyboardDidChangeHeight(height: Int) {
|
|
1134
|
+
// Skip focus check during active keyboard transitions (focus may be lost during hide)
|
|
1135
|
+
val skipFocusCheck = detentIndexBeforeKeyboard >= 0 || isKeyboardTransitioning
|
|
1136
|
+
if (!shouldHandleKeyboard(checkFocus = !skipFocusCheck)) return
|
|
1137
|
+
positionFooter()
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
override fun focusDidChange(newFocus: View) {
|
|
1141
|
+
// Handle case where keyboard is already visible and focus moves into the sheet
|
|
1142
|
+
if (!shouldHandleKeyboard()) return
|
|
1143
|
+
if (detentIndexBeforeKeyboard < 0 && (keyboardObserver?.currentHeight ?: 0) > 0) {
|
|
1144
|
+
if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
|
|
1145
|
+
detentIndexBeforeKeyboard = currentDetentIndex
|
|
1146
|
+
currentDetentIndex = detents.size - 1
|
|
1147
|
+
setupSheetDetents()
|
|
1148
|
+
positionFooter()
|
|
1149
|
+
}
|
|
722
1150
|
}
|
|
723
1151
|
}
|
|
1152
|
+
start()
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
fun cleanupKeyboardObserver() {
|
|
1157
|
+
keyboardObserver?.stop()
|
|
1158
|
+
keyboardObserver = null
|
|
1159
|
+
}
|
|
724
1160
|
|
|
725
|
-
|
|
1161
|
+
// =============================================================================
|
|
1162
|
+
// MARK: - Drag Handling
|
|
1163
|
+
// =============================================================================
|
|
1164
|
+
|
|
1165
|
+
private fun getPositionDpForView(sheetView: View): Float =
|
|
1166
|
+
detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
1167
|
+
|
|
1168
|
+
fun commitKeyboardDetent() {
|
|
1169
|
+
detentIndexBeforeKeyboard = -1
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
private fun handleDragBegin(sheetView: View) {
|
|
1173
|
+
detentIndexBeforeKeyboard = -1
|
|
1174
|
+
pendingDetentIndex = -1
|
|
1175
|
+
|
|
1176
|
+
val position = getPositionDpForView(sheetView)
|
|
1177
|
+
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
1178
|
+
delegate?.viewControllerDidDragBegin(currentDetentIndex, position, detent)
|
|
1179
|
+
interactionState = InteractionState.Dragging(startTop = sheetView.top)
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private fun handleSettling(sheetView: View) {
|
|
1183
|
+
if (interactionState !is InteractionState.Dragging) return
|
|
1184
|
+
if (keyboardInset <= 0) return
|
|
1185
|
+
|
|
1186
|
+
// After drag release, check if the sheet was dragged past the midpoint between the
|
|
1187
|
+
// keyboard-expanded position and a non-keyboard detent position. If so, dismiss
|
|
1188
|
+
// keyboard and commit to that detent.
|
|
1189
|
+
val maxAvailableHeight = realScreenHeight - topInset
|
|
1190
|
+
val keyboardTop = detentCalculator.getSheetTopForDetentIndex(detents.size - 1)
|
|
1191
|
+
|
|
1192
|
+
for (i in detents.indices) {
|
|
1193
|
+
val nonKeyboardHeight = minOf(detentCalculator.getDetentHeight(detents[i], includeKeyboard = false), maxAvailableHeight)
|
|
1194
|
+
val nonKeyboardTop = realScreenHeight - nonKeyboardHeight
|
|
1195
|
+
val midpoint = keyboardTop + (nonKeyboardTop - keyboardTop) / 2
|
|
1196
|
+
|
|
1197
|
+
if (sheetView.top >= midpoint) {
|
|
1198
|
+
// Target position matches the keyboard-expanded position — keep keyboard open
|
|
1199
|
+
if (nonKeyboardTop == keyboardTop) break
|
|
1200
|
+
|
|
1201
|
+
detentIndexBeforeKeyboard = -1
|
|
1202
|
+
currentDetentIndex = i
|
|
1203
|
+
dismissKeyboard()
|
|
1204
|
+
setupDimmedBackground()
|
|
1205
|
+
|
|
1206
|
+
val position = getPositionDpForView(sheetView)
|
|
1207
|
+
val detent = detentCalculator.getDetentValueForIndex(i)
|
|
1208
|
+
delegate?.viewControllerDidChangeDetent(i, position, detent)
|
|
1209
|
+
break
|
|
1210
|
+
}
|
|
726
1211
|
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private fun handleDragChange(sheetView: View) {
|
|
1215
|
+
if (interactionState !is InteractionState.Dragging) return
|
|
1216
|
+
|
|
1217
|
+
updateStateDimensionsForDrag(sheetView.top)
|
|
1218
|
+
|
|
1219
|
+
val position = getPositionDpForView(sheetView)
|
|
1220
|
+
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
1221
|
+
delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// =============================================================================
|
|
1225
|
+
// MARK: - Event Emission
|
|
1226
|
+
// =============================================================================
|
|
1227
|
+
|
|
1228
|
+
private fun emitWillPresentEvents() {
|
|
1229
|
+
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
1230
|
+
parentSheetView?.viewControllerWillBlur()
|
|
1231
|
+
delegate?.viewControllerWillPresent(index, position, detent)
|
|
1232
|
+
delegate?.viewControllerWillFocus()
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
private fun emitWillDismissEvents() {
|
|
1236
|
+
delegate?.viewControllerWillBlur()
|
|
1237
|
+
delegate?.viewControllerWillDismiss()
|
|
1238
|
+
parentSheetView?.viewControllerWillFocus()
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
private fun emitDidDismissEvents() {
|
|
1242
|
+
val parent = parentSheetView
|
|
1243
|
+
parentSheetView = null
|
|
1244
|
+
|
|
1245
|
+
delegate?.viewControllerDidBlur()
|
|
1246
|
+
delegate?.viewControllerDidDismiss(parent)
|
|
1247
|
+
|
|
1248
|
+
dismissPromise?.invoke()
|
|
1249
|
+
dismissPromise = null
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
|
|
1253
|
+
// Dedupe emissions for same position
|
|
1254
|
+
if (currentTop == lastEmittedPositionPx) return
|
|
1255
|
+
|
|
1256
|
+
lastEmittedPositionPx = currentTop
|
|
1257
|
+
val visibleHeight = realScreenHeight - currentTop
|
|
1258
|
+
val position = detentCalculator.getPositionDp(visibleHeight)
|
|
1259
|
+
val interpolatedIndex = detentCalculator.getInterpolatedIndexForPosition(currentTop)
|
|
1260
|
+
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
1261
|
+
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
1262
|
+
}
|
|
727
1263
|
|
|
728
1264
|
/**
|
|
729
|
-
*
|
|
730
|
-
*
|
|
1265
|
+
* Updates position emission, footer, and dim amount together.
|
|
1266
|
+
* This pattern is commonly used during animations and state changes.
|
|
731
1267
|
*/
|
|
732
|
-
private fun
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1268
|
+
private fun updateSheetVisuals(effectiveTop: Int, slideOffset: Float? = null) {
|
|
1269
|
+
emitChangePositionDelegate(effectiveTop)
|
|
1270
|
+
positionFooter(slideOffset)
|
|
1271
|
+
updateDimAmount(effectiveTop)
|
|
1272
|
+
}
|
|
736
1273
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1274
|
+
// =============================================================================
|
|
1275
|
+
// MARK: - Detent Helpers
|
|
1276
|
+
// =============================================================================
|
|
1277
|
+
|
|
1278
|
+
fun updateBehaviorMaxWidth() {
|
|
1279
|
+
val behavior = this.behavior ?: return
|
|
1280
|
+
val applyMaxWidth = maxContentWidth != null && !ScreenUtils.isPortraitPhone(reactContext)
|
|
1281
|
+
val newMaxWidth = if (applyMaxWidth) maxContentWidth!! else DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
1282
|
+
if (behavior.maxWidth == newMaxWidth) return
|
|
746
1283
|
|
|
747
|
-
|
|
748
|
-
|
|
1284
|
+
behavior.maxWidth = newMaxWidth
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Updates the Fabric state with the current/target detent size so Yoga sizes
|
|
1289
|
+
* the container to the sheet's visible height.
|
|
1290
|
+
*
|
|
1291
|
+
* Unlike iOS, resize animations run over multiple frames (ViewDragHelper), so
|
|
1292
|
+
* shrinking is deferred to settle ([deferShrink]) when a resize animation is
|
|
1293
|
+
* about to run — growing applies immediately so content is laid out before
|
|
1294
|
+
* the sheet reveals it.
|
|
1295
|
+
*/
|
|
1296
|
+
private fun updateStateDimensions(deferShrink: Boolean = false) {
|
|
1297
|
+
if (detents.isEmpty()) return
|
|
749
1298
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
val
|
|
1299
|
+
val targetIndex = (if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex)
|
|
1300
|
+
.coerceIn(0, detents.size - 1)
|
|
1301
|
+
val maxAvailableHeight = realScreenHeight - topInset
|
|
1302
|
+
val newHeight = minOf(detentCalculator.getDetentHeight(detents[targetIndex]), maxAvailableHeight)
|
|
1303
|
+
val newWidth = getStateWidth()
|
|
755
1304
|
|
|
756
|
-
|
|
1305
|
+
if (deferShrink && newWidth == lastStateWidth && newHeight < lastStateHeight) return
|
|
1306
|
+
|
|
1307
|
+
setStateDimensions(newWidth, newHeight)
|
|
757
1308
|
}
|
|
758
1309
|
|
|
759
1310
|
/**
|
|
760
|
-
*
|
|
1311
|
+
* Tracks the sheet's visible height during drag (and the post-release settle
|
|
1312
|
+
* animation) so the container resizes with the finger, like iOS. Clamped to the
|
|
1313
|
+
* smallest detent — dragging below it slides the sheet out without resizing.
|
|
761
1314
|
*/
|
|
762
|
-
fun
|
|
1315
|
+
private fun updateStateDimensionsForDrag(sheetTop: Int) {
|
|
1316
|
+
if (detents.isEmpty()) return
|
|
763
1317
|
|
|
764
|
-
|
|
1318
|
+
val maxAvailableHeight = realScreenHeight - topInset
|
|
1319
|
+
val minHeight = minOf(detentCalculator.getDetentHeight(detents.first()), maxAvailableHeight)
|
|
1320
|
+
val newHeight = detentCalculator.getVisibleSheetHeight(sheetTop).coerceIn(minHeight, maxAvailableHeight)
|
|
765
1321
|
|
|
766
|
-
|
|
767
|
-
|
|
1322
|
+
setStateDimensions(getStateWidth(), newHeight)
|
|
1323
|
+
}
|
|
768
1324
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1325
|
+
private fun getStateWidth(): Int {
|
|
1326
|
+
val applyMaxWidth = maxContentWidth != null && !ScreenUtils.isPortraitPhone(reactContext)
|
|
1327
|
+
val effectiveMaxWidth = if (applyMaxWidth) maxContentWidth!! else DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
1328
|
+
return minOf(screenWidth, effectiveMaxWidth)
|
|
773
1329
|
}
|
|
774
1330
|
|
|
775
|
-
|
|
776
|
-
|
|
1331
|
+
private fun setStateDimensions(width: Int, height: Int) {
|
|
1332
|
+
if (lastStateWidth == width && lastStateHeight == height) return
|
|
777
1333
|
|
|
778
|
-
|
|
779
|
-
|
|
1334
|
+
lastStateWidth = width
|
|
1335
|
+
lastStateHeight = height
|
|
1336
|
+
delegate?.viewControllerDidChangeSize(width, height)
|
|
1337
|
+
}
|
|
780
1338
|
|
|
781
|
-
|
|
782
|
-
val
|
|
783
|
-
maxScreenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
1339
|
+
fun translateSheet(translationY: Int, onEnd: (() -> Unit)? = null) {
|
|
1340
|
+
val sheet = sheetView ?: return
|
|
784
1341
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1342
|
+
sheet.animate()
|
|
1343
|
+
.translationY(translationY.toFloat())
|
|
1344
|
+
.setDuration(TRANSLATE_ANIMATION_DURATION)
|
|
1345
|
+
.setUpdateListener {
|
|
1346
|
+
val effectiveTop = sheet.top + sheet.translationY.toInt()
|
|
1347
|
+
emitChangePositionDelegate(effectiveTop)
|
|
1348
|
+
}
|
|
1349
|
+
.withEndAction { onEnd?.invoke() }
|
|
1350
|
+
.start()
|
|
1351
|
+
}
|
|
789
1352
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1353
|
+
private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
|
|
1354
|
+
val state = detentCalculator.getStateForDetentIndex(index)
|
|
1355
|
+
val detentIndex = detentCalculator.getDetentIndexForState(state) ?: 0
|
|
1356
|
+
val position = getPositionForDetentIndex(detentIndex)
|
|
1357
|
+
val detent = detentCalculator.getDetentValueForIndex(detentIndex)
|
|
1358
|
+
return Triple(detentIndex, position, detent)
|
|
1359
|
+
}
|
|
793
1360
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1361
|
+
private fun getPositionForDetentIndex(index: Int): Float {
|
|
1362
|
+
if (index < 0 || index >= detents.size) return screenHeight.pxToDp()
|
|
1363
|
+
|
|
1364
|
+
sheetView?.let {
|
|
1365
|
+
val visibleSheetHeight = detentCalculator.getVisibleSheetHeight(it.top)
|
|
1366
|
+
if (visibleSheetHeight in 1..<realScreenHeight) {
|
|
1367
|
+
return detentCalculator.getPositionDp(visibleSheetHeight)
|
|
797
1368
|
}
|
|
798
1369
|
}
|
|
799
1370
|
|
|
800
|
-
|
|
801
|
-
|
|
1371
|
+
val detentHeight = detentCalculator.getDetentHeight(detents[index])
|
|
1372
|
+
return detentCalculator.getPositionDp(detentHeight)
|
|
802
1373
|
}
|
|
803
1374
|
|
|
804
|
-
|
|
805
|
-
|
|
1375
|
+
// =============================================================================
|
|
1376
|
+
// MARK: - RootView Implementation
|
|
1377
|
+
// =============================================================================
|
|
1378
|
+
|
|
1379
|
+
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
|
|
1380
|
+
super.onInitializeAccessibilityNodeInfo(info)
|
|
1381
|
+
(getTag(R.id.react_test_id) as? String)?.let { info.viewIdResourceName = it }
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// =============================================================================
|
|
1385
|
+
// MARK: - RootView Touch Handling
|
|
1386
|
+
// =============================================================================
|
|
1387
|
+
|
|
1388
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1389
|
+
val footer = containerView?.footerView
|
|
1390
|
+
val action = event.actionMasked
|
|
1391
|
+
|
|
1392
|
+
if (footer != null && footer.isShown) {
|
|
1393
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
1394
|
+
val loc = ScreenUtils.getScreenLocation(footer)
|
|
1395
|
+
val x = event.rawX.toInt()
|
|
1396
|
+
val y = event.rawY.toInt()
|
|
1397
|
+
footerOwnsTouchStream = x >= loc[0] &&
|
|
1398
|
+
x <= loc[0] + footer.width &&
|
|
1399
|
+
y >= loc[1] &&
|
|
1400
|
+
y <= loc[1] + footer.height
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (footerOwnsTouchStream) {
|
|
1404
|
+
val loc = ScreenUtils.getScreenLocation(footer)
|
|
1405
|
+
val localEvent = MotionEvent.obtain(event)
|
|
1406
|
+
localEvent.setLocation(event.rawX - loc[0], event.rawY - loc[1])
|
|
1407
|
+
val handled = footer.dispatchTouchEvent(localEvent)
|
|
1408
|
+
localEvent.recycle()
|
|
1409
|
+
|
|
1410
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
1411
|
+
footerOwnsTouchStream = false
|
|
1412
|
+
}
|
|
1413
|
+
if (handled) return true
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
1418
|
+
footerOwnsTouchStream = false
|
|
1419
|
+
}
|
|
1420
|
+
return super.dispatchTouchEvent(event)
|
|
806
1421
|
}
|
|
807
1422
|
|
|
808
1423
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
809
|
-
eventDispatcher?.let {
|
|
810
|
-
|
|
811
|
-
|
|
1424
|
+
eventDispatcher?.let {
|
|
1425
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
1426
|
+
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1427
|
+
}
|
|
1428
|
+
jsPointerDispatcher.handleMotionEvent(event, it, true)
|
|
812
1429
|
}
|
|
813
1430
|
return super.onInterceptTouchEvent(event)
|
|
814
1431
|
}
|
|
815
1432
|
|
|
1433
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
1434
|
+
// Mirrors ReactSurfaceView: keep receiving onInterceptTouchEvent so
|
|
1435
|
+
// jsTouchDispatcher sees the gesture end, but forward the request up the tree.
|
|
1436
|
+
parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
1437
|
+
}
|
|
1438
|
+
|
|
816
1439
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
817
|
-
eventDispatcher?.let {
|
|
818
|
-
|
|
819
|
-
|
|
1440
|
+
eventDispatcher?.let {
|
|
1441
|
+
if (touchDeduper.shouldDispatch(event)) {
|
|
1442
|
+
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1443
|
+
}
|
|
1444
|
+
jsPointerDispatcher.handleMotionEvent(event, it, false)
|
|
820
1445
|
}
|
|
821
1446
|
super.onTouchEvent(event)
|
|
822
|
-
// In case when there is no children interested in handling touch event, we return true from
|
|
823
|
-
// the root view in order to receive subsequent events related to that gesture
|
|
824
1447
|
return true
|
|
825
1448
|
}
|
|
826
1449
|
|
|
827
1450
|
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
|
828
|
-
eventDispatcher?.let {
|
|
1451
|
+
eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, true) }
|
|
829
1452
|
return super.onHoverEvent(event)
|
|
830
1453
|
}
|
|
831
1454
|
|
|
832
1455
|
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
833
|
-
eventDispatcher?.let {
|
|
1456
|
+
eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, false) }
|
|
834
1457
|
return super.onHoverEvent(event)
|
|
835
1458
|
}
|
|
836
1459
|
|
|
837
|
-
@OptIn(UnstableReactNativeAPI::class)
|
|
838
|
-
@Suppress("DEPRECATION")
|
|
839
1460
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1461
|
+
coordinatorLayout?.childDidClaimNativeGesture()
|
|
1462
|
+
eventDispatcher?.let {
|
|
1463
|
+
jsTouchDispatcher.onChildStartedNativeGesture(ev, it)
|
|
1464
|
+
jsPointerDispatcher.onChildStartedNativeGesture(childView, ev, it)
|
|
843
1465
|
}
|
|
844
1466
|
}
|
|
845
1467
|
|
|
846
1468
|
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
847
|
-
eventDispatcher?.let {
|
|
848
|
-
|
|
1469
|
+
eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
|
|
1470
|
+
jsPointerDispatcher.onChildEndedNativeGesture()
|
|
849
1471
|
}
|
|
850
1472
|
|
|
851
|
-
override fun
|
|
852
|
-
|
|
853
|
-
super.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
1473
|
+
override fun handleException(t: Throwable) {
|
|
1474
|
+
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
854
1475
|
}
|
|
855
1476
|
}
|