@lodev09/react-native-true-sheet 3.3.5 → 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) {
@@ -584,10 +603,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
584
603
  }
585
604
 
586
605
  bottomSheet.addView(grabberView)
587
- grabberView.bringToFront()
588
606
  }
589
607
 
590
608
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
609
+ private var positionAnimator: ValueAnimator? = null
591
610
 
592
611
  fun setupKeyboardObserver() {
593
612
  val bottomSheet = bottomSheetView ?: return
@@ -606,11 +625,51 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
606
625
  keyboardObserver = null
607
626
  }
608
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
+
609
660
  fun setupBackground() {
610
661
  val bottomSheet = bottomSheetView ?: return
611
662
 
612
- val cornerRadius = if (sheetCornerRadius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else sheetCornerRadius
613
- 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
+ )
614
673
  val backgroundColor = sheetBackgroundColor ?: getDefaultBackgroundColor()
615
674
 
616
675
  bottomSheet.background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
@@ -619,24 +678,46 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
619
678
  bottomSheet.clipToOutline = true
620
679
  }
621
680
 
622
- /** Configures dim and touch-through behavior based on detent index. */
623
681
  fun setupDimmedBackground(detentIndex: Int) {
624
682
  val dialog = this.dialog ?: return
683
+
625
684
  dialog.window?.apply {
626
- 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
627
692
 
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
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)
632
711
  dialog.setCanceledOnTouchOutside(dismissible)
633
712
  } else {
634
- view.setOnTouchListener { v, event ->
713
+ touchOutside.setOnTouchListener { v, event ->
635
714
  event.setLocation(event.rawX - v.x, event.rawY - v.y)
636
- reactContext.currentActivity?.dispatchTouchEvent(event)
715
+ (
716
+ parentSheetView?.viewController?.dialog?.window?.decorView
717
+ ?: reactContext.currentActivity?.window?.decorView
718
+ )?.dispatchTouchEvent(event)
637
719
  false
638
720
  }
639
- clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
640
721
  dialog.setCanceledOnTouchOutside(false)
641
722
  }
642
723
  }
@@ -646,6 +727,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
646
727
  dialog?.window?.setWindowAnimations(windowAnimation)
647
728
  }
648
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
+
649
744
  /** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
650
745
  fun positionFooter(slideOffset: Float? = null) {
651
746
  val footerView = containerView?.footerView ?: return
@@ -693,37 +788,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
693
788
  * Calculate the visible sheet height from a sheet view.
694
789
  * Uses real screen height for consistency across API levels.
695
790
  */
696
- private fun getVisibleSheetHeight(sheetView: View): Int {
697
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
698
- return realHeight - sheetView.top
699
- }
791
+ private fun getVisibleSheetHeight(sheetView: View): Int = realScreenHeight - sheetView.top
700
792
 
701
793
  private fun getPositionDp(visibleSheetHeight: Int): Float = (screenHeight - visibleSheetHeight).pxToDp()
702
794
 
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
795
  /**
720
796
  * Get the expected sheetTop position for a detent index.
721
797
  */
722
798
  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
799
+ if (index < 0 || index >= detents.size) return realScreenHeight
800
+ return realScreenHeight - getDetentHeight(detents[index])
727
801
  }
728
802
 
729
803
  /** Returns (fromIndex, toIndex, progress) for interpolation, or null if < 2 detents. */
@@ -731,12 +805,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
731
805
  val count = detents.size
732
806
  if (count == 0) return null
733
807
 
734
- val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
735
808
  val firstPos = getSheetTopForDetentIndex(0)
736
809
 
737
810
  // Above first detent - interpolating toward closed
738
811
  if (positionPx > firstPos) {
739
- val range = realHeight - firstPos
812
+ val range = realScreenHeight - firstPos
740
813
  val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
741
814
  return Triple(-1, 0, progress)
742
815
  }
@@ -847,47 +920,37 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
847
920
  return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
848
921
  }
849
922
 
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
- }
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
+ }
866
928
 
867
- else -> BottomSheetBehavior.STATE_HIDDEN
868
- }
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
+ }
869
935
 
870
- fun getDetentInfoForState(state: Int): DetentInfo? =
936
+ /** Returns state-to-index mapping based on detent count. */
937
+ private fun getDetentStateMap(): Map<Int, Int>? =
871
938
  when (detents.size) {
872
- 1 -> when (state) {
873
- BottomSheetBehavior.STATE_COLLAPSED,
874
- BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(0, getPositionForDetentIndex(0))
875
-
876
- else -> null
877
- }
939
+ 1 -> mapOf(
940
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
941
+ BottomSheetBehavior.STATE_EXPANDED to 0
942
+ )
878
943
 
879
- 2 -> when (state) {
880
- BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
881
- BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
882
- else -> null
883
- }
944
+ 2 -> mapOf(
945
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
946
+ BottomSheetBehavior.STATE_EXPANDED to 1
947
+ )
884
948
 
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
- }
949
+ 3 -> mapOf(
950
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
951
+ BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
952
+ BottomSheetBehavior.STATE_EXPANDED to 2
953
+ )
891
954
 
892
955
  else -> null
893
956
  }
@@ -929,7 +992,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
929
992
  this.post {
930
993
  setupSheetDetents()
931
994
  positionFooter()
932
- bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
995
+ bottomSheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
933
996
  }
934
997
  }
935
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
+ }
@@ -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.5",
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" />