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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  The true native bottom sheet experience for your React Native Apps. 💩
10
10
 
11
- <img alt="React Native True Sheet - IOS" src="docs/static/img/preview-ios.gif" width="246 height="500" /><img alt="React Native True Sheet - Android" src="docs/static/img/preview-android.gif" width="246 height="500" /><img alt="React Native True Sheet - Web" src="docs/static/img/preview-web.gif" width="246 height="500" />
11
+ <img alt="React Native True Sheet - IOS" src="docs/static/img/preview-ios.gif" width="248" height="500" /><img alt="React Native True Sheet - Android" src="docs/static/img/preview-android.gif" width="248" height="500" /><img alt="React Native True Sheet - Web" src="docs/static/img/preview-web.gif" width="248" height="500" />
12
12
 
13
13
  ## Features
14
14
 
@@ -251,6 +251,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
251
251
  viewController.dimmed = dimmed
252
252
  if (viewController.isPresented) {
253
253
  viewController.setupDimmedBackground(viewController.currentDetentIndex)
254
+ viewController.updateDimAmount()
254
255
  }
255
256
  }
256
257
 
@@ -259,6 +260,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
259
260
  viewController.dimmedDetentIndex = index
260
261
  if (viewController.isPresented) {
261
262
  viewController.setupDimmedBackground(viewController.currentDetentIndex)
263
+ viewController.updateDimAmount()
262
264
  }
263
265
  }
264
266
 
@@ -366,8 +368,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
366
368
  isSheetUpdatePending = true
367
369
  viewController.post {
368
370
  isSheetUpdatePending = false
369
- viewController.setupSheetDetents()
370
- viewController.positionFooter()
371
+ viewController.setupSheetDetentsForSizeChange()
372
+ TrueSheetDialogObserver.onSheetSizeChanged(this)
371
373
  }
372
374
  }
373
375
 
@@ -382,9 +384,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
382
384
  }
383
385
 
384
386
  override fun containerViewFooterDidChangeSize(width: Int, height: Int) {
385
- if (viewController.isPresented) {
386
- viewController.positionFooter()
387
- }
387
+ viewController.positionFooter()
388
388
  }
389
389
 
390
390
  companion object {
@@ -1,5 +1,6 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
+ import android.animation.ValueAnimator
3
4
  import android.annotation.SuppressLint
4
5
  import android.graphics.Color
5
6
  import android.graphics.drawable.ShapeDrawable
@@ -28,6 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
28
29
  import com.google.android.material.bottomsheet.BottomSheetDialog
29
30
  import com.lodev09.truesheet.core.GrabberOptions
30
31
  import com.lodev09.truesheet.core.RNScreensFragmentObserver
32
+ import com.lodev09.truesheet.core.TrueSheetDimView
31
33
  import com.lodev09.truesheet.core.TrueSheetGrabberView
32
34
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
33
35
  import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
@@ -72,8 +74,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
72
74
  private const val DEFAULT_CORNER_RADIUS = 16 // dp
73
75
 
74
76
  // Animation durations from res/anim/true_sheet_slide_in.xml and true_sheet_slide_out.xml
75
- private const val PRESENT_ANIMATION_DURATION = 200L
77
+ private const val PRESENT_ANIMATION_DURATION = 250L
76
78
  private const val DISMISS_ANIMATION_DURATION = 150L
79
+ private const val TRANSLATE_ANIMATION_DURATION = 100L
77
80
  }
78
81
 
79
82
  // ====================================================================
@@ -87,6 +90,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
87
90
  // ====================================================================
88
91
 
89
92
  private var dialog: BottomSheetDialog? = null
93
+ private var dimView: TrueSheetDimView? = null
94
+ private var parentDimView: TrueSheetDimView? = null
90
95
 
91
96
  private val behavior: BottomSheetBehavior<FrameLayout>?
92
97
  get() = dialog?.behavior
@@ -144,6 +149,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
144
149
  get() = ScreenUtils.getScreenHeight(reactContext)
145
150
  val screenWidth: Int
146
151
  get() = ScreenUtils.getScreenWidth(reactContext)
152
+ val realScreenHeight: Int
153
+ get() = ScreenUtils.getRealScreenHeight(reactContext)
147
154
 
148
155
  var maxSheetHeight: Int? = null
149
156
  var detents = mutableListOf(0.5, 1.0)
@@ -152,7 +159,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
152
159
  var dimmedDetentIndex = 0
