@lodev09/react-native-true-sheet 3.5.0-beta.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
4
4
  import android.graphics.Color
5
5
  import android.graphics.drawable.ShapeDrawable
6
6
  import android.graphics.drawable.shapes.RoundRectShape
7
- import android.util.Log
8
7
  import android.util.TypedValue
9
8
  import android.view.MotionEvent
10
9
  import android.view.View
@@ -38,6 +37,10 @@ import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
38
37
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
39
38
  import com.lodev09.truesheet.utils.ScreenUtils
40
39
 
40
+ // =============================================================================
41
+ // MARK: - Data Types & Delegate Protocol
42
+ // =============================================================================
43
+
41
44
  data class DetentInfo(val index: Int, val position: Float)
42
45
 
43
46
  interface TrueSheetViewControllerDelegate {
@@ -58,9 +61,13 @@ interface TrueSheetViewControllerDelegate {
58
61
  fun viewControllerDidBackPress()
59
62
  }
60
63
 
64
+ // =============================================================================
65
+ // MARK: - TrueSheetViewController
66
+ // =============================================================================
67
+
61
68
  /**
62
- * TrueSheetViewController manages the bottom sheet dialog and its presentation lifecycle.
63
- * This view also acts as a RootView to properly handle and dispatch touch events to React Native.
69
+ * Manages the bottom sheet dialog and its presentation lifecycle.
70
+ * Acts as a RootView to properly dispatch touch events to React Native.
64
71
  */
65
72
  @SuppressLint("ClickableViewAccessibility", "ViewConstructor")
66
73
  class TrueSheetViewController(private val reactContext: ThemedReactContext) :
@@ -72,6 +79,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
72
79
  companion object {
73
80
  const val TAG_NAME = "TrueSheet"
74
81
 
82
+ // Prevents fully expanded ratio which causes behavior issues
75
83
  private const val MAX_HALF_EXPANDED_RATIO = 0.999f
76
84
  private const val GRABBER_TAG = "TrueSheetGrabber"
77
85
  private const val DEFAULT_MAX_WIDTH = 640 // dp
@@ -79,49 +87,28 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
79
87
  private const val TRANSLATE_ANIMATION_DURATION = 200L
80
88
  }
81
89
 
82
- // ====================================================================
83
- // MARK: - Delegate
84
- // ====================================================================
85
-
86
- var delegate: TrueSheetViewControllerDelegate? = null
87
-
88
- // ====================================================================
89
- // MARK: - Dialog & Views
90
- // ====================================================================
91
-
92
- private var dialog: BottomSheetDialog? = null
93
- private var dimView: TrueSheetDimView? = null
94
- private var parentDimView: TrueSheetDimView? = null
95
-
96
- private val behavior: BottomSheetBehavior<FrameLayout>?
97
- get() = dialog?.behavior
90
+ // =============================================================================
91
+ // MARK: - Types
92
+ // =============================================================================
98
93
 
99
- private val sheetContainer: FrameLayout?
100
- get() = this.parent as? FrameLayout
101
-
102
- override val bottomSheetView: FrameLayout?
103
- get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
104
-
105
- private val containerView: TrueSheetContainerView?
106
- get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
107
-
108
- override val contentHeight: Int
109
- get() = containerView?.contentHeight ?: 0
110
-
111
- override val headerHeight: Int
112
- get() = containerView?.headerHeight ?: 0
113
-
114
- // ====================================================================
115
- // MARK: - State
116
- // ====================================================================
117
-
118
- /** Interaction state for the sheet */
119
94
  private sealed class InteractionState {
120
95
  data object Idle : InteractionState()
121
96
  data class Dragging(val startTop: Int) : InteractionState()
122
97
  data object Reconfiguring : InteractionState()
123
98
  }
124
99
 
100
+ // =============================================================================
101
+ // MARK: - Properties
102
+ // =============================================================================
103
+
104
+ var delegate: TrueSheetViewControllerDelegate? = null
105
+
106
+ // Dialog & Views
107
+ private var dialog: BottomSheetDialog? = null
108
+ private var dimView: TrueSheetDimView? = null
109
+ private var parentDimView: TrueSheetDimView? = null
110
+
111
+ // Presentation State
125
112
  var isPresented = false
126
113
  private set
127
114
 
@@ -139,48 +126,47 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
139
126
  private var lastStateWidth: Int = 0
140
127
  private var lastStateHeight: Int = 0
141
128
  private var lastEmittedPositionPx: Int = -1
129
+
130
+ // Keyboard State
142
131
  private var detentIndexBeforeKeyboard: Int = -1
143
132
  private var isKeyboardTransitioning: Boolean = false
144
133
 
134
+ // Promises
145
135
  var presentPromise: (() -> Unit)? = null
146
136
  var dismissPromise: (() -> Unit)? = null
147
137
 
148
- // Reference to parent TrueSheetView (if presented from another sheet)
138
+ // For stacked sheets
149
139
  var parentSheetView: TrueSheetView? = null
150
140
 
151
- // ====================================================================
152
- // MARK: - Helper Classes
153
- // ====================================================================
154
-
141
+ // Helper Objects
155
142
  private val sheetAnimator = TrueSheetAnimator(this)
156
143
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
157
144
  private var rnScreensObserver: RNScreensFragmentObserver? = null
145
+ private val detentCalculator = TrueSheetDetentCalculator(this)
158
146
 
159
- // ====================================================================
160
- // MARK: - Configuration Properties
161
- // ====================================================================
162
-
163
- override val screenHeight: Int
164
- get() = ScreenUtils.getScreenHeight(reactContext)
165
- val screenWidth: Int
166
- get() = ScreenUtils.getScreenWidth(reactContext)
167
- override val realScreenHeight: Int
168
- get() = ScreenUtils.getRealScreenHeight(reactContext)
147
+ // Touch Dispatchers
148
+ internal var eventDispatcher: EventDispatcher? = null
149
+ private val jSTouchDispatcher = JSTouchDispatcher(this)
150
+ private var jSPointerDispatcher: JSPointerDispatcher? = null
169
151
 
152
+ // Detent Configuration
170
153
  override var maxSheetHeight: Int? = null
171
154
  override var detents: MutableList<Double> = mutableListOf(0.5, 1.0)
172
155
 
156
+ // Appearance Configuration
173
157
  var dimmed = true
174
158
  var dimmedDetentIndex = 0
175
159
  var grabber: Boolean = true
176
160
  var grabberOptions: GrabberOptions? = null
161
+ var sheetBackgroundColor: Int? = null
162
+ var edgeToEdgeFullScreen: Boolean = false
163
+ var insetAdjustment: String = "automatic"
164
+
177
165
  var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
178
166
  set(value) {
179
167
  field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
180
168
  if (isPresented) setupBackground()
181
169
  }
182
- var sheetBackgroundColor: Int? = null
183
- var edgeToEdgeFullScreen: Boolean = false
184
170
 
185
171
  var dismissible: Boolean = true
186
172
  set(value) {
@@ -198,13 +184,47 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
198
184
  behavior?.isDraggable = value
199
185
  }
200
186
 
201
- // ====================================================================
187
+ // =============================================================================
202
188
  // MARK: - Computed Properties
203
- // ====================================================================
189
+ // =============================================================================
190
+
191
+ // Dialog
192
+ private val behavior: BottomSheetBehavior<FrameLayout>?
193
+ get() = dialog?.behavior
194
+
195
+ private val sheetContainer: FrameLayout?
196
+ get() = this.parent as? FrameLayout
197
+
198
+ override val bottomSheetView: FrameLayout?
199
+ get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
200
+
201
+ private val containerView: TrueSheetContainerView?
202
+ get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
203
+
204
+ // Screen Measurements
205
+ override val screenHeight: Int
206
+ get() = ScreenUtils.getScreenHeight(reactContext)
207
+
208
+ val screenWidth: Int
209
+ get() = ScreenUtils.getScreenWidth(reactContext)
210
+
211
+ // Includes system bars for accurate positioning
212
+ override val realScreenHeight: Int
213
+ get() = ScreenUtils.getRealScreenHeight(reactContext)
214
+
215
+ // Content Measurements
216
+ override val contentHeight: Int
217
+ get() = containerView?.contentHeight ?: 0
204
218
 
219
+ override val headerHeight: Int
220
+ get() = containerView?.headerHeight ?: 0
221
+
222
+ // Insets
223
+ // Target keyboard height used for detent calculations
205
224
  override val keyboardInset: Int
206
225
  get() = keyboardObserver?.targetHeight ?: 0
207
226
 
227
+ // Current animated keyboard height for positioning
208
228
  private val currentKeyboardInset: Int
209
229
  get() = keyboardObserver?.currentHeight ?: 0
210
230
 
@@ -214,8 +234,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
214
234
  val topInset: Int
215
235
  get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).top else 0
