@lodev09/react-native-true-sheet 3.1.0-beta.1 → 3.1.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/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +12 -10
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +19 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +119 -208
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +17 -5
- package/android/src/main/jni/CMakeLists.txt +6 -30
- package/android/src/main/res/anim/true_sheet_slide_in.xml +4 -4
- package/android/src/main/res/anim/true_sheet_slide_out.xml +4 -3
- package/ios/TrueSheetContainerView.mm +4 -4
- package/ios/TrueSheetContentView.mm +4 -4
- package/ios/TrueSheetFooterView.mm +4 -4
- package/ios/TrueSheetHeaderView.mm +4 -4
- package/ios/TrueSheetModule.mm +24 -5
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +17 -5
- package/ios/TrueSheetViewController.h +3 -2
- package/ios/TrueSheetViewController.mm +204 -95
- package/lib/module/TrueSheet.js +12 -8
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/navigation/TrueSheetRouter.js +119 -0
- package/lib/module/navigation/TrueSheetRouter.js.map +1 -0
- package/lib/module/navigation/TrueSheetView.js +169 -0
- package/lib/module/navigation/TrueSheetView.js.map +1 -0
- package/lib/module/navigation/createTrueSheetNavigator.js +59 -0
- package/lib/module/navigation/createTrueSheetNavigator.js.map +1 -0
- package/lib/module/navigation/index.js +6 -0
- package/lib/module/navigation/index.js.map +1 -0
- package/lib/module/navigation/types.js +4 -0
- package/lib/module/navigation/types.js.map +1 -0
- package/lib/module/navigation/useTrueSheetNavigation.js +26 -0
- package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -0
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +3 -3
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -1
- package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +8 -4
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +57 -0
- package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +1 -0
- package/lib/typescript/src/navigation/TrueSheetView.d.ts +10 -0
- package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +35 -0
- package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +6 -0
- package/lib/typescript/src/navigation/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/types.d.ts +125 -0
- package/lib/typescript/src/navigation/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +23 -0
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +4 -2
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
- package/package.json +13 -2
- package/src/TrueSheet.tsx +16 -8
- package/src/__mocks__/index.js +6 -5
- package/src/navigation/TrueSheetRouter.ts +172 -0
- package/src/navigation/TrueSheetView.tsx +271 -0
- package/src/navigation/createTrueSheetNavigator.tsx +89 -0
- package/src/navigation/index.ts +14 -0
- package/src/navigation/types.ts +176 -0
- package/src/navigation/useTrueSheetNavigation.ts +28 -0
- package/src/reanimated/ReanimatedTrueSheetProvider.tsx +6 -9
- package/src/specs/NativeTrueSheetModule.ts +4 -2
|
@@ -61,15 +61,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
61
61
|
companion object {
|
|
62
62
|
const val TAG_NAME = "TrueSheet"
|
|
63
63
|
|
|
64
|
+
private const val MAX_HALF_EXPANDED_RATIO = 0.999f
|
|
65
|
+
|
|
64
66
|
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
65
67
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
66
68
|
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*/
|
|
72
|
-
fun getEffectiveSheetHeight(sheetHeight: Int, headerHeight: Int): Int = sheetHeight - headerHeight * 2
|
|
70
|
+
// Animation durations from res/anim/true_sheet_slide_in.xml and true_sheet_slide_out.xml
|
|
71
|
+
private const val PRESENT_ANIMATION_DURATION = 200L
|
|
72
|
+
private const val DISMISS_ANIMATION_DURATION = 150L
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// ====================================================================
|
|
@@ -119,6 +119,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
119
119
|
private val resolvedDetentPositions = mutableListOf<Int>()
|
|
120
120
|
|
|
121
121
|
private var isDragging = false
|
|
122
|
+
private var isDismissing = false
|
|
122
123
|
private var isReconfiguring = false
|
|
123
124
|
private var windowAnimation: Int = 0
|
|
124
125
|
private var lastEmittedPositionPx: Int = -1
|
|
@@ -169,48 +170,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
169
170
|
val statusBarHeight: Int
|
|
170
171
|
get() = ScreenUtils.getStatusBarHeight(reactContext)
|
|
171
172
|
|
|
172
|
-
/**
|
|
173
|
-
* The bottom inset (navigation bar height) to add to sheet height.
|
|
174
|
-
* This matches iOS behavior where the system adds bottom safe area inset internally.
|
|
175
|
-
*/
|
|
173
|
+
/** Navigation bar height, added to sheet height to match iOS behavior. */
|
|
176
174
|
private val bottomInset: Int
|
|
177
175
|
get() = ScreenUtils.getNavigationBarHeight(reactContext)
|
|
178
176
|
|
|
179
|
-
/**
|
|
180
|
-
* Edge-to-edge is enabled by default on API 36+, or when explicitly configured.
|
|
181
|
-
*/
|
|
177
|
+
/** Edge-to-edge enabled by default on API 36+, or when explicitly configured. */
|
|
182
178
|
private val edgeToEdgeEnabled: Boolean
|
|
183
179
|
get() {
|
|
184
180
|
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
185
181
|
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
186
182
|
}
|
|
187
183
|
|
|
188
|
-
/**
|
|
189
|
-
* The top inset to apply when edge-to-edge is enabled but not full-screen.
|
|
190
|
-
* This prevents the sheet from going under the status bar.
|
|
191
|
-
*/
|
|
184
|
+
/** Top inset when edge-to-edge is enabled but not full-screen. */
|
|
192
185
|
private val sheetTopInset: Int
|
|
193
186
|
get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) statusBarHeight else 0
|
|
194
187
|
|
|
195
|
-
// ====================================================================
|
|
196
|
-
// MARK: - Touch Dispatchers
|
|
197
|
-
// ====================================================================
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Touch dispatchers are required for RootView to properly forward touch events to React Native.
|
|
201
|
-
*/
|
|
202
188
|
internal var eventDispatcher: EventDispatcher? = null
|
|
203
189
|
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
204
190
|
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
205
191
|
|
|
206
|
-
|
|
207
|
-
// MARK: - Modal Observer
|
|
208
|
-
// ====================================================================
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Observes react-native-screens modal fragments to hide/show the sheet appropriately.
|
|
212
|
-
* This prevents the sheet from rendering on top of modals.
|
|
213
|
-
*/
|
|
192
|
+
/** Hides/shows the sheet when RN Screens modals are presented/dismissed. */
|
|
214
193
|
private var rnScreensObserver: RNScreensFragmentObserver? = null
|
|
215
194
|
|
|
216
195
|
// ====================================================================
|
|
@@ -252,7 +231,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
252
231
|
behavior.isHideable = dismissible
|
|
253
232
|
behavior.isDraggable = draggable
|
|
254
233
|
|
|
255
|
-
// Handle back press
|
|
256
234
|
onBackPressedDispatcher.addCallback(object : androidx.activity.OnBackPressedCallback(true) {
|
|
257
235
|
override fun handleOnBackPressed() {
|
|
258
236
|
this@TrueSheetViewController.delegate?.viewControllerDidBackPress()
|
|
@@ -276,6 +254,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
276
254
|
|
|
277
255
|
dialog = null
|
|
278
256
|
isDragging = false
|
|
257
|
+
isDismissing = false
|
|
279
258
|
isPresented = false
|
|
280
259
|
isDialogVisible = false
|
|
281
260
|
lastEmittedPositionPx = -1
|
|
@@ -290,44 +269,38 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
290
269
|
setupGrabber()
|
|
291
270
|
|
|
292
271
|
sheetContainer?.post {
|
|
272
|
+
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
273
|
+
storeResolvedPosition(currentDetentIndex)
|
|
274
|
+
emitChangePositionDelegate(positionPx, realtime = false)
|
|
275
|
+
positionFooter()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
sheetContainer?.postDelayed({
|
|
293
279
|
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
294
280
|
val detent = getDetentValueForIndex(detentInfo.index)
|
|
295
|
-
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
296
281
|
|
|
297
282
|
delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position, detent)
|
|
298
|
-
|
|
299
|
-
// Store resolved position for initial detent
|
|
300
|
-
storeResolvedPosition(detentInfo.index)
|
|
301
|
-
emitChangePositionDelegate(detentInfo.index, positionPx, realtime = false)
|
|
302
|
-
|
|
303
|
-
// Notify parent sheet that it has lost focus (after this sheet appeared)
|
|
304
283
|
parentSheetView?.viewControllerDidBlur()
|
|
284
|
+
delegate?.viewControllerDidFocus()
|
|
305
285
|
|
|
306
286
|
presentPromise?.invoke()
|
|
307
287
|
presentPromise = null
|
|
308
|
-
|
|
309
|
-
positionFooter()
|
|
310
|
-
}
|
|
288
|
+
}, PRESENT_ANIMATION_DURATION)
|
|
311
289
|
}
|
|
312
290
|
|
|
313
291
|
dialog.setOnCancelListener {
|
|
314
|
-
|
|
292
|
+
if (isDismissing) return@setOnCancelListener
|
|
315
293
|
|
|
316
|
-
|
|
317
|
-
|
|
294
|
+
isDismissing = true
|
|
295
|
+
emitWillDismissEvents()
|
|
296
|
+
emitChangePositionDelegate(screenHeight, realtime = false)
|
|
318
297
|
}
|
|
319
298
|
|
|
320
299
|
dialog.setOnDismissListener {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
parentSheetView = null
|
|
326
|
-
|
|
327
|
-
dismissPromise?.invoke()
|
|
328
|
-
dismissPromise = null
|
|
329
|
-
delegate?.viewControllerDidDismiss(hadParent)
|
|
330
|
-
cleanupDialog()
|
|
300
|
+
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
301
|
+
emitDidDismissEvents()
|
|
302
|
+
cleanupDialog()
|
|
303
|
+
}, DISMISS_ANIMATION_DURATION)
|
|
331
304
|
}
|
|
332
305
|
}
|
|
333
306
|
|
|
@@ -337,9 +310,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
337
310
|
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
338
311
|
val behavior = behavior ?: return
|
|
339
312
|
val positionPx = getCurrentPositionPx(sheetView)
|
|
340
|
-
val detentIndex = getDetentIndexForPosition(positionPx)
|
|
341
313
|
|
|
342
|
-
emitChangePositionDelegate(
|
|
314
|
+
emitChangePositionDelegate(positionPx, realtime = true)
|
|
343
315
|
|
|
344
316
|
when (behavior.state) {
|
|
345
317
|
BottomSheetBehavior.STATE_DRAGGING,
|
|
@@ -353,7 +325,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
353
325
|
|
|
354
326
|
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
355
327
|
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
|
356
|
-
|
|
328
|
+
if (isDismissing) return
|
|
329
|
+
isDismissing = true
|
|
330
|
+
emitWillDismissEvents()
|
|
331
|
+
dialog.dismiss()
|
|
357
332
|
return
|
|
358
333
|
}
|
|
359
334
|
|
|
@@ -365,15 +340,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
365
340
|
BottomSheetBehavior.STATE_EXPANDED,
|
|
366
341
|
BottomSheetBehavior.STATE_COLLAPSED,
|
|
367
342
|
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
|
|
368
|
-
// Ignore state changes triggered by content size reconfiguration
|
|
369
343
|
if (isReconfiguring) return
|
|
370
344
|
|
|
371
345
|
getDetentInfoForState(newState)?.let { detentInfo ->
|
|
372
|
-
// Store resolved position when sheet settles
|
|
373
346
|
storeResolvedPosition(detentInfo.index)
|
|
374
347
|
|
|
375
348
|
if (isDragging) {
|
|
376
|
-
// Handle drag end
|
|
377
349
|
val detent = getDetentValueForIndex(detentInfo.index)
|
|
378
350
|
delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
|
|
379
351
|
|
|
@@ -386,13 +358,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
386
358
|
}
|
|
387
359
|
|
|
388
360
|
isDragging = false
|
|
389
|
-
} else {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
currentDetentIndex = detentInfo.index
|
|
394
|
-
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
395
|
-
}
|
|
361
|
+
} else if (detentInfo.index != currentDetentIndex) {
|
|
362
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
363
|
+
currentDetentIndex = detentInfo.index
|
|
364
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
396
365
|
}
|
|
397
366
|
}
|
|
398
367
|
}
|
|
@@ -407,14 +376,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
407
376
|
private fun setupModalObserver() {
|
|
408
377
|
rnScreensObserver = RNScreensFragmentObserver(
|
|
409
378
|
reactContext = reactContext,
|
|
379
|
+
onModalWillPresent = {
|
|
380
|
+
if (isPresented) {
|
|
381
|
+
delegate?.viewControllerWillBlur()
|
|
382
|
+
}
|
|
383
|
+
},
|
|
410
384
|
onModalPresented = {
|
|
411
385
|
if (isPresented) {
|
|
412
386
|
hideDialog()
|
|
387
|
+
delegate?.viewControllerDidBlur()
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
onModalWillDismiss = {
|
|
391
|
+
if (isPresented) {
|
|
392
|
+
delegate?.viewControllerWillFocus()
|
|
413
393
|
}
|
|
414
394
|
},
|
|
415
395
|
onModalDismissed = {
|
|
416
396
|
if (isPresented) {
|
|
417
397
|
showDialog()
|
|
398
|
+
delegate?.viewControllerDidFocus()
|
|
418
399
|
}
|
|
419
400
|
}
|
|
420
401
|
)
|
|
@@ -426,59 +407,53 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
426
407
|
rnScreensObserver = null
|
|
427
408
|
}
|
|
428
409
|
|
|
410
|
+
private fun emitWillDismissEvents() {
|
|
411
|
+
delegate?.viewControllerWillBlur()
|
|
412
|
+
delegate?.viewControllerWillDismiss()
|
|
413
|
+
parentSheetView?.viewControllerWillFocus()
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private fun emitDidDismissEvents() {
|
|
417
|
+
val hadParent = parentSheetView != null
|
|
418
|
+
parentSheetView?.viewControllerDidFocus()
|
|
419
|
+
parentSheetView = null
|
|
420
|
+
|
|
421
|
+
delegate?.viewControllerDidBlur()
|
|
422
|
+
delegate?.viewControllerDidDismiss(hadParent)
|
|
423
|
+
|
|
424
|
+
dismissPromise?.invoke()
|
|
425
|
+
dismissPromise = null
|
|
426
|
+
}
|
|
427
|
+
|
|
429
428
|
// ====================================================================
|
|
430
429
|
// MARK: - Dialog Visibility (for stacking)
|
|
431
430
|
// ====================================================================
|
|
432
431
|
|
|
433
|
-
/**
|
|
434
|
-
* Returns true if the sheet's top is at or above the status bar.
|
|
435
|
-
*/
|
|
436
432
|
val isExpanded: Boolean
|
|
437
433
|
get() {
|
|
438
434
|
val sheetTop = bottomSheetView?.top ?: return false
|
|
439
435
|
return sheetTop <= statusBarHeight
|
|
440
436
|
}
|
|
441
437
|
|
|
442
|
-
/**
|
|
443
|
-
* Returns the current top position of the sheet (Y coordinate from screen top).
|
|
444
|
-
* Used for comparing sheet positions during stacking.
|
|
445
|
-
*/
|
|
446
438
|
val currentSheetTop: Int
|
|
447
439
|
get() = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
448
440
|
|
|
449
|
-
/**
|
|
450
|
-
* Returns the expected top position of the sheet when presented at the given detent index.
|
|
451
|
-
* Used for comparing sheet positions before presentation.
|
|
452
|
-
*/
|
|
453
441
|
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
454
442
|
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
455
443
|
val detentHeight = getDetentHeight(detents[detentIndex])
|
|
456
444
|
return screenHeight - detentHeight
|
|
457
445
|
}
|
|
458
446
|
|
|
459
|
-
/**
|
|
460
|
-
* Hides the dialog without dismissing it.
|
|
461
|
-
* Used when another TrueSheet presents on top or when RN screen is presented.
|
|
462
|
-
*/
|
|
447
|
+
/** Hides without dismissing. Used for sheet stacking and RN Screens modals. */
|
|
463
448
|
fun hideDialog() {
|
|
464
449
|
isDialogVisible = false
|
|
465
|
-
dialog?.window?.decorView?.visibility =
|
|
466
|
-
|
|
467
|
-
// Emit off-screen position (detent = 0 since sheet is fully hidden)
|
|
468
|
-
emitChangePositionDelegate(currentDetentIndex, screenHeight, realtime = false)
|
|
450
|
+
dialog?.window?.decorView?.visibility = INVISIBLE
|
|
469
451
|
}
|
|
470
452
|
|
|
471
|
-
/**
|
|
472
|
-
* Shows a previously hidden dialog.
|
|
473
|
-
* Used when the sheet on top dismisses.
|
|
474
|
-
*/
|
|
453
|
+
/** Shows a previously hidden dialog. */
|
|
475
454
|
fun showDialog() {
|
|
476
455
|
isDialogVisible = true
|
|
477
|
-
dialog?.window?.decorView?.visibility =
|
|
478
|
-
|
|
479
|
-
// Emit current position
|
|
480
|
-
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
481
|
-
emitChangePositionDelegate(currentDetentIndex, positionPx, realtime = false)
|
|
456
|
+
dialog?.window?.decorView?.visibility = VISIBLE
|
|
482
457
|
}
|
|
483
458
|
|
|
484
459
|
// ====================================================================
|
|
@@ -494,8 +469,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
494
469
|
setupDimmedBackground(detentIndex)
|
|
495
470
|
|
|
496
471
|
if (isPresented) {
|
|
497
|
-
// Detent change will be emitted when sheet settles in onStateChanged
|
|
498
|
-
// Don't update currentDetentIndex here - it will be updated when sheet settles
|
|
499
472
|
setStateForDetentIndex(detentIndex)
|
|
500
473
|
} else {
|
|
501
474
|
currentDetentIndex = detentIndex
|
|
@@ -506,10 +479,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
506
479
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
507
480
|
val detent = getDetentValueForIndex(detentInfo.index)
|
|
508
481
|
|
|
509
|
-
// Notify parent sheet that it is about to lose focus (before this sheet appears)
|
|
510
482
|
parentSheetView?.viewControllerWillBlur()
|
|
511
|
-
|
|
512
483
|
delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position, detent)
|
|
484
|
+
delegate?.viewControllerWillFocus()
|
|
513
485
|
|
|
514
486
|
if (!animated) {
|
|
515
487
|
dialog.window?.setWindowAnimations(0)
|
|
@@ -519,11 +491,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
519
491
|
}
|
|
520
492
|
}
|
|
521
493
|
|
|
522
|
-
fun dismiss() {
|
|
494
|
+
fun dismiss(animated: Boolean = true) {
|
|
495
|
+
if (isDismissing) return
|
|
496
|
+
|
|
497
|
+
isDismissing = true
|
|
498
|
+
emitWillDismissEvents()
|
|
499
|
+
|
|
523
500
|
this.post {
|
|
524
|
-
|
|
525
|
-
emitChangePositionDelegate(currentDetentIndex, screenHeight, realtime = false)
|
|
501
|
+
emitChangePositionDelegate(screenHeight, realtime = false)
|
|
526
502
|
}
|
|
503
|
+
|
|
504
|
+
if (!animated) {
|
|
505
|
+
dialog?.window?.setWindowAnimations(0)
|
|
506
|
+
}
|
|
507
|
+
|
|
527
508
|
dialog?.dismiss()
|
|
528
509
|
}
|
|
529
510
|
|
|
@@ -534,13 +515,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
534
515
|
fun setupSheetDetents() {
|
|
535
516
|
val behavior = this.behavior ?: return
|
|
536
517
|
|
|
537
|
-
// Reset resolved positions if detents count changed
|
|
538
518
|
if (resolvedDetentPositions.size != detents.size) {
|
|
539
519
|
resolvedDetentPositions.clear()
|
|
540
520
|
repeat(detents.size) { resolvedDetentPositions.add(0) }
|
|
541
521
|
}
|
|
542
522
|
|
|
543
|
-
// Always update auto detent positions based on current content height
|
|
544
523
|
for (i in detents.indices) {
|
|
545
524
|
if (detents[i] == -1.0) {
|
|
546
525
|
val detentHeight = getDetentHeight(detents[i])
|
|
@@ -548,39 +527,48 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
548
527
|
}
|
|
549
528
|
}
|
|
550
529
|
|
|
551
|
-
// Flag to prevent state change callbacks from updating detent index during reconfiguration
|
|
552
530
|
isReconfiguring = true
|
|
553
531
|
|
|
554
532
|
behavior.apply {
|
|
555
|
-
skipCollapsed = false
|
|
556
533
|
isFitToContents = false
|
|
557
534
|
maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
558
535
|
|
|
536
|
+
val oldExpandOffset = expandedOffset
|
|
537
|
+
|
|
559
538
|
when (detents.size) {
|
|
560
539
|
1 -> {
|
|
561
540
|
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
541
|
+
halfExpandedRatio = minOf(peekHeight.toFloat() / screenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
|
|
562
542
|
expandedOffset = screenHeight - peekHeight
|
|
543
|
+
isFitToContents = expandedOffset == 0
|
|
563
544
|
}
|
|
564
545
|
|
|
565
546
|
2 -> {
|
|
566
547
|
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
548
|
+
halfExpandedRatio = minOf(getDetentHeight(detents[1]).toFloat() / screenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
|
|
567
549
|
expandedOffset = screenHeight - getDetentHeight(detents[1])
|
|
550
|
+
isFitToContents = expandedOffset == 0
|
|
568
551
|
}
|
|
569
552
|
|
|
570
553
|
3 -> {
|
|
571
554
|
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
572
|
-
halfExpandedRatio = minOf(getDetentHeight(detents[1]).toFloat() / screenHeight.toFloat(),
|
|
555
|
+
halfExpandedRatio = minOf(getDetentHeight(detents[1]).toFloat() / screenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
|
|
573
556
|
expandedOffset = screenHeight - getDetentHeight(detents[2])
|
|
574
557
|
}
|
|
575
558
|
}
|
|
576
|
-
}
|
|
577
559
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
560
|
+
if (oldExpandOffset != expandedOffset || expandedOffset == 0) {
|
|
561
|
+
val offset = if (expandedOffset == 0) statusBarHeight else 0
|
|
562
|
+
val newHeight = screenHeight - expandedOffset - offset
|
|
563
|
+
delegate?.viewControllerDidChangeSize(width, newHeight)
|
|
564
|
+
}
|
|
582
565
|
|
|
583
|
-
|
|
566
|
+
if (isPresented) {
|
|
567
|
+
setStateForDetentIndex(currentDetentIndex)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
isReconfiguring = false
|
|
571
|
+
}
|
|
584
572
|
}
|
|
585
573
|
|
|
586
574
|
fun setupGrabber() {
|
|
@@ -612,10 +600,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
612
600
|
bottomSheet.clipToOutline = true
|
|
613
601
|
}
|
|
614
602
|
|
|
615
|
-
/**
|
|
616
|
-
* Configures the dimmed background based on the current detent index.
|
|
617
|
-
* When not dimmed, touch events pass through to the activity behind the sheet.
|
|
618
|
-
*/
|
|
603
|
+
/** Configures dim and touch-through behavior based on detent index. */
|
|
619
604
|
fun setupDimmedBackground(detentIndex: Int) {
|
|
620
605
|
val dialog = this.dialog ?: return
|
|
621
606
|
dialog.window?.apply {
|
|
@@ -624,9 +609,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
624
609
|
if (dimmed && detentIndex >= dimmedDetentIndex) {
|
|
625
610
|
view.setOnTouchListener(null)
|
|
626
611
|
setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
612
|
+
setDimAmount(0.32f) // M3 scrim opacity
|
|
627
613
|
dialog.setCanceledOnTouchOutside(dismissible)
|
|
628
614
|
} else {
|
|
629
|
-
// Forward touch events to the activity when not dimmed
|
|
630
615
|
view.setOnTouchListener { v, event ->
|
|
631
616
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
632
617
|
reactContext.currentActivity?.dispatchTouchEvent(event)
|
|
@@ -642,11 +627,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
642
627
|
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
643
628
|
}
|
|
644
629
|
|
|
645
|
-
/**
|
|
646
|
-
* Positions the footer view at the bottom of the sheet.
|
|
647
|
-
* The footer stays fixed at the bottom edge of the visible sheet area,
|
|
648
|
-
* adjusting during drag gestures via slideOffset.
|
|
649
|
-
*/
|
|
630
|
+
/** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
|
|
650
631
|
fun positionFooter(slideOffset: Float? = null) {
|
|
651
632
|
val footerView = containerView?.footerView ?: return
|
|
652
633
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -656,13 +637,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
656
637
|
|
|
657
638
|
var footerY = (screenHeight - bottomSheetY - footerHeight).toFloat()
|
|
658
639
|
|
|
659
|
-
// Animate footer down with sheet when below peek height
|
|
660
640
|
if (slideOffset != null && slideOffset < 0) {
|
|
661
641
|
footerY -= (footerHeight * slideOffset)
|
|
662
642
|
}
|
|
663
643
|
|
|
664
|
-
// Clamp footer position to prevent it from going off screen when positioning at the top
|
|
665
|
-
// This happens when fullScreen is enabled in edge-to-edge mode
|
|
666
644
|
val maxAllowedY = (screenHeight - statusBarHeight - footerHeight).toFloat()
|
|
667
645
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
668
646
|
}
|
|
@@ -690,16 +668,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
690
668
|
}
|
|
691
669
|
|
|
692
670
|
// ====================================================================
|
|
693
|
-
// MARK: - Position
|
|
671
|
+
// MARK: - Position & Drag Handling
|
|
694
672
|
// ====================================================================
|
|
695
673
|
|
|
696
|
-
|
|
697
|
-
* Emits position change to the delegate if the position has changed.
|
|
698
|
-
* @param index The current detent index (discrete, used as fallback)
|
|
699
|
-
* @param positionPx The current position in pixels (screen Y coordinate)
|
|
700
|
-
* @param realtime Whether the position is a real-time value (during drag or animation tracking)
|
|
701
|
-
*/
|
|
702
|
-
private fun emitChangePositionDelegate(index: Int, positionPx: Int, realtime: Boolean) {
|
|
674
|
+
private fun emitChangePositionDelegate(positionPx: Int, realtime: Boolean) {
|
|
703
675
|
if (positionPx == lastEmittedPositionPx) return
|
|
704
676
|
|
|
705
677
|
lastEmittedPositionPx = positionPx
|
|
@@ -709,11 +681,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
709
681
|
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
710
682
|
}
|
|
711
683
|
|
|
712
|
-
/**
|
|
713
|
-
* Stores the current Y position as the resolved position for the given detent index.
|
|
714
|
-
* This is called when the sheet settles at a detent to capture the actual position
|
|
715
|
-
* which may differ from the calculated position due to system adjustments.
|
|
716
|
-
*/
|
|
717
684
|
private fun storeResolvedPosition(index: Int) {
|
|
718
685
|
if (index < 0 || index >= resolvedDetentPositions.size) return
|
|
719
686
|
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: return
|
|
@@ -722,24 +689,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
722
689
|
}
|
|
723
690
|
}
|
|
724
691
|
|
|
725
|
-
/**
|
|
726
|
-
* Stores the resolved position for the current detent.
|
|
727
|
-
* Called from TrueSheetView when content size changes.
|
|
728
|
-
*/
|
|
729
692
|
fun storeCurrentResolvedPosition() {
|
|
730
693
|
storeResolvedPosition(currentDetentIndex)
|
|
731
694
|
}
|
|
732
695
|
|
|
733
|
-
/**
|
|
734
|
-
* Returns the estimated Y position for a detent index, using stored positions when available.
|
|
735
|
-
*/
|
|
736
696
|
private fun getEstimatedPositionForIndex(index: Int): Int {
|
|
737
697
|
if (index < 0 || index >= resolvedDetentPositions.size) return screenHeight
|
|
738
698
|
|
|
739
699
|
val storedPos = resolvedDetentPositions[index]
|
|
740
700
|
if (storedPos > 0) return storedPos
|
|
741
701
|
|
|
742
|
-
// Estimate based on getDetentHeight which accounts for bottomInset and maxAllowedHeight
|
|
743
702
|
if (index < detents.size) {
|
|
744
703
|
val detentHeight = getDetentHeight(detents[index])
|
|
745
704
|
return screenHeight - detentHeight
|
|
@@ -748,11 +707,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
748
707
|
return screenHeight
|
|
749
708
|
}
|
|
750
709
|
|
|
751
|
-
/**
|
|
752
|
-
* Finds the segment index and interpolation progress for a given position.
|
|
753
|
-
* Returns a Triple of (fromIndex, toIndex, progress) where progress is 0-1 within that segment.
|
|
754
|
-
* Returns null if position count is less than 2.
|
|
755
|
-
*/
|
|
710
|
+
/** Returns (fromIndex, toIndex, progress) for interpolation, or null if < 2 detents. */
|
|
756
711
|
private fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
|
|
757
712
|
val count = resolvedDetentPositions.size
|
|
758
713
|
if (count < 2) return null
|
|
@@ -760,24 +715,21 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
760
715
|
val firstPos = getEstimatedPositionForIndex(0)
|
|
761
716
|
val lastPos = getEstimatedPositionForIndex(count - 1)
|
|
762
717
|
|
|
763
|
-
// Below first detent
|
|
764
718
|
if (positionPx > firstPos) {
|
|
765
719
|
val range = screenHeight - firstPos
|
|
766
720
|
val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
|
|
767
|
-
return Triple(-1, 0, progress)
|
|
721
|
+
return Triple(-1, 0, progress)
|
|
768
722
|
}
|
|
769
723
|
|
|
770
|
-
// Above last detent
|
|
771
724
|
if (positionPx < lastPos) {
|
|
772
725
|
return Triple(count - 1, count - 1, 0f)
|
|
773
726
|
}
|
|
774
727
|
|
|
775
|
-
// Find segment (positions decrease as index increases)
|
|
776
728
|
for (i in 0 until count - 1) {
|
|
777
729
|
val pos = getEstimatedPositionForIndex(i)
|
|
778
730
|
val nextPos = getEstimatedPositionForIndex(i + 1)
|
|
779
731
|
|
|
780
|
-
if (positionPx
|
|
732
|
+
if (positionPx in nextPos..pos) {
|
|
781
733
|
val range = pos - nextPos
|
|
782
734
|
val progress = if (range > 0) (pos - positionPx).toFloat() / range else 0f
|
|
783
735
|
return Triple(i, i + 1, maxOf(0f, minOf(1f, progress)))
|
|
@@ -787,10 +739,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
787
739
|
return Triple(count - 1, count - 1, 0f)
|
|
788
740
|
}
|
|
789
741
|
|
|
790
|
-
/**
|
|
791
|
-
* Calculates the interpolated index based on position.
|
|
792
|
-
* Returns a continuous value (e.g., 0.5 means halfway between detent 0 and 1).
|
|
793
|
-
*/
|
|
742
|
+
/** Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1). */
|
|
794
743
|
private fun getInterpolatedIndexForPosition(positionPx: Int): Float {
|
|
795
744
|
val count = resolvedDetentPositions.size
|
|
796
745
|
if (count == 0) return -1f
|
|
@@ -799,16 +748,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
799
748
|
val segment = findSegmentForPosition(positionPx) ?: return 0f
|
|
800
749
|
val (fromIndex, _, progress) = segment
|
|
801
750
|
|
|
802
|
-
// Below first detent
|
|
803
751
|
if (fromIndex == -1) return -progress
|
|
804
|
-
|
|
805
752
|
return fromIndex + progress
|
|
806
753
|
}
|
|
807
754
|
|
|
808
|
-
/**
|
|
809
|
-
* Calculates the interpolated detent value based on position.
|
|
810
|
-
* Returns the actual screen fraction, clamped to valid detent range.
|
|
811
|
-
*/
|
|
755
|
+
/** Returns interpolated screen fraction for position. */
|
|
812
756
|
private fun getInterpolatedDetentForPosition(positionPx: Int): Float {
|
|
813
757
|
val count = resolvedDetentPositions.size
|
|
814
758
|
if (count == 0) return 0f
|
|
@@ -816,7 +760,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
816
760
|
val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
|
|
817
761
|
val (fromIndex, toIndex, progress) = segment
|
|
818
762
|
|
|
819
|
-
// Below first detent
|
|
820
763
|
if (fromIndex == -1) {
|
|
821
764
|
val firstDetent = getDetentValueForIndex(0)
|
|
822
765
|
return maxOf(0f, firstDetent * (1 - progress))
|
|
@@ -827,11 +770,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
827
770
|
return fromDetent + progress * (toDetent - fromDetent)
|
|
828
771
|
}
|
|
829
772
|
|
|
830
|
-
/**
|
|
831
|
-
* Gets the detent value (fraction) for a given index.
|
|
832
|
-
* Returns the raw screen fraction without bottomInset for interpolation calculations.
|
|
833
|
-
* Note: bottomInset is only added in getDetentHeight() for actual sheet sizing.
|
|
834
|
-
*/
|
|
773
|
+
/** Returns raw screen fraction for index (without bottomInset). */
|
|
835
774
|
private fun getDetentValueForIndex(index: Int): Float {
|
|
836
775
|
if (index < 0 || index >= detents.size) return 0f
|
|
837
776
|
val value = detents[index]
|
|
@@ -842,10 +781,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
842
781
|
}
|
|
843
782
|
}
|
|
844
783
|
|
|
845
|
-
// ====================================================================
|
|
846
|
-
// MARK: - Drag Handling
|
|
847
|
-
// ====================================================================
|
|
848
|
-
|
|
849
784
|
private fun getCurrentDetentInfo(sheetView: View): DetentInfo {
|
|
850
785
|
val screenY = ScreenUtils.getScreenY(sheetView)
|
|
851
786
|
return DetentInfo(currentDetentIndex, screenY.pxToDp())
|
|
@@ -853,26 +788,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
853
788
|
|
|
854
789
|
private fun getCurrentPositionPx(sheetView: View): Int = ScreenUtils.getScreenY(sheetView)
|
|
855
790
|
|
|
856
|
-
/**
|
|
857
|
-
* Returns the detent index for the current position.
|
|
858
|
-
* Only reports a higher index when the sheet has reached that detent's height.
|
|
859
|
-
*/
|
|
860
|
-
private fun getDetentIndexForPosition(positionPx: Int): Int {
|
|
861
|
-
if (detents.isEmpty()) return 0
|
|
862
|
-
|
|
863
|
-
val sheetHeight = screenHeight - positionPx
|
|
864
|
-
|
|
865
|
-
// Find the highest detent index that the sheet has reached
|
|
866
|
-
for (i in detents.indices.reversed()) {
|
|
867
|
-
val detentHeight = getDetentHeight(detents[i])
|
|
868
|
-
if (sheetHeight >= detentHeight) {
|
|
869
|
-
return i
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return 0
|
|
874
|
-
}
|
|
875
|
-
|
|
876
791
|
private fun handleDragBegin(sheetView: View) {
|
|
877
792
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
878
793
|
val detent = getDetentValueForIndex(detentInfo.index)
|
|
@@ -893,14 +808,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
893
808
|
|
|
894
809
|
private fun getDetentHeight(detent: Double): Int {
|
|
895
810
|
val height: Int = if (detent == -1.0) {
|
|
896
|
-
// For auto detent, add bottomInset to match iOS behavior where the system
|
|
897
|
-
// adds bottom safe area inset internally to the sheet height
|
|
898
811
|
contentHeight + headerHeight + bottomInset
|
|
899
812
|
} else {
|
|
900
813
|
if (detent <= 0.0 || detent > 1.0) {
|
|
901
814
|
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
902
815
|
}
|
|
903
|
-
// For fractional detents, add bottomInset to match iOS behavior
|
|
904
816
|
(detent * screenHeight).toInt() + bottomInset
|
|
905
817
|
}
|
|
906
818
|
|
|
@@ -980,7 +892,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
980
892
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
981
893
|
if (w == oldw && h == oldh) return
|
|
982
894
|
|
|
983
|
-
|
|
895
|
+
// Skip continuous size changes when fullScreen + edge-to-edge
|
|
896
|
+
if (h + statusBarHeight > screenHeight && isExpanded && oldw == w) {
|
|
897
|
+
return
|
|
898
|
+
}
|
|
984
899
|
|
|
985
900
|
val oldScreenHeight = screenHeight
|
|
986
901
|
screenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
@@ -991,7 +906,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
991
906
|
positionFooter()
|
|
992
907
|
storeResolvedPosition(currentDetentIndex)
|
|
993
908
|
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
994
|
-
emitChangePositionDelegate(
|
|
909
|
+
emitChangePositionDelegate(positionPx, realtime = false)
|
|
995
910
|
}
|
|
996
911
|
}
|
|
997
912
|
}
|
|
@@ -1004,11 +919,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1004
919
|
// MARK: - Touch Event Handling
|
|
1005
920
|
// ====================================================================
|
|
1006
921
|
|
|
1007
|
-
/**
|
|
1008
|
-
* Custom touch dispatch to handle footer touch events.
|
|
1009
|
-
* The footer is positioned outside the normal view hierarchy, so we need to
|
|
1010
|
-
* manually check if touches fall within its bounds and forward them.
|
|
1011
|
-
*/
|
|
922
|
+
/** Forwards touch events to footer which is positioned outside normal hierarchy. */
|
|
1012
923
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1013
924
|
val footer = containerView?.footerView
|
|
1014
925
|
if (footer != null && footer.isVisible) {
|