153
160
  var grabber: Boolean = true
154
161
  var grabberOptions: GrabberOptions? = null
155
- var sheetCornerRadius: Float = -1f
162
+ var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
163
+ set(value) {
164
+ field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
165
+ setupBackground()
166
+ }
156
167
  var sheetBackgroundColor: Int? = null
157
168
  var edgeToEdgeFullScreen: Boolean = false
158
169
 
@@ -261,6 +272,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
261
272
 
262
273
  cleanupKeyboardObserver()
263
274
  cleanupModalObserver()
275
+ dimView?.detach()
276
+ dimView = null
277
+ parentDimView?.detach()
278
+ parentDimView = null
264
279
  sheetContainer?.removeView(this)
265
280
 
266
281
  dialog = null
@@ -281,16 +296,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
281
296
  setupGrabber()
282
297
  setupKeyboardObserver()
283
298
 
299
+ val toTop = getExpectedSheetTop(currentDetentIndex)
300
+ setupTransitionTracker(realScreenHeight, toTop, PRESENT_ANIMATION_DURATION)
301
+ animateDimAlpha(show = true)
302
+
284
303
  sheetContainer?.post {
285
- bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
286
304
  positionFooter()
287
305
  }
288
306
 
289
307
  sheetContainer?.postDelayed({
290
- val detentInfo = getDetentInfoForIndex(currentDetentIndex)
291
- val detent = getDetentValueForIndex(detentInfo.index)
308
+ val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
292
309
 
293
- delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position, detent)
310
+ delegate?.viewControllerDidPresent(index, position, detent)
294
311
  parentSheetView?.viewControllerDidBlur()
295
312
  delegate?.viewControllerDidFocus()
296
313
 
@@ -303,8 +320,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
303
320
  if (isDismissing) return@setOnCancelListener
304
321
 
305
322
  isDismissing = true
323
+ val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
324
+ setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
325
+ animateDimAlpha(show = false)
306
326
  emitWillDismissEvents()
307
- emitDismissedPosition()
308
327
  }
309
328
 
310
329
  dialog.setOnDismissListener {
@@ -321,7 +340,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
321
340
  override fun onSlide(sheetView: View, slideOffset: Float) {
322
341
  val behavior = behavior ?: return
323
342
 
324
- emitChangePositionDelegate(sheetView, realtime = true)
343
+ emitChangePositionDelegate(sheetView.top)
325
344
 
326
345
  when (behavior.state) {
327
346
  BottomSheetBehavior.STATE_DRAGGING,
@@ -331,6 +350,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
331
350
  }
332
351
 
333
352
  positionFooter(slideOffset)
353
+ updateDimAmount()
334
354
  }
335
355
 
336
356
  override fun onStateChanged(sheetView: View, newState: Int) {
@@ -386,14 +406,25 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
386
406
  reactContext = reactContext,
387
407
  onModalPresented = {
388
408
  if (isPresented && isDialogVisible) {
389
- hideDialog(animated = true)
409
+ isDialogVisible = false
410
+ dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFadeOutAnimation)
411
+ dialog?.window?.decorView?.visibility = INVISIBLE
412
+ dimView?.visibility = INVISIBLE
413
+ parentDimView?.visibility = INVISIBLE
390
414
  wasHiddenByModal = true
391
415
  }
392
416
  },
393
417
  onModalDismissed = {
394
418
  // Only show if we were the one hidden by modal, not by sheet stacking
395
419
  if (isPresented && wasHiddenByModal) {
396
- showDialog(animated = true)
420
+ isDialogVisible = true
421
+ dialog?.window?.decorView?.visibility = VISIBLE
422
+ dimView?.visibility = VISIBLE
423
+ parentDimView?.visibility = VISIBLE
424
+ // Restore animation after visibility change to avoid slide animation
425
+ sheetContainer?.post {
426
+ dialog?.window?.setWindowAnimations(windowAnimation)
427
+ }
397
428
  wasHiddenByModal = false
398
429
  }
399
430
  }
@@ -424,6 +455,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
424
455
  dismissPromise = null
425
456
  }
426
457
 
458
+ /** Helper to get detent info with its screen fraction value. */
459
+ private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
460
+ val detentInfo = getDetentInfoForIndex(index)
461
+ val detent = getDetentValueForIndex(detentInfo.index)
462
+ return Triple(detentInfo.index, detentInfo.position, detent)
463
+ }
464
+
427
465
  // ====================================================================
