@lodev09/react-native-true-sheet 3.3.4 → 3.4.0-beta.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.
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
 
@@ -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,7 +74,7 @@ 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
77
79
  }
78
80
 
@@ -87,6 +89,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
87
89
  // ====================================================================
88
90
 
89
91
  private var dialog: BottomSheetDialog? = null
92
+ private var dimView: TrueSheetDimView? = null
93
+ private var parentDimView: TrueSheetDimView? = null
90
94
 
91
95
  private val behavior: BottomSheetBehavior<FrameLayout>?
92
96
  get() = dialog?.behavior
@@ -144,6 +148,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
144
148
  get() = ScreenUtils.getScreenHeight(reactContext)
145
149
  val screenWidth: Int
146
150
  get() = ScreenUtils.getScreenWidth(reactContext)
151
+ val realScreenHeight: Int
152
+ get() = ScreenUtils.getRealScreenHeight(reactContext)
147
153
 
148
154
  var maxSheetHeight: Int? = null
149
155
  var detents = mutableListOf(0.5, 1.0)
@@ -152,7 +158,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
152
158
  var dimmedDetentIndex = 0
153
159
  var grabber: Boolean = true
154
160
  var grabberOptions: GrabberOptions? = null
155
- var sheetCornerRadius: Float = -1f
161
+ var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
162
+ set(value) {
163
+ field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
164
+ setupBackground()
165
+ }
156
166
  var sheetBackgroundColor: Int? = null
157
167
  var edgeToEdgeFullScreen: Boolean = false
158
168
 
@@ -261,6 +271,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
261
271
 
262
272
  cleanupKeyboardObserver()
263
273
  cleanupModalObserver()
274
+ dimView?.detach()
275
+ dimView = null
276
+ parentDimView?.detach()
277
+ parentDimView = null
264
278
  sheetContainer?.removeView(this)
265
279
 
266
280
  dialog = null
@@ -281,16 +295,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
281
295
  setupGrabber()
282
296
  setupKeyboardObserver()
283
297
 
298
+ val toTop = getExpectedSheetTop(currentDetentIndex)
299
+ setupTransitionTracker(realScreenHeight, toTop, PRESENT_ANIMATION_DURATION)
300
+ animateDimAlpha(show = true)
301
+
284
302
  sheetContainer?.post {
285
- bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
286
303
  positionFooter()
287
304
  }
288
305
 
289
306
  sheetContainer?.postDelayed({
290
- val detentInfo = getDetentInfoForIndex(currentDetentIndex)
291
- val detent = getDetentValueForIndex(detentInfo.index)
307
+ val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
292
308
 
293
- delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position, detent)
309
+ delegate?.viewControllerDidPresent(index, position, detent)
294
310
  parentSheetView?.viewControllerDidBlur()
295
311
  delegate?.viewControllerDidFocus()
296
312
 
@@ -303,8 +319,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
303
319
  if (isDismissing) return@setOnCancelListener
304
320
 
305
321
  isDismissing = true
322
+ val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
323
+ setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
324
+ animateDimAlpha(show = false)
306
325
  emitWillDismissEvents()
307
- emitDismissedPosition()
308
326
  }
309
327
 
310
328
  dialog.setOnDismissListener {
@@ -321,7 +339,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
321
339
  override fun onSlide(sheetView: View, slideOffset: Float) {
322
340
  val behavior = behavior ?: return
323
341
 
324
- emitChangePositionDelegate(sheetView, realtime = true)
342
+ emitChangePositionDelegate(sheetView.top)
325
343
 
326
344
  when (behavior.state) {
327
345
  BottomSheetBehavior.STATE_DRAGGING,
@@ -331,6 +349,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
331
349
  }
332
350
 
333
351
  positionFooter(slideOffset)
352
+ updateDimAmount()
334
353
  }
335
354
 
336
355
  override fun onStateChanged(sheetView: View, newState: Int) {
@@ -386,14 +405,21 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
386
405
  reactContext = reactContext,
387
406
  onModalPresented = {
388
407
  if (isPresented && isDialogVisible) {
389
- hideDialog(animated = true)
408
+ isDialogVisible = false
409
+ dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFadeOutAnimation)
410
+ dialog?.window?.decorView?.visibility = INVISIBLE
390
411
  wasHiddenByModal = true
391
412
  }
392
413
  },
393
414
  onModalDismissed = {
394
415
  // Only show if we were the one hidden by modal, not by sheet stacking
395
416
  if (isPresented && wasHiddenByModal) {
396
- showDialog(animated = true)
417
+ isDialogVisible = true
418
+ dialog?.window?.decorView?.visibility = VISIBLE
419
+ // Restore animation after visibility change to avoid slide animation
420
+ sheetContainer?.post {
421
+ dialog?.window?.setWindowAnimations(windowAnimation)
422
+ }
397
423
  wasHiddenByModal = false
398
424
  }
399
425
  }
@@ -424,6 +450,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
424
450
  dismissPromise = null
425
451
  }
426
452
 
453
+ /** Helper to get detent info with its screen fraction value. */
454
+ private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
455
+ val detentInfo = getDetentInfoForIndex(index)
456
+ val detent = getDetentValueForIndex(detentInfo.index)
457
+ return Triple(detentInfo.index, detentInfo.position, detent)
458
+ }
459
+
427
460
  // ====================================================================
428
461
  // MARK: - Dialog Visibility (for stacking)
429
462
  // ====================================================================
@@ -439,35 +472,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
439
472
 
440
473
  fun getExpectedSheetTop(detentIndex: Int): Int {
441
474
  if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
442
- val detentHeight = getDetentHeight(detents[detentIndex])
443
- return screenHeight - detentHeight
444
- }
445
-
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
- }
475
+ return realScreenHeight - getDetentHeight(detents[detentIndex])
456
476
  }
457
477
 
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
- }
478
+ /** Translates the sheet when stacking. Pass 0 to reset. */
479
+ fun translateDialog(translationY: Int) {
480
+ val bottomSheet = bottomSheetView ?: return
481
+ val duration = if (translationY > 0) PRESENT_ANIMATION_DURATION else DISMISS_ANIMATION_DURATION
482
+
483
+ bottomSheet.animate()
484
+ .translationY(translationY.toFloat())
485
+ .setDuration(duration)
486
+ .setUpdateListener {
487
+ val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
488
+ emitChangePositionDelegate(effectiveTop)
489
+ }
490
+ .start()
471
491
  }
472
492
 
473
493
  // ====================================================================
@@ -490,11 +510,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
490
510
  setupSheetDetents()
491
511
  setStateForDetentIndex(detentIndex)
492
512
 
493
- val detentInfo = getDetentInfoForIndex(detentIndex)
494
- val detent = getDetentValueForIndex(detentInfo.index)
513
+ val (index, position, detent) = getDetentInfoWithValue(detentIndex)
495
514
 
496
515
  parentSheetView?.viewControllerWillBlur()
497
- delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position, detent)
516
+ delegate?.viewControllerWillPresent(index, position, detent)
498
517
  delegate?.viewControllerWillFocus()
499
518
 