216
236
 
217
- var insetAdjustment: String = "automatic"
218
-
219
237
  override val contentBottomInset: Int
220
238
  get() = if (insetAdjustment == "automatic") bottomInset else 0
221
239
 
@@ -225,24 +243,33 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
225
243
  return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
226
244
  }
227
245
 
228
- internal var eventDispatcher: EventDispatcher? = null
229
- private val jSTouchDispatcher = JSTouchDispatcher(this)
230
- private var jSPointerDispatcher: JSPointerDispatcher? = null
246
+ // Sheet State
247
+ val isExpanded: Boolean
248
+ get() {
249
+ val sheetTop = bottomSheetView?.top ?: return false
250
+ return sheetTop <= topInset
251
+ }
231
252
 
232
- /** Single instance that reads current values via TrueSheetDetentMeasurements interface */
233
- private val detentCalculator = TrueSheetDetentCalculator(this)
253
+ val currentTranslationY: Int
254
+ get() = bottomSheetView?.translationY?.toInt() ?: 0
234
255
 
235
- // ====================================================================
256
+ private val isTopmostSheet: Boolean
257
+ get() {
258
+ val hostView = delegate as? TrueSheetView ?: return true
259
+ return TrueSheetDialogObserver.isTopmostSheet(hostView)
260
+ }
261
+
262
+ // =============================================================================
236
263
  // MARK: - Initialization