428
466
  // MARK: - Dialog Visibility (for stacking)
429
467
  // ====================================================================
@@ -439,35 +477,21 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
439
477
 
440
478
  fun getExpectedSheetTop(detentIndex: Int): Int {
441
479
  if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
442
- val detentHeight = getDetentHeight(detents[detentIndex])
443
- return screenHeight - detentHeight
480
+ return realScreenHeight - getDetentHeight(detents[detentIndex])
444
481
  }
445
482
 
446
- /** Hides without dismissing. Used for sheet stacking and RN Screens modals. */
447
- fun hideDialog(emitPosition: Boolean = false, animated: Boolean = false) {
448
- isDialogVisible = false
449
- if (animated) {
450
- dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFadeAnimation)
451
- }
452
- dialog?.window?.decorView?.visibility = INVISIBLE
453
- if (emitPosition) {
454
- emitDismissedPosition()
455
- }
456
- }
483
+ /** Translates the sheet when stacking. Pass 0 to reset. */
484
+ fun translateDialog(translationY: Int) {
485
+ val bottomSheet = bottomSheetView ?: return
457
486
 
458
- /** Shows a previously hidden dialog. */
459
- fun showDialog(emitPosition: Boolean = false, animated: Boolean = false) {
460
- isDialogVisible = true
461
- dialog?.window?.decorView?.visibility = VISIBLE
462
- if (emitPosition) {
463
- bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
464
- }
465
- if (animated) {
466
- // Restore original animation after fade-in completes (100ms)
467
- sheetContainer?.postDelayed({
468
- dialog?.window?.setWindowAnimations(windowAnimation)
469
- }, 100)
470
- }
487
+ bottomSheet.animate()
488
+ .translationY(translationY.toFloat())
489
+ .setDuration(TRANSLATE_ANIMATION_DURATION)
490
+ .setUpdateListener {
491
+ val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
492
+ emitChangePositionDelegate(effectiveTop)
493
+ }
494
+ .start()
471
495
  }
472
496
 
473
497
  // ====================================================================
@@ -490,11 +514,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
490
514
  setupSheetDetents()
491
515
  setStateForDetentIndex(detentIndex)
492
516
 
493
- val detentInfo = getDetentInfoForIndex(detentIndex)
494
- val detent = getDetentValueForIndex(detentInfo.index)
517
+ val (index, position, detent) = getDetentInfoWithValue(detentIndex)
495
518
 
496
519
  parentSheetView?.viewControllerWillBlur()
497
- delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position, detent)
520
+ delegate?.viewControllerWillPresent(index, position, detent)
498
521
  delegate?.viewControllerWillFocus()
499
522
 
500
523
  if (!animated) {
@@ -509,8 +532,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
509
532
  if (isDismissing) return
510
533
 
511
534
  isDismissing = true
535
+ val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
536
+ setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
512
537
  emitWillDismissEvents()
513
- emitDismissedPosition()
514
538
 
515
539
  if (!animated) {
516
540
  dialog?.window?.setWindowAnimations(0)
@@ -528,14 +552,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
528
552
  val behavior = this.behavior ?: return
529
553
 
530
554
  isReconfiguring = true
531
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
532
555
  val edgeToEdgeTopInset: Int = if (!edgeToEdgeFullScreen) topInset else 0
533
556
 
534
557
  behavior.apply {
535
558
  isFitToContents = false
536
559
  maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
537
560
 
538
- val maxAvailableHeight = realHeight - edgeToEdgeTopInset
561
+ val maxAvailableHeight = realScreenHeight - edgeToEdgeTopInset
539
562
 
540
563
  setPeekHeight(getDetentHeight(detents[0]), isPresented)
541
564
 
@@ -547,13 +570,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
547
570
  val maxDetentHeight = getDetentHeight(detents.last())
548
571
 
549
572
  val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
550
- halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() / realHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
573
+ halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
551
574
 
552
- expandedOffset = maxOf(edgeToEdgeTopInset, realHeight - maxDetentHeight)
575
+ expandedOffset = maxOf(edgeToEdgeTopInset, realScreenHeight - maxDetentHeight)
553
576
  isFitToContents = detents.size < 3 && expandedOffset == 0
554
577
 
555
578
  val offset = if (expandedOffset == 0) topInset else 0
556
- val newHeight = realHeight - expandedOffset - offset
579
+ val newHeight = realScreenHeight - expandedOffset - offset
557
580
  val newWidth = minOf(screenWidth, maxWidth)
558
581
 
559
582
  if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
@@ -570,6 +593,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
570
593
  }
571
594
  }
572
595
 
596
+ fun setupSheetDetentsForSizeChange() {
597
+ setupSheetDetents()
598
+ positionFooter()
599
+ }
600
+
573
601
  fun setupGrabber() {
574
602
  val bottomSheet = bottomSheetView ?: return
575
603
 
@@ -584,10 +612,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
584
612
  }
585
613
 
586
614
  bottomSheet.addView(grabberView)
587
- grabberView.bringToFront()
588
615
  }
