@lodev09/react-native-true-sheet 3.1.0-beta.7 → 3.1.0-beta.9
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 +0 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +30 -175
- 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/TrueSheetViewController.mm +2 -83
- package/package.json +1 -1
|
@@ -22,10 +22,6 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
|
|
|
22
22
|
|
|
23
23
|
override fun getName(): String = NAME
|
|
24
24
|
|
|
25
|
-
override fun initialize() {
|
|
26
|
-
super.initialize()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
override fun invalidate() {
|
|
30
26
|
super.invalidate()
|
|
31
27
|
// Clear all registered views and observer on module invalidation
|
|
@@ -170,48 +170,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
170
170
|
val statusBarHeight: Int
|
|
171
171
|
get() = ScreenUtils.getStatusBarHeight(reactContext)
|
|
172
172
|
|
|
173
|
-
/**
|
|
174
|
-
* The bottom inset (navigation bar height) to add to sheet height.
|
|
175
|
-
* This matches iOS behavior where the system adds bottom safe area inset internally.
|
|
176
|
-
*/
|
|
173
|
+
/** Navigation bar height, added to sheet height to match iOS behavior. */
|
|
177
174
|
private val bottomInset: Int
|
|
178
175
|
get() = ScreenUtils.getNavigationBarHeight(reactContext)
|
|
179
176
|
|
|
180
|
-
/**
|
|
181
|
-
* Edge-to-edge is enabled by default on API 36+, or when explicitly configured.
|
|
182
|
-
*/
|
|
177
|
+
/** Edge-to-edge enabled by default on API 36+, or when explicitly configured. */
|
|
183
178
|
private val edgeToEdgeEnabled: Boolean
|
|
184
179
|
get() {
|
|
185
180
|
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
186
181
|
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
187
182
|
}
|
|
188
183
|
|
|
189
|
-
/**
|
|
190
|
-
* The top inset to apply when edge-to-edge is enabled but not full-screen.
|
|
191
|
-
* This prevents the sheet from going under the status bar.
|
|
192
|
-
*/
|
|
184
|
+
/** Top inset when edge-to-edge is enabled but not full-screen. */
|
|
193
185
|
private val sheetTopInset: Int
|
|
194
186
|
get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) statusBarHeight else 0
|
|
195
187
|
|
|
196
|
-
// ====================================================================
|
|
197
|
-
// MARK: - Touch Dispatchers
|
|
198
|
-
// ====================================================================
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Touch dispatchers are required for RootView to properly forward touch events to React Native.
|
|
202
|
-
*/
|
|
203
188
|
internal var eventDispatcher: EventDispatcher? = null
|
|
204
189
|
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
205
190
|
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
206
191
|
|
|
207
|
-
|
|
208
|
-
// MARK: - Modal Observer
|
|
209
|
-
// ====================================================================
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Observes react-native-screens modal fragments to hide/show the sheet appropriately.
|
|
213
|
-
* This prevents the sheet from rendering on top of modals.
|
|
214
|
-
*/
|
|
192
|
+
/** Hides/shows the sheet when RN Screens modals are presented/dismissed. */
|
|
215
193
|
private var rnScreensObserver: RNScreensFragmentObserver? = null
|
|
216
194
|
|
|
217
195
|
// ====================================================================
|
|
@@ -253,7 +231,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
253
231
|
behavior.isHideable = dismissible
|
|
254
232
|
behavior.isDraggable = draggable
|
|
255
233
|
|
|
256
|
-
// Handle back press
|
|
257
234
|
onBackPressedDispatcher.addCallback(object : androidx.activity.OnBackPressedCallback(true) {
|
|
258
235
|
override fun handleOnBackPressed() {
|
|
259
236
|
this@TrueSheetViewController.delegate?.viewControllerDidBackPress()
|
|
@@ -293,25 +270,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
293
270
|
|
|
294
271
|
sheetContainer?.post {
|
|
295
272
|
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
296
|
-
|
|
297
|
-
// Store resolved position for initial detent
|
|
298
273
|
storeResolvedPosition(currentDetentIndex)
|
|
299
274
|
emitChangePositionDelegate(positionPx, realtime = false)
|
|
300
|
-
|
|
301
275
|
positionFooter()
|
|
302
276
|
}
|
|
303
277
|
|
|
304
|
-
// Emit didPresent/didFocus after present animation completes
|
|
305
278
|
sheetContainer?.postDelayed({
|
|
306
279
|
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
307
280
|
val detent = getDetentValueForIndex(detentInfo.index)
|
|
308
281
|
|
|
309
282
|
delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position, detent)
|
|
310
|
-
|
|
311
|
-
// Notify parent sheet that it has lost focus (after this sheet appeared)
|
|
312
283
|
parentSheetView?.viewControllerDidBlur()
|
|
313
|
-
|
|
314
|
-
// Emit didFocus with didPresent
|
|
315
284
|
delegate?.viewControllerDidFocus()
|
|
316
285
|
|
|
317
286
|
presentPromise?.invoke()
|
|
@@ -320,25 +289,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
320
289
|
}
|
|
321
290
|
|
|
322
291
|
dialog.setOnCancelListener {
|
|
323
|
-
// Skip if already handled by STATE_HIDDEN (programmatic dismiss)
|
|
324
292
|
if (isDismissing) return@setOnCancelListener
|
|
325
293
|
|
|
326
|
-
// User-initiated dismiss (back button, tap outside)
|
|
327
294
|
isDismissing = true
|
|
328
295
|
emitWillDismissEvents()
|
|
329
|
-
|
|
330
|
-
// Emit off-screen position since onSlide isn't triggered for user-initiated dismiss
|
|
331
296
|
emitChangePositionDelegate(screenHeight, realtime = false)
|
|
297
|
+
}
|
|
332
298
|
|
|
333
|
-
|
|
299
|
+
dialog.setOnDismissListener {
|
|
334
300
|
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
335
301
|
emitDidDismissEvents()
|
|
302
|
+
cleanupDialog()
|
|
336
303
|
}, DISMISS_ANIMATION_DURATION)
|
|
337
304
|
}
|
|
338
|
-
|
|
339
|
-
dialog.setOnDismissListener {
|
|
340
|
-
cleanupDialog()
|
|
341
|
-
}
|
|
342
305
|
}
|
|
343
306
|
|
|
344
307
|
private fun setupBottomSheetBehavior(dialog: BottomSheetDialog) {
|
|
@@ -362,9 +325,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
362
325
|
|
|
363
326
|
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
364
327
|
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
|
365
|
-
|
|
328
|
+
if (isDismissing) return
|
|
366
329
|
isDismissing = true
|
|
367
|
-
|
|
330
|
+
emitWillDismissEvents()
|
|
368
331
|
dialog.dismiss()
|
|
369
332
|
return
|
|
370
333
|
}
|
|
@@ -377,15 +340,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
377
340
|
BottomSheetBehavior.STATE_EXPANDED,
|
|
378
341
|
BottomSheetBehavior.STATE_COLLAPSED,
|
|
379
342
|
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
|
|
380
|
-
// Ignore state changes triggered by content size reconfiguration
|
|
381
343
|
if (isReconfiguring) return
|
|
382
344
|
|
|
383
345
|
getDetentInfoForState(newState)?.let { detentInfo ->
|
|
384
|
-
// Store resolved position when sheet settles
|
|
385
346
|
storeResolvedPosition(detentInfo.index)
|
|
386
347
|
|
|
387
348
|
if (isDragging) {
|
|
388
|
-
// Handle drag end
|
|
389
349
|
val detent = getDetentValueForIndex(detentInfo.index)
|
|
390
350
|
delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
|
|
391
351
|
|
|
@@ -398,13 +358,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
398
358
|
}
|
|
399
359
|
|
|
400
360
|
isDragging = false
|
|
401
|
-
} else {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
currentDetentIndex = detentInfo.index
|
|
406
|
-
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
407
|
-
}
|
|
361
|
+
} else if (detentInfo.index != currentDetentIndex) {
|
|
362
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
363
|
+
currentDetentIndex = detentInfo.index
|
|
364
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
408
365
|
}
|
|
409
366
|
}
|
|
410
367
|
}
|
|
@@ -450,22 +407,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
450
407
|
rnScreensObserver = null
|
|
451
408
|
}
|
|
452
409
|
|
|
453
|
-
// ====================================================================
|
|
454
|
-
// MARK: - Dismiss Event Helpers
|
|
455
|
-
// ====================================================================
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Emits willBlur and willDismiss events, and notifies parent sheet of upcoming focus change.
|
|
459
|
-
*/
|
|
460
410
|
private fun emitWillDismissEvents() {
|
|
461
411
|
delegate?.viewControllerWillBlur()
|
|
462
412
|
delegate?.viewControllerWillDismiss()
|
|
463
413
|
parentSheetView?.viewControllerWillFocus()
|
|
464
414
|
}
|
|
465
415
|
|
|
466
|
-
/**
|
|
467
|
-
* Emits didBlur and didDismiss events, notifies parent sheet, and invokes dismiss promise.
|
|
468
|
-
*/
|
|
469
416
|
private fun emitDidDismissEvents() {
|
|
470
417
|
val hadParent = parentSheetView != null
|
|
471
418
|
parentSheetView?.viewControllerDidFocus()
|
|
@@ -482,53 +429,32 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
482
429
|
// MARK: - Dialog Visibility (for stacking)
|
|
483
430
|
// ====================================================================
|
|
484
431
|
|
|
485
|
-
/**
|
|
486
|
-
* Returns true if the sheet's top is at or above the status bar.
|
|
487
|
-
*/
|
|
488
432
|
val isExpanded: Boolean
|
|
489
433
|
get() {
|
|
490
434
|
val sheetTop = bottomSheetView?.top ?: return false
|
|
491
435
|
return sheetTop <= statusBarHeight
|
|
492
436
|
}
|
|
493
437
|
|
|
494
|
-
/**
|
|
495
|
-
* Returns the current top position of the sheet (Y coordinate from screen top).
|
|
496
|
-
* Used for comparing sheet positions during stacking.
|
|
497
|
-
*/
|
|
498
438
|
val currentSheetTop: Int
|
|
499
439
|
get() = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
500
440
|
|
|
501
|
-
/**
|
|
502
|
-
* Returns the expected top position of the sheet when presented at the given detent index.
|
|
503
|
-
* Used for comparing sheet positions before presentation.
|
|
504
|
-
*/
|
|
505
441
|
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
506
442
|
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
507
443
|
val detentHeight = getDetentHeight(detents[detentIndex])
|
|
508
444
|
return screenHeight - detentHeight
|
|
509
445
|
}
|
|
510
446
|
|
|
511
|
-
/**
|
|
512
|
-
* Hides the dialog without dismissing it.
|
|
513
|
-
* Used when another TrueSheet presents on top or when RN screen is presented.
|
|
514
|
-
*/
|
|
447
|
+
/** Hides without dismissing. Used for sheet stacking and RN Screens modals. */
|
|
515
448
|
fun hideDialog() {
|
|
516
449
|
isDialogVisible = false
|
|
517
450
|
dialog?.window?.decorView?.visibility = INVISIBLE
|
|
518
|
-
|
|
519
|
-
// Emit off-screen position (detent = 0 since sheet is fully hidden)
|
|
520
451
|
emitChangePositionDelegate(screenHeight, realtime = false)
|
|
521
452
|
}
|
|
522
453
|
|
|
523
|
-
/**
|
|
524
|
-
* Shows a previously hidden dialog.
|
|
525
|
-
* Used when the sheet on top dismisses.
|
|
526
|
-
*/
|
|
454
|
+
/** Shows a previously hidden dialog. */
|
|
527
455
|
fun showDialog() {
|
|
528
456
|
isDialogVisible = true
|
|
529
457
|
dialog?.window?.decorView?.visibility = VISIBLE
|
|
530
|
-
|
|
531
|
-
// Emit current position
|
|
532
458
|
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
533
459
|
emitChangePositionDelegate(positionPx, realtime = false)
|
|
534
460
|
}
|
|
@@ -546,8 +472,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
546
472
|
setupDimmedBackground(detentIndex)
|
|
547
473
|
|
|
548
474
|
if (isPresented) {
|
|
549
|
-
// Detent change will be emitted when sheet settles in onStateChanged
|
|
550
|
-
// Don't update currentDetentIndex here - it will be updated when sheet settles
|
|
551
475
|
setStateForDetentIndex(detentIndex)
|
|
552
476
|
} else {
|
|
553
477
|
currentDetentIndex = detentIndex
|
|
@@ -558,12 +482,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
558
482
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
559
483
|
val detent = getDetentValueForIndex(detentInfo.index)
|
|
560
484
|
|
|
561
|
-
// Notify parent sheet that it is about to lose focus (before this sheet appears)
|
|
562
485
|
parentSheetView?.viewControllerWillBlur()
|
|
563
|
-
|
|
564
486
|
delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position, detent)
|
|
565
|
-
|
|
566
|
-
// Emit willFocus with willPresent
|
|
567
487
|
delegate?.viewControllerWillFocus()
|
|
568
488
|
|
|
569
489
|
if (!animated) {
|
|
@@ -575,10 +495,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
575
495
|
}
|
|
576
496
|
|
|
577
497
|
fun dismiss(animated: Boolean = true) {
|
|
498
|
+
if (isDismissing) return
|
|
499
|
+
|
|
500
|
+
isDismissing = true
|
|
578
501
|
emitWillDismissEvents()
|
|
579
502
|
|
|
580
503
|
this.post {
|
|
581
|
-
// Emit off-screen position (detent = 0 since sheet is fully hidden)
|
|
582
504
|
emitChangePositionDelegate(screenHeight, realtime = false)
|
|
583
505
|
}
|
|
584
506
|
|
|
@@ -586,9 +508,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
586
508
|
dialog?.window?.setWindowAnimations(0)
|
|
587
509
|
}
|
|
588
510
|
|
|
589
|
-
|
|
590
|
-
behavior?.isHideable = true
|
|
591
|
-
behavior?.state = BottomSheetBehavior.STATE_HIDDEN
|
|
511
|
+
dialog?.dismiss()
|
|
592
512
|
}
|
|
593
513
|
|
|
594
514
|
// ====================================================================
|
|
@@ -598,13 +518,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
598
518
|
fun setupSheetDetents() {
|
|
599
519
|
val behavior = this.behavior ?: return
|
|
600
520
|
|
|
601
|
-
// Reset resolved positions if detents count changed
|
|
602
521
|
if (resolvedDetentPositions.size != detents.size) {
|
|
603
522
|
resolvedDetentPositions.clear()
|
|
604
523
|
repeat(detents.size) { resolvedDetentPositions.add(0) }
|
|
605
524
|
}
|
|
606
525
|
|
|
607
|
-
// Always update auto detent positions based on current content height
|
|
608
526
|
for (i in detents.indices) {
|
|
609
527
|
if (detents[i] == -1.0) {
|
|
610
528
|
val detentHeight = getDetentHeight(detents[i])
|
|
@@ -612,7 +530,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
612
530
|
}
|
|
613
531
|
}
|
|
614
532
|
|
|
615
|
-
// Flag to prevent state change callbacks from updating detent index during reconfiguration
|
|
616
533
|
isReconfiguring = true
|
|
617
534
|
|
|
618
535
|
behavior.apply {
|
|
@@ -643,7 +560,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
643
560
|
}
|
|
644
561
|
}
|
|
645
562
|
|
|
646
|
-
// Keep container size in sync with sheet size
|
|
647
563
|
if (oldExpandOffset != expandedOffset || expandedOffset == 0) {
|
|
648
564
|
val offset = if (expandedOffset == 0) statusBarHeight else 0
|
|
649
565
|
val newHeight = screenHeight - expandedOffset - offset
|
|
@@ -651,7 +567,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
651
567
|
}
|
|
652
568
|
|
|
653
569
|
if (isPresented) {
|
|
654
|
-
// Re-apply current state to update position after config changes
|
|
655
570
|
setStateForDetentIndex(currentDetentIndex)
|
|
656
571
|
}
|
|
657
572
|
|
|
@@ -688,10 +603,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
688
603
|
bottomSheet.clipToOutline = true
|
|
689
604
|
}
|
|
690
605
|
|
|
691
|
-
/**
|
|
692
|
-
* Configures the dimmed background based on the current detent index.
|
|
693
|
-
* When not dimmed, touch events pass through to the activity behind the sheet.
|
|
694
|
-
*/
|
|
606
|
+
/** Configures dim and touch-through behavior based on detent index. */
|
|
695
607
|
fun setupDimmedBackground(detentIndex: Int) {
|
|
696
608
|
val dialog = this.dialog ?: return
|
|
697
609
|
dialog.window?.apply {
|
|
@@ -700,9 +612,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
700
612
|
if (dimmed && detentIndex >= dimmedDetentIndex) {
|
|
701
613
|
view.setOnTouchListener(null)
|
|
702
614
|
setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
615
|
+
setDimAmount(0.32f) // M3 scrim opacity
|
|
703
616
|
dialog.setCanceledOnTouchOutside(dismissible)
|
|
704
617
|
} else {
|
|
705
|
-
// Forward touch events to the activity when not dimmed
|
|
706
618
|
view.setOnTouchListener { v, event ->
|
|
707
619
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
708
620
|
reactContext.currentActivity?.dispatchTouchEvent(event)
|
|
@@ -718,11 +630,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
718
630
|
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
719
631
|
}
|
|
720
632
|
|
|
721
|
-
/**
|
|
722
|
-
* Positions the footer view at the bottom of the sheet.
|
|
723
|
-
* The footer stays fixed at the bottom edge of the visible sheet area,
|
|
724
|
-
* adjusting during drag gestures via slideOffset.
|
|
725
|
-
*/
|
|
633
|
+
/** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
|
|
726
634
|
fun positionFooter(slideOffset: Float? = null) {
|
|
727
635
|
val footerView = containerView?.footerView ?: return
|
|
728
636
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -732,13 +640,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
732
640
|
|
|
733
641
|
var footerY = (screenHeight - bottomSheetY - footerHeight).toFloat()
|
|
734
642
|
|
|
735
|
-
// Animate footer down with sheet when below peek height
|
|
736
643
|
if (slideOffset != null && slideOffset < 0) {
|
|
737
644
|
footerY -= (footerHeight * slideOffset)
|
|
738
645
|
}
|
|
739
646
|
|
|
740
|
-
// Clamp footer position to prevent it from going off screen when positioning at the top
|
|
741
|
-
// This happens when fullScreen is enabled in edge-to-edge mode
|
|
742
647
|
val maxAllowedY = (screenHeight - statusBarHeight - footerHeight).toFloat()
|
|
743
648
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
744
649
|
}
|
|
@@ -766,14 +671,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
766
671
|
}
|
|
767
672
|
|
|
768
673
|
// ====================================================================
|
|
769
|
-
// MARK: - Position
|
|
674
|
+
// MARK: - Position & Drag Handling
|
|
770
675
|
// ====================================================================
|
|
771
676
|
|
|
772
|
-
/**
|
|
773
|
-
* Emits position change to the delegate if the position has changed.
|
|
774
|
-
* @param positionPx The current position in pixels (screen Y coordinate)
|
|
775
|
-
* @param realtime Whether the position is a real-time value (during drag or animation tracking)
|
|
776
|
-
*/
|
|
777
677
|
private fun emitChangePositionDelegate(positionPx: Int, realtime: Boolean) {
|
|
778
678
|
if (positionPx == lastEmittedPositionPx) return
|
|
779
679
|
|
|
@@ -784,11 +684,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
784
684
|
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
785
685
|
}
|
|
786
686
|
|
|
787
|
-
/**
|
|
788
|
-
* Stores the current Y position as the resolved position for the given detent index.
|
|
789
|
-
* This is called when the sheet settles at a detent to capture the actual position
|
|
790
|
-
* which may differ from the calculated position due to system adjustments.
|
|
791
|
-
*/
|
|
792
687
|
private fun storeResolvedPosition(index: Int) {
|
|
793
688
|
if (index < 0 || index >= resolvedDetentPositions.size) return
|
|
794
689
|
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: return
|
|
@@ -797,24 +692,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
797
692
|
}
|
|
798
693
|
}
|
|
799
694
|
|
|
800
|
-
/**
|
|
801
|
-
* Stores the resolved position for the current detent.
|
|
802
|
-
* Called from TrueSheetView when content size changes.
|
|
803
|
-
*/
|
|
804
695
|
fun storeCurrentResolvedPosition() {
|
|
805
696
|
storeResolvedPosition(currentDetentIndex)
|
|
806
697
|
}
|
|
807
698
|
|
|
808
|
-
/**
|
|
809
|
-
* Returns the estimated Y position for a detent index, using stored positions when available.
|
|
810
|
-
*/
|
|
811
699
|
private fun getEstimatedPositionForIndex(index: Int): Int {
|
|
812
700
|
if (index < 0 || index >= resolvedDetentPositions.size) return screenHeight
|
|
813
701
|
|
|
814
702
|
val storedPos = resolvedDetentPositions[index]
|
|
815
703
|
if (storedPos > 0) return storedPos
|
|
816
704
|
|
|
817
|
-
// Estimate based on getDetentHeight which accounts for bottomInset and maxAllowedHeight
|
|
818
705
|
if (index < detents.size) {
|
|
819
706
|
val detentHeight = getDetentHeight(detents[index])
|
|
820
707
|
return screenHeight - detentHeight
|
|
@@ -823,11 +710,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
823
710
|
return screenHeight
|
|
824
711
|
}
|
|
825
712
|
|
|
826
|
-
/**
|
|
827
|
-
* Finds the segment index and interpolation progress for a given position.
|
|
828
|
-
* Returns a Triple of (fromIndex, toIndex, progress) where progress is 0-1 within that segment.
|
|
829
|
-
* Returns null if position count is less than 2.
|
|
830
|
-
*/
|
|
713
|
+
/** Returns (fromIndex, toIndex, progress) for interpolation, or null if < 2 detents. */
|
|
831
714
|
private fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
|
|
832
715
|
val count = resolvedDetentPositions.size
|
|
833
716
|
if (count < 2) return null
|
|
@@ -835,19 +718,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
835
718
|
val firstPos = getEstimatedPositionForIndex(0)
|
|
836
719
|
val lastPos = getEstimatedPositionForIndex(count - 1)
|
|
837
720
|
|
|
838
|
-
// Below first detent
|
|
839
721
|
if (positionPx > firstPos) {
|
|
840
722
|
val range = screenHeight - firstPos
|
|
841
723
|
val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
|
|
842
|
-
return Triple(-1, 0, progress)
|
|
724
|
+
return Triple(-1, 0, progress)
|
|
843
725
|
}
|
|
844
726
|
|
|
845
|
-
// Above last detent
|
|
846
727
|
if (positionPx < lastPos) {
|
|
847
728
|
return Triple(count - 1, count - 1, 0f)
|
|
848
729
|
}
|
|
849
730
|
|
|
850
|
-
// Find segment (positions decrease as index increases)
|
|
851
731
|
for (i in 0 until count - 1) {
|
|
852
732
|
val pos = getEstimatedPositionForIndex(i)
|
|
853
733
|
val nextPos = getEstimatedPositionForIndex(i + 1)
|
|
@@ -862,10 +742,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
862
742
|
return Triple(count - 1, count - 1, 0f)
|
|
863
743
|
}
|
|
864
744
|
|
|
865
|
-
/**
|
|
866
|
-
* Calculates the interpolated index based on position.
|
|
867
|
-
* Returns a continuous value (e.g., 0.5 means halfway between detent 0 and 1).
|
|
868
|
-
*/
|
|
745
|
+
/** Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1). */
|
|
869
746
|
private fun getInterpolatedIndexForPosition(positionPx: Int): Float {
|
|
870
747
|
val count = resolvedDetentPositions.size
|
|
871
748
|
if (count == 0) return -1f
|
|
@@ -874,16 +751,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
874
751
|
val segment = findSegmentForPosition(positionPx) ?: return 0f
|
|
875
752
|
val (fromIndex, _, progress) = segment
|
|
876
753
|
|
|
877
|
-
// Below first detent
|
|
878
754
|
if (fromIndex == -1) return -progress
|
|
879
|
-
|
|
880
755
|
return fromIndex + progress
|
|
881
756
|
}
|
|
882
757
|
|
|
883
|
-
/**
|
|
884
|
-
* Calculates the interpolated detent value based on position.
|
|
885
|
-
* Returns the actual screen fraction, clamped to valid detent range.
|
|
886
|
-
*/
|
|
758
|
+
/** Returns interpolated screen fraction for position. */
|
|
887
759
|
private fun getInterpolatedDetentForPosition(positionPx: Int): Float {
|
|
888
760
|
val count = resolvedDetentPositions.size
|
|
889
761
|
if (count == 0) return 0f
|
|
@@ -891,7 +763,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
891
763
|
val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
|
|
892
764
|
val (fromIndex, toIndex, progress) = segment
|
|
893
765
|
|
|
894
|
-
// Below first detent
|
|
895
766
|
if (fromIndex == -1) {
|
|
896
767
|
val firstDetent = getDetentValueForIndex(0)
|
|
897
768
|
return maxOf(0f, firstDetent * (1 - progress))
|
|
@@ -902,11 +773,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
902
773
|
return fromDetent + progress * (toDetent - fromDetent)
|
|
903
774
|
}
|
|
904
775
|
|
|
905
|
-
/**
|
|
906
|
-
* Gets the detent value (fraction) for a given index.
|
|
907
|
-
* Returns the raw screen fraction without bottomInset for interpolation calculations.
|
|
908
|
-
* Note: bottomInset is only added in getDetentHeight() for actual sheet sizing.
|
|
909
|
-
*/
|
|
776
|
+
/** Returns raw screen fraction for index (without bottomInset). */
|
|
910
777
|
private fun getDetentValueForIndex(index: Int): Float {
|
|
911
778
|
if (index < 0 || index >= detents.size) return 0f
|
|
912
779
|
val value = detents[index]
|
|
@@ -917,10 +784,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
917
784
|
}
|
|
918
785
|
}
|
|
919
786
|
|
|
920
|
-
// ====================================================================
|
|
921
|
-
// MARK: - Drag Handling
|
|
922
|
-
// ====================================================================
|
|
923
|
-
|
|
924
787
|
private fun getCurrentDetentInfo(sheetView: View): DetentInfo {
|
|
925
788
|
val screenY = ScreenUtils.getScreenY(sheetView)
|
|
926
789
|
return DetentInfo(currentDetentIndex, screenY.pxToDp())
|
|
@@ -948,14 +811,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
948
811
|
|
|
949
812
|
private fun getDetentHeight(detent: Double): Int {
|
|
950
813
|
val height: Int = if (detent == -1.0) {
|
|
951
|
-
// For auto detent, add bottomInset to match iOS behavior where the system
|
|
952
|
-
// adds bottom safe area inset internally to the sheet height
|
|
953
814
|
contentHeight + headerHeight + bottomInset
|
|
954
815
|
} else {
|
|
955
816
|
if (detent <= 0.0 || detent > 1.0) {
|
|
956
817
|
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
957
818
|
}
|
|
958
|
-
// For fractional detents, add bottomInset to match iOS behavior
|
|
959
819
|
(detent * screenHeight).toInt() + bottomInset
|
|
960
820
|
}
|
|
961
821
|
|
|
@@ -1035,8 +895,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1035
895
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
1036
896
|
if (w == oldw && h == oldh) return
|
|
1037
897
|
|
|
1038
|
-
// Skip
|
|
1039
|
-
// Size keeps changing on this case
|
|
898
|
+
// Skip continuous size changes when fullScreen + edge-to-edge
|
|
1040
899
|
if (h + statusBarHeight > screenHeight && isExpanded && oldw == w) {
|
|
1041
900
|
return
|
|
1042
901
|
}
|
|
@@ -1063,11 +922,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1063
922
|
// MARK: - Touch Event Handling
|
|
1064
923
|
// ====================================================================
|
|
1065
924
|
|
|
1066
|
-
/**
|
|
1067
|
-
* Custom touch dispatch to handle footer touch events.
|
|
1068
|
-
* The footer is positioned outside the normal view hierarchy, so we need to
|
|
1069
|
-
* manually check if touches fall within its bounds and forward them.
|
|
1070
|
-
*/
|
|
925
|
+
/** Forwards touch events to footer which is positioned outside normal hierarchy. */
|
|
1071
926
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1072
927
|
val footer = containerView?.footerView
|
|
1073
928
|
if (footer != null && footer.isVisible) {
|
|
@@ -28,36 +28,12 @@ target_include_directories(
|
|
|
28
28
|
${LIB_ANDROID_GENERATED_COMPONENTS_DIR}
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
log
|
|
38
|
-
)
|
|
39
|
-
else()
|
|
40
|
-
target_link_libraries(
|
|
41
|
-
${LIB_TARGET_NAME}
|
|
42
|
-
fbjni
|
|
43
|
-
folly_runtime
|
|
44
|
-
glog
|
|
45
|
-
jsi
|
|
46
|
-
react_codegen_rncore
|
|
47
|
-
react_debug
|
|
48
|
-
react_nativemodule_core
|
|
49
|
-
react_render_core
|
|
50
|
-
react_render_debug
|
|
51
|
-
react_render_graphics
|
|
52
|
-
react_render_mapbuffer
|
|
53
|
-
react_render_componentregistry
|
|
54
|
-
react_utils
|
|
55
|
-
rrc_view
|
|
56
|
-
turbomodulejsijni
|
|
57
|
-
yoga
|
|
58
|
-
log
|
|
59
|
-
)
|
|
60
|
-
endif()
|
|
31
|
+
target_link_libraries(
|
|
32
|
+
${LIB_TARGET_NAME}
|
|
33
|
+
ReactAndroid::reactnative
|
|
34
|
+
ReactAndroid::jsi
|
|
35
|
+
fbjni::fbjni
|
|
36
|
+
)
|
|
61
37
|
|
|
62
38
|
target_include_directories(
|
|
63
39
|
${CMAKE_PROJECT_NAME}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
3
|
<translate
|
|
4
|
-
android:duration="
|
|
4
|
+
android:duration="250"
|
|
5
5
|
android:fromYDelta="20%p"
|
|
6
6
|
android:toYDelta="0"
|
|
7
|
-
android:interpolator="@android:interpolator/
|
|
7
|
+
android:interpolator="@android:interpolator/decelerate_cubic" />
|
|
8
8
|
<alpha
|
|
9
|
-
android:duration="
|
|
9
|
+
android:duration="150"
|
|
10
10
|
android:fromAlpha="0.0"
|
|
11
11
|
android:toAlpha="1.0"
|
|
12
|
-
android:interpolator="@android:interpolator/
|
|
12
|
+
android:interpolator="@android:interpolator/decelerate_cubic" />
|
|
13
13
|
</set>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
3
|
<translate
|
|
4
|
-
android:duration="
|
|
4
|
+
android:duration="200"
|
|
5
5
|
android:fromYDelta="0"
|
|
6
6
|
android:toYDelta="20%p"
|
|
7
|
-
android:interpolator="@android:interpolator/
|
|
7
|
+
android:interpolator="@android:interpolator/accelerate_cubic" />
|
|
8
8
|
<alpha
|
|
9
9
|
android:duration="150"
|
|
10
|
+
android:startOffset="50"
|
|
10
11
|
android:fromAlpha="1.0"
|
|
11
12
|
android:toAlpha="0.0"
|
|
12
|
-
android:interpolator="@android:interpolator/
|
|
13
|
+
android:interpolator="@android:interpolator/accelerate_cubic" />
|
|
13
14
|
</set>
|
|
@@ -25,26 +25,18 @@
|
|
|
25
25
|
NSInteger _pendingDetentIndex;
|
|
26
26
|
BOOL _pendingContentSizeChange;
|
|
27
27
|
|
|
28
|
-
// Position tracking
|
|
29
28
|
CADisplayLink *_transitioningTimer;
|
|
30
29
|
UIView *_transitionFakeView;
|
|
31
30
|
BOOL _isDragging;
|
|
32
31
|
BOOL _isTransitioning;
|
|
33
32
|
BOOL _isTrackingPositionFromLayout;
|
|
34
33
|
|
|
35
|
-
// Reference to parent TrueSheetViewController (if presented from another sheet)
|
|
36
34
|
__weak TrueSheetViewController *_parentSheetController;
|
|
37
35
|
|
|
38
|
-
// Blur effect view
|
|
39
36
|
TrueSheetBlurView *_blurView;
|
|
40
|
-
|
|
41
|
-
// Custom grabber view
|
|
42
37
|
TrueSheetGrabberView *_grabberView;
|
|
43
38
|
|
|
44
|
-
// Resolved detent positions (Y coordinate when sheet rests at each detent)
|
|
45
39
|
NSMutableArray<NSNumber *> *_resolvedDetentPositions;
|
|
46
|
-
|
|
47
|
-
// Tracks whether this sheet has a presented controller (e.g., RN Screens modal)
|
|
48
40
|
BOOL _hasPresentedController;
|
|
49
41
|
}
|
|
50
42
|
|
|
@@ -138,7 +130,6 @@
|
|
|
138
130
|
[super viewDidLoad];
|
|
139
131
|
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
|
|
140
132
|
|
|
141
|
-
// Create custom grabber view (hidden by default, shown when grabberOptions is set)
|
|
142
133
|
_grabberView = [[TrueSheetGrabberView alloc] init];
|
|
143
134
|
_grabberView.hidden = YES;
|
|
144
135
|
[_grabberView addToView:self.view];
|
|
@@ -147,18 +138,14 @@
|
|
|
147
138
|
- (void)viewWillAppear:(BOOL)animated {
|
|
148
139
|
[super viewWillAppear:animated];
|
|
149
140
|
|
|
150
|
-
// Only trigger on initial presentation, not repositioning
|
|
151
141
|
if (!_isPresented) {
|
|
152
|
-
// Initially store resolved position during presentation
|
|
153
142
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
154
143
|
[self storeResolvedPositionForIndex:self.currentDetentIndex];
|
|
155
144
|
});
|
|
156
145
|
|
|
157
|
-
// Capture parent sheet reference if presented from another TrueSheet
|
|
158
146
|
UIViewController *presenter = self.presentingViewController;
|
|
159
147
|
if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
|
|
160
148
|
_parentSheetController = (TrueSheetViewController *)presenter;
|
|
161
|
-
// Notify parent that it is about to lose focus
|
|
162
149
|
if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
|
|
163
150
|
[_parentSheetController.delegate viewControllerWillBlur];
|
|
164
151
|
}
|
|
@@ -173,7 +160,6 @@
|
|
|
173
160
|
[self.delegate viewControllerWillPresentAtIndex:index position:position detent:detent];
|
|
174
161
|
}
|
|
175
162
|
|
|
176
|
-
// Emit willFocus with willPresent
|
|
177
163
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
|
|
178
164
|
[self.delegate viewControllerWillFocus];
|
|
179
165
|
}
|
|
@@ -187,7 +173,6 @@
|
|
|
187
173
|
[super viewDidAppear:animated];
|
|
188
174
|
|
|
189
175
|
if (!_isPresented) {
|
|
190
|
-
// Notify parent that it has lost focus (after the child sheet appeared)
|
|
191
176
|
if (_parentSheetController) {
|
|
192
177
|
if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
|
|
193
178
|
[_parentSheetController.delegate viewControllerDidBlur];
|
|
@@ -201,12 +186,10 @@
|
|
|
201
186
|
[self.delegate viewControllerDidPresentAtIndex:index position:self.currentPosition detent:detent];
|
|
202
187
|
}
|
|
203
188
|
|
|
204
|
-
// Emit didFocus with didPresent
|
|
205
189
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
|
|
206
190
|
[self.delegate viewControllerDidFocus];
|
|
207
191
|
}
|
|
208
192
|
|
|
209
|
-
// Emit correct position after presentation
|
|
210
193
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"did present"];
|
|
211
194
|
});
|
|
212
195
|
|
|
@@ -224,7 +207,6 @@
|
|
|
224
207
|
|
|
225
208
|
if (self.isDismissing) {
|
|
226
209
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
227
|
-
// Emit willBlur with willDismiss
|
|
228
210
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
|
|
229
211
|
[self.delegate viewControllerWillBlur];
|
|
230
212
|
}
|
|
@@ -234,7 +216,6 @@
|
|
|
234
216
|
}
|
|
235
217
|
});
|
|
236
218
|
|
|
237
|
-
// Notify the parent sheet (if any) that it is about to regain focus
|
|
238
219
|
if (_parentSheetController) {
|
|
239
220
|
if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
|
|
240
221
|
[_parentSheetController.delegate viewControllerWillFocus];
|
|
@@ -248,12 +229,10 @@
|
|
|
248
229
|
- (void)viewDidDisappear:(BOOL)animated {
|
|
249
230
|
[super viewDidDisappear:animated];
|
|
250
231
|
|
|
251
|
-
// Only dispatch didDismiss when actually dismissing (not when another modal is presented on top)
|
|
252
232
|
if (self.isDismissing) {
|
|
253
233
|
_isPresented = NO;
|
|
254
234
|
_activeDetentIndex = -1;
|
|
255
235
|
|
|
256
|
-
// Notify the parent sheet (if any) that it regained focus
|
|
257
236
|
if (_parentSheetController) {
|
|
258
237
|
if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
|
|
259
238
|
[_parentSheetController.delegate viewControllerDidFocus];
|
|
@@ -261,7 +240,6 @@
|
|
|
261
240
|
_parentSheetController = nil;
|
|
262
241
|
}
|
|
263
242
|
|
|
264
|
-
// Emit didBlur with didDismiss
|
|
265
243
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
|
|
266
244
|
[self.delegate viewControllerDidBlur];
|
|
267
245
|
}
|
|
@@ -279,17 +257,12 @@
|
|
|
279
257
|
_isTrackingPositionFromLayout = YES;
|
|
280
258
|
|
|
281
259
|
UIViewController *presented = self.presentedViewController;
|
|
282
|
-
|
|
283
|
-
// Not realtime when another controller is presented that triggers our layout
|
|
284
260
|
BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
|
|
285
|
-
|
|
286
261
|
BOOL realtime = !hasPresentedController;
|
|
287
262
|
|
|
288
263
|
if (_pendingContentSizeChange) {
|
|
289
264
|
_pendingContentSizeChange = NO;
|
|
290
265
|
realtime = NO;
|
|
291
|
-
|
|
292
|
-
// Store resolved position after content size changes
|
|
293
266
|
[self storeResolvedPositionForIndex:self.currentDetentIndex];
|
|
294
267
|
}
|
|
295
268
|
|
|
@@ -304,7 +277,6 @@
|
|
|
304
277
|
[self.delegate viewControllerDidChangeSize:self.view.frame.size];
|
|
305
278
|
}
|
|
306
279
|
|
|
307
|
-
// Emit pending detent change after programmatic resize settles
|
|
308
280
|
if (_pendingDetentIndex >= 0) {
|
|
309
281
|
NSInteger pendingIndex = _pendingDetentIndex;
|
|
310
282
|
_pendingDetentIndex = -1;
|
|
@@ -313,8 +285,6 @@
|
|
|
313
285
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:detent:)]) {
|
|
314
286
|
CGFloat detent = [self detentValueForIndex:pendingIndex];
|
|
315
287
|
[self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
|
|
316
|
-
|
|
317
|
-
// Emit position for the final position
|
|
318
288
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"pending detent change"];
|
|
319
289
|
}
|
|
320
290
|
});
|
|
@@ -323,18 +293,15 @@
|
|
|
323
293
|
_isTrackingPositionFromLayout = NO;
|
|
324
294
|
}
|
|
325
295
|
|
|
326
|
-
#pragma mark - Presentation Tracking (
|
|
296
|
+
#pragma mark - Presentation Tracking (RN Screens)
|
|
327
297
|
|
|
328
298
|
- (void)presentViewController:(UIViewController *)viewControllerToPresent
|
|
329
299
|
animated:(BOOL)flag
|
|
330
300
|
completion:(void (^)(void))completion {
|
|
331
|
-
// Check if this is a non-TrueSheet controller (e.g., RN Screens modal)
|
|
332
301
|
BOOL isExternalController = ![viewControllerToPresent isKindOfClass:[TrueSheetViewController class]];
|
|
333
302
|
|
|
334
303
|
if (isExternalController && !_hasPresentedController) {
|
|
335
304
|
_hasPresentedController = YES;
|
|
336
|
-
|
|
337
|
-
// Emit blur events when an external controller is presented on top
|
|
338
305
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
|
|
339
306
|
[self.delegate viewControllerWillBlur];
|
|
340
307
|
}
|
|
@@ -359,7 +326,6 @@
|
|
|
359
326
|
BOOL isExternalController = presented && ![presented isKindOfClass:[TrueSheetViewController class]];
|
|
360
327
|
|
|
361
328
|
if (isExternalController && _hasPresentedController) {
|
|
362
|
-
// Emit focus events when external controller is dismissed
|
|
363
329
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
|
|
364
330
|
[self.delegate viewControllerWillFocus];
|
|
365
331
|
}
|
|
@@ -379,7 +345,7 @@
|
|
|
379
345
|
}];
|
|
380
346
|
}
|
|
381
347
|
|
|
382
|
-
#pragma mark - Gesture Handling
|
|
348
|
+
#pragma mark - Position & Gesture Handling
|
|
383
349
|
|
|
384
350
|
- (TrueSheetContentView *)findContentView:(UIView *)view {
|
|
385
351
|
if ([view isKindOfClass:[TrueSheetContentView class]]) {
|
|
@@ -401,11 +367,9 @@
|
|
|
401
367
|
if (!presentedView)
|
|
402
368
|
return;
|
|
403
369
|
|
|
404
|
-
// Disable pan gestures if draggable is NO
|
|
405
370
|
if (!self.draggable) {
|
|
406
371
|
[GestureUtil setPanGesturesEnabled:NO forView:presentedView];
|
|
407
372
|
|
|
408
|
-
// Also disable ScrollView's pan gesture if present
|
|
409
373
|
TrueSheetContentView *contentView = [self findContentView:presentedView];
|
|
410
374
|
if (contentView) {
|
|
411
375
|
RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
|
|
@@ -416,10 +380,8 @@
|
|
|
416
380
|
return;
|
|
417
381
|
}
|
|
418
382
|
|
|
419
|
-
// Attach to presented view's pan gesture (sheet's drag gesture from UIKit)
|
|
420
383
|
[GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
|
|
421
384
|
|
|
422
|
-
// Also attach to ScrollView's pan gesture if present
|
|
423
385
|
TrueSheetContentView *contentView = [self findContentView:presentedView];
|
|
424
386
|
if (contentView) {
|
|
425
387
|
RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
|
|
@@ -438,7 +400,6 @@
|
|
|
438
400
|
|
|
439
401
|
[GestureUtil setPanGesturesEnabled:self.draggable forView:presentedView];
|
|
440
402
|
|
|
441
|
-
// Also update ScrollView's pan gesture if present
|
|
442
403
|
TrueSheetContentView *contentView = [self findContentView:presentedView];
|
|
443
404
|
if (contentView) {
|
|
444
405
|
RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
|
|
@@ -469,10 +430,7 @@
|
|
|
469
430
|
case UIGestureRecognizerStateCancelled: {
|
|
470
431
|
if (!_isTransitioning) {
|
|
471
432
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
472
|
-
// Store resolved position when drag ends
|
|
473
433
|
[self storeResolvedPositionForIndex:self.currentDetentIndex];
|
|
474
|
-
|
|
475
|
-
// Emit the correct position after dragging
|
|
476
434
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"drag end"];
|
|
477
435
|
});
|
|
478
436
|
}
|
|
@@ -485,8 +443,6 @@
|
|
|
485
443
|
}
|
|
486
444
|
}
|
|
487
445
|
|
|
488
|
-
#pragma mark - Position Tracking
|
|
489
|
-
|
|
490
446
|
- (void)setupTransitionTracker {
|
|
491
447
|
if (!self.transitionCoordinator)
|
|
492
448
|
return;
|
|
@@ -496,14 +452,11 @@
|
|
|
496
452
|
CGRect dismissedFrame = CGRectMake(0, self.screenHeight, 0, 0);
|
|
497
453
|
CGRect presentedFrame = CGRectMake(0, self.currentPosition, 0, 0);
|
|
498
454
|
|
|
499
|
-
// Set starting fake view position
|
|
500
455
|
_transitionFakeView.frame = self.isDismissing ? presentedFrame : dismissedFrame;
|
|
501
456
|
[self storeResolvedPositionForIndex:self.currentDetentIndex];
|
|
502
457
|
|
|
503
458
|
auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
504
459
|
[[context containerView] addSubview:self->_transitionFakeView];
|
|
505
|
-
|
|
506
|
-
// Set ending fake view position
|
|
507
460
|
self->_transitionFakeView.frame = self.isDismissing ? dismissedFrame : presentedFrame;
|
|
508
461
|
|
|
509
462
|
self->_transitioningTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleTransitionTracker)];
|
|
@@ -523,15 +476,12 @@
|
|
|
523
476
|
- (void)handleTransitionTracker {
|
|
524
477
|
if (!_isDragging && _transitionFakeView.layer) {
|
|
525
478
|
CALayer *layer = _transitionFakeView.layer;
|
|
526
|
-
|
|
527
479
|
CGFloat layerPosition = layer.presentationLayer.frame.origin.y;
|
|
528
480
|
|
|
529
481
|
if (self.currentPosition >= self.screenHeight) {
|
|
530
|
-
// Dismissing position
|
|
531
482
|
CGFloat position = fmax(_lastPosition, layerPosition);
|
|
532
483
|
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition out"];
|
|
533
484
|
} else {
|
|
534
|
-
// Presenting position
|
|
535
485
|
CGFloat position = fmax(self.currentPosition, layerPosition);
|
|
536
486
|
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition in"];
|
|
537
487
|
}
|
|
@@ -539,7 +489,6 @@
|
|
|
539
489
|
}
|
|
540
490
|
|
|
541
491
|
- (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime debug:(NSString *)debug {
|
|
542
|
-
// Use epsilon comparison to avoid missing updates due to floating point precision
|
|
543
492
|
if (fabs(_lastPosition - position) > 0.01) {
|
|
544
493
|
_lastPosition = position;
|
|
545
494
|
|
|
@@ -548,20 +497,15 @@
|
|
|
548
497
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:detent:realtime:)]) {
|
|
549
498
|
[self.delegate viewControllerDidChangePosition:index position:position detent:detent realtime:realtime];
|
|
550
499
|
}
|
|
551
|
-
|
|
552
|
-
// Debug position tracking
|
|
553
|
-
// NSLog(@"position from %@: %f, realtime: %i", debug, position, realtime);
|
|
554
500
|
}
|
|
555
501
|
}
|
|
556
502
|
|
|
557
|
-
/// Stores the current position for the given detent index
|
|
558
503
|
- (void)storeResolvedPositionForIndex:(NSInteger)index {
|
|
559
504
|
if (index >= 0 && index < (NSInteger)_resolvedDetentPositions.count) {
|
|
560
505
|
_resolvedDetentPositions[index] = @(self.currentPosition);
|
|
561
506
|
}
|
|
562
507
|
}
|
|
563
508
|
|
|
564
|
-
/// Returns the estimated Y position for a detent index, using stored positions when available
|
|
565
509
|
- (CGFloat)estimatedPositionForIndex:(NSInteger)index {
|
|
566
510
|
if (index < 0 || index >= (NSInteger)_resolvedDetentPositions.count)
|
|
567
511
|
return 0;
|
|
@@ -571,11 +515,9 @@
|
|
|
571
515
|
return storedPos;
|
|
572
516
|
}
|
|
573
517
|
|
|
574
|
-
// Estimate based on detent value and known offset from first resolved position
|
|
575
518
|
CGFloat detentValue = [self detentValueForIndex:index];
|
|
576
519
|
CGFloat basePosition = self.screenHeight - (detentValue * self.screenHeight);
|
|
577
520
|
|
|
578
|
-
// Find a resolved position to calculate offset
|
|
579
521
|
for (NSInteger i = 0; i < (NSInteger)_resolvedDetentPositions.count; i++) {
|
|
580
522
|
CGFloat pos = [_resolvedDetentPositions[i] doubleValue];
|
|
581
523
|
if (pos > 0) {
|
|
@@ -589,8 +531,6 @@
|
|
|
589
531
|
return basePosition;
|
|
590
532
|
}
|
|
591
533
|
|
|
592
|
-
/// Finds the segment containing the given position and returns the lower index and progress within that segment.
|
|
593
|
-
/// Returns YES if a segment was found, NO otherwise. When NO, `outIndex` contains the boundary index.
|
|
594
534
|
- (BOOL)findSegmentForPosition:(CGFloat)position outIndex:(NSInteger *)outIndex outProgress:(CGFloat *)outProgress {
|
|
595
535
|
NSInteger count = _resolvedDetentPositions.count;
|
|
596
536
|
if (count == 0) {
|
|
@@ -608,7 +548,6 @@
|
|
|
608
548
|
CGFloat firstPos = [self estimatedPositionForIndex:0];
|
|
609
549
|
CGFloat lastPos = [self estimatedPositionForIndex:count - 1];
|
|
610
550
|
|
|
611
|
-
// Below first detent (position > firstPos means sheet is smaller)
|
|
612
551
|
if (position > firstPos) {
|
|
613
552
|
CGFloat range = self.screenHeight - firstPos;
|
|
614
553
|
*outIndex = -1;
|
|
@@ -616,14 +555,12 @@
|
|
|
616
555
|
return NO;
|
|
617
556
|
}
|
|
618
557
|
|
|
619
|
-
// Above last detent
|
|
620
558
|
if (position < lastPos) {
|
|
621
559
|
*outIndex = count - 1;
|
|
622
560
|
*outProgress = 0;
|
|
623
561
|
return NO;
|
|
624
562
|
}
|
|
625
563
|
|
|
626
|
-
// Find segment (positions decrease as index increases)
|
|
627
564
|
for (NSInteger i = 0; i < count - 1; i++) {
|
|
628
565
|
CGFloat pos = [self estimatedPositionForIndex:i];
|
|
629
566
|
CGFloat nextPos = [self estimatedPositionForIndex:i + 1];
|
|
@@ -648,14 +585,11 @@
|
|
|
648
585
|
|
|
649
586
|
if (!found) {
|
|
650
587
|
if (index == -1) {
|
|
651
|
-
// Below first detent - return negative progress
|
|
652
588
|
return -progress;
|
|
653
589
|
}
|
|
654
|
-
// At or beyond boundary
|
|
655
590
|
return index;
|
|
656
591
|
}
|
|
657
592
|
|
|
658
|
-
// Within a segment - interpolate
|
|
659
593
|
return index + fmax(0, fmin(1, progress));
|
|
660
594
|
}
|
|
661
595
|
|
|
@@ -666,15 +600,12 @@
|
|
|
666
600
|
|
|
667
601
|
if (!found) {
|
|
668
602
|
if (index == -1) {
|
|
669
|
-
// Below first detent
|
|
670
603
|
CGFloat firstDetent = [self detentValueForIndex:0];
|
|
671
604
|
return fmax(0, firstDetent * (1 - progress));
|
|
672
605
|
}
|
|
673
|
-
// At or beyond boundary
|
|
674
606
|
return [self detentValueForIndex:index];
|
|
675
607
|
}
|
|
676
608
|
|
|
677
|
-
// Within a segment - interpolate between detent values
|
|
678
609
|
CGFloat detent = [self detentValueForIndex:index];
|
|
679
610
|
CGFloat nextDetent = [self detentValueForIndex:index + 1];
|
|
680
611
|
return detent + progress * (nextDetent - detent);
|
|
@@ -683,7 +614,6 @@
|
|
|
683
614
|
- (CGFloat)detentValueForIndex:(NSInteger)index {
|
|
684
615
|
if (index >= 0 && index < (NSInteger)_detents.count) {
|
|
685
616
|
CGFloat value = [_detents[index] doubleValue];
|
|
686
|
-
// For auto (-1), calculate actual fraction from content + header height
|
|
687
617
|
if (value == -1) {
|
|
688
618
|
CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue];
|
|
689
619
|
return autoHeight / self.screenHeight;
|
|
@@ -716,13 +646,11 @@
|
|
|
716
646
|
withAutoHeight:autoHeight
|
|
717
647
|
atIndex:index];
|
|
718
648
|
[detents addObject:sheetDetent];
|
|
719
|
-
// Initialize with placeholder - will be updated when sheet settles at each detent
|
|
720
649
|
[_resolvedDetentPositions addObject:@(0)];
|
|
721
650
|
}
|
|
722
651
|
|
|
723
652
|
sheet.detents = detents;
|
|
724
653
|
|
|
725
|
-
// Setup dimmed background
|
|
726
654
|
if (self.dimmed && [self.dimmedDetentIndex integerValue] == 0) {
|
|
727
655
|
sheet.largestUndimmedDetentIdentifier = nil;
|
|
728
656
|
} else {
|
|
@@ -752,7 +680,6 @@
|
|
|
752
680
|
|
|
753
681
|
CGFloat value = [detent doubleValue];
|
|
754
682
|
|
|
755
|
-
// -1 represents "auto" (fit content height)
|
|
756
683
|
if (value == -1) {
|
|
757
684
|
if (@available(iOS 16.0, *)) {
|
|
758
685
|
return [self customDetentWithIdentifier:@"custom-auto" height:autoHeight];
|
|
@@ -818,7 +745,6 @@
|
|
|
818
745
|
if (detentCount == 0)
|
|
819
746
|
return;
|
|
820
747
|
|
|
821
|
-
// Clamp index to valid range
|
|
822
748
|
NSInteger clampedIndex = _activeDetentIndex;
|
|
823
749
|
if (clampedIndex < 0) {
|
|
824
750
|
clampedIndex = 0;
|
|
@@ -875,7 +801,6 @@
|
|
|
875
801
|
|
|
876
802
|
self.view.backgroundColor = self.backgroundColor;
|
|
877
803
|
|
|
878
|
-
// Setup blur effect view - recreate only when blurTint changes
|
|
879
804
|
BOOL blurTintChanged = ![_blurView.blurTint isEqualToString:self.blurTint];
|
|
880
805
|
|
|
881
806
|
if (_blurView && blurTintChanged) {
|
|
@@ -894,11 +819,9 @@
|
|
|
894
819
|
[_blurView applyBlurEffect];
|
|
895
820
|
}
|
|
896
821
|
|
|
897
|
-
// Setup grabber
|
|
898
822
|
BOOL showGrabber = self.grabber && self.draggable;
|
|
899
823
|
|
|
900
824
|
if (self.grabberOptions) {
|
|
901
|
-
// Use custom grabber view when options are provided
|
|
902
825
|
sheet.prefersGrabberVisible = NO;
|
|
903
826
|
|
|
904
827
|
NSDictionary *options = self.grabberOptions;
|
|
@@ -910,7 +833,6 @@
|
|
|
910
833
|
[_grabberView applyConfiguration];
|
|
911
834
|
_grabberView.hidden = !showGrabber;
|
|
912
835
|
} else {
|
|
913
|
-
// Use system default grabber when no options provided
|
|
914
836
|
sheet.prefersGrabberVisible = showGrabber;
|
|
915
837
|
_grabberView.hidden = YES;
|
|
916
838
|
}
|
|
@@ -935,13 +857,10 @@
|
|
|
935
857
|
|
|
936
858
|
#if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
|
|
937
859
|
- (BOOL)isDismissible {
|
|
938
|
-
// Prevent react-native-screens from dismissing this sheet when presenting a modal
|
|
939
860
|
return NO;
|
|
940
861
|
}
|
|
941
862
|
|
|
942
863
|
- (UIViewController *)newPresentingViewController {
|
|
943
|
-
// Find the topmost TrueSheetViewController in the chain
|
|
944
|
-
// This handles cases where this sheet is presenting another sheet (child sheet)
|
|
945
864
|
UIViewController *topmost = self;
|
|
946
865
|
while (topmost.presentedViewController != nil && !topmost.presentedViewController.isBeingDismissed &&
|
|
947
866
|
[topmost.presentedViewController isKindOfClass:[TrueSheetViewController class]]) {
|
package/package.json
CHANGED