@lodev09/react-native-true-sheet 3.5.1-beta.3 → 3.5.1-beta.4

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.
Files changed (33) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +2 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +31 -28
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +336 -272
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +0 -5
  5. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +7 -8
  6. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +150 -0
  7. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +55 -0
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +18 -12
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +91 -2
  10. package/android/src/main/java/com/lodev09/truesheet/core/{TrueSheetDialogObserver.kt → TrueSheetStackManager.kt} +7 -5
  11. package/ios/TrueSheetViewController.h +2 -3
  12. package/ios/TrueSheetViewController.mm +11 -4
  13. package/ios/core/TrueSheetDetentCalculator.h +2 -3
  14. package/ios/core/TrueSheetDetentCalculator.mm +7 -9
  15. package/lib/module/TrueSheet.js +0 -2
  16. package/lib/module/TrueSheet.js.map +1 -1
  17. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +0 -1
  18. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  19. package/lib/typescript/src/TrueSheet.types.d.ts +0 -8
  20. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  21. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +0 -1
  22. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  23. package/lib/typescript/src/navigation/types.d.ts +1 -1
  24. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/TrueSheet.tsx +0 -2
  27. package/src/TrueSheet.types.ts +0 -9
  28. package/src/fabric/TrueSheetViewNativeComponent.ts +0 -1
  29. package/src/navigation/types.ts +0 -1
  30. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetAnimator.kt +0 -144
  31. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogFragment.kt +0 -317
  32. package/android/src/main/res/anim/fast_fade_out.xml +0 -6
  33. package/android/src/main/res/values/styles.xml +0 -21
@@ -4,13 +4,13 @@ import android.annotation.SuppressLint
4
4
  import android.os.Build
5
5
  import android.view.MotionEvent
6
6
  import android.view.View
7
- import android.view.WindowManager
7
+ import android.view.ViewGroup
8
8
  import android.view.accessibility.AccessibilityNodeInfo
9
- import android.widget.FrameLayout
9
+ import androidx.activity.OnBackPressedCallback
10
10
  import androidx.appcompat.app.AppCompatActivity
11
+ import androidx.coordinatorlayout.widget.CoordinatorLayout
11
12
  import androidx.core.view.isNotEmpty
12
13
  import androidx.core.view.isVisible
13
- import androidx.core.view.postDelayed
14
14
  import com.facebook.react.R
15
15
  import com.facebook.react.uimanager.JSPointerDispatcher
16
16
  import com.facebook.react.uimanager.JSTouchDispatcher
@@ -22,19 +22,19 @@ import com.facebook.react.uimanager.events.EventDispatcher
22
22
  import com.facebook.react.util.RNLog
23
23
  import com.facebook.react.views.view.ReactViewGroup
24
24
  import com.google.android.material.bottomsheet.BottomSheetBehavior
25
- import com.google.android.material.bottomsheet.BottomSheetDialog
26
25
  import com.lodev09.truesheet.core.GrabberOptions
27
26
  import com.lodev09.truesheet.core.RNScreensFragmentObserver
28
- import com.lodev09.truesheet.core.TrueSheetAnimator
29
- import com.lodev09.truesheet.core.TrueSheetAnimatorProvider
27
+ import com.lodev09.truesheet.core.TrueSheetBottomSheetView
28
+ import com.lodev09.truesheet.core.TrueSheetBottomSheetViewDelegate
29
+ import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
30
+ import com.lodev09.truesheet.core.TrueSheetCoordinatorLayoutDelegate
30
31
  import com.lodev09.truesheet.core.TrueSheetDetentCalculator
31
- import com.lodev09.truesheet.core.TrueSheetDetentMeasurements
32
- import com.lodev09.truesheet.core.TrueSheetDialogFragment
33
- import com.lodev09.truesheet.core.TrueSheetDialogFragmentDelegate
34
- import com.lodev09.truesheet.core.TrueSheetDialogObserver
32
+ import com.lodev09.truesheet.core.TrueSheetDetentCalculatorDelegate
35
33
  import com.lodev09.truesheet.core.TrueSheetDimView
34
+ import com.lodev09.truesheet.core.TrueSheetDimViewDelegate
36
35
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
37
36
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
37
+ import com.lodev09.truesheet.core.TrueSheetStackManager
38
38
  import com.lodev09.truesheet.utils.ScreenUtils
39
39
 
40
40
  // =============================================================================