589
616
 
590
617
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
618
+ private var positionAnimator: ValueAnimator? = null
591
619
 
592
620
  fun setupKeyboardObserver() {
593
621
  val bottomSheet = bottomSheetView ?: return
@@ -606,11 +634,51 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
606
634
  keyboardObserver = null
607
635
  }
608
636
 
637
+ private fun setupTransitionTracker(fromTop: Int, toTop: Int, duration: Long) {
638
+ positionAnimator?.cancel()
639
+ positionAnimator = ValueAnimator.ofInt(fromTop, toTop).apply {
640
+ this.duration = duration
641
+ interpolator = if (fromTop > toTop) {
642
+ android.view.animation.DecelerateInterpolator(2f) // present
643
+ } else {
644
+ android.view.animation.AccelerateInterpolator(2f) // dismiss
645
+ }
646
+ addUpdateListener { animator ->
647
+ emitChangePositionDelegate(animator.animatedValue as Int)
648
+ }
649
+ addListener(object : android.animation.AnimatorListenerAdapter() {
650
+ override fun onAnimationEnd(animation: android.animation.Animator) {
651
+ positionAnimator = null
652
+ }
653
+ })
654
+ start()
655
+ }
656
+ }
657
+
658
+ private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
659
+ if (currentTop == lastEmittedPositionPx) return
660
+
661
+ lastEmittedPositionPx = currentTop
662
+ val visibleHeight = realScreenHeight - currentTop
663
+ val position = getPositionDp(visibleHeight)
664
+ val interpolatedIndex = getInterpolatedIndexForPosition(currentTop)
665
+ val detent = getInterpolatedDetentForPosition(currentTop)
666
+ delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
667
+ }
668
+
609
669
  fun setupBackground() {
610
670
  val bottomSheet = bottomSheetView ?: return
611
671
 
612
- val cornerRadius = if (sheetCornerRadius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else sheetCornerRadius
613
- val outerRadii = floatArrayOf(cornerRadius, cornerRadius, cornerRadius, cornerRadius, 0f, 0f, 0f, 0f)
672
+ val outerRadii = floatArrayOf(
673
+ sheetCornerRadius,
674
+ sheetCornerRadius,
675
+ sheetCornerRadius,
676
+ sheetCornerRadius,
677
+ 0f,
678
+ 0f,
679
+ 0f,
680
+ 0f
681
+ )
614
682
  val backgroundColor = sheetBackgroundColor ?: getDefaultBackgroundColor()
615
683
 
616
684
  bottomSheet.background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
@@ -619,24 +687,46 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
619
687
  bottomSheet.clipToOutline = true
620
688
  }
621
689
 
622
- /** Configures dim and touch-through behavior based on detent index. */
623
690
  fun setupDimmedBackground(detentIndex: Int) {
624
691
  val dialog = this.dialog ?: return
692
+
625
693
  dialog.window?.apply {
626
- val view = findViewById<View>(com.google.android.material.R.id.touch_outside)
694
+ val touchOutside = findViewById<View>(com.google.android.material.R.id.touch_outside)
695
+ clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
696
+
697
+ val shouldDimAtDetent = dimmed && detentIndex >= dimmedDetentIndex
627
698
 
628
- if (dimmed && detentIndex >= dimmedDetentIndex) {
629
- view.setOnTouchListener(null)
630
- setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
631
- setDimAmount(0.32f) // M3 scrim opacity
699
+ if (dimmed) {
700
+ val parentDimVisible = (parentSheetView?.viewController?.dimView?.alpha ?: 0f) > 0f
701
+
702
+ if (dimView == null) dimView = TrueSheetDimView(reactContext)
703
+ if (!parentDimVisible) dimView?.attach(null)
704
+
705
+ val parentController = parentSheetView?.viewController
706
+ val parentBottomSheet = parentController?.bottomSheetView
707
+ if (parentBottomSheet != null) {
708
+ if (parentDimView == null) parentDimView = TrueSheetDimView(reactContext)
709
+ parentDimView?.attach(parentBottomSheet, parentController.sheetCornerRadius)
710
+ }
711
+ } else {
712
+ dimView?.detach()
713
+ dimView = null
714
+ parentDimView?.detach()
715
+ parentDimView = null
716
+ }
717
+
718
+ if (shouldDimAtDetent) {
719
+ touchOutside.setOnTouchListener(null)
632
720
  dialog.setCanceledOnTouchOutside(dismissible)
633
721
  } else {
634
- view.setOnTouchListener { v, event ->
722
+ touchOutside.setOnTouchListener { v, event ->
635
723
  event.setLocation(event.rawX - v.x, event.rawY - v.y)
636
- reactContext.currentActivity?.dispatchTouchEvent(event)
724
+ (
725
+ parentSheetView?.viewController?.dialog?.window?.decorView
726
+ ?: reactContext.currentActivity?.window?.decorView
727
+ )?.dispatchTouchEvent(event)
637
728
  false
638
729
  }
639
- clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
640
730
  dialog.setCanceledOnTouchOutside(false)
641
731
  }
642
732
  }
@@ -646,6 +736,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
646
736
  dialog?.window?.setWindowAnimations(windowAnimation)
647
737
  }
648
738
 
739
+ private fun animateDimAlpha(show: Boolean) {
740
+ if (!dimmed) return
741
+ val duration = if (show) PRESENT_ANIMATION_DURATION else DISMISS_ANIMATION_DURATION
742
+ dimView?.animateAlpha(show, duration, dimmedDetentIndex, currentDetentIndex)
743
+ parentDimView?.animateAlpha(show, duration, dimmedDetentIndex, currentDetentIndex)
744
+ }
745
+
746
+ fun updateDimAmount() {
747
+ if (!dimmed) return
748
+ val sheetTop = bottomSheetView?.top ?: return
749
+ dimView?.interpolateAlpha(sheetTop, dimmedDetentIndex, ::getSheetTopForDetentIndex)
750
+ parentDimView?.interpolateAlpha(sheetTop, dimmedDetentIndex, ::getSheetTopForDetentIndex)
751
+ }
752
+
649
753
  /** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
650
754
  fun positionFooter(slideOffset: Float? = null) {
651
755
  val footerView = containerView?.footerView ?: return
@@ -693,37 +797,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
693
797
  * Calculate the visible sheet height from a sheet view.
694
798
  * Uses real screen height for consistency across API levels.
695
799
  */
696
- private fun getVisibleSheetHeight(sheetView: View): Int {
697
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
698
- return realHeight - sheetView.top
699
- }
800
+ private fun getVisibleSheetHeight(sheetView: View): Int = realScreenHeight - sheetView.top
700
801
 
701
802
  private fun getPositionDp(visibleSheetHeight: Int): Float = (screenHeight - visibleSheetHeight).pxToDp()
702
803
 
703
- private fun emitChangePositionDelegate(sheetView: View, realtime: Boolean) {
704
- if (sheetView.top == lastEmittedPositionPx) return
705
-
706
- lastEmittedPositionPx = sheetView.top
707
- val position = getPositionDp(getVisibleSheetHeight(sheetView))
708
- val interpolatedIndex = getInterpolatedIndexForPosition(sheetView.top)
709
- val detent = getInterpolatedDetentForPosition(sheetView.top)
710
- delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
711
- }
712
-
713
- private fun emitDismissedPosition() {
714
- val position = screenHeight.pxToDp()
715
- lastEmittedPositionPx = -1
716
- delegate?.viewControllerDidChangePosition(-1f, position, 0f, false)
717
- }
718
-
719
804
  /**
720
805
  * Get the expected sheetTop position for a detent index.
721
806
  */
722
807
  private fun getSheetTopForDetentIndex(index: Int): Int {
723
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
724
- if (index < 0 || index >= detents.size) return realHeight
725
- val detentHeight = getDetentHeight(detents[index])
726
- return realHeight - detentHeight
808
+ if (index < 0 || index >= detents.size) return realScreenHeight
809
+ return realScreenHeight - getDetentHeight(detents[index])
727
810
  }
728
811
 
729
812
  /** Returns (fromIndex, toIndex, progress) for interpolation, or null if < 2 detents. */
@@ -731,12 +814,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
731
814
  val count = detents.size
732
815
  if (count == 0) return null
733
816
 
734
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
735
817
  val firstPos = getSheetTopForDetentIndex(0)
736
818
 
737
819
  // Above first detent - interpolating toward closed
738
820
  if (positionPx > firstPos) {
739
- val range = realHeight - firstPos
821
+ val range = realScreenHeight - firstPos
740
822
  val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
741
823
  return Triple(-1, 0, progress)
742
824
  }
@@ -847,47 +929,37 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
847
929
  return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
848
930
  }
849
931
 
850
- private fun getStateForDetentIndex(index: Int): Int =
851
- when (detents.size) {
852
- 1 -> BottomSheetBehavior.STATE_EXPANDED
853
-
854
- 2 -> when (index) {
855
- 0 -> BottomSheetBehavior.STATE_COLLAPSED
856
- 1 -> BottomSheetBehavior.STATE_EXPANDED
857
- else -> BottomSheetBehavior.STATE_HIDDEN
858
- }
859
-
860
- 3 -> when (index) {
861
- 0 -> BottomSheetBehavior.STATE_COLLAPSED
862
- 1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
863
- 2 -> BottomSheetBehavior.STATE_EXPANDED
864
- else -> BottomSheetBehavior.STATE_HIDDEN
865
- }
932
+ /** Maps detent index to BottomSheetBehavior state based on detent count. */
933
+ private fun getStateForDetentIndex(index: Int): Int {
934
+ val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
935
+ return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
936
+ }
866
937
 
867
- else -> BottomSheetBehavior.STATE_HIDDEN
868
- }
938
+ /** Maps BottomSheetBehavior state to DetentInfo based on detent count. */
939
+ fun getDetentInfoForState(state: Int): DetentInfo? {
940
+ val stateMap = getDetentStateMap() ?: return null
941
+ val index = stateMap[state] ?: return null
942
+ return DetentInfo(index, getPositionForDetentIndex(index))
943
+ }
869
944
 
870
- fun getDetentInfoForState(state: Int): DetentInfo? =
945
+ /** Returns state-to-index mapping based on detent count. */
946
+ private fun getDetentStateMap(): Map<Int, Int>? =
871
947
  when (detents.size) {
872
- 1 -> when (state) {
873
- BottomSheetBehavior.STATE_COLLAPSED,
874
- BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(0, getPositionForDetentIndex(0))
875
-
876
- else -> null
877
- }
948
+ 1 -> mapOf(
949
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
950
+ BottomSheetBehavior.STATE_EXPANDED to 0
951
+ )
878
952
 
879
- 2 -> when (state) {
880
- BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
881
- BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
882
- else -> null
883
- }
953
+ 2 -> mapOf(
954
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
955
+ BottomSheetBehavior.STATE_EXPANDED to 1
956
+ )
884
957
 
885
- 3 -> when (state) {
886
- BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
887
- BottomSheetBehavior.STATE_HALF_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
888
- BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(2, getPositionForDetentIndex(2))
889
- else -> null
890
- }
958
+ 3 -> mapOf(
959
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
960
+ BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
961
+ BottomSheetBehavior.STATE_EXPANDED to 2
962
+ )
891
963
 
892
964
  else -> null
893
965
  }
@@ -929,7 +1001,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
929
1001
  this.post {
930
1002
  setupSheetDetents()
931
1003
  positionFooter()
932
- bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
1004
+ bottomSheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
933
1005
  }
934
1006
  }
935
1007
 
@@ -27,8 +27,8 @@ class RNScreensFragmentObserver(
27
27
  val fragmentManager = activity.supportFragmentManager
28
28
 
29
29
  fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
30
- override fun onFragmentPreAttached(fm: FragmentManager, fragment: Fragment, context: android.content.Context) {
31
- super.onFragmentPreAttached(fm, fragment, context)
30
+ override fun onFragmentAttached(fm: FragmentManager, fragment: Fragment, context: android.content.Context) {
31
+ super.onFragmentAttached(fm, fragment, context)
32
32
 
33
33
  if (isModalFragment(fragment) && !activeModalFragments.contains(fragment)) {
34
34
  activeModalFragments.add(fragment)
@@ -20,17 +20,8 @@ object TrueSheetDialogObserver {
20
20
  val parentSheet = presentedSheetStack.lastOrNull()
21
21
  ?.takeIf { it.viewController.isPresented && it.viewController.isDialogVisible }
22
22
 
23
- // Hide any parent sheets that would be visible behind the new sheet
24
- val newSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
25
- for (sheet in presentedSheetStack) {
26
- if (!sheet.viewController.isDialogVisible) continue
27
- if (sheet.viewController.isExpanded) continue
28
-
29
- val sheetTop = sheet.viewController.currentSheetTop
30
- if (sheetTop < newSheetTop) {
31
- sheet.viewController.hideDialog(emitPosition = true)
32
- }
33
- }
23
+ val childSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
24
+ updateParentTranslations(childSheetTop)
34
25
 
35
26
  if (!presentedSheetStack.contains(sheetView)) {
36
27
  presentedSheetStack.add(sheetView)
@@ -42,14 +33,35 @@ object TrueSheetDialogObserver {
42
33
 
43
34
  /**
44
35
  * Called when a sheet has been dismissed.
45
- * Shows the parent sheet if this sheet was stacked on it.
36
+ * Resets parent sheet translation if this sheet was stacked on it.
46
37
  */
47
38
  @JvmStatic
48
39
  fun onSheetDidDismiss(sheetView: TrueSheetView, hadParent: Boolean) {
49
40
  synchronized(presentedSheetStack) {
50
41
  presentedSheetStack.remove(sheetView)
51
42
  if (hadParent) {
52
- presentedSheetStack.lastOrNull()?.viewController?.showDialog(emitPosition = true)
43
+ presentedSheetStack.lastOrNull()?.viewController?.translateDialog(0)
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Called when a presented sheet's size changes (e.g., after setupSheetDetents).
50
+ * Updates parent sheet translations to match the new sheet position.
51
+ */
52
+ @JvmStatic
53
+ fun onSheetSizeChanged(sheetView: TrueSheetView) {
54
+ synchronized(presentedSheetStack) {
55
+ val index = presentedSheetStack.indexOf(sheetView)
56
+ if (index <= 0) return
57
+
58
+ // Post to ensure layout is complete before reading position
59
+ sheetView.viewController.post {
60
+ val childMinSheetTop = sheetView.viewController.getExpectedSheetTop(0)
61
+ val childCurrentSheetTop = sheetView.viewController.getExpectedSheetTop(sheetView.viewController.currentDetentIndex)
62
+ // Cap to minimum detent position
63
+ val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
64
+ updateParentTranslations(childSheetTop, untilIndex = index)
53
65
  }
54
66
  }
55
67
  }
@@ -80,4 +92,25 @@ object TrueSheetDialogObserver {
80
92
  presentedSheetStack.clear()
81
93
  }
82
94
  }
95
+
96
+ /**
97
+ * Translates parent sheets down to match the child sheet's position.
98
+ * @param childSheetTop The top position of the child sheet
99
+ * @param untilIndex If specified, only update sheets up to this index (exclusive)
100
+ */
101
+ private fun updateParentTranslations(childSheetTop: Int, untilIndex: Int = presentedSheetStack.size) {
102
+ for (i in 0 until untilIndex) {
103
+ val parentSheet = presentedSheetStack[i]
104
+ if (!parentSheet.viewController.isDialogVisible) continue
105
+ if (parentSheet.viewController.isExpanded) continue
106
+
107
+ val parentSheetTop = parentSheet.viewController.getExpectedSheetTop(parentSheet.viewController.currentDetentIndex)
108
+ if (parentSheetTop < childSheetTop) {
109
+ val translationY = childSheetTop - parentSheetTop
110
+ parentSheet.viewController.translateDialog(translationY)
111
+ } else {
112
+ parentSheet.viewController.translateDialog(0)
113
+ }
114
+ }
115
+ }
83
116
  }
@@ -0,0 +1,72 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.graphics.Color
5
+ import android.graphics.Outline
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import android.view.ViewOutlineProvider
9
+ import com.facebook.react.uimanager.ThemedReactContext
10
+ import com.lodev09.truesheet.utils.ScreenUtils
11
+
12
+ @SuppressLint("ViewConstructor")
13
+ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reactContext) {
14
+
15
+ companion object {
16
+ private const val MAX_ALPHA = 0.4f
17
+ }
18
+
19
+ private var targetView: ViewGroup? = null
20
+
21
+ init {
22
+ layoutParams = ViewGroup.LayoutParams(
23
+ ViewGroup.LayoutParams.MATCH_PARENT,
24
+ ViewGroup.LayoutParams.MATCH_PARENT
25
+ )
26
+ setBackgroundColor(Color.BLACK)
27
+ alpha = 0f
28
+ }
29
+
30
+ fun attach(view: ViewGroup? = null, cornerRadius: Float = 0f) {
31
+ if (parent != null) return
32
+ targetView = view ?: reactContext.currentActivity?.window?.decorView as? ViewGroup
33
+
34
+ if (cornerRadius > 0f) {
35
+ outlineProvider = object : ViewOutlineProvider() {
36
+ override fun getOutline(v: View, outline: Outline) {
37
+ outline.setRoundRect(0, 0, v.width, v.height, cornerRadius)
38
+ }
39
+ }
40
+ clipToOutline = true
41
+ }
42
+
43
+ targetView?.addView(this)
44
+ }
45
+
46
+ fun detach() {
47
+ targetView?.removeView(this)
48
+ targetView = null
49
+ }
50
+
51
+ fun animateAlpha(show: Boolean, duration: Long, dimmedDetentIndex: Int, currentDetentIndex: Int) {
52
+ val targetAlpha = if (show && currentDetentIndex >= dimmedDetentIndex) MAX_ALPHA else 0f
53
+ animate().alpha(targetAlpha).setDuration(duration).start()
54
+ }
55
+
56
+ fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
57
+ val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
58
+ val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
59
+ val belowDimmedTop = if (dimmedDetentIndex > 0) getSheetTopForDetentIndex(dimmedDetentIndex - 1) else realHeight
60
+
61
+ alpha = when {
62
+ sheetTop <= dimmedDetentTop -> MAX_ALPHA
63
+
64
+ sheetTop >= belowDimmedTop -> 0f
65
+
66
+ else -> {
67
+ val progress = 1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)
68
+ (progress * MAX_ALPHA).coerceIn(0f, MAX_ALPHA)
69
+ }
70
+ }
71
+ }
72
+ }
@@ -6,9 +6,8 @@
6
6
  <item name="android:windowExitAnimation">@anim/true_sheet_slide_out</item>
7
7
  </style>
8
8
 
9
- <!-- Fast fade animation - used for hide/show when modal is presented -->
10
- <style name="TrueSheetFadeAnimation" parent="Animation.AppCompat.Dialog">
11
- <item name="android:windowEnterAnimation">@anim/true_sheet_fade_in</item>
9
+ <!-- Fade out only - used when hiding sheet for RN Screens modal -->
10
+ <style name="TrueSheetFadeOutAnimation" parent="Animation.AppCompat.Dialog">
12
11
  <item name="android:windowExitAnimation">@anim/true_sheet_fade_out</item>
13
12
  </style>
14
13
 
@@ -403,10 +403,7 @@ using namespace facebook::react;
403
403
 
404
404
  dispatch_async(dispatch_get_main_queue(), ^{
405
405
  self->_isSheetUpdatePending = NO;
406
-
407
- [self->_controller.sheetPresentationController animateChanges:^{
408
- [self->_controller setupSheetDetentsForSizeChange];
409
- }];
406
+ [self->_controller setupSheetDetentsForSizeChange];
410
407
  });
411
408
  }
412
409
 
@@ -594,8 +594,10 @@
594
594
  #pragma mark - Sheet Configuration
595
595
 
596
596
  - (void)setupSheetDetentsForSizeChange {
597
- _pendingContentSizeChange = YES;
598
- [self setupSheetDetents];
597
+ [self.sheetPresentationController animateChanges:^{
598
+ _pendingContentSizeChange = YES;
599
+ [self setupSheetDetents];
600
+ }];
599
601
  }
600
602
 
601
603
  - (void)setupSheetDetentsForDetentsChange {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.3.5",
3
+ "version": "3.4.0-beta.1",
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",
@@ -53,7 +53,7 @@
53
53
  "clean": "scripts/clean.sh",
54
54
  "prepare": "bob build",
55
55
  "release": "release-it",
56
- "release:beta": "yarn release -i prerelease"
56
+ "release:beta": "yarn release --preRelease=beta"
57
57
  },
58
58
  "keywords": [
59
59
  "react-native",
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <alpha xmlns:android="http://schemas.android.com/apk/res/android"
3
- android:duration="100"
4
- android:fromAlpha="0.0"
5
- android:toAlpha="1.0"
6
- android:interpolator="@android:interpolator/decelerate_cubic" />