237
- // ====================================================================
264
+ // =============================================================================
238
265
 
239
266
  init {
240
267
  jSPointerDispatcher = JSPointerDispatcher(this)
241
268
  }
242
269
 
243
- // ====================================================================
244
- // MARK: - Dialog Lifecycle
245
- // ====================================================================
270
+ // =============================================================================
271
+ // MARK: - Dialog Creation & Cleanup
272
+ // =============================================================================
246
273
 
247
274
  fun createDialog() {
248
275
  if (dialog != null) return
@@ -307,6 +334,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
307
334
  shouldAnimatePresent = true
308
335
  }
309
336
 
337
+ // =============================================================================
338
+ // MARK: - Dialog Listeners
339
+ // =============================================================================
340
+
310
341
  private fun setupDialogListeners(dialog: BottomSheetDialog) {
311
342
  dialog.setOnShowListener {
312
343
  bottomSheetView?.visibility = VISIBLE
@@ -421,33 +452,30 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
421
452
  }
422
453
  }
423
454
 
455
+ // =============================================================================
456
+ // MARK: - Modal Observer (react-native-screens)
457
+ // =============================================================================
458
+
424
459
  private fun setupModalObserver() {
425
460
  rnScreensObserver = RNScreensFragmentObserver(
426
461
  reactContext = reactContext,
427
462
  onModalPresented = {
428
- if (isPresented && isDialogVisible) {
429
- isDialogVisible = false
430
- wasHiddenByModal = true
431
-
432
- dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFastFadeOut)
433
- dialog?.window?.decorView?.visibility = GONE
434
- dimView?.visibility = INVISIBLE
435
- parentDimView?.visibility = INVISIBLE
463
+ if (isPresented && isDialogVisible && isTopmostSheet) {
464
+ hideForModal()
436
465
  }
437
466
  },
438
467
  onModalWillDismiss = {
439
- if (isPresented && wasHiddenByModal) {
440
- isDialogVisible = true
441
-
442
- dialog?.window?.setWindowAnimations(0)
443
- dialog?.window?.decorView?.visibility = VISIBLE
444
- dimView?.visibility = VISIBLE
445
- parentDimView?.visibility = VISIBLE
468
+ if (isPresented && wasHiddenByModal && isTopmostSheet) {
469
+ showAfterModal()
446
470
  }
447
471
  },
448
472
  onModalDidDismiss = {
449
473
  if (isPresented && wasHiddenByModal) {
450
474
  wasHiddenByModal = false
475
+ // Restore parent sheet after this sheet is restored
476
+ parentSheetView?.viewController?.let { parent ->
477
+ post { parent.showAfterModal() }
478
+ }
451
479
  }
452
480
  }
453
481
  )
@@ -459,88 +487,36 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
459
487
  rnScreensObserver = null
460
488
  }
461
489
 
462
- // ====================================================================
463
- // MARK: - Event Emission
464
- // ====================================================================
465
-
466
- private fun emitWillPresentEvents() {
467
- val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
468
- parentSheetView?.viewControllerWillBlur()
469
- delegate?.viewControllerWillPresent(index, position, detent)
470
- delegate?.viewControllerWillFocus()
471
- }
472
-
473
- private fun emitWillDismissEvents() {
474
- delegate?.viewControllerWillBlur()
475
- delegate?.viewControllerWillDismiss()
476
- parentSheetView?.viewControllerWillFocus()
477
- }
478
-
479
- private fun emitDidDismissEvents() {
480
- val hadParent = parentSheetView != null
481
- parentSheetView?.viewControllerDidFocus()
482
- parentSheetView = null
483
-
484
- delegate?.viewControllerDidBlur()
485
- delegate?.viewControllerDidDismiss(hadParent)
486
-
487
- dismissPromise?.invoke()
488
- dismissPromise = null
489
- }
490
+ private fun hideForModal() {
491
+ isDialogVisible = false
492
+ wasHiddenByModal = true
490
493
 
491
- private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
492
- if (currentTop == lastEmittedPositionPx) return
494
+ // Prepare for fast fade out
495
+ dimView?.alpha = 0f
496
+ parentDimView?.alpha = 0f
493
497
 
494
- lastEmittedPositionPx = currentTop
495
- val visibleHeight = realScreenHeight - currentTop
496
- val position = detentCalculator.getPositionDp(visibleHeight)
497
- val interpolatedIndex = detentCalculator.getInterpolatedIndexForPosition(currentTop)
498
- val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
499
- delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
500
- }
498
+ dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFastFadeOut)
499
+ dialog?.window?.decorView?.visibility = GONE
500
+ dimView?.visibility = INVISIBLE
501
+ parentDimView?.visibility = INVISIBLE
501
502
 
502
- private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
503
- val state = detentCalculator.getStateForDetentIndex(index)
504
- val detentIndex = detentCalculator.getDetentIndexForState(state) ?: 0
505
- val position = getPositionForDetentIndex(detentIndex)
506
- val detent = detentCalculator.getDetentValueForIndex(detentIndex)
507
- return Triple(detentIndex, position, detent)
503
+ parentSheetView?.viewController?.hideForModal()
508
504
  }
509
505
 
510
- // ====================================================================
511
- // MARK: - Dialog Visibility (for stacking)
512
- // ====================================================================
513
-
514
- val isExpanded: Boolean
515
- get() {
516
- val sheetTop = bottomSheetView?.top ?: return false
517
- return sheetTop <= topInset
518
- }
519
-
520
- val currentTranslationY: Int
521
- get() = bottomSheetView?.translationY?.toInt() ?: 0
522
-
523
- fun getExpectedSheetTop(detentIndex: Int): Int {
524
- if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
525
- return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
526
- }
506
+ private fun showAfterModal() {
507
+ isDialogVisible = true
527
508
 
528
- fun translateDialog(translationY: Int) {
529
- val bottomSheet = bottomSheetView ?: return
509
+ dialog?.window?.setWindowAnimations(0)
510
+ dialog?.window?.decorView?.visibility = VISIBLE
511
+ dimView?.visibility = VISIBLE
512
+ parentDimView?.visibility = VISIBLE
530
513
 
531
- bottomSheet.animate()
532
- .translationY(translationY.toFloat())
533
- .setDuration(TRANSLATE_ANIMATION_DURATION)
534
- .setUpdateListener {
535
- val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
536
- emitChangePositionDelegate(effectiveTop)
537
- }
538
- .start()
514
+ updateDimAmount(animated = true)
539
515
  }
540
516
 
541
- // ====================================================================
517
+ // =============================================================================
542
518
  // MARK: - Presentation
543
- // ====================================================================
519
+ // =============================================================================
544
520
 
545
521
  fun present(detentIndex: Int, animated: Boolean = true) {
546
522
  val dialog = this.dialog ?: run {
@@ -564,6 +540,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
564
540
  setupBackground()
565
541
  setupGrabber()
566
542
 
543
+ // Hide until animation starts
567
544
  bottomSheetView?.visibility = INVISIBLE
568
545
 
569
546
  dialog.show()
@@ -601,9 +578,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
601
578
  presentPromise = null
602
579
  }
603
580
 
604
- // ====================================================================
581
+ // =============================================================================
605
582
  // MARK: - Sheet Configuration
606
- // ====================================================================
583
+ // =============================================================================
607
584
 
608
585
  fun setupSheetDetents() {
609
586
  val behavior = this.behavior ?: return
@@ -627,9 +604,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
627
604
  val maxDetentHeight = detentCalculator.getDetentHeight(detents.last())
628
605
 
629
606
  val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
630
- halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
607
+ halfExpandedRatio = minOf(
608
+ adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(),
609
+ MAX_HALF_EXPANDED_RATIO
610
+ )
631
611
 
632
612
  expandedOffset = maxOf(edgeToEdgeTopInset, realScreenHeight - maxDetentHeight)
613
+
614
+ // fitToContents works better with <= 2 detents when no expanded offset
633
615
  isFitToContents = detents.size < 3 && expandedOffset == 0
634
616
 
635
617
  val offset = if (expandedOffset == 0) topInset else 0
@@ -655,6 +637,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
655
637
  positionFooter()
656
638
  }
657
639
 
640
+ fun setStateForDetentIndex(index: Int) {
641
+ behavior?.state = detentCalculator.getStateForDetentIndex(index)
642
+ }
643
+
644
+ // =============================================================================
645
+ // MARK: - Grabber
646
+ // =============================================================================
647
+
658
648
  fun setupGrabber() {
659
649
  val bottomSheet = bottomSheetView ?: return
660
650
 
@@ -671,61 +661,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
671
661
  bottomSheet.addView(grabberView)
672
662
  }
673
663
 
674
- // ====================================================================
675
- // MARK: - Keyboard Handling
676
- // ====================================================================
677
-
678
- private fun shouldHandleKeyboard(): Boolean {
679
- if (wasHiddenByModal) return false
680
-
681
- val parentView = parentSheetView ?: return true
682
- return TrueSheetDialogObserver.getSheetsAbove(parentView).firstOrNull()?.viewController == this
683
- }
684
-
685
- fun setupKeyboardObserver() {
686
- val bottomSheet = bottomSheetView ?: return
687
- keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
688
- delegate = object : TrueSheetKeyboardObserverDelegate {
689
- override fun keyboardWillShow(height: Int) {
690
- if (!shouldHandleKeyboard()) return
691
- detentIndexBeforeKeyboard = currentDetentIndex
692
- isKeyboardTransitioning = true
693
- setupSheetDetents()
694
- setStateForDetentIndex(detents.size - 1)
695
- }
696
-
697
- override fun keyboardWillHide() {
698
- if (!shouldHandleKeyboard()) return
699
- setupSheetDetents()
700
- if (detentIndexBeforeKeyboard >= 0) {
701
- setStateForDetentIndex(detentIndexBeforeKeyboard)
702
- detentIndexBeforeKeyboard = -1
703
- }
704
- }
705
-
706
- override fun keyboardDidHide() {
707
- if (!shouldHandleKeyboard()) return
708
- isKeyboardTransitioning = false
709
- }
710
-
711
- override fun keyboardDidChangeHeight(height: Int) {}
712
- }
713
- start()
714
- }
715
- }
716
-
717
- fun cleanupKeyboardObserver() {
718
- keyboardObserver?.stop()
719
- keyboardObserver = null
720
- }
721
-
722
- // ====================================================================
664
+ // =============================================================================
723
665
  // MARK: - Background & Dimming
724
- // ====================================================================
666
+ // =============================================================================
725
667
 
726
668
  fun setupBackground() {
727
669
  val bottomSheet = bottomSheetView ?: return
728
670
 
671
+ // Rounded corners only on top
729
672
  val outerRadii = floatArrayOf(
730
673
  sheetCornerRadius,
731
674
  sheetCornerRadius,
@@ -759,6 +702,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
759
702
  if (dimView == null) dimView = TrueSheetDimView(reactContext)
760
703
  if (!parentDimVisible) dimView?.attach(null)
761
704
 
705
+ // Attach dim view to parent sheet if stacked
762
706
  val parentController = parentSheetView?.viewController
763
707
  val parentBottomSheet = parentController?.bottomSheetView
764
708
  if (parentBottomSheet != null) {
@@ -780,6 +724,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
780
724
  true
781
725
  }
782
726
  } else {
727
+ // Pass through touches to parent or activity when not dimmed
783
728
  touchOutside.setOnTouchListener { v, event ->
784
729
  event.setLocation(event.rawX - v.x, event.rawY - v.y)
785
730
  (
@@ -793,13 +738,42 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
793
738
  }
794
739
  }
795
740
 
796
- fun updateDimAmount(sheetTop: Int? = null) {
741
+ fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
797
742
  if (!dimmed) return
798
743
  val top = (sheetTop ?: bottomSheetView?.top ?: return) + currentKeyboardInset
799
- dimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
800
- parentDimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
744
+
745
+ if (animated) {
746
+ val targetAlpha = dimView?.calculateAlpha(
747
+ top,
748
+ dimmedDetentIndex,
749
+ detentCalculator::getSheetTopForDetentIndex
750
+ ) ?: 0f
751
+ dimView?.animate()?.alpha(targetAlpha)?.setDuration(200)?.start()
752
+ parentDimView?.animate()?.alpha(targetAlpha)?.setDuration(200)?.start()
753
+ } else {
754
+ dimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
755
+ parentDimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
756
+ }
757
+ }
758
+
759
+ fun getDefaultBackgroundColor(): Int {
760
+ val typedValue = TypedValue()
761
+ return if (reactContext.theme.resolveAttribute(
762
+ com.google.android.material.R.attr.colorSurfaceContainerLow,
763
+ typedValue,
764
+ true
765
+ )
766
+ ) {
767
+ typedValue.data
768
+ } else {
769
+ Color.WHITE
770
+ }
801
771
  }
802
772
 
773
+ // =============================================================================
774
+ // MARK: - Footer Positioning
775
+ // =============================================================================
776
+
803
777
  fun positionFooter(slideOffset: Float? = null) {
804
778
  val footerView = containerView?.footerView ?: return
805
779
  val bottomSheet = bottomSheetView ?: return
@@ -809,35 +783,66 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
809
783
  val sheetTop = bottomSheet.top
810
784
 
811
785
  var footerY = (sheetHeight - sheetTop - footerHeight - currentKeyboardInset).toFloat()
786
+
787
+ // Adjust during dismiss animation when slideOffset is negative
812
788
  if (slideOffset != null && slideOffset < 0) {
813
789
  footerY -= (footerHeight * slideOffset)
814
790
  }
815
791
 
792
+ // Clamp to prevent footer going above safe area
816
793
  val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
817
794
  footerView.y = minOf(footerY, maxAllowedY)
818
795
  }
819
796
 
820
- fun setStateForDetentIndex(index: Int) {
821
- behavior?.state = detentCalculator.getStateForDetentIndex(index)
797
+ // =============================================================================
798
+ // MARK: - Keyboard Handling
799
+ // =============================================================================
800
+
801
+ private fun shouldHandleKeyboard(): Boolean {
802
+ if (wasHiddenByModal) return false
803
+ return isTopmostSheet
822
804
  }
823
805
 
824
- fun getDefaultBackgroundColor(): Int {
825
- val typedValue = TypedValue()
826
- return if (reactContext.theme.resolveAttribute(
827
- com.google.android.material.R.attr.colorSurfaceContainerLow,
828
- typedValue,
829
- true
830
- )
831
- ) {
832
- typedValue.data
833
- } else {
834
- Color.WHITE
806
+ fun setupKeyboardObserver() {
807
+ val bottomSheet = bottomSheetView ?: return
808
+ keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
809
+ delegate = object : TrueSheetKeyboardObserverDelegate {
810
+ override fun keyboardWillShow(height: Int) {
811
+ if (!shouldHandleKeyboard()) return
812
+ detentIndexBeforeKeyboard = currentDetentIndex
813
+ isKeyboardTransitioning = true
814
+ setupSheetDetents()
815
+ setStateForDetentIndex(detents.size - 1)
816
+ }
817
+
818
+ override fun keyboardWillHide() {
819
+ if (!shouldHandleKeyboard()) return
820
+ setupSheetDetents()
821
+ if (detentIndexBeforeKeyboard >= 0) {
822
+ setStateForDetentIndex(detentIndexBeforeKeyboard)
823
+ detentIndexBeforeKeyboard = -1
824
+ }
825
+ }
826
+
827
+ override fun keyboardDidHide() {
828
+ if (!shouldHandleKeyboard()) return
829
+ isKeyboardTransitioning = false
830
+ }
831
+
832
+ override fun keyboardDidChangeHeight(height: Int) {}
833
+ }
834
+ start()
835
835
  }
836
836
  }
837
837
 
838
- // ====================================================================
838
+ fun cleanupKeyboardObserver() {
839
+ keyboardObserver?.stop()
840
+ keyboardObserver = null
841
+ }
842
+
843
+ // =============================================================================
839
844
  // MARK: - Drag Handling
840
- // ====================================================================
845
+ // =============================================================================
841
846
 
842
847
  private fun handleDragBegin(sheetView: View) {
843
848
  val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
@@ -853,13 +858,75 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
853
858
  delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
854
859
  }
855
860
 
856
- // ====================================================================
861
+ // =============================================================================
862
+ // MARK: - Event Emission
863
+ // =============================================================================
864
+
865
+ private fun emitWillPresentEvents() {
866
+ val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
867
+ parentSheetView?.viewControllerWillBlur()
868
+ delegate?.viewControllerWillPresent(index, position, detent)
869
+ delegate?.viewControllerWillFocus()
870
+ }
871
+
872
+ private fun emitWillDismissEvents() {
873
+ delegate?.viewControllerWillBlur()
874
+ delegate?.viewControllerWillDismiss()
875
+ parentSheetView?.viewControllerWillFocus()
876
+ }
877
+
878
+ private fun emitDidDismissEvents() {
879
+ val hadParent = parentSheetView != null
880
+ parentSheetView?.viewControllerDidFocus()
881
+ parentSheetView = null
882
+
883
+ delegate?.viewControllerDidBlur()
884
+ delegate?.viewControllerDidDismiss(hadParent)
885
+
886
+ dismissPromise?.invoke()
887
+ dismissPromise = null
888
+ }
889
+
890
+ private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
891
+ // Dedupe emissions for same position
892
+ if (currentTop == lastEmittedPositionPx) return
893
+
894
+ lastEmittedPositionPx = currentTop
895
+ val visibleHeight = realScreenHeight - currentTop
896
+ val position = detentCalculator.getPositionDp(visibleHeight)
897
+ val interpolatedIndex = detentCalculator.getInterpolatedIndexForPosition(currentTop)
898
+ val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
899
+ delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
900
+ }
901
+
902
+ // =============================================================================
857
903
  // MARK: - Detent Helpers
858
- // ====================================================================
904
+ // =============================================================================
905
+
906
+ fun getExpectedSheetTop(detentIndex: Int): Int {
907
+ if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
908
+ return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
909
+ }
910
+
911
+ fun translateDialog(translationY: Int) {
912
+ val bottomSheet = bottomSheetView ?: return
913
+
914
+ bottomSheet.animate()
915
+ .translationY(translationY.toFloat())
916
+ .setDuration(TRANSLATE_ANIMATION_DURATION)
917
+ .setUpdateListener {
918
+ val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
919
+ emitChangePositionDelegate(effectiveTop)
920
+ }
921
+ .start()
922
+ }
859
923
 
860
- fun getDetentInfoForState(state: Int): DetentInfo? {
861
- val index = detentCalculator.getDetentIndexForState(state) ?: return null
862
- return DetentInfo(index, getPositionForDetentIndex(index))
924
+ private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
925
+ val state = detentCalculator.getStateForDetentIndex(index)
926
+ val detentIndex = detentCalculator.getDetentIndexForState(state) ?: 0
927
+ val position = getPositionForDetentIndex(detentIndex)
928
+ val detent = detentCalculator.getDetentValueForIndex(detentIndex)
929
+ return Triple(detentIndex, position, detent)
863
930
  }
864
931
 
865
932
  private fun getPositionForDetentIndex(index: Int): Float {
@@ -867,7 +934,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
867
934
 
868
935
  bottomSheetView?.let {
869
936
  val visibleSheetHeight = detentCalculator.getVisibleSheetHeight(it.top)
870
- if (visibleSheetHeight > 0 && visibleSheetHeight < realScreenHeight) {
937
+ if (visibleSheetHeight in 1..<realScreenHeight) {
871
938
  return detentCalculator.getPositionDp(visibleSheetHeight)
872
939
  }
873
940
  }
@@ -876,12 +943,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
876
943
  return detentCalculator.getPositionDp(detentHeight)
877
944
  }
878
945
 
879
- fun getDetentInfoForIndex(index: Int): DetentInfo =
880
- getDetentInfoForState(detentCalculator.getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
881
-
882
- // ====================================================================
946
+ // =============================================================================
883
947
  // MARK: - RootView Implementation
884
- // ====================================================================
948
+ // =============================================================================
885
949
 
886
950
  override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
887
951
  super.onInitializeAccessibilityNodeInfo(info)
@@ -893,6 +957,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
893
957
 
894
958
  if (w == oldw && h == oldh) return
895
959
  if (!isPresented) return
960
+
961
+ // Skip reconfiguration if expanded and only height changed (e.g., keyboard)
896
962
  if (h + topInset >= screenHeight && isExpanded && oldw == w) return
897
963
 
898
964
  this.post {
@@ -906,17 +972,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
906
972
  reactContext.reactApplicationContext.handleException(RuntimeException(t))
907
973
  }
908
974
 
909
- // ====================================================================
975
+ // =============================================================================
910
976
  // MARK: - Touch Event Handling
911
- // ====================================================================
977
+ // =============================================================================
912
978
 
913
979
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
980
+ // Footer needs special handling since it's positioned absolutely
914
981
  val footer = containerView?.footerView
915
982
  if (footer != null && footer.isVisible) {
916
983
  val footerLocation = ScreenUtils.getScreenLocation(footer)
917
984
  val touchScreenX = event.rawX.toInt()
918
985
  val touchScreenY = event.rawY.toInt()
919
986
 
987
+ // Check if touch is within footer bounds
920
988
  if (touchScreenX >= footerLocation[0] &&
921
989
  touchScreenX <= footerLocation[0] + footer.width &&
922
990
  touchScreenY >= footerLocation[1] &&
@@ -106,4 +106,14 @@ object TrueSheetDialogObserver {
106
106
  return presentedSheetStack[index - 1]
107
107
  }
108
108
  }
109
+
110
+ /**
111
+ * Returns true if the given sheet is the topmost presented sheet.
112
+ */
113
+ @JvmStatic
114
+ fun isTopmostSheet(sheetView: TrueSheetView): Boolean {
115
+ synchronized(presentedSheetStack) {
116
+ return presentedSheetStack.lastOrNull() == sheetView
117
+ }
118
+ }
109
119
  }
@@ -48,12 +48,12 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
48
48
  targetView = null
49
49
  }
50
50
 
51
- fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
51
+ fun calculateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
52
52
  val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
53
53
  val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
54
54
  val belowDimmedTop = if (dimmedDetentIndex > 0) getSheetTopForDetentIndex(dimmedDetentIndex - 1) else realHeight
55
55
 
56
- alpha = when {
56
+ return when {
57
57
  sheetTop <= dimmedDetentTop -> MAX_ALPHA
58
58
 
59
59
  sheetTop >= belowDimmedTop -> 0f
@@ -64,4 +64,8 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
64
64
  }
65
65
  }
66
66
  }
67
+
68
+ fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
69
+ alpha = calculateAlpha(sheetTop, dimmedDetentIndex, getSheetTopForDetentIndex)
70
+ }
67
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.5.0-beta.1",
3
+ "version": "3.5.0",
4
4
  "description": "The true native bottom sheet experience for your React Native Apps.",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",