@lodev09/react-native-true-sheet 2.0.5 → 4.0.0-beta.0
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/README.md +36 -8
- package/RNTrueSheet.podspec +20 -0
- package/android/build.gradle +26 -14
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +108 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerViewManager.kt +21 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +46 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +21 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +47 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +21 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +165 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +36 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +257 -299
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +855 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +104 -82
- package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +26 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +20 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +26 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +26 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +26 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +26 -0
- package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +20 -0
- package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +32 -0
- package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +27 -0
- package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +20 -0
- package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +26 -0
- package/android/src/main/java/com/lodev09/truesheet/{core/Utils.kt → utils/ScreenUtils.kt} +47 -17
- package/android/src/main/res/values/styles.xml +8 -0
- package/ios/TrueSheetComponentDescriptor.h +24 -0
- package/ios/TrueSheetContainerView.h +47 -0
- package/ios/TrueSheetContainerView.mm +117 -0
- package/ios/TrueSheetContentView.h +37 -0
- package/ios/TrueSheetContentView.mm +114 -0
- package/ios/TrueSheetFooterView.h +27 -0
- package/ios/TrueSheetFooterView.mm +101 -0
- package/ios/TrueSheetModule.h +44 -0
- package/ios/TrueSheetModule.mm +133 -0
- package/ios/TrueSheetView.h +53 -0
- package/ios/TrueSheetView.mm +433 -0
- package/ios/TrueSheetViewController.h +53 -0
- package/ios/TrueSheetViewController.mm +649 -0
- package/ios/events/OnDetentChangeEvent.h +28 -0
- package/ios/events/OnDetentChangeEvent.mm +30 -0
- package/ios/events/OnDidDismissEvent.h +26 -0
- package/ios/events/OnDidDismissEvent.mm +25 -0
- package/ios/events/OnDidPresentEvent.h +28 -0
- package/ios/events/OnDidPresentEvent.mm +30 -0
- package/ios/events/OnDragBeginEvent.h +28 -0
- package/ios/events/OnDragBeginEvent.mm +30 -0
- package/ios/events/OnDragChangeEvent.h +28 -0
- package/ios/events/OnDragChangeEvent.mm +30 -0
- package/ios/events/OnDragEndEvent.h +28 -0
- package/ios/events/OnDragEndEvent.mm +30 -0
- package/ios/events/OnMountEvent.h +26 -0
- package/ios/events/OnMountEvent.mm +25 -0
- package/ios/events/OnPositionChangeEvent.h +29 -0
- package/ios/events/OnPositionChangeEvent.mm +32 -0
- package/ios/events/OnSizeChangeEvent.h +28 -0
- package/ios/events/OnSizeChangeEvent.mm +30 -0
- package/ios/events/OnWillDismissEvent.h +26 -0
- package/ios/events/OnWillDismissEvent.mm +25 -0
- package/ios/events/OnWillPresentEvent.h +28 -0
- package/ios/events/OnWillPresentEvent.mm +30 -0
- package/ios/utils/GestureUtil.h +25 -0
- package/ios/utils/GestureUtil.mm +26 -0
- package/ios/utils/LayoutUtil.h +44 -0
- package/ios/utils/LayoutUtil.mm +50 -0
- package/ios/utils/WindowUtil.h +27 -0
- package/ios/utils/WindowUtil.mm +42 -0
- package/lib/module/TrueSheet.js +231 -135
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheetGrabber.js +16 -14
- package/lib/module/TrueSheetGrabber.js.map +1 -1
- package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +63 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/reanimated/ReanimatedTrueSheet.js +87 -0
- package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -0
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +72 -0
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -0
- package/lib/module/reanimated/index.js +6 -0
- package/lib/module/reanimated/index.js.map +1 -0
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.js +19 -0
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.js.map +1 -0
- package/lib/module/specs/NativeTrueSheetModule.js +12 -0
- package/lib/module/specs/NativeTrueSheetModule.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/TrueSheet.d.ts +79 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheet.types.d.ts +260 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -0
- package/lib/typescript/{commonjs/src → src}/TrueSheetGrabber.d.ts +1 -1
- package/lib/typescript/src/TrueSheetGrabber.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +44 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/{commonjs/src → src}/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts +43 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +57 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/index.d.ts +4 -0
- package/lib/typescript/src/reanimated/index.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.d.ts +6 -0
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +34 -0
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -0
- package/package.json +104 -75
- package/react-native.config.js +17 -0
- package/src/TrueSheet.tsx +285 -188
- package/src/TrueSheet.types.ts +119 -106
- package/src/TrueSheetGrabber.tsx +29 -28
- package/src/__mocks__/index.js +60 -12
- package/src/fabric/TrueSheetContainerViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +63 -0
- package/src/index.ts +4 -3
- package/src/reanimated/ReanimatedTrueSheet.tsx +95 -0
- package/src/reanimated/ReanimatedTrueSheetProvider.tsx +92 -0
- package/src/reanimated/index.ts +3 -0
- package/src/reanimated/useReanimatedPositionChangeHandler.ts +26 -0
- package/src/specs/NativeTrueSheetModule.ts +38 -0
- package/TrueSheet.podspec +0 -49
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetDialog.kt +0 -400
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetEvent.kt +0 -22
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewModule.kt +0 -63
- package/android/src/main/java/com/lodev09/truesheet/core/KeyboardManager.kt +0 -58
- package/android/src/main/java/com/lodev09/truesheet/core/RootSheetView.kt +0 -102
- package/ios/Extensions/UIBlurEffect+withTint.swift +0 -62
- package/ios/Extensions/UIView+pinTo.swift +0 -74
- package/ios/Extensions/UIViewController+detentForSize.swift +0 -134
- package/ios/TrueSheet-Bridging-Header.h +0 -14
- package/ios/TrueSheetEvent.swift +0 -48
- package/ios/TrueSheetView.swift +0 -461
- package/ios/TrueSheetViewController.swift +0 -275
- package/ios/TrueSheetViewManager.m +0 -53
- package/ios/TrueSheetViewManager.swift +0 -48
- package/ios/Utils/Logger.swift +0 -39
- package/ios/Utils/Promise.swift +0 -25
- package/lib/commonjs/TrueSheet.js +0 -258
- package/lib/commonjs/TrueSheet.js.map +0 -1
- package/lib/commonjs/TrueSheet.types.js +0 -6
- package/lib/commonjs/TrueSheet.types.js.map +0 -1
- package/lib/commonjs/TrueSheetFooter.js +0 -19
- package/lib/commonjs/TrueSheetFooter.js.map +0 -1
- package/lib/commonjs/TrueSheetGrabber.js +0 -54
- package/lib/commonjs/TrueSheetGrabber.js.map +0 -1
- package/lib/commonjs/TrueSheetModule.js +0 -19
- package/lib/commonjs/TrueSheetModule.js.map +0 -1
- package/lib/commonjs/__mocks__/index.js +0 -52
- package/lib/commonjs/__mocks__/index.js.map +0 -1
- package/lib/commonjs/index.js +0 -39
- package/lib/commonjs/index.js.map +0 -1
- package/lib/module/TrueSheetFooter.js +0 -14
- package/lib/module/TrueSheetFooter.js.map +0 -1
- package/lib/module/TrueSheetModule.js +0 -15
- package/lib/module/TrueSheetModule.js.map +0 -1
- package/lib/module/__mocks__/index.js +0 -21
- package/lib/module/__mocks__/index.js.map +0 -1
- package/lib/typescript/commonjs/package.json +0 -1
- package/lib/typescript/commonjs/src/TrueSheet.d.ts +0 -70
- package/lib/typescript/commonjs/src/TrueSheet.d.ts.map +0 -1
- package/lib/typescript/commonjs/src/TrueSheet.types.d.ts +0 -241
- package/lib/typescript/commonjs/src/TrueSheet.types.d.ts.map +0 -1
- package/lib/typescript/commonjs/src/TrueSheetFooter.d.ts +0 -7
- package/lib/typescript/commonjs/src/TrueSheetFooter.d.ts.map +0 -1
- package/lib/typescript/commonjs/src/TrueSheetGrabber.d.ts.map +0 -1
- package/lib/typescript/commonjs/src/TrueSheetModule.d.ts +0 -2
- package/lib/typescript/commonjs/src/TrueSheetModule.d.ts.map +0 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +0 -1
- package/lib/typescript/module/src/TrueSheet.d.ts +0 -70
- package/lib/typescript/module/src/TrueSheet.d.ts.map +0 -1
- package/lib/typescript/module/src/TrueSheet.types.d.ts +0 -241
- package/lib/typescript/module/src/TrueSheet.types.d.ts.map +0 -1
- package/lib/typescript/module/src/TrueSheetFooter.d.ts +0 -7
- package/lib/typescript/module/src/TrueSheetFooter.d.ts.map +0 -1
- package/lib/typescript/module/src/TrueSheetGrabber.d.ts +0 -39
- package/lib/typescript/module/src/TrueSheetGrabber.d.ts.map +0 -1
- package/lib/typescript/module/src/TrueSheetModule.d.ts +0 -2
- package/lib/typescript/module/src/TrueSheetModule.d.ts.map +0 -1
- package/lib/typescript/module/src/index.d.ts +0 -4
- package/lib/typescript/module/src/index.d.ts.map +0 -1
- package/src/TrueSheetFooter.tsx +0 -17
- package/src/TrueSheetModule.ts +0 -19
- /package/lib/{typescript/module → module}/package.json +0 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
package com.lodev09.truesheet
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.drawable.ShapeDrawable
|
|
6
|
+
import android.graphics.drawable.shapes.RoundRectShape
|
|
7
|
+
import android.view.MotionEvent
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.view.WindowManager
|
|
10
|
+
import android.view.accessibility.AccessibilityNodeInfo
|
|
11
|
+
import android.widget.FrameLayout
|
|
12
|
+
import androidx.core.view.isNotEmpty
|
|
13
|
+
import com.facebook.react.R
|
|
14
|
+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
15
|
+
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
16
|
+
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
17
|
+
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
18
|
+
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
19
|
+
import com.facebook.react.uimanager.RootView
|
|
20
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
21
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
22
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
23
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
24
|
+
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
25
|
+
import com.lodev09.truesheet.utils.ScreenUtils
|
|
26
|
+
|
|
27
|
+
data class DetentInfo(val index: Int, val position: Float)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Delegate protocol for TrueSheetViewController lifecycle and interaction events.
|
|
31
|
+
* Similar to iOS TrueSheetViewControllerDelegate pattern.
|
|
32
|
+
*/
|
|
33
|
+
interface TrueSheetViewControllerDelegate {
|
|
34
|
+
fun viewControllerWillPresent(index: Int, position: Float)
|
|
35
|
+
fun viewControllerDidPresent(index: Int, position: Float)
|
|
36
|
+
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: Int, position: Float, transitioning: Boolean)
|
|
43
|
+
fun viewControllerDidChangeSize(width: Int, height: Int)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* TrueSheetViewController manages the bottom sheet dialog lifecycle and properties.
|
|
48
|
+
* Similar to iOS TrueSheetViewController pattern.
|
|
49
|
+
*
|
|
50
|
+
* This view acts as both the RootView (handles touch events) and the controller (manages dialog).
|
|
51
|
+
*/
|
|
52
|
+
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
53
|
+
class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
54
|
+
ReactViewGroup(reactContext),
|
|
55
|
+
RootView {
|
|
56
|
+
|
|
57
|
+
companion object {
|
|
58
|
+
private const val TAG_NAME = "TrueSheet"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ==================== RootView Touch Handling ====================
|
|
62
|
+
|
|
63
|
+
internal var eventDispatcher: EventDispatcher? = null
|
|
64
|
+
|
|
65
|
+
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
66
|
+
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Delegate for handling view controller events
|
|
70
|
+
*/
|
|
71
|
+
var delegate: TrueSheetViewControllerDelegate? = null
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The BottomSheetDialog instance - created lazily when container mounts
|
|
75
|
+
*/
|
|
76
|
+
private var dialog: BottomSheetDialog? = null
|
|
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
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The actual bottom sheet view used by Material BottomSheetBehavior
|
|
92
|
+
* This is the view whose position changes during drag
|
|
93
|
+
*/
|
|
94
|
+
private val bottomSheetView: FrameLayout?
|
|
95
|
+
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Our sheet container view from this root view's only child
|
|
99
|
+
*/
|
|
100
|
+
private val containerView: TrueSheetContainerView?
|
|
101
|
+
get() = if (this.isNotEmpty()) {
|
|
102
|
+
this.getChildAt(0) as? TrueSheetContainerView
|
|
103
|
+
} else {
|
|
104
|
+
null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Footer view from the container
|
|
109
|
+
*/
|
|
110
|
+
private val footerView: TrueSheetFooterView?
|
|
111
|
+
get() = containerView?.footerView
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Track if the dialog is currently being dragged
|
|
115
|
+
*/
|
|
116
|
+
private var isDragging = false
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Track if the sheet has been presented (after onShow callback)
|
|
120
|
+
*/
|
|
121
|
+
var isPresented = false
|
|
122
|
+
private set
|
|
123
|
+
|
|
124
|
+
private val edgeToEdgeEnabled: Boolean
|
|
125
|
+
get() {
|
|
126
|
+
// Auto-enable edge-to-edge for Android 16+ (API level 36) if not explicitly set
|
|
127
|
+
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
128
|
+
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether to allow the sheet to extend behind the status bar in edge-to-edge mode
|
|
133
|
+
*/
|
|
134
|
+
var edgeToEdgeFullScreen: Boolean = false
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Top inset to apply to sheet max height calculation (only when not edgeToEdgeFullScreen)
|
|
138
|
+
*/
|
|
139
|
+
private val sheetTopInset: Int
|
|
140
|
+
get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) ScreenUtils.getStatusBarHeight(reactContext) else 0
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Current active detent index
|
|
144
|
+
*/
|
|
145
|
+
var currentDetentIndex: Int = -1
|
|
146
|
+
private set
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Promise callback to be invoked after present is called
|
|
150
|
+
*/
|
|
151
|
+
var presentPromise: (() -> Unit)? = null
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Promise callback to be invoked after dismiss is called
|
|
155
|
+
*/
|
|
156
|
+
var dismissPromise: (() -> Unit)? = null
|
|
157
|
+
|
|
158
|
+
// ==================== Properties ====================
|
|
159
|
+
|
|
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
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The detent index that the sheet should start to dim the background.
|
|
168
|
+
* This is ignored if `dimmed` is set to `false`.
|
|
169
|
+
*/
|
|
170
|
+
var dimmedIndex = 0
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The maximum window height
|
|
174
|
+
*/
|
|
175
|
+
var maxScreenHeight = 0
|
|
176
|
+
|
|
177
|
+
var maxSheetHeight: Int? = null
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The content height from the container view.
|
|
181
|
+
* Set by the host view when content size changes.
|
|
182
|
+
*/
|
|
183
|
+
var contentHeight: Int = 0
|
|
184
|
+
|
|
185
|
+
var dismissible: Boolean = true
|
|
186
|
+
set(value) {
|
|
187
|
+
field = value
|
|
188
|
+
dialog?.apply {
|
|
189
|
+
setCanceledOnTouchOutside(value)
|
|
190
|
+
setCancelable(value)
|
|
191
|
+
behavior.isHideable = value
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
var cornerRadius: Float = 0f
|
|
196
|
+
var sheetBackgroundColor: Int = Color.WHITE
|
|
197
|
+
var detents = mutableListOf(0.5, 1.0)
|
|
198
|
+
|
|
199
|
+
private var windowAnimation: Int = 0
|
|
200
|
+
|
|
201
|
+
init {
|
|
202
|
+
maxScreenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
203
|
+
jSPointerDispatcher = JSPointerDispatcher(this)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ==================== Lifecycle ====================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Creates the dialog instance. Should be called when container view is mounted.
|
|
210
|
+
*/
|
|
211
|
+
fun createDialog() {
|
|
212
|
+
if (dialog != null) return
|
|
213
|
+
|
|
214
|
+
val style = if (edgeToEdgeEnabled) {
|
|
215
|
+
com.lodev09.truesheet.R.style.TrueSheetEdgeToEdgeEnabledDialog
|
|
216
|
+
} else {
|
|
217
|
+
0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
dialog = BottomSheetDialog(reactContext, style).apply {
|
|
221
|
+
setContentView(this@TrueSheetViewController)
|
|
222
|
+
|
|
223
|
+
// Setup window params
|
|
224
|
+
window?.apply {
|
|
225
|
+
// Store current windowAnimation value to toggle later
|
|
226
|
+
windowAnimation = attributes.windowAnimations
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Setup dialog lifecycle listeners
|
|
230
|
+
setupDialogListeners(this)
|
|
231
|
+
|
|
232
|
+
// Setup bottom sheet behavior callbacks
|
|
233
|
+
setupBottomSheetBehavior(this)
|
|
234
|
+
|
|
235
|
+
// Apply initial properties
|
|
236
|
+
setCanceledOnTouchOutside(dismissible)
|
|
237
|
+
setCancelable(dismissible)
|
|
238
|
+
behavior.isHideable = dismissible
|
|
239
|
+
|
|
240
|
+
// Apply background color and corner radius
|
|
241
|
+
setupBackground()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Cleans up the dialog instance. Called when dismissed to ensure clean state.
|
|
247
|
+
*/
|
|
248
|
+
private fun cleanupDialog() {
|
|
249
|
+
dialog?.apply {
|
|
250
|
+
setOnShowListener(null)
|
|
251
|
+
setOnCancelListener(null)
|
|
252
|
+
setOnDismissListener(null)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Remove this view from its parent to allow re-attachment on next presentation
|
|
256
|
+
sheetContainer?.removeView(this)
|
|
257
|
+
|
|
258
|
+
dialog = null
|
|
259
|
+
isDragging = false
|
|
260
|
+
isPresented = false
|
|
261
|
+
}
|
|
262
|
+
|
|
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
|
+
}
|
|
289
|
+
|
|
290
|
+
// Initialize footer position after layout is complete
|
|
291
|
+
positionFooter()
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Setup listener when the dialog is about to be dismissed
|
|
296
|
+
dialog.setOnCancelListener {
|
|
297
|
+
// Notify delegate
|
|
298
|
+
delegate?.viewControllerWillDismiss()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Setup listener when the dialog has been dismissed
|
|
302
|
+
dialog.setOnDismissListener {
|
|
303
|
+
// Resolve the dismiss promise
|
|
304
|
+
dismissPromise?.let { promise ->
|
|
305
|
+
promise()
|
|
306
|
+
dismissPromise = null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Notify delegate
|
|
310
|
+
delegate?.viewControllerDidDismiss()
|
|
311
|
+
|
|
312
|
+
// Clean up the dialog for next presentation
|
|
313
|
+
cleanupDialog()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Setup bottom sheet behavior callbacks
|
|
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
|
+
}
|
|
338
|
+
|
|
339
|
+
// Update footer position during slide
|
|
340
|
+
positionFooter(slideOffset)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
344
|
+
// Handle STATE_HIDDEN before checking isPresented
|
|
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
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!isPresented) return
|
|
352
|
+
|
|
353
|
+
when (newState) {
|
|
354
|
+
// When changed to dragging, we know that the drag has started
|
|
355
|
+
BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
|
|
356
|
+
|
|
357
|
+
// Either of the following state determines drag end
|
|
358
|
+
BottomSheetBehavior.STATE_EXPANDED,
|
|
359
|
+
BottomSheetBehavior.STATE_COLLAPSED,
|
|
360
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED -> handleDragEnd(newState)
|
|
361
|
+
|
|
362
|
+
else -> {}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ==================== Presentation ====================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Present the sheet.
|
|
373
|
+
*/
|
|
374
|
+
fun present(detentIndex: Int, animated: Boolean = true) {
|
|
375
|
+
val dialog = this.dialog ?: run {
|
|
376
|
+
// Dialog not created yet - this shouldn't happen but handle gracefully
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
currentDetentIndex = detentIndex
|
|
381
|
+
setupDimmedBackground(detentIndex)
|
|
382
|
+
|
|
383
|
+
if (isPresented) {
|
|
384
|
+
// For consistency with iOS, we notify detent change immediately
|
|
385
|
+
// when already presented (not waiting for state to change)
|
|
386
|
+
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
387
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position)
|
|
388
|
+
|
|
389
|
+
// Note: onSlide will be called during resize animation, no need to emit position change here
|
|
390
|
+
setStateForDetentIndex(detentIndex)
|
|
391
|
+
} else {
|
|
392
|
+
// Reset drag state before presenting
|
|
393
|
+
isDragging = false
|
|
394
|
+
|
|
395
|
+
setupSheetDetents()
|
|
396
|
+
setStateForDetentIndex(detentIndex)
|
|
397
|
+
|
|
398
|
+
// Notify delegate before showing
|
|
399
|
+
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
400
|
+
delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position)
|
|
401
|
+
|
|
402
|
+
if (!animated) {
|
|
403
|
+
// Disable animation
|
|
404
|
+
dialog.window?.setWindowAnimations(0)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
dialog.show()
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Dismiss the sheet.
|
|
413
|
+
*/
|
|
414
|
+
fun dismiss() {
|
|
415
|
+
this.post {
|
|
416
|
+
// Emit position change with transitioning=true to animate dismissal
|
|
417
|
+
// Use maxScreenHeight as the off-screen position (sheet slides down off screen)
|
|
418
|
+
val offScreenPosition = maxScreenHeight.pxToDp()
|
|
419
|
+
delegate?.viewControllerDidChangePosition(currentDetentIndex, offScreenPosition, transitioning = true)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
dialog?.dismiss()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ==================== Configuration ====================
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Setup sheet detents based on the detent preference.
|
|
429
|
+
*/
|
|
430
|
+
fun setupSheetDetents() {
|
|
431
|
+
val behavior = this.behavior ?: return
|
|
432
|
+
|
|
433
|
+
// Configure sheet sizes
|
|
434
|
+
behavior.apply {
|
|
435
|
+
skipCollapsed = false
|
|
436
|
+
isFitToContents = true
|
|
437
|
+
|
|
438
|
+
// m3 max width 640dp
|
|
439
|
+
maxWidth = 640.0.dpToPx().toInt()
|
|
440
|
+
|
|
441
|
+
when (detents.size) {
|
|
442
|
+
1 -> {
|
|
443
|
+
val detentHeight = getDetentHeight(detents[0])
|
|
444
|
+
maxHeight = detentHeight
|
|
445
|
+
skipCollapsed = true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
2 -> {
|
|
449
|
+
val peekHeight = getDetentHeight(detents[0])
|
|
450
|
+
val maxHeightValue = getDetentHeight(detents[1])
|
|
451
|
+
maxHeight = maxHeightValue
|
|
452
|
+
setPeekHeight(peekHeight, isPresented)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
3 -> {
|
|
456
|
+
// Enables half expanded
|
|
457
|
+
isFitToContents = false
|
|
458
|
+
|
|
459
|
+
val peekHeightValue = getDetentHeight(detents[0])
|
|
460
|
+
val middleDetentHeight = getDetentHeight(detents[1])
|
|
461
|
+
val maxHeightValue = getDetentHeight(detents[2])
|
|
462
|
+
|
|
463
|
+
setPeekHeight(peekHeightValue, isPresented)
|
|
464
|
+
maxHeight = maxHeightValue
|
|
465
|
+
expandedOffset = sheetTopInset
|
|
466
|
+
halfExpandedRatio = minOf(middleDetentHeight.toFloat() / maxScreenHeight.toFloat(), 1.0f)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Force a layout update when sheet is presented (e.g., during rotation)
|
|
471
|
+
// This ensures the container respects the new maxHeight constraint
|
|
472
|
+
if (isPresented) {
|
|
473
|
+
sheetContainer?.apply {
|
|
474
|
+
val params = layoutParams
|
|
475
|
+
params.height = maxHeight
|
|
476
|
+
layoutParams = params
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Setup background color and corner radius.
|
|
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
|
+
)
|
|
497
|
+
|
|
498
|
+
val background = ShapeDrawable(RoundRectShape(outerRadii, null, null))
|
|
499
|
+
background.paint.color = sheetBackgroundColor
|
|
500
|
+
|
|
501
|
+
this.background = background
|
|
502
|
+
this.clipToOutline = true
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Setup dimmed sheet.
|
|
508
|
+
* `dimmedIndex` will further customize the dimming behavior.
|
|
509
|
+
*/
|
|
510
|
+
fun setupDimmedBackground(detentIndex: Int) {
|
|
511
|
+
val dialog = this.dialog ?: return
|
|
512
|
+
dialog.window?.apply {
|
|
513
|
+
val view = findViewById<View>(com.google.android.material.R.id.touch_outside)
|
|
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
|
+
}
|
|
533
|
+
|
|
534
|
+
// Remove the dimmed background
|
|
535
|
+
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
536
|
+
|
|
537
|
+
dialog.setCanceledOnTouchOutside(false)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
fun resetAnimation() {
|
|
543
|
+
dialog?.window?.apply {
|
|
544
|
+
setWindowAnimations(windowAnimation)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
fun positionFooter(slideOffset: Float? = null) {
|
|
549
|
+
val footer = footerView ?: return
|
|
550
|
+
val bottomSheet = bottomSheetView ?: return
|
|
551
|
+
val footerHeight = footer.height
|
|
552
|
+
|
|
553
|
+
val bottomSheetY = ScreenUtils.getScreenY(bottomSheet)
|
|
554
|
+
|
|
555
|
+
// Calculate footer Y position based on bottom sheet position
|
|
556
|
+
var footerY = (maxScreenHeight - bottomSheetY - footerHeight).toFloat()
|
|
557
|
+
|
|
558
|
+
// Animate footer down with sheet when below peek height
|
|
559
|
+
if (slideOffset != null && slideOffset < 0) {
|
|
560
|
+
footerY -= (footerHeight * slideOffset)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Clamp footer position to prevent it from going off screen when positioning at the top
|
|
564
|
+
// This happens when fullScreen is enabled in edge-to-edge mode
|
|
565
|
+
val statusBarHeight = ScreenUtils.getStatusBarHeight(reactContext)
|
|
566
|
+
val maxAllowedY = (maxScreenHeight - statusBarHeight - footerHeight).toFloat()
|
|
567
|
+
footer.y = minOf(footerY, maxAllowedY)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Set the state based for the given detent index.
|
|
572
|
+
*/
|
|
573
|
+
fun setStateForDetentIndex(index: Int) {
|
|
574
|
+
behavior?.state = getStateForDetentIndex(index)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
fun setSoftInputMode(mode: Int) {
|
|
578
|
+
dialog?.window?.setSoftInputMode(mode)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ==================== Drag Handling ====================
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Get current detent info from sheet view position
|
|
585
|
+
*/
|
|
586
|
+
private fun getCurrentDetentInfo(sheetView: View): DetentInfo {
|
|
587
|
+
// Get the Y position in screen coordinates (like iOS presentedView.frame.origin.y)
|
|
588
|
+
val screenY = ScreenUtils.getScreenY(sheetView)
|
|
589
|
+
val position = screenY.pxToDp()
|
|
590
|
+
return DetentInfo(currentDetentIndex, position)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Handle drag begin
|
|
595
|
+
*/
|
|
596
|
+
private fun handleDragBegin(sheetView: View) {
|
|
597
|
+
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
598
|
+
delegate?.viewControllerDidDragBegin(detentInfo.index, detentInfo.position)
|
|
599
|
+
isDragging = true
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Handle drag change
|
|
604
|
+
*/
|
|
605
|
+
private fun handleDragChange(sheetView: View) {
|
|
606
|
+
if (!isDragging) return
|
|
607
|
+
|
|
608
|
+
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
609
|
+
delegate?.viewControllerDidDragChange(detentInfo.index, detentInfo.position)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Handle drag end
|
|
614
|
+
*/
|
|
615
|
+
private fun handleDragEnd(state: Int) {
|
|
616
|
+
if (!isDragging) return
|
|
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
|
+
}
|
|
632
|
+
|
|
633
|
+
currentDetentIndex = it.index
|
|
634
|
+
setupDimmedBackground(it.index)
|
|
635
|
+
|
|
636
|
+
// Notify delegate of detent change
|
|
637
|
+
delegate?.viewControllerDidChangeDetent(it.index, it.position)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
isDragging = false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ==================== Detent Calculations ====================
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Get the height value based on the detent config value.
|
|
648
|
+
*/
|
|
649
|
+
private fun getDetentHeight(detent: Double): Int {
|
|
650
|
+
val height: Int = if (detent == -1.0) {
|
|
651
|
+
// -1.0 represents "auto"
|
|
652
|
+
contentHeight
|
|
653
|
+
} else {
|
|
654
|
+
if (detent <= 0.0 || detent > 1.0) {
|
|
655
|
+
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
656
|
+
}
|
|
657
|
+
(detent * maxScreenHeight).toInt()
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Apply top inset when edge-to-edge is enabled and fullScreen is false
|
|
661
|
+
val maxAllowedHeight = maxScreenHeight - sheetTopInset
|
|
662
|
+
val finalHeight = maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
663
|
+
return finalHeight
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Determines the state based from the given detent index.
|
|
668
|
+
*/
|
|
669
|
+
private fun getStateForDetentIndex(index: Int): Int =
|
|
670
|
+
when (detents.size) {
|
|
671
|
+
1 -> {
|
|
672
|
+
BottomSheetBehavior.STATE_EXPANDED
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
2 -> {
|
|
676
|
+
when (index) {
|
|
677
|
+
0 -> BottomSheetBehavior.STATE_COLLAPSED
|
|
678
|
+
1 -> BottomSheetBehavior.STATE_EXPANDED
|
|
679
|
+
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
3 -> {
|
|
684
|
+
when (index) {
|
|
685
|
+
0 -> BottomSheetBehavior.STATE_COLLAPSED
|
|
686
|
+
1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
687
|
+
2 -> BottomSheetBehavior.STATE_EXPANDED
|
|
688
|
+
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Get the DetentInfo data by state.
|
|
697
|
+
*/
|
|
698
|
+
fun getDetentInfoForState(state: Int): DetentInfo? =
|
|
699
|
+
when (detents.size) {
|
|
700
|
+
1 -> {
|
|
701
|
+
when (state) {
|
|
702
|
+
BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
703
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
704
|
+
else -> null
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
2 -> {
|
|
709
|
+
when (state) {
|
|
710
|
+
BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
711
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
|
|
712
|
+
else -> null
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
3 -> {
|
|
717
|
+
when (state) {
|
|
718
|
+
BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
719
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
|
|
720
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(2, getPositionForDetentIndex(2))
|
|
721
|
+
else -> null
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
else -> null
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Calculate the expected Y position for a given detent index.
|
|
730
|
+
* Uses actual screen position if available, otherwise calculates based on screen height.
|
|
731
|
+
*/
|
|
732
|
+
private fun getPositionForDetentIndex(index: Int): Float {
|
|
733
|
+
if (index < 0 || index >= detents.size) {
|
|
734
|
+
return 0f
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Try to get actual position from bottom sheet view first (same view used in behavior callbacks)
|
|
738
|
+
bottomSheetView?.let {
|
|
739
|
+
it
|
|
740
|
+
val screenY = ScreenUtils.getScreenY(it)
|
|
741
|
+
// Only use actual position if sheet has been positioned (screenY > 0)
|
|
742
|
+
if (screenY > 0) {
|
|
743
|
+
return screenY.pxToDp()
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Fallback: calculate expected position
|
|
748
|
+
val detentHeight = getDetentHeight(detents[index])
|
|
749
|
+
|
|
750
|
+
// Position calculation is simple: screen height - sheet height
|
|
751
|
+
// In both edge-to-edge and non-edge-to-edge modes, getScreenY returns
|
|
752
|
+
// coordinates in screen space, and maxScreenHeight represents the available height
|
|
753
|
+
// for the sheet, so the calculation is the same
|
|
754
|
+
val positionPx = maxScreenHeight - detentHeight
|
|
755
|
+
|
|
756
|
+
return positionPx.pxToDp()
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Get DetentInfo data for given detent index.
|
|
761
|
+
*/
|
|
762
|
+
fun getDetentInfoForIndex(index: Int) = getDetentInfoForState(getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
|
|
763
|
+
|
|
764
|
+
// ==================== RootView Implementation ====================
|
|
765
|
+
|
|
766
|
+
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
|
|
767
|
+
super.onInitializeAccessibilityNodeInfo(info)
|
|
768
|
+
|
|
769
|
+
val testId = getTag(R.id.react_test_id) as String?
|
|
770
|
+
if (testId != null) {
|
|
771
|
+
info.viewIdResourceName = testId
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
776
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
777
|
+
|
|
778
|
+
// Only proceed if size actually changed
|
|
779
|
+
if (w == oldw && h == oldh) return
|
|
780
|
+
|
|
781
|
+
// Update screen height based on new dimensions
|
|
782
|
+
val oldMaxScreenHeight = maxScreenHeight
|
|
783
|
+
maxScreenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
784
|
+
|
|
785
|
+
// Only handle rotation if sheet is presented and screen height actually changed
|
|
786
|
+
if (isPresented && oldMaxScreenHeight != maxScreenHeight && oldMaxScreenHeight > 0) {
|
|
787
|
+
// Recalculate sheet detents with new screen dimensions
|
|
788
|
+
setupSheetDetents()
|
|
789
|
+
|
|
790
|
+
this.post {
|
|
791
|
+
// Update footer position after rotation
|
|
792
|
+
positionFooter()
|
|
793
|
+
|
|
794
|
+
// Notify JS about position change after rotation settles
|
|
795
|
+
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
796
|
+
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = true)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Notify delegate about size change
|
|
801
|
+
delegate?.viewControllerDidChangeSize(w, h)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
override fun handleException(t: Throwable) {
|
|
805
|
+
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
809
|
+
eventDispatcher?.let { eventDispatcher ->
|
|
810
|
+
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext)
|
|
811
|
+
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
|
|
812
|
+
}
|
|
813
|
+
return super.onInterceptTouchEvent(event)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
817
|
+
eventDispatcher?.let { eventDispatcher ->
|
|
818
|
+
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext)
|
|
819
|
+
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
|
|
820
|
+
}
|
|
821
|
+
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
|
+
return true
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
|
828
|
+
eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, true) }
|
|
829
|
+
return super.onHoverEvent(event)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
833
|
+
eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, false) }
|
|
834
|
+
return super.onHoverEvent(event)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
838
|
+
@Suppress("DEPRECATION")
|
|
839
|
+
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
840
|
+
eventDispatcher?.let { eventDispatcher ->
|
|
841
|
+
jSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher, reactContext)
|
|
842
|
+
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
847
|
+
eventDispatcher?.let { jSTouchDispatcher.onChildEndedNativeGesture(ev, it) }
|
|
848
|
+
jSPointerDispatcher?.onChildEndedNativeGesture()
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
852
|
+
// Allow the request to propagate to parent
|
|
853
|
+
super.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
854
|
+
}
|
|
855
|
+
}
|