@@ -66,24 +66,29 @@ interface TrueSheetViewControllerDelegate {
66
66
  // =============================================================================
67
67
 
68
68
  /**
69
- * Manages the bottom sheet dialog fragment and its presentation lifecycle.
70
- * Acts as a RootView to properly dispatch touch events to React Native.
69
+ * Manages the bottom sheet using CoordinatorLayout + BottomSheetBehavior.
70
+ *
71
+ * This approach keeps the sheet in the same activity window (no separate dialog window),
72
+ * which allows touch events to pass through to underlying views when the sheet is not
73
+ * covering them. This solves the touch lag issue when sheets are presented over
74
+ * interactive components like Maps.
71
75
  */
72
76
  @SuppressLint("ClickableViewAccessibility", "ViewConstructor")
73
77
  class TrueSheetViewController(private val reactContext: ThemedReactContext) :
74
78
  ReactViewGroup(reactContext),
75
79
  RootView,
76
- TrueSheetDetentMeasurements,
77
- TrueSheetAnimatorProvider,
78
- TrueSheetDialogFragmentDelegate {
80
+ TrueSheetDetentCalculatorDelegate,
81
+ TrueSheetDimViewDelegate,
82
+ TrueSheetCoordinatorLayoutDelegate,
83
+ TrueSheetBottomSheetViewDelegate {
79
84
 
80
85
  companion object {
81
86
  const val TAG_NAME = "TrueSheet"
82
87
 
83
- private const val FRAGMENT_TAG = "TrueSheetDialogFragment"
84
88
  private const val DEFAULT_MAX_WIDTH = 640 // dp
85
89
  private const val DEFAULT_CORNER_RADIUS = 16 // dp
86
90
  private const val TRANSLATE_ANIMATION_DURATION = 200L
91
+ private const val DISMISS_DURATION = 200L
87
92
  }
88
93
 
89
94
  // =============================================================================
@@ -102,16 +107,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
102
107
 
103
108
  var delegate: TrueSheetViewControllerDelegate? = null
104
109
 
105
- // Dialog Fragment
106
- private var dialogFragment: TrueSheetDialogFragment? = null
110
+ // CoordinatorLayout components (replaces DialogFragment)
111
+ internal var sheetView: TrueSheetBottomSheetView? = null
112
+ private var coordinatorLayout: TrueSheetCoordinatorLayout? = null
107
113
  private var dimView: TrueSheetDimView? = null
108
114
  private var parentDimView: TrueSheetDimView? = null
109
115
 
116
+ // Back button handling
117
+ private var backCallback: OnBackPressedCallback? = null
118
+
110
119
  // Presentation State
111
120
  var isPresented = false
112
121
  private set
113
122
 
114
- var isDialogVisible = false
123
+ var isSheetVisible = false
115
124
  private set
116
125
 
117
126
  var currentDetentIndex: Int = -1
@@ -121,8 +130,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
121
130
  private var isDismissing = false
122
131
  private var wasHiddenByModal = false
123
132
  private var shouldAnimatePresent = false
124
- private var wasPresentingWithAnimation = false
125
- private var isPresentingWithoutAnimation = false
133
+ private var isPresentAnimating = false
126
134
 
127
135
  private var lastStateWidth: Int = 0
128
136
  private var lastStateHeight: Int = 0
@@ -140,15 +148,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
140
148
  var parentSheetView: TrueSheetView? = null
141
149
 
142
150
  // Helper Objects
143
- private val sheetAnimator = TrueSheetAnimator(this)
144
151
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
145
152
  private var rnScreensObserver: RNScreensFragmentObserver? = null
146
- private val detentCalculator = TrueSheetDetentCalculator(this)
153
+ internal val detentCalculator = TrueSheetDetentCalculator(reactContext).apply {
154
+ delegate = this@TrueSheetViewController
155
+ }
147
156
 
148
157
  // Touch Dispatchers
149
158
  internal var eventDispatcher: EventDispatcher? = null
150
- private val jSTouchDispatcher = JSTouchDispatcher(this)
151
- private var jSPointerDispatcher: JSPointerDispatcher? = null
159
+ private val jsTouchDispatcher = JSTouchDispatcher(this)
160
+ private var jsPointerDispatcher: JSPointerDispatcher? = null
152
161
 
153
162
  // Detent Configuration
154
163
  override var maxSheetHeight: Int? = null
@@ -157,47 +166,37 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
157
166
  // Appearance Configuration
158
167
  var dimmed = true
159
168
  var dimmedDetentIndex = 0
160
- var grabber: Boolean = true
161
- var grabberOptions: GrabberOptions? = null
162
- var sheetBackgroundColor: Int? = null
163
- var edgeToEdgeFullScreen: Boolean = false
169
+ override var grabber: Boolean = true
170
+ override var grabberOptions: GrabberOptions? = null
171
+ override var sheetBackgroundColor: Int? = null
164
172
  var insetAdjustment: String = "automatic"
165
173
 
166
- var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
174
+ override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
167
175
  set(value) {
168
176
  field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
169
- dialogFragment?.sheetCornerRadius = field
170
- if (isPresented) dialogFragment?.setupBackground()
177
+ if (isPresented) sheetView?.setupBackground()
171
178
  }
172
179
 
173
180
  var dismissible: Boolean = true
174
181
  set(value) {
175
182
  field = value
176
- dialogFragment?.dismissible = value
183
+ behavior?.isHideable = value
177
184
  }
178
185
 
179
186
  var draggable: Boolean = true
180
187
  set(value) {
181
188
  field = value
182
- dialogFragment?.updateDraggable(value)
189
+ behavior?.isDraggable = value
190
+ if (isPresented) sheetView?.setupGrabber()
183
191
  }
184
192
 
185
193
  // =============================================================================
186
194
  // MARK: - Computed Properties
187
195
  // =============================================================================
188
196
 
189
- // Dialog
190
- private val dialog: BottomSheetDialog?
191
- get() = dialogFragment?.bottomSheetDialog
192
-
193
- private val behavior: BottomSheetBehavior<FrameLayout>?
194
- get() = dialogFragment?.behavior
195
-
196
- private val sheetContainer: FrameLayout?
197
- get() = this.parent as? FrameLayout
198
-
199
- override val bottomSheetView: FrameLayout?
200
- get() = dialogFragment?.bottomSheetView
197
+ // Behavior
198
+ private val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
199
+ get() = sheetView?.behavior
201
200
 
202
201
  private val containerView: TrueSheetContainerView?
203
202
  get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
@@ -241,23 +240,23 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
241
240
  private val edgeToEdgeEnabled: Boolean
242
241
  get() {
243
242
  val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
244
- return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
243
+ return BuildConfig.EDGE_TO_EDGE_ENABLED || defaultEnabled
245
244
  }
246
245
 
247
246
  // Sheet State
248
247
  val isExpanded: Boolean
249
248
  get() {
250
- val sheetTop = bottomSheetView?.top ?: return false
249
+ val sheetTop = sheetView?.top ?: return false
251
250
  return sheetTop <= topInset
252
251
  }
253
252
 
254
253
  val currentTranslationY: Int
255
- get() = bottomSheetView?.translationY?.toInt() ?: 0
254
+ get() = sheetView?.translationY?.toInt() ?: 0
256
255
 
257
- private val isTopmostSheet: Boolean
256
+ override val isTopmostSheet: Boolean
258
257
  get() {
259
258
  val hostView = delegate as? TrueSheetView ?: return true
260
- return TrueSheetDialogObserver.isTopmostSheet(hostView)
259
+ return TrueSheetStackManager.isTopmostSheet(hostView)
261
260
  }
262
261
 
263
262
  private val dimViews: List<TrueSheetDimView>
@@ -268,120 +267,147 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
268
267
  // =============================================================================
269
268
 
270
269
  init {
271
- jSPointerDispatcher = JSPointerDispatcher(this)
270
+ jsPointerDispatcher = JSPointerDispatcher(this)
272
271
  }
273
272
 
274
273
  // =============================================================================
275
- // MARK: - Fragment Creation & Cleanup
274
+ // MARK: - Sheet Creation & Cleanup
276
275
  // =============================================================================
277
276
 
278
- fun createDialog() {
279
- if (dialogFragment != null) return
277
+ fun createSheet() {
278
+ if (coordinatorLayout != null) return
280
279
 
281
- dialogFragment = TrueSheetDialogFragment.newInstance().apply {
280
+ // Create coordinator layout
281
+ coordinatorLayout = TrueSheetCoordinatorLayout(reactContext).apply {
282
282
  delegate = this@TrueSheetViewController
283
- contentView = this@TrueSheetViewController
284
- syncFragmentProperties(this)
285
283
  }
286
284
 
287
- setupModalObserver()
288
- }
289
-
290
- private fun syncFragmentProperties(fragment: TrueSheetDialogFragment) {
291
- fragment.apply {
292
- reactContext = this@TrueSheetViewController.reactContext
293
- sheetCornerRadius = this@TrueSheetViewController.sheetCornerRadius
294
- sheetBackgroundColor = this@TrueSheetViewController.sheetBackgroundColor
295
- edgeToEdgeFullScreen = this@TrueSheetViewController.edgeToEdgeFullScreen
296
- grabberEnabled = this@TrueSheetViewController.grabber
297
- grabberOptions = this@TrueSheetViewController.grabberOptions
298
- dismissible = this@TrueSheetViewController.dismissible
299
- draggable = this@TrueSheetViewController.draggable
285
+ sheetView = TrueSheetBottomSheetView(reactContext).apply {
286
+ delegate = this@TrueSheetViewController
300
287
  }
288
+
289
+ setupModalObserver()
301
290
  }
302
291
 
303
- private fun cleanupDialog() {
292
+ private fun cleanupSheet() {
304
293
  cleanupKeyboardObserver()
305
294
  cleanupModalObserver()
306
- sheetAnimator.cancel()
295
+ cleanupBackCallback()
296
+ sheetView?.animate()?.cancel()
297
+
298
+ // Remove from activity
299
+ removeFromActivity()
300
+
301
+ // Cleanup dim views
307
302
  dimView?.detach()
308
303
  dimView = null
309
304
  parentDimView?.detach()
310
305
  parentDimView = null
311
- sheetContainer?.removeView(this)
312
306
 
313
- dialogFragment = null
307
+ // Detach content from sheet
308
+ sheetView?.removeView(this)
309
+
310
+ coordinatorLayout = null
311
+ sheetView = null
312
+
314
313
  interactionState = InteractionState.Idle
315
314
  isDismissing = false
316
315
  isPresented = false
317
- isDialogVisible = false
316
+ isSheetVisible = false
318
317
  wasHiddenByModal = false
318
+ isPresentAnimating = false
319
319
  lastEmittedPositionPx = -1
320
320
  shouldAnimatePresent = true
321
321
  }
322
322
 
323
+ private fun removeFromActivity() {
324
+ val coordinator = coordinatorLayout ?: return
325
+ val contentView = reactContext.currentActivity?.findViewById<ViewGroup>(android.R.id.content)
326
+ contentView?.removeView(coordinator)
327
+ }
328
+
323
329
  // =============================================================================
324
- // MARK: - TrueSheetDialogFragmentDelegate
330
+ // MARK: - Back Button Handling
325
331
  // =============================================================================
326
332
 
327
- override fun onDialogCreated() {
328
- bottomSheetView?.visibility = INVISIBLE
333
+ private fun setupBackCallback() {
334
+ val activity = reactContext.currentActivity as? AppCompatActivity ?: return
329
335
 
330
- // Ensure sheet starts off-screen to prevent flicker
331
- bottomSheetView?.y = realScreenHeight.toFloat()
332
- }
336
+ backCallback = object : OnBackPressedCallback(true) {
337
+ override fun handleOnBackPressed() {
338
+ delegate?.viewControllerDidBackPress()
339
+ if (dismissible) {
340
+ dismiss(animated = true)
341
+ }
342
+ }
343
+ }
333
344
 
334
- override fun onDialogShow() {
335
- bottomSheetView?.visibility = VISIBLE
345
+ activity.onBackPressedDispatcher.addCallback(backCallback!!)
346
+ }
336
347
 
337
- isPresented = true
338
- isDialogVisible = true
348
+ private fun cleanupBackCallback() {
349
+ backCallback?.remove()
350
+ backCallback = null
351
+ }
339
352
 
340
- emitWillPresentEvents()
353
+ // =============================================================================
354
+ // MARK: - TrueSheetCoordinatorLayout.Delegate
355
+ // =============================================================================
341
356
 
342
- setupSheetDetents()
343
- setupDimmedBackground(currentDetentIndex)
344
- setupKeyboardObserver()
357
+ override fun coordinatorLayoutDidLayout(changed: Boolean) {
358
+ // Reposition footer when layout changes
359
+ if (isPresented && changed) {
360
+ positionFooter()
361
+ }
362
+ }
345
363
 
346
- if (shouldAnimatePresent) {
347
- wasPresentingWithAnimation = true
348
- post {
349
- val toTop = getExpectedSheetTop(currentDetentIndex)
350
- sheetAnimator.animatePresent(
351
- toTop = toTop,
352
- onUpdate = { effectiveTop -> updateSheetVisuals(effectiveTop) },
353
- onStart = { wasPresentingWithAnimation = false },
354
- onEnd = { finishPresent() }
355
- )
356
- }
357
- } else {
358
- isPresentingWithoutAnimation = true
364
+ // =============================================================================
365
+ // MARK: - TrueSheetDimViewDelegate
366
+ // =============================================================================
359
367
 
360
- post {
361
- val toTop = getExpectedSheetTop(currentDetentIndex)
362
- bottomSheetView?.y = toTop.toFloat()
368
+ override fun dimViewDidTap() {
369
+ val hostView = delegate as? TrueSheetView
370
+ if (hostView == null) {
371
+ RNLog.e(reactContext, "TrueSheet: Expected delegate to be TrueSheetView")
372
+ return
373
+ }
363
374
 
364
- updateSheetVisuals(toTop)
365
- finishPresent()
375
+ // If there's a child sheet on top, handle it instead
376
+ val topmostChild = TrueSheetStackManager.getSheetsAbove(hostView).firstOrNull()
377
+ if (topmostChild != null) {
378
+ if (topmostChild.viewController.dismissible) {
379
+ topmostChild.viewController.dismiss(animated = true)
366
380
  }
381
+ return
367
382
  }
368
- }
369
383
 
370
- override fun onDialogDismiss() {
371
- emitDidDismissEvents()
372
- cleanupDialog()
384
+ if (dismissible) {
385
+ dismiss(animated = true)
386
+ } else if (parentSheetView == null && currentDetentIndex > 0) {
387
+ setStateForDetentIndex(0)
388
+ }
373
389
  }
374
390
 
375
- override fun onDialogCancel() {
376
- // Cancel is called before dismiss for user-initiated cancellation
391
+ // =============================================================================
392
+ // MARK: - BottomSheetCallback
393
+ // =============================================================================
394
+
395
+ private val sheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
396
+ override fun onStateChanged(sheetView: View, newState: Int) {
397
+ handleStateChanged(sheetView, newState)
398
+ }
399
+
400
+ override fun onSlide(sheetView: View, slideOffset: Float) {
401
+ handleSlide(sheetView, slideOffset)
402
+ }
377
403
  }
378
404
 
379
- override fun onStateChanged(sheetView: View, newState: Int) {
405
+ private fun handleStateChanged(sheetView: View, newState: Int) {
380
406
  if (newState == BottomSheetBehavior.STATE_HIDDEN) {
381
407
  if (isDismissing) return
382
408
  isDismissing = true
383
409
  emitWillDismissEvents()
384
- dialogFragment?.dismiss()
410
+ finishDismiss()
385
411
  return
386
412
  }
387
413
 
@@ -398,21 +424,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
398
424
  }
399
425
  }
400
426
 
401
- override fun onSlide(sheetView: View, slideOffset: Float) {
402
- // Skip if our custom animator is handling the animation
403
- if (sheetAnimator.isAnimating) return
404
-
405
- // Keep it off screen to prevent flicker
406
- if (wasPresentingWithAnimation) {
407
- sheetView.y = realScreenHeight.toFloat()
408
- return
409
- }
410
-
411
- // When presenting without animation, keep the sheet at the target position
412
- if (isPresentingWithoutAnimation) {
413
- sheetView.y = getExpectedSheetTop(currentDetentIndex).toFloat()
414
- return
415
- }
427
+ private fun handleSlide(sheetView: View, slideOffset: Float) {
428
+ // Skip during dismiss animation
429
+ if (isDismissing) return
416
430
 
417
431
  val behavior = behavior ?: return
418
432
 
@@ -436,28 +450,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
436
450
  }
437
451
  }
438
452
 
439
- override fun onBackPressed() {
440
- delegate?.viewControllerDidBackPress()
441
- if (dismissible) {
442
- dismiss(animated = true)
443
- }
444
- }
445
-
446
453
  private fun handleStateSettled(sheetView: View, newState: Int) {
447
454
  if (interactionState is InteractionState.Reconfiguring) return
448
455
 
449
- // Reset non-animated presentation flag once behavior has settled at the target
450
- if (isPresentingWithoutAnimation) {
451
- val targetTop = getExpectedSheetTop(currentDetentIndex)
452
- sheetView.y = targetTop.toFloat()
453
-
454
- isPresentingWithoutAnimation = false
455
- }
456
-
457
456
  val index = detentCalculator.getDetentIndexForState(newState) ?: return
458
457
  val position = getPositionDpForView(sheetView)
459
458
  val detentInfo = DetentInfo(index, position)
460
459
 
460
+ // Handle present animation completion
461
+ if (isPresentAnimating) {
462
+ isPresentAnimating = false
463
+ finishPresent()
464
+ return
465
+ }
466
+
461
467
  when (interactionState) {
462
468
  is InteractionState.Dragging -> {
463
469
  val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
@@ -494,7 +500,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
494
500
  rnScreensObserver = RNScreensFragmentObserver(
495
501
  reactContext = reactContext,
496
502
  onModalPresented = {
497
- if (isPresented && isDialogVisible && isTopmostSheet) {
503
+ if (isPresented && isSheetVisible && isTopmostSheet) {
498
504
  hideForModal()
499
505
  }
500
506
  },
@@ -521,39 +527,32 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
521
527
  rnScreensObserver = null
522
528
  }
523
529
 
530
+ private fun setSheetVisibility(visible: Boolean) {
531
+ coordinatorLayout?.visibility = if (visible) VISIBLE else GONE
532
+ dimViews.forEach { it.visibility = if (visible) VISIBLE else INVISIBLE }
533
+ }
534
+
524
535
  private fun hideForModal() {
525
- isDialogVisible = false
536
+ isSheetVisible = false
526
537
  wasHiddenByModal = true
527
-
528
- // Prepare for fast fade out
529
538
  dimViews.forEach { it.alpha = 0f }
530
-
531
- dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFastFadeOut)
532
- dialog?.window?.decorView?.visibility = GONE
533
- dimViews.forEach { it.visibility = INVISIBLE }
534
-
539
+ setSheetVisibility(false)
535
540
  parentSheetView?.viewController?.hideForModal()
536
541
  }
537
542
 
538
543
  private fun showAfterModal() {
539
- isDialogVisible = true
540
-
541
- dialog?.window?.setWindowAnimations(0)
542
- dialog?.window?.decorView?.visibility = VISIBLE
543
- dimViews.forEach { it.visibility = VISIBLE }
544
-
544
+ isSheetVisible = true
545
+ setSheetVisibility(true)
545
546
  updateDimAmount(animated = true)
546
547
  }
547
548
 
548
549
  /**
549
550
  * Re-applies hidden state after returning from background.
550
- * Android may restore dialog visibility on activity resume, so we need to hide it again.
551
+ * Android may restore visibility on activity resume, so we need to hide it again.
551
552
  */
552
553
  fun reapplyHiddenState() {
553
554
  if (!wasHiddenByModal) return
554
-
555
- dialog?.window?.decorView?.visibility = GONE
556
- dimViews.forEach { it.visibility = INVISIBLE }
555
+ setSheetVisibility(false)
557
556
  }
558
557
 
559
558
  // =============================================================================
@@ -561,13 +560,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
561
560
  // =============================================================================
562
561
 
563
562
  fun present(detentIndex: Int, animated: Boolean = true) {
564
- val fragment = this.dialogFragment ?: run {
565
- RNLog.w(reactContext, "TrueSheet: No dialog fragment available. Ensure the sheet is mounted before presenting.")
563
+ val coordinator = this.coordinatorLayout ?: run {
564
+ RNLog.w(reactContext, "TrueSheet: No coordinator layout available. Ensure the sheet is mounted before presenting.")
565
+ return
566
+ }
567
+
568
+ val sheet = this.sheetView ?: run {
569
+ RNLog.w(reactContext, "TrueSheet: No sheet view available.")
566
570
  return
567
571
  }
568
572
 
569
- val activity = reactContext.currentActivity as? AppCompatActivity ?: run {
570
- RNLog.w(reactContext, "TrueSheet: No AppCompatActivity available for fragment transaction.")
573
+ val activity = reactContext.currentActivity ?: run {
574
+ RNLog.w(reactContext, "TrueSheet: No activity available for presentation.")
571
575
  return
572
576
  }
573
577
 
@@ -579,14 +583,66 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
579
583
  currentDetentIndex = detentIndex
580
584
  interactionState = InteractionState.Idle
581
585
 
582
- // Show the fragment - detents are configured in onDialogShow
583
- if (!fragment.isAdded) {
584
- fragment.show(activity.supportFragmentManager, FRAGMENT_TAG)
585
- }
586
+ // Setup sheet in coordinator layout
587
+ setupSheetInCoordinator(coordinator, sheet)
588
+
589
+ // Add coordinator to activity
590
+ val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
591
+ contentView?.addView(coordinator)
592
+
593
+ // Setup back button handling
594
+ setupBackCallback()
595
+
596
+ // Start presentation
597
+ onSheetShow()
598
+ }
599
+ }
600
+
601
+ private fun setupSheetInCoordinator(coordinator: TrueSheetCoordinatorLayout, sheet: TrueSheetBottomSheetView) {
602
+ // Add this controller as content to the sheet
603
+ (parent as? ViewGroup)?.removeView(this)
604
+ sheet.addView(this)
605
+
606
+ // Create layout params with behavior
607
+ val params = sheet.createLayoutParams()
608
+ val behavior = params.behavior as BottomSheetBehavior<TrueSheetBottomSheetView>
586
609
 
587
- // Execute pending transactions to ensure fragment is added
588
- activity.supportFragmentManager.executePendingTransactions()
610
+ // Configure behavior
611
+ behavior.isHideable = true
612
+ behavior.isDraggable = draggable
613
+ behavior.state = BottomSheetBehavior.STATE_HIDDEN
614
+ behavior.addBottomSheetCallback(sheetCallback)
615
+
616
+ // Add sheet to coordinator
617
+ coordinator.addView(sheet, params)
618
+ }
619
+
620
+ private fun onSheetShow() {
621
+ val sheet = sheetView ?: run {
622
+ RNLog.e(reactContext, "TrueSheet: sheetView is null in onSheetShow")
623
+ return
624
+ }
625
+
626
+ emitWillPresentEvents()
627
+
628
+ setupSheetDetents()
629
+ setupDimmedBackground(currentDetentIndex)
630
+ setupKeyboardObserver()
631
+ sheet.setupBackground()
632
+ sheet.setupGrabber()
633
+
634
+ if (shouldAnimatePresent) {
635
+ isPresentAnimating = true
636
+ post { setStateForDetentIndex(currentDetentIndex) }
637
+ } else {
638
+ setStateForDetentIndex(currentDetentIndex)
639
+ emitChangePositionDelegate(detentCalculator.getSheetTopForDetentIndex(currentDetentIndex))
640
+ updateDimAmount()
641
+ finishPresent()
589
642
  }
643
+
644
+ isPresented = true
645
+ isSheetVisible = true
590
646
  }
591
647
 
592
648
  fun dismiss(animated: Boolean = true) {
@@ -596,17 +652,32 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
596
652
  emitWillDismissEvents()
597
653
 
598
654
  if (animated) {
599
- sheetAnimator.animateDismiss(
600
- onUpdate = { effectiveTop -> updateSheetVisuals(effectiveTop) },
601
- onEnd = { dialogFragment?.dismiss() }
602
- )
655
+ animateDismiss()
603
656
  } else {
604
657
  emitChangePositionDelegate(realScreenHeight)
605
- dialogFragment?.dismiss()
658
+ finishDismiss()
606
659
  }
607
660
  }
608
661
 
662
+ private fun animateDismiss() {
663
+ val sheet = sheetView ?: run {
664
+ finishDismiss()
665
+ return
666
+ }
667
+
668
+ sheet.animate()
669
+ .y(realScreenHeight.toFloat())
670
+ .setDuration(DISMISS_DURATION)
671
+ .setInterpolator(android.view.animation.AccelerateInterpolator())
672
+ .setUpdateListener { updateSheetVisuals(sheet.y.toInt()) }
673
+ .withEndAction { finishDismiss() }
674
+ .start()
675
+ }
676
+
609
677
  private fun finishPresent() {
678
+ // Restore isHideable to actual value after present animation
679
+ behavior?.isHideable = dismissible
680
+
610
681
  val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
611
682
  delegate?.viewControllerDidPresent(index, position, detent)
612
683
  parentSheetView?.viewControllerDidBlur()
@@ -616,40 +687,47 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
616
687
  presentPromise = null
617
688
  }
618
689
 
690
+ private fun finishDismiss() {
691
+ emitDidDismissEvents()
692
+ cleanupSheet()
693
+ }
694
+
619
695
  // =============================================================================
620
696
  // MARK: - Sheet Configuration
621
697
  // =============================================================================
622
698
 
623
699
  fun setupSheetDetents() {
624
- val fragment = this.dialogFragment ?: return
625
- val behavior = this.behavior ?: return
700
+ val behavior = this.behavior ?: run {
701
+ RNLog.e(reactContext, "TrueSheet: behavior is null in setupSheetDetents")
702
+ return
703
+ }
626
704
 
627
705
  interactionState = InteractionState.Reconfiguring
628
- val edgeToEdgeTopInset: Int = if (!edgeToEdgeFullScreen) topInset else 0
629
706
 
630
707
  behavior.isFitToContents = false
631
708
 
632
- val maxAvailableHeight = realScreenHeight - edgeToEdgeTopInset
709
+ val maxAvailableHeight = realScreenHeight - topInset
633
710
 
634
- val peekHeight = detentCalculator.getDetentHeight(detents[0])
711
+ val peekHeight = minOf(detentCalculator.getDetentHeight(detents[0]), maxAvailableHeight)
635
712
 
636
713
  val halfExpandedDetentHeight = when (detents.size) {
637
714
  1 -> peekHeight
638
715
  else -> detentCalculator.getDetentHeight(detents[1])
639
716
  }
640
717
 
641
- val maxDetentHeight = detentCalculator.getDetentHeight(detents.last())
718
+ val maxDetentHeight = minOf(detentCalculator.getDetentHeight(detents.last()), maxAvailableHeight)
642
719
 
643
720
  val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
644
721
  val halfExpandedRatio = (adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat())
645
722
  .coerceIn(0f, 0.999f)
646
723
 
647
- val expandedOffset = maxOf(edgeToEdgeTopInset, realScreenHeight - maxDetentHeight)
724
+ val expandedOffset = realScreenHeight - maxDetentHeight
648
725
 
649
726
  // fitToContents works better with <= 2 detents when no expanded offset
650
727
  val fitToContents = detents.size < 3 && expandedOffset == 0
651
728
 
652
- fragment.configureDetents(
729
+ configureDetents(
730
+ behavior = behavior,
653
731
  peekHeight = peekHeight,
654
732
  halfExpandedRatio = halfExpandedRatio,
655
733
  expandedOffset = expandedOffset,
@@ -674,87 +752,76 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
674
752
  interactionState = InteractionState.Idle
675
753
  }
676
754
 
755
+ private fun configureDetents(
756
+ behavior: BottomSheetBehavior<TrueSheetBottomSheetView>,
757
+ peekHeight: Int,
758
+ halfExpandedRatio: Float,
759
+ expandedOffset: Int,
760
+ fitToContents: Boolean,
761
+ animate: Boolean
762
+ ) {
763
+ behavior.apply {
764
+ isFitToContents = fitToContents
765
+ skipCollapsed = false
766
+ setPeekHeight(peekHeight, animate)
767
+ this.halfExpandedRatio = halfExpandedRatio.coerceIn(0f, 0.999f)
768
+ this.expandedOffset = expandedOffset
769
+ }
770
+ }
771
+
677
772
  fun setupSheetDetentsForSizeChange() {
678
773
  setupSheetDetents()
679
774
  positionFooter()
680
775
  }
681
776
 
682
777
  fun setStateForDetentIndex(index: Int) {
683
- dialogFragment?.setState(detentCalculator.getStateForDetentIndex(index))
778
+ behavior?.state = detentCalculator.getStateForDetentIndex(index)
684
779
  }
685
780
 
686
781
  // =============================================================================
687
- // MARK: - Grabber & Background
782
+ // MARK: - Dimmed Background
688
783
  // =============================================================================
689
784
 
690
- fun setupGrabber() {
691
- dialogFragment?.let {
692
- syncFragmentProperties(it)
693
- it.setupGrabber()
694
- }
695
- }
696
-
697
- fun setupBackground() {
698
- dialogFragment?.let {
699
- syncFragmentProperties(it)
700
- it.setupBackground()
701
- }
702
- }
703
-
704
785
  fun setupDimmedBackground(detentIndex: Int) {
705
- val dialog = this.dialog ?: return
706
-
707
- dialog.window?.apply {
708
- val touchOutside = findViewById<View>(com.google.android.material.R.id.touch_outside)
709
- clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
710
-
711
- val shouldDimAtDetent = dimmed && detentIndex >= dimmedDetentIndex
712
-
713
- if (dimmed) {
714
- val parentDimVisible = (parentSheetView?.viewController?.dimView?.alpha ?: 0f) > 0f
786
+ val coordinator = this.coordinatorLayout ?: run {
787
+ RNLog.e(reactContext, "TrueSheet: coordinatorLayout is null in setupDimmedBackground")
788
+ return
789
+ }
715
790
 
716
- if (dimView == null) dimView = TrueSheetDimView(reactContext)
717
- if (!parentDimVisible) dimView?.attach(null)
791
+ if (dimmed) {
792
+ val parentDimVisible = (parentSheetView?.viewController?.dimView?.alpha ?: 0f) > 0f
718
793
 
719
- // Attach dim view to parent sheet if stacked
720
- val parentController = parentSheetView?.viewController
721
- val parentBottomSheet = parentController?.bottomSheetView
722
- if (parentBottomSheet != null) {
723
- if (parentDimView == null) parentDimView = TrueSheetDimView(reactContext)
724
- parentDimView?.attach(parentBottomSheet, parentController.sheetCornerRadius)
794
+ if (dimView == null) {
795
+ dimView = TrueSheetDimView(reactContext).apply {
796
+ delegate = this@TrueSheetViewController
725
797
  }
726
- } else {
727
- dimView?.detach()
728
- dimView = null
729
- parentDimView?.detach()
730
- parentDimView = null
798
+ }
799
+ if (!parentDimVisible) {
800
+ dimView?.attachToCoordinator(coordinator)
731
801
  }
732
802
 
733
- if (shouldDimAtDetent) {
734
- touchOutside.setOnTouchListener { _, event ->
735
- if (event.action == MotionEvent.ACTION_UP && dismissible) {
736
- dismiss()
803
+ // Attach dim view to parent sheet if stacked
804
+ val parentController = parentSheetView?.viewController
805
+ val parentBottomSheet = parentController?.sheetView
806
+ if (parentBottomSheet != null) {
807
+ if (parentDimView == null) {
808
+ parentDimView = TrueSheetDimView(reactContext).apply {
809
+ delegate = this@TrueSheetViewController
737
810
  }
738
- true
739
- }
740
- } else {
741
- // Pass through touches to parent or activity when not dimmed
742
- touchOutside.setOnTouchListener { v, event ->
743
- event.setLocation(event.rawX - v.x, event.rawY - v.y)
744
- (
745
- parentSheetView?.viewController?.dialog?.window?.decorView
746
- ?: reactContext.currentActivity?.window?.decorView
747
- )?.dispatchTouchEvent(event)
748
- false
749
811
  }
750
- dialog.setCanceledOnTouchOutside(false)
812
+ parentDimView?.attach(parentBottomSheet, parentController.sheetCornerRadius)
751
813
  }
814
+ } else {
815
+ dimView?.detach()
816
+ dimView = null
817
+ parentDimView?.detach()
818
+ parentDimView = null
752
819
  }
753
820
  }
754
821
 
755
822
  fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
756
823
  if (!dimmed) return
757
- val top = (sheetTop ?: bottomSheetView?.top ?: return) + currentKeyboardInset
824
+ val top = (sheetTop ?: sheetView?.top ?: return) + currentKeyboardInset
758
825
 
759
826
  if (animated) {
760
827
  val targetAlpha = dimView?.calculateAlpha(
@@ -775,11 +842,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
775
842
  fun positionFooter(slideOffset: Float? = null) {
776
843
  if (!isPresented) return
777
844
  val footerView = containerView?.footerView ?: return
778
- val bottomSheet = bottomSheetView ?: return
845
+ val sheet = sheetView ?: return
779
846
 
780
847
  val footerHeight = footerView.height
781
- val sheetHeight = bottomSheet.height
782
- val sheetTop = bottomSheet.top
848
+ val sheetHeight = sheet.height
849
+ val sheetTop = sheet.top
783
850
 
784
851
  var footerY = (sheetHeight - sheetTop - footerHeight - currentKeyboardInset).toFloat()
785
852
 
@@ -803,14 +870,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
803
870
  }
804
871
 
805
872
  fun setupKeyboardObserver() {
806
- val bottomSheet = bottomSheetView ?: return
873
+ val coordinator = coordinatorLayout ?: run {
874
+ RNLog.e(reactContext, "TrueSheet: coordinatorLayout is null in setupKeyboardObserver")
875
+ return
876
+ }
807
877
  cleanupKeyboardObserver()
808
- keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
878
+ keyboardObserver = TrueSheetKeyboardObserver(coordinator, reactContext).apply {
809
879
  delegate = object : TrueSheetKeyboardObserverDelegate {
810
880
  override fun keyboardWillShow(height: Int) {
881
+ isKeyboardTransitioning = true
811
882
  if (!shouldHandleKeyboard()) return
812
883
  detentIndexBeforeKeyboard = currentDetentIndex
813
- isKeyboardTransitioning = true
814
884
  setupSheetDetents()
815
885
  setStateForDetentIndex(detents.size - 1)
816
886
  }
@@ -825,7 +895,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
825
895
  }
826
896
 
827
897
  override fun keyboardDidHide() {
828
- if (!shouldHandleKeyboard()) return
829
898
  isKeyboardTransitioning = false
830
899
  }
831
900
 
@@ -919,19 +988,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
919
988
  // MARK: - Detent Helpers
920
989
  // =============================================================================
921
990
 
922
- fun getExpectedSheetTop(detentIndex: Int): Int {
923
- if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
924
- return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
925
- }
926
-
927
- fun translateDialog(translationY: Int) {
928
- val bottomSheet = bottomSheetView ?: return
991
+ fun translateSheet(translationY: Int) {
992
+ val sheet = sheetView ?: return
929
993
 
930
- bottomSheet.animate()
994
+ sheet.animate()
931
995
  .translationY(translationY.toFloat())
932
996
  .setDuration(TRANSLATE_ANIMATION_DURATION)
933
997
  .setUpdateListener {
934
- val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
998
+ val effectiveTop = sheet.top + sheet.translationY.toInt()
935
999
  emitChangePositionDelegate(effectiveTop)
936
1000
  }
937
1001
  .start()
@@ -948,7 +1012,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
948
1012
  private fun getPositionForDetentIndex(index: Int): Float {
949
1013
  if (index < 0 || index >= detents.size) return screenHeight.pxToDp()
950
1014
 
951
- bottomSheetView?.let {
1015
+ sheetView?.let {
952
1016
  val visibleSheetHeight = detentCalculator.getVisibleSheetHeight(it.top)
953
1017
  if (visibleSheetHeight in 1..<realScreenHeight) {
954
1018
  return detentCalculator.getPositionDp(visibleSheetHeight)
@@ -980,7 +1044,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
980
1044
  post {
981
1045
  setupSheetDetents()
982
1046
  positionFooter()
983
- bottomSheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
1047
+ sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
984
1048
  }
985
1049
  }
986
1050
 
@@ -1021,41 +1085,41 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1021
1085
 
1022
1086
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
1023
1087
  eventDispatcher?.let {
1024
- jSTouchDispatcher.handleTouchEvent(event, it, reactContext)
1025
- jSPointerDispatcher?.handleMotionEvent(event, it, true)
1088
+ jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1089
+ jsPointerDispatcher?.handleMotionEvent(event, it, true)
1026
1090
  }
1027
1091
  return super.onInterceptTouchEvent(event)
1028
1092
  }
1029
1093
 
1030
1094
  override fun onTouchEvent(event: MotionEvent): Boolean {
1031
1095
  eventDispatcher?.let {
1032
- jSTouchDispatcher.handleTouchEvent(event, it, reactContext)
1033
- jSPointerDispatcher?.handleMotionEvent(event, it, false)
1096
+ jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1097
+ jsPointerDispatcher?.handleMotionEvent(event, it, false)
1034
1098
  }
1035
1099
  super.onTouchEvent(event)
1036
1100
  return true
1037
1101
  }
1038
1102
 
1039
1103
  override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
1040
- eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, true) }
1104
+ eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, true) }
1041
1105
  return super.onHoverEvent(event)
1042
1106
  }
1043
1107
 
1044
1108
  override fun onHoverEvent(event: MotionEvent): Boolean {
1045
- eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, false) }
1109
+ eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, false) }
1046
1110
  return super.onHoverEvent(event)
1047
1111
  }
1048
1112
 
1049
1113
  override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
1050
1114
  eventDispatcher?.let {
1051
- jSTouchDispatcher.onChildStartedNativeGesture(ev, it)
1052
- jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
1115
+ jsTouchDispatcher.onChildStartedNativeGesture(ev, it)
1116
+ jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
1053
1117
  }
1054
1118
  }
1055
1119
 
1056
1120
  override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
1057
- eventDispatcher?.let { jSTouchDispatcher.onChildEndedNativeGesture(ev, it) }
1058
- jSPointerDispatcher?.onChildEndedNativeGesture()
1121
+ eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
1122
+ jsPointerDispatcher?.onChildEndedNativeGesture()
1059
1123
  }
1060
1124
 
1061
1125
  override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {