@lodev09/react-native-true-sheet 3.0.0-beta.0 → 3.0.0-beta.10
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 +7 -21
- package/RNTrueSheet.podspec +5 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +58 -60
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +10 -18
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +76 -20
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +21 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +104 -146
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +315 -403
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +24 -11
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +116 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +33 -5
- package/android/src/main/jni/CMakeLists.txt +87 -0
- package/android/src/main/jni/TrueSheetSpec.h +17 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +46 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +42 -0
- package/ios/TrueSheetContainerView.h +23 -0
- package/ios/TrueSheetContainerView.mm +80 -13
- package/ios/TrueSheetContentView.h +9 -1
- package/ios/TrueSheetContentView.mm +102 -24
- package/ios/TrueSheetFooterView.mm +2 -2
- package/ios/TrueSheetHeaderView.h +29 -0
- package/ios/TrueSheetHeaderView.mm +60 -0
- package/ios/TrueSheetView.h +0 -16
- package/ios/TrueSheetView.mm +208 -190
- package/ios/TrueSheetViewController.h +17 -4
- package/ios/TrueSheetViewController.mm +197 -266
- package/ios/utils/ConversionUtil.h +24 -0
- package/ios/utils/ConversionUtil.mm +50 -0
- package/ios/utils/LayoutUtil.h +11 -1
- package/ios/utils/LayoutUtil.mm +32 -1
- package/lib/module/TrueSheet.js +30 -33
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +6 -8
- package/lib/typescript/src/TrueSheet.d.ts +0 -3
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +30 -43
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.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/TrueSheetViewNativeComponent.d.ts +3 -6
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/package.json +9 -5
- package/react-native.config.js +5 -2
- package/src/TrueSheet.tsx +34 -31
- package/src/TrueSheet.types.ts +47 -61
- package/src/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +6 -8
- package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +0 -27
- package/ios/events/OnSizeChangeEvent.h +0 -28
- package/ios/events/OnSizeChangeEvent.mm +0 -30
|
@@ -2,8 +2,11 @@ package com.lodev09.truesheet
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.graphics.Color
|
|
5
|
+
import android.graphics.drawable.GradientDrawable
|
|
5
6
|
import android.graphics.drawable.ShapeDrawable
|
|
6
7
|
import android.graphics.drawable.shapes.RoundRectShape
|
|
8
|
+
import android.util.TypedValue
|
|
9
|
+
import android.view.Gravity
|
|
7
10
|
import android.view.MotionEvent
|
|
8
11
|
import android.view.View
|
|
9
12
|
import android.view.WindowManager
|
|
@@ -11,7 +14,6 @@ import android.view.accessibility.AccessibilityNodeInfo
|
|
|
11
14
|
import android.widget.FrameLayout
|
|
12
15
|
import androidx.core.view.isNotEmpty
|
|
13
16
|
import com.facebook.react.R
|
|
14
|
-
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
15
17
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
16
18
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
17
19
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
@@ -19,17 +21,15 @@ import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
|
19
21
|
import com.facebook.react.uimanager.RootView
|
|
20
22
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
21
23
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
24
|
+
import com.facebook.react.util.RNLog
|
|
22
25
|
import com.facebook.react.views.view.ReactViewGroup
|
|
23
26
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
24
27
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
28
|
+
import com.lodev09.truesheet.core.RNScreensFragmentObserver
|
|
25
29
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
26
30
|
|
|
27
31
|
data class DetentInfo(val index: Int, val position: Float)
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
* Delegate protocol for TrueSheetViewController lifecycle and interaction events.
|
|
31
|
-
* Similar to iOS TrueSheetViewControllerDelegate pattern.
|
|
32
|
-
*/
|
|
33
33
|
interface TrueSheetViewControllerDelegate {
|
|
34
34
|
fun viewControllerWillPresent(index: Int, position: Float)
|
|
35
35
|
fun viewControllerDidPresent(index: Int, position: Float)
|
|
@@ -44,10 +44,8 @@ interface TrueSheetViewControllerDelegate {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* TrueSheetViewController manages the bottom sheet dialog
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* This view acts as both the RootView (handles touch events) and the controller (manages dialog).
|
|
47
|
+
* TrueSheetViewController manages the bottom sheet dialog and its presentation lifecycle.
|
|
48
|
+
* This view also acts as a RootView to properly handle and dispatch touch events to React Native.
|
|
51
49
|
*/
|
|
52
50
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
53
51
|
class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
@@ -55,132 +53,84 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
55
53
|
RootView {
|
|
56
54
|
|
|
57
55
|
companion object {
|
|
58
|
-
|
|
56
|
+
const val TAG_NAME = "TrueSheet"
|
|
57
|
+
|
|
58
|
+
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
59
|
+
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
60
|
+
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
61
|
+
private const val GRABBER_WIDTH = 32f // dp
|
|
62
|
+
private const val GRABBER_HEIGHT = 4f // dp
|
|
63
|
+
private const val GRABBER_TOP_MARGIN = 16f // dp
|
|
64
|
+
private val GRABBER_COLOR = Color.argb((0.4 * 255).toInt(), 73, 69, 79) // #49454F @ 40%
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets the effective sheet height by subtracting headerHeight * 2.
|
|
68
|
+
* This is needed because both native layout and Yoga layout account for the header separately.
|
|
69
|
+
*/
|
|
70
|
+
fun getEffectiveSheetHeight(sheetHeight: Int, headerHeight: Int): Int = sheetHeight - headerHeight * 2
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
66
|
-
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
73
|
+
// ====================================================================
|
|
74
|
+
// MARK: - Delegate
|
|
75
|
+
// ====================================================================
|
|
67
76
|
|
|
68
|
-
/**
|
|
69
|
-
* Delegate for handling view controller events
|
|
70
|
-
*/
|
|
71
77
|
var delegate: TrueSheetViewControllerDelegate? = null
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
// ====================================================================
|
|
80
|
+
// MARK: - Dialog & Views
|
|
81
|
+
// ====================================================================
|
|
82
|
+
|
|
76
83
|
private var dialog: BottomSheetDialog? = null
|
|
77
84
|
|
|
78
|
-
/**
|
|
79
|
-
* The sheet behavior from the dialog
|
|
80
|
-
*/
|
|
81
85
|
private val behavior: BottomSheetBehavior<FrameLayout>?
|
|
82
86
|
get() = dialog?.behavior
|
|
83
87
|
|
|
84
|
-
/**
|
|
85
|
-
* The sheet container view from Material BottomSheetDialog (our parent)
|
|
86
|
-
*/
|
|
87
88
|
private val sheetContainer: FrameLayout?
|
|
88
89
|
get() = this.parent as? FrameLayout
|
|
89
90
|
|
|
90
|
-
/**
|
|
91
|
-
* The actual bottom sheet view used by Material BottomSheetBehavior
|
|
92
|
-
* This is the view whose position changes during drag
|
|
93
|
-
*/
|
|
94
91
|
private val bottomSheetView: FrameLayout?
|
|
95
92
|
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
96
93
|
|
|
97
|
-
/**
|
|
98
|
-
* Our sheet container view from this root view's only child
|
|
99
|
-
*/
|
|
100
94
|
private val containerView: TrueSheetContainerView?
|
|
101
|
-
get() = if (this.isNotEmpty())
|
|
102
|
-
this.getChildAt(0) as? TrueSheetContainerView
|
|
103
|
-
} else {
|
|
104
|
-
null
|
|
105
|
-
}
|
|
95
|
+
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
106
96
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
*/
|
|
110
|
-
private val footerView: TrueSheetFooterView?
|
|
111
|
-
get() = containerView?.footerView
|
|
97
|
+
private val contentHeight: Int
|
|
98
|
+
get() = containerView?.contentHeight ?: 0
|
|
112
99
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
100
|
+
private val headerHeight: Int
|
|
101
|
+
get() = containerView?.headerHeight ?: 0
|
|
102
|
+
|
|
103
|
+
// ====================================================================
|
|
104
|
+
// MARK: - State
|
|
105
|
+
// ====================================================================
|
|
117
106
|
|
|
118
|
-
/**
|
|
119
|
-
* Track if the sheet has been presented (after onShow callback)
|
|
120
|
-
*/
|
|
121
107
|
var isPresented = false
|
|
122
108
|
private set
|
|
123
109
|
|
|
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
110
|
var currentDetentIndex: Int = -1
|
|
146
111
|
private set
|
|
147
112
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
*/
|
|
151
|
-
var presentPromise: (() -> Unit)? = null
|
|
113
|
+
private var isDragging = false
|
|
114
|
+
private var windowAnimation: Int = 0
|
|
152
115
|
|
|
153
|
-
|
|
154
|
-
* Promise callback to be invoked after dismiss is called
|
|
155
|
-
*/
|
|
116
|
+
var presentPromise: (() -> Unit)? = null
|
|
156
117
|
var dismissPromise: (() -> Unit)? = null
|
|
157
118
|
|
|
158
|
-
//
|
|
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
|
|
119
|
+
// ====================================================================
|
|
120
|
+
// MARK: - Configuration Properties
|
|
121
|
+
// ====================================================================
|
|
176
122
|
|
|
123
|
+
var screenHeight = 0
|
|
124
|
+
var screenWidth = 0
|
|
177
125
|
var maxSheetHeight: Int? = null
|
|
126
|
+
var detents = mutableListOf(0.5, 1.0)
|
|
178
127
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
var
|
|
128
|
+
var dimmed = true
|
|
129
|
+
var dimmedDetentIndex = 0
|
|
130
|
+
var grabber: Boolean = true
|
|
131
|
+
var sheetCornerRadius: Float = -1f
|
|
132
|
+
var sheetBackgroundColor: Int = 0
|
|
133
|
+
var edgeToEdgeFullScreen: Boolean = false
|
|
184
134
|
|
|
185
135
|
var dismissible: Boolean = true
|
|
186
136
|
set(value) {
|
|
@@ -192,22 +142,66 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
192
142
|
}
|
|
193
143
|
}
|
|
194
144
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
145
|
+
// ====================================================================
|
|
146
|
+
// MARK: - Computed Properties
|
|
147
|
+
// ====================================================================
|
|
198
148
|
|
|
199
|
-
|
|
149
|
+
val statusBarHeight: Int
|
|
150
|
+
get() = ScreenUtils.getStatusBarHeight(reactContext)
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Edge-to-edge is enabled by default on API 36+, or when explicitly configured.
|
|
154
|
+
*/
|
|
155
|
+
private val edgeToEdgeEnabled: Boolean
|
|
156
|
+
get() {
|
|
157
|
+
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
158
|
+
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The top inset to apply when edge-to-edge is enabled but not full-screen.
|
|
163
|
+
* This prevents the sheet from going under the status bar.
|
|
164
|
+
*/
|
|
165
|
+
private val sheetTopInset: Int
|
|
166
|
+
get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) statusBarHeight else 0
|
|
167
|
+
|
|
168
|
+
// ====================================================================
|
|
169
|
+
// MARK: - Touch Dispatchers
|
|
170
|
+
// ====================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Touch dispatchers are required for RootView to properly forward touch events to React Native.
|
|
174
|
+
*/
|
|
175
|
+
internal var eventDispatcher: EventDispatcher? = null
|
|
176
|
+
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
177
|
+
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
178
|
+
|
|
179
|
+
// ====================================================================
|
|
180
|
+
// MARK: - Modal Observer
|
|
181
|
+
// ====================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Observes react-native-screens modal fragments to hide/show the sheet appropriately.
|
|
185
|
+
* This prevents the sheet from rendering on top of modals.
|
|
186
|
+
*/
|
|
187
|
+
private var rnScreensObserver: RNScreensFragmentObserver? = null
|
|
188
|
+
|
|
189
|
+
fun hasActiveModals(): Boolean = rnScreensObserver?.hasActiveModals() ?: false
|
|
190
|
+
|
|
191
|
+
// ====================================================================
|
|
192
|
+
// MARK: - Initialization
|
|
193
|
+
// ====================================================================
|
|
200
194
|
|
|
201
195
|
init {
|
|
202
|
-
|
|
196
|
+
screenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
197
|
+
screenWidth = ScreenUtils.getScreenWidth(reactContext)
|
|
203
198
|
jSPointerDispatcher = JSPointerDispatcher(this)
|
|
204
199
|
}
|
|
205
200
|
|
|
206
|
-
//
|
|
201
|
+
// ====================================================================
|
|
202
|
+
// MARK: - Dialog Lifecycle
|
|
203
|
+
// ====================================================================
|
|
207
204
|
|
|
208
|
-
/**
|
|
209
|
-
* Creates the dialog instance. Should be called when container view is mounted.
|
|
210
|
-
*/
|
|
211
205
|
fun createDialog() {
|
|
212
206
|
if (dialog != null) return
|
|
213
207
|
|
|
@@ -220,31 +214,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
220
214
|
dialog = BottomSheetDialog(reactContext, style).apply {
|
|
221
215
|
setContentView(this@TrueSheetViewController)
|
|
222
216
|
|
|
223
|
-
// Setup window params
|
|
224
217
|
window?.apply {
|
|
225
|
-
// Store current windowAnimation value to toggle later
|
|
226
218
|
windowAnimation = attributes.windowAnimations
|
|
227
219
|
}
|
|
228
220
|
|
|
229
|
-
|
|
221
|
+
setupModalObserver()
|
|
230
222
|
setupDialogListeners(this)
|
|
231
|
-
|
|
232
|
-
// Setup bottom sheet behavior callbacks
|
|
233
223
|
setupBottomSheetBehavior(this)
|
|
234
224
|
|
|
235
|
-
// Apply initial properties
|
|
236
225
|
setCanceledOnTouchOutside(dismissible)
|
|
237
226
|
setCancelable(dismissible)
|
|
238
227
|
behavior.isHideable = dismissible
|
|
239
|
-
|
|
240
|
-
// Apply background color and corner radius
|
|
241
|
-
setupBackground()
|
|
242
228
|
}
|
|
243
229
|
}
|
|
244
230
|
|
|
245
|
-
/**
|
|
246
|
-
* Cleans up the dialog instance. Called when dismissed to ensure clean state.
|
|
247
|
-
*/
|
|
248
231
|
private fun cleanupDialog() {
|
|
249
232
|
dialog?.apply {
|
|
250
233
|
setOnShowListener(null)
|
|
@@ -252,7 +235,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
252
235
|
setOnDismissListener(null)
|
|
253
236
|
}
|
|
254
237
|
|
|
255
|
-
|
|
238
|
+
cleanupModalObserver()
|
|
256
239
|
sheetContainer?.removeView(this)
|
|
257
240
|
|
|
258
241
|
dialog = null
|
|
@@ -260,89 +243,56 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
260
243
|
isPresented = false
|
|
261
244
|
}
|
|
262
245
|
|
|
263
|
-
/**
|
|
264
|
-
* Setup dialog lifecycle listeners
|
|
265
|
-
*/
|
|
266
246
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
267
|
-
// Setup listener when the dialog has been presented
|
|
268
247
|
dialog.setOnShowListener {
|
|
269
248
|
isPresented = true
|
|
270
|
-
|
|
271
|
-
// Re-enable animation
|
|
272
249
|
resetAnimation()
|
|
250
|
+
setupBackground()
|
|
251
|
+
setupGrabber()
|
|
273
252
|
|
|
274
|
-
// Wait for the sheet to settle before notifying didPresent
|
|
275
|
-
// The sheet animates to its final position after onShow fires
|
|
276
253
|
sheetContainer?.post {
|
|
277
|
-
// Notify delegate with the settled position
|
|
278
254
|
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
279
255
|
delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position)
|
|
280
|
-
|
|
281
|
-
// Emit position change with transitioning=true so Reanimated can animate it
|
|
282
256
|
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = true)
|
|
283
257
|
|
|
284
|
-
|
|
285
|
-
presentPromise
|
|
286
|
-
promise()
|
|
287
|
-
presentPromise = null
|
|
288
|
-
}
|
|
258
|
+
presentPromise?.invoke()
|
|
259
|
+
presentPromise = null
|
|
289
260
|
|
|
290
|
-
// Initialize footer position after layout is complete
|
|
291
261
|
positionFooter()
|
|
292
262
|
}
|
|
293
263
|
}
|
|
294
264
|
|
|
295
|
-
// Setup listener when the dialog is about to be dismissed
|
|
296
265
|
dialog.setOnCancelListener {
|
|
297
|
-
// Notify delegate
|
|
298
266
|
delegate?.viewControllerWillDismiss()
|
|
299
267
|
}
|
|
300
268
|
|
|
301
|
-
// Setup listener when the dialog has been dismissed
|
|
302
269
|
dialog.setOnDismissListener {
|
|
303
|
-
|
|
304
|
-
dismissPromise
|
|
305
|
-
promise()
|
|
306
|
-
dismissPromise = null
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Notify delegate
|
|
270
|
+
dismissPromise?.invoke()
|
|
271
|
+
dismissPromise = null
|
|
310
272
|
delegate?.viewControllerDidDismiss()
|
|
311
|
-
|
|
312
|
-
// Clean up the dialog for next presentation
|
|
313
273
|
cleanupDialog()
|
|
314
274
|
}
|
|
315
275
|
}
|
|
316
276
|
|
|
317
|
-
/**
|
|
318
|
-
* Setup bottom sheet behavior callbacks
|
|
319
|
-
*/
|
|
320
277
|
private fun setupBottomSheetBehavior(dialog: BottomSheetDialog) {
|
|
321
278
|
dialog.behavior.addBottomSheetCallback(
|
|
322
279
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
|
323
280
|
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
324
281
|
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
282
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
329
283
|
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = false)
|
|
330
284
|
|
|
331
285
|
when (behavior.state) {
|
|
332
|
-
// For consistency with iOS, we consider SETTLING as dragging change
|
|
333
286
|
BottomSheetBehavior.STATE_DRAGGING,
|
|
334
287
|
BottomSheetBehavior.STATE_SETTLING -> handleDragChange(sheetView)
|
|
335
288
|
|
|
336
289
|
else -> { }
|
|
337
290
|
}
|
|
338
291
|
|
|
339
|
-
// Update footer position during slide
|
|
340
292
|
positionFooter(slideOffset)
|
|
341
293
|
}
|
|
342
294
|
|
|
343
295
|
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
296
|
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
|
347
297
|
dismiss()
|
|
348
298
|
return
|
|
@@ -351,10 +301,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
351
301
|
if (!isPresented) return
|
|
352
302
|
|
|
353
303
|
when (newState) {
|
|
354
|
-
// When changed to dragging, we know that the drag has started
|
|
355
304
|
BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
|
|
356
305
|
|
|
357
|
-
// Either of the following state determines drag end
|
|
358
306
|
BottomSheetBehavior.STATE_EXPANDED,
|
|
359
307
|
BottomSheetBehavior.STATE_COLLAPSED,
|
|
360
308
|
BottomSheetBehavior.STATE_HALF_EXPANDED -> handleDragEnd(newState)
|
|
@@ -366,14 +314,35 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
366
314
|
)
|
|
367
315
|
}
|
|
368
316
|
|
|
369
|
-
|
|
317
|
+
private fun setupModalObserver() {
|
|
318
|
+
rnScreensObserver = RNScreensFragmentObserver(
|
|
319
|
+
reactContext = reactContext,
|
|
320
|
+
onModalPresented = {
|
|
321
|
+
if (isPresented) {
|
|
322
|
+
dialog?.window?.decorView?.visibility = View.INVISIBLE
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
onModalDismissed = {
|
|
326
|
+
if (isPresented) {
|
|
327
|
+
dialog?.window?.decorView?.visibility = View.VISIBLE
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
rnScreensObserver?.start()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private fun cleanupModalObserver() {
|
|
335
|
+
rnScreensObserver?.stop()
|
|
336
|
+
rnScreensObserver = null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ====================================================================
|
|
340
|
+
// MARK: - Presentation
|
|
341
|
+
// ====================================================================
|
|
370
342
|
|
|
371
|
-
/**
|
|
372
|
-
* Present the sheet.
|
|
373
|
-
*/
|
|
374
343
|
fun present(detentIndex: Int, animated: Boolean = true) {
|
|
375
344
|
val dialog = this.dialog ?: run {
|
|
376
|
-
|
|
345
|
+
RNLog.w(reactContext, "TrueSheet: No dialog available. Ensure the sheet is mounted before presenting.")
|
|
377
346
|
return
|
|
378
347
|
}
|
|
379
348
|
|
|
@@ -381,26 +350,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
381
350
|
setupDimmedBackground(detentIndex)
|
|
382
351
|
|
|
383
352
|
if (isPresented) {
|
|
384
|
-
// For consistency with iOS, we notify detent change immediately
|
|
385
|
-
// when already presented (not waiting for state to change)
|
|
386
353
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
387
354
|
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position)
|
|
388
|
-
|
|
389
|
-
// Note: onSlide will be called during resize animation, no need to emit position change here
|
|
390
355
|
setStateForDetentIndex(detentIndex)
|
|
391
356
|
} else {
|
|
392
|
-
// Reset drag state before presenting
|
|
393
357
|
isDragging = false
|
|
394
|
-
|
|
395
358
|
setupSheetDetents()
|
|
396
359
|
setStateForDetentIndex(detentIndex)
|
|
397
360
|
|
|
398
|
-
// Notify delegate before showing
|
|
399
361
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
400
362
|
delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position)
|
|
401
363
|
|
|
402
364
|
if (!animated) {
|
|
403
|
-
// Disable animation
|
|
404
365
|
dialog.window?.setWindowAnimations(0)
|
|
405
366
|
}
|
|
406
367
|
|
|
@@ -408,152 +369,141 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
408
369
|
}
|
|
409
370
|
}
|
|
410
371
|
|
|
411
|
-
/**
|
|
412
|
-
* Dismiss the sheet.
|
|
413
|
-
*/
|
|
414
372
|
fun dismiss() {
|
|
415
373
|
this.post {
|
|
416
|
-
|
|
417
|
-
// Use maxScreenHeight as the off-screen position (sheet slides down off screen)
|
|
418
|
-
val offScreenPosition = maxScreenHeight.pxToDp()
|
|
374
|
+
val offScreenPosition = screenHeight.pxToDp()
|
|
419
375
|
delegate?.viewControllerDidChangePosition(currentDetentIndex, offScreenPosition, transitioning = true)
|
|
420
376
|
}
|
|
421
|
-
|
|
422
377
|
dialog?.dismiss()
|
|
423
378
|
}
|
|
424
379
|
|
|
425
|
-
//
|
|
380
|
+
// ====================================================================
|
|
381
|
+
// MARK: - Sheet Configuration
|
|
382
|
+
// ====================================================================
|
|
426
383
|
|
|
427
|
-
/**
|
|
428
|
-
* Setup sheet detents based on the detent preference.
|
|
429
|
-
*/
|
|
430
384
|
fun setupSheetDetents() {
|
|
431
385
|
val behavior = this.behavior ?: return
|
|
432
386
|
|
|
433
|
-
// Configure sheet sizes
|
|
434
387
|
behavior.apply {
|
|
435
388
|
skipCollapsed = false
|
|
436
389
|
isFitToContents = true
|
|
437
|
-
|
|
438
|
-
// m3 max width 640dp
|
|
439
|
-
maxWidth = 640.0.dpToPx().toInt()
|
|
390
|
+
maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
440
391
|
|
|
441
392
|
when (detents.size) {
|
|
442
393
|
1 -> {
|
|
443
|
-
|
|
444
|
-
maxHeight = detentHeight
|
|
394
|
+
maxHeight = getDetentHeight(detents[0])
|
|
445
395
|
skipCollapsed = true
|
|
396
|
+
|
|
397
|
+
if (isPresented && detents[0] == -1.0) {
|
|
398
|
+
sheetContainer?.apply {
|
|
399
|
+
val params = layoutParams
|
|
400
|
+
params.height = maxHeight
|
|
401
|
+
layoutParams = params
|
|
402
|
+
}
|
|
403
|
+
}
|
|
446
404
|
}
|
|
447
405
|
|
|
448
406
|
2 -> {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
maxHeight = maxHeightValue
|
|
452
|
-
setPeekHeight(peekHeight, isPresented)
|
|
407
|
+
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
408
|
+
maxHeight = getDetentHeight(detents[1])
|
|
453
409
|
}
|
|
454
410
|
|
|
455
411
|
3 -> {
|
|
456
|
-
// Enables half expanded
|
|
457
412
|
isFitToContents = false
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
val middleDetentHeight = getDetentHeight(detents[1])
|
|
461
|
-
val maxHeightValue = getDetentHeight(detents[2])
|
|
462
|
-
|
|
463
|
-
setPeekHeight(peekHeightValue, isPresented)
|
|
464
|
-
maxHeight = maxHeightValue
|
|
413
|
+
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
414
|
+
maxHeight = getDetentHeight(detents[2])
|
|
465
415
|
expandedOffset = sheetTopInset
|
|
466
|
-
halfExpandedRatio = minOf(
|
|
416
|
+
halfExpandedRatio = minOf(getDetentHeight(detents[1]).toFloat() / screenHeight.toFloat(), 1.0f)
|
|
467
417
|
}
|
|
468
418
|
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
469
421
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
422
|
+
fun setupGrabber() {
|
|
423
|
+
val bottomSheet = bottomSheetView ?: return
|
|
424
|
+
|
|
425
|
+
bottomSheet.findViewWithTag<View>(GRABBER_TAG)?.let {
|
|
426
|
+
bottomSheet.removeView(it)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!grabber) return
|
|
430
|
+
|
|
431
|
+
val grabberView = View(reactContext).apply {
|
|
432
|
+
tag = GRABBER_TAG
|
|
433
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
434
|
+
GRABBER_WIDTH.dpToPx().toInt(),
|
|
435
|
+
GRABBER_HEIGHT.dpToPx().toInt()
|
|
436
|
+
).apply {
|
|
437
|
+
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
|
438
|
+
topMargin = GRABBER_TOP_MARGIN.dpToPx().toInt()
|
|
439
|
+
}
|
|
440
|
+
background = GradientDrawable().apply {
|
|
441
|
+
shape = GradientDrawable.RECTANGLE
|
|
442
|
+
cornerRadius = (GRABBER_HEIGHT / 2).dpToPx()
|
|
443
|
+
setColor(GRABBER_COLOR)
|
|
478
444
|
}
|
|
445
|
+
elevation = 1f
|
|
479
446
|
}
|
|
447
|
+
|
|
448
|
+
bottomSheet.addView(grabberView)
|
|
480
449
|
}
|
|
481
450
|
|
|
482
|
-
/**
|
|
483
|
-
* Setup background color and corner radius.
|
|
484
|
-
*/
|
|
485
451
|
fun setupBackground() {
|
|
486
|
-
|
|
487
|
-
val outerRadii = floatArrayOf(
|
|
488
|
-
cornerRadius,
|
|
489
|
-
cornerRadius,
|
|
490
|
-
cornerRadius,
|
|
491
|
-
cornerRadius,
|
|
492
|
-
0f,
|
|
493
|
-
0f,
|
|
494
|
-
0f,
|
|
495
|
-
0f
|
|
496
|
-
)
|
|
452
|
+
val bottomSheet = bottomSheetView ?: return
|
|
497
453
|
|
|
498
|
-
|
|
499
|
-
|
|
454
|
+
val cornerRadius = if (sheetCornerRadius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else sheetCornerRadius
|
|
455
|
+
val outerRadii = floatArrayOf(cornerRadius, cornerRadius, cornerRadius, cornerRadius, 0f, 0f, 0f, 0f)
|
|
456
|
+
val backgroundColor = if (sheetBackgroundColor != 0) sheetBackgroundColor else getDefaultBackgroundColor()
|
|
500
457
|
|
|
501
|
-
|
|
502
|
-
|
|
458
|
+
bottomSheet.background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
|
|
459
|
+
paint.color = backgroundColor
|
|
503
460
|
}
|
|
461
|
+
bottomSheet.clipToOutline = true
|
|
504
462
|
}
|
|
505
463
|
|
|
506
464
|
/**
|
|
507
|
-
*
|
|
508
|
-
*
|
|
465
|
+
* Configures the dimmed background based on the current detent index.
|
|
466
|
+
* When not dimmed, touch events pass through to the activity behind the sheet.
|
|
509
467
|
*/
|
|
510
468
|
fun setupDimmedBackground(detentIndex: Int) {
|
|
511
469
|
val dialog = this.dialog ?: return
|
|
512
470
|
dialog.window?.apply {
|
|
513
471
|
val view = findViewById<View>(com.google.android.material.R.id.touch_outside)
|
|
514
472
|
|
|
515
|
-
if (dimmed && detentIndex >=
|
|
516
|
-
// Remove touch listener
|
|
473
|
+
if (dimmed && detentIndex >= dimmedDetentIndex) {
|
|
517
474
|
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
|
-
|
|
475
|
+
setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
525
476
|
dialog.setCanceledOnTouchOutside(dismissible)
|
|
526
477
|
} else {
|
|
527
|
-
//
|
|
478
|
+
// Forward touch events to the activity when not dimmed
|
|
528
479
|
view.setOnTouchListener { v, event ->
|
|
529
480
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
530
481
|
reactContext.currentActivity?.dispatchTouchEvent(event)
|
|
531
482
|
false
|
|
532
483
|
}
|
|
533
|
-
|
|
534
|
-
// Remove the dimmed background
|
|
535
484
|
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
536
|
-
|
|
537
485
|
dialog.setCanceledOnTouchOutside(false)
|
|
538
486
|
}
|
|
539
487
|
}
|
|
540
488
|
}
|
|
541
489
|
|
|
542
490
|
fun resetAnimation() {
|
|
543
|
-
dialog?.window?.
|
|
544
|
-
setWindowAnimations(windowAnimation)
|
|
545
|
-
}
|
|
491
|
+
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
546
492
|
}
|
|
547
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Positions the footer view at the bottom of the sheet.
|
|
496
|
+
* The footer stays fixed at the bottom edge of the visible sheet area,
|
|
497
|
+
* adjusting during drag gestures via slideOffset.
|
|
498
|
+
*/
|
|
548
499
|
fun positionFooter(slideOffset: Float? = null) {
|
|
549
|
-
val
|
|
500
|
+
val footerView = containerView?.footerView ?: return
|
|
550
501
|
val bottomSheet = bottomSheetView ?: return
|
|
551
|
-
val footerHeight = footer.height
|
|
552
502
|
|
|
503
|
+
val footerHeight = footerView.height
|
|
553
504
|
val bottomSheetY = ScreenUtils.getScreenY(bottomSheet)
|
|
554
505
|
|
|
555
|
-
|
|
556
|
-
var footerY = (maxScreenHeight - bottomSheetY - footerHeight).toFloat()
|
|
506
|
+
var footerY = (screenHeight - bottomSheetY - footerHeight).toFloat()
|
|
557
507
|
|
|
558
508
|
// Animate footer down with sheet when below peek height
|
|
559
509
|
if (slideOffset != null && slideOffset < 0) {
|
|
@@ -562,14 +512,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
562
512
|
|
|
563
513
|
// Clamp footer position to prevent it from going off screen when positioning at the top
|
|
564
514
|
// This happens when fullScreen is enabled in edge-to-edge mode
|
|
565
|
-
val
|
|
566
|
-
|
|
567
|
-
footer.y = minOf(footerY, maxAllowedY)
|
|
515
|
+
val maxAllowedY = (screenHeight - statusBarHeight - footerHeight).toFloat()
|
|
516
|
+
footerView.y = minOf(footerY, maxAllowedY)
|
|
568
517
|
}
|
|
569
518
|
|
|
570
|
-
/**
|
|
571
|
-
* Set the state based for the given detent index.
|
|
572
|
-
*/
|
|
573
519
|
fun setStateForDetentIndex(index: Int) {
|
|
574
520
|
behavior?.state = getStateForDetentIndex(index)
|
|
575
521
|
}
|
|
@@ -578,62 +524,54 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
578
524
|
dialog?.window?.setSoftInputMode(mode)
|
|
579
525
|
}
|
|
580
526
|
|
|
581
|
-
|
|
527
|
+
fun getDefaultBackgroundColor(): Int {
|
|
528
|
+
val typedValue = TypedValue()
|
|
529
|
+
return if (reactContext.theme.resolveAttribute(
|
|
530
|
+
com.google.android.material.R.attr.colorSurfaceContainerLow,
|
|
531
|
+
typedValue,
|
|
532
|
+
true
|
|
533
|
+
)
|
|
534
|
+
) {
|
|
535
|
+
typedValue.data
|
|
536
|
+
} else {
|
|
537
|
+
Color.WHITE
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ====================================================================
|
|
542
|
+
// MARK: - Drag Handling
|
|
543
|
+
// ====================================================================
|
|
582
544
|
|
|
583
|
-
/**
|
|
584
|
-
* Get current detent info from sheet view position
|
|
585
|
-
*/
|
|
586
545
|
private fun getCurrentDetentInfo(sheetView: View): DetentInfo {
|
|
587
|
-
// Get the Y position in screen coordinates (like iOS presentedView.frame.origin.y)
|
|
588
546
|
val screenY = ScreenUtils.getScreenY(sheetView)
|
|
589
|
-
|
|
590
|
-
return DetentInfo(currentDetentIndex, position)
|
|
547
|
+
return DetentInfo(currentDetentIndex, screenY.pxToDp())
|
|
591
548
|
}
|
|
592
549
|
|
|
593
|
-
/**
|
|
594
|
-
* Handle drag begin
|
|
595
|
-
*/
|
|
596
550
|
private fun handleDragBegin(sheetView: View) {
|
|
597
551
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
598
552
|
delegate?.viewControllerDidDragBegin(detentInfo.index, detentInfo.position)
|
|
599
553
|
isDragging = true
|
|
600
554
|
}
|
|
601
555
|
|
|
602
|
-
/**
|
|
603
|
-
* Handle drag change
|
|
604
|
-
*/
|
|
605
556
|
private fun handleDragChange(sheetView: View) {
|
|
606
557
|
if (!isDragging) return
|
|
607
|
-
|
|
608
558
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
609
559
|
delegate?.viewControllerDidDragChange(detentInfo.index, detentInfo.position)
|
|
610
560
|
}
|
|
611
561
|
|
|
612
|
-
/**
|
|
613
|
-
* Handle drag end
|
|
614
|
-
*/
|
|
615
562
|
private fun handleDragEnd(state: Int) {
|
|
616
563
|
if (!isDragging) return
|
|
617
564
|
|
|
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
565
|
val detentInfo = getDetentInfoForState(state)
|
|
623
566
|
detentInfo?.let {
|
|
624
|
-
// Notify delegate of drag end
|
|
625
567
|
delegate?.viewControllerDidDragEnd(it.index, it.position)
|
|
626
568
|
|
|
627
569
|
if (it.index != currentDetentIndex) {
|
|
628
|
-
presentPromise?.
|
|
629
|
-
|
|
630
|
-
presentPromise = null
|
|
631
|
-
}
|
|
570
|
+
presentPromise?.invoke()
|
|
571
|
+
presentPromise = null
|
|
632
572
|
|
|
633
573
|
currentDetentIndex = it.index
|
|
634
574
|
setupDimmedBackground(it.index)
|
|
635
|
-
|
|
636
|
-
// Notify delegate of detent change
|
|
637
575
|
delegate?.viewControllerDidChangeDetent(it.index, it.position)
|
|
638
576
|
}
|
|
639
577
|
}
|
|
@@ -641,186 +579,163 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
641
579
|
isDragging = false
|
|
642
580
|
}
|
|
643
581
|
|
|
644
|
-
//
|
|
582
|
+
// ====================================================================
|
|
583
|
+
// MARK: - Detent Calculations
|
|
584
|
+
// ====================================================================
|
|
645
585
|
|
|
646
|
-
/**
|
|
647
|
-
* Get the height value based on the detent config value.
|
|
648
|
-
*/
|
|
649
586
|
private fun getDetentHeight(detent: Double): Int {
|
|
650
587
|
val height: Int = if (detent == -1.0) {
|
|
651
|
-
|
|
652
|
-
contentHeight
|
|
588
|
+
contentHeight + headerHeight
|
|
653
589
|
} else {
|
|
654
590
|
if (detent <= 0.0 || detent > 1.0) {
|
|
655
591
|
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
656
592
|
}
|
|
657
|
-
(detent *
|
|
593
|
+
(detent * screenHeight).toInt()
|
|
658
594
|
}
|
|
659
595
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
val finalHeight = maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
663
|
-
return finalHeight
|
|
596
|
+
val maxAllowedHeight = screenHeight - sheetTopInset
|
|
597
|
+
return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
664
598
|
}
|
|
665
599
|
|
|
666
|
-
/**
|
|
667
|
-
* Determines the state based from the given detent index.
|
|
668
|
-
*/
|
|
669
600
|
private fun getStateForDetentIndex(index: Int): Int =
|
|
670
601
|
when (detents.size) {
|
|
671
|
-
1 ->
|
|
672
|
-
BottomSheetBehavior.STATE_EXPANDED
|
|
673
|
-
}
|
|
602
|
+
1 -> BottomSheetBehavior.STATE_EXPANDED
|
|
674
603
|
|
|
675
|
-
2 -> {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
680
|
-
}
|
|
604
|
+
2 -> when (index) {
|
|
605
|
+
0 -> BottomSheetBehavior.STATE_COLLAPSED
|
|
606
|
+
1 -> BottomSheetBehavior.STATE_EXPANDED
|
|
607
|
+
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
681
608
|
}
|
|
682
609
|
|
|
683
|
-
3 -> {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
689
|
-
}
|
|
610
|
+
3 -> when (index) {
|
|
611
|
+
0 -> BottomSheetBehavior.STATE_COLLAPSED
|
|
612
|
+
1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
613
|
+
2 -> BottomSheetBehavior.STATE_EXPANDED
|
|
614
|
+
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
690
615
|
}
|
|
691
616
|
|
|
692
617
|
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
693
618
|
}
|
|
694
619
|
|
|
695
|
-
/**
|
|
696
|
-
* Get the DetentInfo data by state.
|
|
697
|
-
*/
|
|
698
620
|
fun getDetentInfoForState(state: Int): DetentInfo? =
|
|
699
621
|
when (detents.size) {
|
|
700
|
-
1 -> {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
622
|
+
1 -> when (state) {
|
|
623
|
+
BottomSheetBehavior.STATE_COLLAPSED,
|
|
624
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
625
|
+
|
|
626
|
+
else -> null
|
|
706
627
|
}
|
|
707
628
|
|
|
708
|
-
2 -> {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
else -> null
|
|
713
|
-
}
|
|
629
|
+
2 -> when (state) {
|
|
630
|
+
BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
631
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
|
|
632
|
+
else -> null
|
|
714
633
|
}
|
|
715
634
|
|
|
716
|
-
3 -> {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
else -> null
|
|
722
|
-
}
|
|
635
|
+
3 -> when (state) {
|
|
636
|
+
BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
637
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
|
|
638
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(2, getPositionForDetentIndex(2))
|
|
639
|
+
else -> null
|
|
723
640
|
}
|
|
724
641
|
|
|
725
642
|
else -> null
|
|
726
643
|
}
|
|
727
644
|
|
|
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
645
|
private fun getPositionForDetentIndex(index: Int): Float {
|
|
733
|
-
if (index < 0 || index >= detents.size)
|
|
734
|
-
return 0f
|
|
735
|
-
}
|
|
646
|
+
if (index < 0 || index >= detents.size) return 0f
|
|
736
647
|
|
|
737
|
-
// Try to get actual position from bottom sheet view first (same view used in behavior callbacks)
|
|
738
648
|
bottomSheetView?.let {
|
|
739
|
-
it
|
|
740
649
|
val screenY = ScreenUtils.getScreenY(it)
|
|
741
|
-
|
|
742
|
-
if (screenY > 0) {
|
|
743
|
-
return screenY.pxToDp()
|
|
744
|
-
}
|
|
650
|
+
if (screenY > 0) return screenY.pxToDp()
|
|
745
651
|
}
|
|
746
652
|
|
|
747
|
-
// Fallback: calculate expected position
|
|
748
653
|
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()
|
|
654
|
+
return (screenHeight - detentHeight).pxToDp()
|
|
757
655
|
}
|
|
758
656
|
|
|
759
|
-
/**
|
|
760
|
-
* Get DetentInfo data for given detent index.
|
|
761
|
-
*/
|
|
762
657
|
fun getDetentInfoForIndex(index: Int) = getDetentInfoForState(getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
|
|
763
658
|
|
|
764
|
-
//
|
|
659
|
+
// ====================================================================
|
|
660
|
+
// MARK: - RootView Implementation
|
|
661
|
+
// ====================================================================
|
|
765
662
|
|
|
766
663
|
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
|
|
767
664
|
super.onInitializeAccessibilityNodeInfo(info)
|
|
768
|
-
|
|
769
|
-
val testId = getTag(R.id.react_test_id) as String?
|
|
770
|
-
if (testId != null) {
|
|
771
|
-
info.viewIdResourceName = testId
|
|
772
|
-
}
|
|
665
|
+
(getTag(R.id.react_test_id) as? String)?.let { info.viewIdResourceName = it }
|
|
773
666
|
}
|
|
774
667
|
|
|
775
668
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
776
669
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
777
|
-
|
|
778
|
-
// Only proceed if size actually changed
|
|
779
670
|
if (w == oldw && h == oldh) return
|
|
780
671
|
|
|
781
|
-
|
|
782
|
-
val oldMaxScreenHeight = maxScreenHeight
|
|
783
|
-
maxScreenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
672
|
+
delegate?.viewControllerDidChangeSize(w, h)
|
|
784
673
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
// Recalculate sheet detents with new screen dimensions
|
|
788
|
-
setupSheetDetents()
|
|
674
|
+
val oldScreenHeight = screenHeight
|
|
675
|
+
screenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
789
676
|
|
|
677
|
+
if (isPresented && oldScreenHeight != screenHeight && oldScreenHeight > 0) {
|
|
678
|
+
setupSheetDetents()
|
|
790
679
|
this.post {
|
|
791
|
-
// Update footer position after rotation
|
|
792
680
|
positionFooter()
|
|
793
|
-
|
|
794
|
-
// Notify JS about position change after rotation settles
|
|
795
681
|
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
796
682
|
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = true)
|
|
797
683
|
}
|
|
798
684
|
}
|
|
799
|
-
|
|
800
|
-
// Notify delegate about size change
|
|
801
|
-
delegate?.viewControllerDidChangeSize(w, h)
|
|
802
685
|
}
|
|
803
686
|
|
|
804
687
|
override fun handleException(t: Throwable) {
|
|
805
688
|
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
806
689
|
}
|
|
807
690
|
|
|
691
|
+
// ====================================================================
|
|
692
|
+
// MARK: - Touch Event Handling
|
|
693
|
+
// ====================================================================
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Custom touch dispatch to handle footer touch events.
|
|
697
|
+
* The footer is positioned outside the normal view hierarchy, so we need to
|
|
698
|
+
* manually check if touches fall within its bounds and forward them.
|
|
699
|
+
*/
|
|
700
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
701
|
+
val footer = containerView?.footerView
|
|
702
|
+
if (footer != null && footer.visibility == View.VISIBLE) {
|
|
703
|
+
val footerLocation = ScreenUtils.getScreenLocation(footer)
|
|
704
|
+
val touchScreenX = event.rawX.toInt()
|
|
705
|
+
val touchScreenY = event.rawY.toInt()
|
|
706
|
+
|
|
707
|
+
if (touchScreenX >= footerLocation[0] &&
|
|
708
|
+
touchScreenX <= footerLocation[0] + footer.width &&
|
|
709
|
+
touchScreenY >= footerLocation[1] &&
|
|
710
|
+
touchScreenY <= footerLocation[1] + footer.height
|
|
711
|
+
) {
|
|
712
|
+
val localEvent = MotionEvent.obtain(event)
|
|
713
|
+
localEvent.setLocation(
|
|
714
|
+
(touchScreenX - footerLocation[0]).toFloat(),
|
|
715
|
+
(touchScreenY - footerLocation[1]).toFloat()
|
|
716
|
+
)
|
|
717
|
+
val handled = footer.dispatchTouchEvent(localEvent)
|
|
718
|
+
localEvent.recycle()
|
|
719
|
+
if (handled) return true
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return super.dispatchTouchEvent(event)
|
|
723
|
+
}
|
|
724
|
+
|
|
808
725
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
809
|
-
eventDispatcher?.let {
|
|
810
|
-
jSTouchDispatcher.handleTouchEvent(event,
|
|
811
|
-
jSPointerDispatcher?.handleMotionEvent(event,
|
|
726
|
+
eventDispatcher?.let {
|
|
727
|
+
jSTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
728
|
+
jSPointerDispatcher?.handleMotionEvent(event, it, true)
|
|
812
729
|
}
|
|
813
730
|
return super.onInterceptTouchEvent(event)
|
|
814
731
|
}
|
|
815
732
|
|
|
816
733
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
817
|
-
eventDispatcher?.let {
|
|
818
|
-
jSTouchDispatcher.handleTouchEvent(event,
|
|
819
|
-
jSPointerDispatcher?.handleMotionEvent(event,
|
|
734
|
+
eventDispatcher?.let {
|
|
735
|
+
jSTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
736
|
+
jSPointerDispatcher?.handleMotionEvent(event, it, false)
|
|
820
737
|
}
|
|
821
738
|
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
739
|
return true
|
|
825
740
|
}
|
|
826
741
|
|
|
@@ -834,12 +749,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
834
749
|
return super.onHoverEvent(event)
|
|
835
750
|
}
|
|
836
751
|
|
|
837
|
-
@OptIn(UnstableReactNativeAPI::class)
|
|
838
|
-
@Suppress("DEPRECATION")
|
|
839
752
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
840
|
-
eventDispatcher?.let {
|
|
841
|
-
jSTouchDispatcher.onChildStartedNativeGesture(ev,
|
|
842
|
-
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev,
|
|
753
|
+
eventDispatcher?.let {
|
|
754
|
+
jSTouchDispatcher.onChildStartedNativeGesture(ev, it)
|
|
755
|
+
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
|
|
843
756
|
}
|
|
844
757
|
}
|
|
845
758
|
|
|
@@ -849,7 +762,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
849
762
|
}
|
|
850
763
|
|
|
851
764
|
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
852
|
-
// Allow the request to propagate to parent
|
|
853
765
|
super.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
854
766
|
}
|
|
855
767
|
}
|