500
519
  if (!animated) {
@@ -509,8 +528,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
509
528
  if (isDismissing) return
510
529
 
511
530
  isDismissing = true
531
+ val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
532
+ setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
512
533
  emitWillDismissEvents()
513
- emitDismissedPosition()
514
534
 
515
535
  if (!animated) {
516
536
  dialog?.window?.setWindowAnimations(0)
@@ -528,14 +548,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
528
548
  val behavior = this.behavior ?: return
529
549
 
530
550
  isReconfiguring = true
531
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
532
551
  val edgeToEdgeTopInset: Int = if (!edgeToEdgeFullScreen) topInset else 0
533
552
 
534
553
  behavior.apply {
535
554
  isFitToContents = false
536
555
  maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
537
556
 
538
- val maxAvailableHeight = realHeight - edgeToEdgeTopInset
557
+ val maxAvailableHeight = realScreenHeight - edgeToEdgeTopInset
539
558
 
540
559
  setPeekHeight(getDetentHeight(detents[0]), isPresented)
541
560
 
@@ -547,13 +566,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
547
566
  val maxDetentHeight = getDetentHeight(detents.last())
548
567
 
549
568
  val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
550
- halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() / realHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
569
+ halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
551
570
 
552
- expandedOffset = maxOf(edgeToEdgeTopInset, realHeight - maxDetentHeight)
571
+ expandedOffset = maxOf(edgeToEdgeTopInset, realScreenHeight - maxDetentHeight)
553
572
  isFitToContents = detents.size < 3 && expandedOffset == 0
554
573
 
555
574
  val offset = if (expandedOffset == 0) topInset else 0
556
- val newHeight = realHeight - expandedOffset - offset
575
+ val newHeight = realScreenHeight - expandedOffset - offset
557
576
  val newWidth = minOf(screenWidth, maxWidth)
558
577
 
559
578
  if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
@@ -587,6 +606,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
587
606
  }
588
607
 
589
608
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
609
+ private var positionAnimator: ValueAnimator? = null
590
610
 
591
611
  fun setupKeyboardObserver() {
592
612
  val bottomSheet = bottomSheetView ?: return
@@ -605,11 +625,51 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
605
625
  keyboardObserver = null
606
626
  }
607
627
 
628
+ private fun setupTransitionTracker(fromTop: Int, toTop: Int, duration: Long) {
629
+ positionAnimator?.cancel()
630
+ positionAnimator = ValueAnimator.ofInt(fromTop, toTop).apply {
631
+ this.duration = duration
632
+ interpolator = if (fromTop > toTop) {
633
+ android.view.animation.DecelerateInterpolator(2f) // present
634
+ } else {
635
+ android.view.animation.AccelerateInterpolator(2f) // dismiss
636
+ }
637
+ addUpdateListener { animator ->
638
+ emitChangePositionDelegate(animator.animatedValue as Int)
639
+ }
640
+ addListener(object : android.animation.AnimatorListenerAdapter() {
641
+ override fun onAnimationEnd(animation: android.animation.Animator) {
642
+ positionAnimator = null
643
+ }
644
+ })
645
+ start()
646
+ }
647
+ }
648
+
649
+ private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
650
+ if (currentTop == lastEmittedPositionPx) return
651
+
652
+ lastEmittedPositionPx = currentTop
653
+ val visibleHeight = realScreenHeight - currentTop
654
+ val position = getPositionDp(visibleHeight)
655
+ val interpolatedIndex = getInterpolatedIndexForPosition(currentTop)
656
+ val detent = getInterpolatedDetentForPosition(currentTop)
657
+ delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
658
+ }
659
+
608
660
  fun setupBackground() {
609
661
  val bottomSheet = bottomSheetView ?: return
610
662
 
611
- val cornerRadius = if (sheetCornerRadius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else sheetCornerRadius
612
- val outerRadii = floatArrayOf(cornerRadius, cornerRadius, cornerRadius, cornerRadius, 0f, 0f, 0f, 0f)
663
+ val outerRadii = floatArrayOf(
664
+ sheetCornerRadius,
665
+ sheetCornerRadius,
666
+ sheetCornerRadius,
667
+ sheetCornerRadius,
668
+ 0f,
669
+ 0f,
670
+ 0f,
671
+ 0f
672
+ )
613
673
  val backgroundColor = sheetBackgroundColor ?: getDefaultBackgroundColor()
614
674
 
615
675
  bottomSheet.background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
@@ -618,24 +678,46 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
618
678
  bottomSheet.clipToOutline = true
619
679
  }
620
680
 
621
- /** Configures dim and touch-through behavior based on detent index. */
622
681
  fun setupDimmedBackground(detentIndex: Int) {
623
682
  val dialog = this.dialog ?: return
683
+
624
684
  dialog.window?.apply {
625
- val view = findViewById<View>(com.google.android.material.R.id.touch_outside)
685
+ val touchOutside = findViewById<View>(com.google.android.material.R.id.touch_outside)
686
+ clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
687
+
688
+ val shouldDimAtDetent = dimmed && detentIndex >= dimmedDetentIndex
689
+
690
+ if (dimmed) {
691
+ val parentDimVisible = (parentSheetView?.viewController?.dimView?.alpha ?: 0f) > 0f
626
692
 
627
- if (dimmed && detentIndex >= dimmedDetentIndex) {
628
- view.setOnTouchListener(null)
629
- setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
630
- setDimAmount(0.32f) // M3 scrim opacity
693
+ if (dimView == null) dimView = TrueSheetDimView(reactContext)
694
+ if (!parentDimVisible) dimView?.attach(null)
695
+
696
+ val parentController = parentSheetView?.viewController
697
+ val parentBottomSheet = parentController?.bottomSheetView
698
+ if (parentBottomSheet != null) {
699
+ if (parentDimView == null) parentDimView = TrueSheetDimView(reactContext)
700
+ parentDimView?.attach(parentBottomSheet, parentController.sheetCornerRadius)
701
+ }
702
+ } else {
703
+ dimView?.detach()
704
+ dimView = null
705
+ parentDimView?.detach()
706
+ parentDimView = null
707
+ }
708
+
709
+ if (shouldDimAtDetent) {
710
+ touchOutside.setOnTouchListener(null)
631
711
  dialog.setCanceledOnTouchOutside(dismissible)
632
712
  } else {
633
- view.setOnTouchListener { v, event ->
713
+ touchOutside.setOnTouchListener { v, event ->
634
714
  event.setLocation(event.rawX - v.x, event.rawY - v.y)
635
- reactContext.currentActivity?.dispatchTouchEvent(event)
715
+ (
716
+ parentSheetView?.viewController?.dialog?.window?.decorView
717
+ ?: reactContext.currentActivity?.window?.decorView
718
+ )?.dispatchTouchEvent(event)
636
719
  false
637
720
  }
638
- clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
639
721
  dialog.setCanceledOnTouchOutside(false)
640
722
  }
641
723
  }
@@ -645,6 +727,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
645
727
  dialog?.window?.setWindowAnimations(windowAnimation)
646
728
  }
647
729
 
730
+ private fun animateDimAlpha(show: Boolean) {
731
+ if (!dimmed) return
732
+ val duration = if (show) PRESENT_ANIMATION_DURATION else DISMISS_ANIMATION_DURATION
733
+ dimView?.animateAlpha(show, duration, dimmedDetentIndex, currentDetentIndex)
734
+ parentDimView?.animateAlpha(show, duration, dimmedDetentIndex, currentDetentIndex)
735
+ }
736
+
737
+ fun updateDimAmount() {
738
+ if (!dimmed) return
739
+ val sheetTop = bottomSheetView?.top ?: return
740
+ dimView?.interpolateAlpha(sheetTop, dimmedDetentIndex, ::getSheetTopForDetentIndex)
741
+ parentDimView?.interpolateAlpha(sheetTop, dimmedDetentIndex, ::getSheetTopForDetentIndex)
742
+ }
743
+
648
744
  /** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
649
745
  fun positionFooter(slideOffset: Float? = null) {
650
746
  val footerView = containerView?.footerView ?: return
@@ -692,37 +788,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
692
788
  * Calculate the visible sheet height from a sheet view.
693
789
  * Uses real screen height for consistency across API levels.
694
790
  */
695
- private fun getVisibleSheetHeight(sheetView: View): Int {
696
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
697
- return realHeight - sheetView.top
698
- }
791
+ private fun getVisibleSheetHeight(sheetView: View): Int = realScreenHeight - sheetView.top
699
792
 
700
793
  private fun getPositionDp(visibleSheetHeight: Int): Float = (screenHeight - visibleSheetHeight).pxToDp()
701
794
 
702
- private fun emitChangePositionDelegate(sheetView: View, realtime: Boolean) {
703
- if (sheetView.top == lastEmittedPositionPx) return
704
-
705
- lastEmittedPositionPx = sheetView.top
706
- val position = getPositionDp(getVisibleSheetHeight(sheetView))
707
- val interpolatedIndex = getInterpolatedIndexForPosition(sheetView.top)
708
- val detent = getInterpolatedDetentForPosition(sheetView.top)
709
- delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
710
- }
711
-
712
- private fun emitDismissedPosition() {
713
- val position = screenHeight.pxToDp()
714
- lastEmittedPositionPx = -1
715
- delegate?.viewControllerDidChangePosition(-1f, position, 0f, false)
716
- }
717
-
718
795
  /**
719
796
  * Get the expected sheetTop position for a detent index.
720
797
  */
721
798
  private fun getSheetTopForDetentIndex(index: Int): Int {
722
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
723
- if (index < 0 || index >= detents.size) return realHeight
724
- val detentHeight = getDetentHeight(detents[index])
725
- return realHeight - detentHeight
799
+ if (index < 0 || index >= detents.size) return realScreenHeight
800
+ return realScreenHeight - getDetentHeight(detents[index])
726
801
  }
727
802
 
728
803
  /** Returns (fromIndex, toIndex, progress) for interpolation, or null if < 2 detents. */
@@ -730,12 +805,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
730
805
  val count = detents.size
731
806
  if (count == 0) return null
732
807
 
733
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
734
808
  val firstPos = getSheetTopForDetentIndex(0)
735
809
 
736
810
  // Above first detent - interpolating toward closed
737
811
  if (positionPx > firstPos) {
738
- val range = realHeight - firstPos
812
+ val range = realScreenHeight - firstPos
739
813
  val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
740
814
  return Triple(-1, 0, progress)
741
815
  }
@@ -846,47 +920,37 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
846
920
  return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
847
921
  }
848
922
 
849
- private fun getStateForDetentIndex(index: Int): Int =
850
- when (detents.size) {
851
- 1 -> BottomSheetBehavior.STATE_EXPANDED
852
-
853
- 2 -> when (index) {
854
- 0 -> BottomSheetBehavior.STATE_COLLAPSED
855
- 1 -> BottomSheetBehavior.STATE_EXPANDED
856
- else -> BottomSheetBehavior.STATE_HIDDEN
857
- }
858
-
859
- 3 -> when (index) {
860
- 0 -> BottomSheetBehavior.STATE_COLLAPSED
861
- 1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
862
- 2 -> BottomSheetBehavior.STATE_EXPANDED
863
- else -> BottomSheetBehavior.STATE_HIDDEN
864
- }
923
+ /** Maps detent index to BottomSheetBehavior state based on detent count. */
924
+ private fun getStateForDetentIndex(index: Int): Int {
925
+ val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
926
+ return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
927
+ }
865
928
 
866
- else -> BottomSheetBehavior.STATE_HIDDEN
867
- }
929
+ /** Maps BottomSheetBehavior state to DetentInfo based on detent count. */
930
+ fun getDetentInfoForState(state: Int): DetentInfo? {
931
+ val stateMap = getDetentStateMap() ?: return null
932
+ val index = stateMap[state] ?: return null
933
+ return DetentInfo(index, getPositionForDetentIndex(index))
934
+ }
868
935
 
869
- fun getDetentInfoForState(state: Int): DetentInfo? =
936
+ /** Returns state-to-index mapping based on detent count. */
937
+ private fun getDetentStateMap(): Map<Int, Int>? =
870
938
  when (detents.size) {
871
- 1 -> when (state) {
872
- BottomSheetBehavior.STATE_COLLAPSED,
873
- BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(0, getPositionForDetentIndex(0))
874
-
875
- else -> null
876
- }
939
+ 1 -> mapOf(
940
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
941
+ BottomSheetBehavior.STATE_EXPANDED to 0
942
+ )
877
943
 
878
- 2 -> when (state) {
879
- BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
880
- BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
881
- else -> null
882
- }
944
+ 2 -> mapOf(
945
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
946
+ BottomSheetBehavior.STATE_EXPANDED to 1
947
+ )
883
948
 
884
- 3 -> when (state) {
885
- BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
886
- BottomSheetBehavior.STATE_HALF_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
887
- BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(2, getPositionForDetentIndex(2))
888
- else -> null
889
- }
949
+ 3 -> mapOf(
950
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
951
+ BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
952
+ BottomSheetBehavior.STATE_EXPANDED to 2
953
+ )
890
954
 
891
955
  else -> null
892
956
  }
@@ -928,7 +992,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
928
992
  this.post {
929
993
  setupSheetDetents()
930
994
  positionFooter()
931
- bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
995
+ bottomSheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
932
996
  }
933
997
  }
934
998
 
@@ -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,7 +20,7 @@ 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
23
+ // Translate parent sheets down to match the new sheet's position
24
24
  val newSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
25
25
  for (sheet in presentedSheetStack) {
26
26
  if (!sheet.viewController.isDialogVisible) continue
@@ -28,7 +28,8 @@ object TrueSheetDialogObserver {
28
28
 
29
29
  val sheetTop = sheet.viewController.currentSheetTop
30
30
  if (sheetTop < newSheetTop) {
31
- sheet.viewController.hideDialog(emitPosition = true)
31
+ val translationY = newSheetTop - sheetTop
32
+ sheet.viewController.translateDialog(translationY)
32
33
  }
33
34
  }
34
35
 
@@ -49,7 +50,7 @@ object TrueSheetDialogObserver {
49
50
  synchronized(presentedSheetStack) {
50
51
  presentedSheetStack.remove(sheetView)
51
52
  if (hadParent) {
52
- presentedSheetStack.lastOrNull()?.viewController?.showDialog(emitPosition = true)
53
+ presentedSheetStack.lastOrNull()?.viewController?.translateDialog(0)
53
54
  }
54
55
  }
55
56
  }
@@ -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
+ }
@@ -70,9 +70,6 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
70
70
  cornerRadius = grabberCornerRadius.dpToPx()
71
71
  setColor(grabberColor)
72
72
  }
73
-
74
- // High elevation to ensure grabber appears above content views
75
- elevation = 100f
76
73
  }
77
74
 
78
75
  private fun getAdaptiveColor(baseColor: Int? = null): Int {
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.3.4",
3
+ "version": "3.4.0-beta.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",
@@ -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" />