@lodev09/react-native-true-sheet 3.3.0-beta.2 → 3.3.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,7 +29,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
29
29
  import com.lodev09.truesheet.core.GrabberOptions
30
30
  import com.lodev09.truesheet.core.RNScreensFragmentObserver
31
31
  import com.lodev09.truesheet.core.TrueSheetGrabberView
32
- import com.lodev09.truesheet.core.TrueSheetKeyboardHandler
32
+ import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
33
+ import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
33
34
  import com.lodev09.truesheet.utils.ScreenUtils
34
35
 
35
36
  data class DetentInfo(val index: Int, val position: Float)
@@ -126,6 +127,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
126
127
  private var windowAnimation: Int = 0
127
128
  private var lastEmittedPositionPx: Int = -1
128
129
 
130
+ /** Tracks if this sheet was hidden due to a RN Screens modal (vs sheet stacking) */
131
+ private var wasHiddenByModal = false
132
+
129
133
  var presentPromise: (() -> Unit)? = null
130
134
  var dismissPromise: (() -> Unit)? = null
131
135
 
@@ -224,7 +228,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
224
228
 
225
229
  window?.apply {
226
230
  windowAnimation = attributes.windowAnimations
227
- // Disable default keyboard avoidance - sheet handles it via setupKeyboardHandler
231
+ // Disable default keyboard avoidance - sheet handles it via setupKeyboardObserver
228
232
  setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
229
233
  }
230
234
 
@@ -255,7 +259,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
255
259
  setOnDismissListener(null)
256
260
  }
257
261
 
258
- cleanupKeyboardHandler()
262
+ cleanupKeyboardObserver()
259
263
  cleanupModalObserver()
260
264
  sheetContainer?.removeView(this)
261
265
 
@@ -264,6 +268,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
264
268
  isDismissing = false
265
269
  isPresented = false
266
270
  isDialogVisible = false
271
+ wasHiddenByModal = false
267
272
  lastEmittedPositionPx = -1
268
273
  }
269
274
 
@@ -274,7 +279,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
274
279
  resetAnimation()
275
280
  setupBackground()
276
281
  setupGrabber()
277
- setupKeyboardHandler()
282
+ setupKeyboardObserver()
278
283
 
279
284
  sheetContainer?.post {
280
285
  bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
@@ -380,13 +385,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
380
385
  rnScreensObserver = RNScreensFragmentObserver(
381
386
  reactContext = reactContext,
382
387
  onModalPresented = {
383
- if (isPresented) {
384
- hideDialog()
388
+ if (isPresented && isDialogVisible) {
389
+ hideDialog(animated = true)
390
+ wasHiddenByModal = true
385
391
  }
386
392
  },
387
393
  onModalDismissed = {
388
- if (isPresented) {
389
- showDialog()
394
+ // Only show if we were the one hidden by modal, not by sheet stacking
395
+ if (isPresented && wasHiddenByModal) {
396
+ showDialog(animated = true)
397
+ wasHiddenByModal = false
390
398
  }
391
399
  }
392
400
  )
@@ -436,8 +444,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
436
444
  }
437
445
 
438
446
  /** Hides without dismissing. Used for sheet stacking and RN Screens modals. */
439
- fun hideDialog(emitPosition: Boolean = false) {
447
+ fun hideDialog(emitPosition: Boolean = false, animated: Boolean = false) {
440
448
  isDialogVisible = false
449
+ if (animated) {
450
+ dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFadeAnimation)
451
+ }
441
452
  dialog?.window?.decorView?.visibility = INVISIBLE
442
453
  if (emitPosition) {
443
454
  emitDismissedPosition()
@@ -445,12 +456,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
445
456
  }
446
457
 
447
458
  /** Shows a previously hidden dialog. */
448
- fun showDialog(emitPosition: Boolean = false) {
459
+ fun showDialog(emitPosition: Boolean = false, animated: Boolean = false) {
449
460
  isDialogVisible = true
450
461
  dialog?.window?.decorView?.visibility = VISIBLE
451
462
  if (emitPosition) {
452
463
  bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
453
464
  }
465
+ if (animated) {
466
+ // Restore original animation after fade-in completes (100ms)
467
+ sheetContainer?.postDelayed({
468
+ dialog?.window?.setWindowAnimations(windowAnimation)
469
+ }, 100)
470
+ }
454
471
  }
455
472
 
456
473
  // ====================================================================
@@ -568,18 +585,23 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
568
585
  bottomSheet.addView(grabberView)
569
586
  }
570
587
 
571
- private var keyboardHandler: TrueSheetKeyboardHandler? = null
588
+ private var keyboardObserver: TrueSheetKeyboardObserver? = null
572
589
 
573
- /** Sets up keyboard handler for IME transitions. */
574
- fun setupKeyboardHandler() {
590
+ fun setupKeyboardObserver() {
575
591
  val bottomSheet = bottomSheetView ?: return
576
- keyboardHandler = TrueSheetKeyboardHandler(bottomSheet, reactContext) { topInset }
577
- keyboardHandler?.setup()
592
+ keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
593
+ delegate = object : TrueSheetKeyboardObserverDelegate {
594
+ override fun keyboardHeightDidChange(height: Int) {
595
+ setupSheetDetents()
596
+ }
597
+ }
598
+ start()
599
+ }
578
600
  }
579
601
 
580
- fun cleanupKeyboardHandler() {
581
- keyboardHandler?.cleanup()
582
- keyboardHandler = null
602
+ fun cleanupKeyboardObserver() {
603
+ keyboardObserver?.stop()
604
+ keyboardObserver = null
583
605
  }
584
606
 
585
607
  fun setupBackground() {
@@ -632,7 +654,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
632
654
  val sheetTop = bottomSheet.top
633
655
 
634
656
  // Footer Y relative to sheet: place at bottom of sheet container minus footer height
635
- var footerY = (sheetHeight - sheetTop - footerHeight).toFloat()
657
+ var footerY = (sheetHeight - sheetTop - footerHeight - keyboardHeight).toFloat()
636
658
 
637
659
  if (slideOffset != null && slideOffset < 0) {
638
660
  footerY -= (footerHeight * slideOffset)
@@ -806,16 +828,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
806
828
  // MARK: - Detent Calculations
807
829
  // ====================================================================
808
830
 
831
+ private val keyboardHeight: Int
832
+ get() = keyboardObserver?.currentHeight ?: 0
833
+
809
834
  private fun getDetentHeight(detent: Double): Int {
810
- val height: Int = if (detent == -1.0) {
811
- // Auto height: add bottomInset to content to match iOS behavior
812
- contentHeight + headerHeight + contentBottomInset
835
+ val height = if (detent == -1.0) {
836
+ contentHeight + headerHeight + contentBottomInset + keyboardHeight
813
837
  } else {
814
838
  if (detent <= 0.0 || detent > 1.0) {
815
839
  throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
816
840
  }
817
- // Fractional detent: add bottomInset to match iOS behavior
818
- (detent * screenHeight).toInt() + contentBottomInset
841
+ (detent * screenHeight).toInt() + contentBottomInset + keyboardHeight
819
842
  }
820
843
 
821
844
  val maxAllowedHeight = screenHeight + contentBottomInset
@@ -27,8 +27,8 @@ class RNScreensFragmentObserver(
27
27
  val fragmentManager = activity.supportFragmentManager
28
28
 
29
29
  fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
30
- override fun onFragmentStarted(fm: FragmentManager, fragment: Fragment) {
31
- super.onFragmentStarted(fm, fragment)
30
+ override fun onFragmentPreAttached(fm: FragmentManager, fragment: Fragment, context: android.content.Context) {
31
+ super.onFragmentPreAttached(fm, fragment, context)
32
32
 
33
33
  if (isModalFragment(fragment) && !activeModalFragments.contains(fragment)) {
34
34
  activeModalFragments.add(fragment)
@@ -20,12 +20,15 @@ object TrueSheetDialogObserver {
20
20
  val parentSheet = presentedSheetStack.lastOrNull()
21
21
  ?.takeIf { it.viewController.isPresented && it.viewController.isDialogVisible }
22
22
 
23
- // Hide parent if the new sheet would cover it
24
- parentSheet?.let {
25
- val parentTop = it.viewController.currentSheetTop
26
- val newSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
27
- if (!it.viewController.isExpanded && parentTop <= newSheetTop) {
28
- it.viewController.hideDialog(emitPosition = true)
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)
29
32
  }
30
33
  }
31
34
 
@@ -9,25 +9,25 @@ import androidx.core.view.WindowInsetsAnimationCompat
9
9
  import androidx.core.view.WindowInsetsCompat
10
10
  import com.facebook.react.uimanager.ThemedReactContext
11
11
 
12
+ interface TrueSheetKeyboardObserverDelegate {
13
+ fun keyboardHeightDidChange(height: Int)
14
+ }
15
+
12
16
  /**
13
- * Handles keyboard (IME) for sheet translation.
14
- * Uses WindowInsetsAnimationCompat for smooth animation on API 30+,
15
- * falls back to ViewTreeObserver on Activity's decor view for API 29 and below.
16
- *
17
- * @param targetView The view to translate (typically the bottom sheet)
18
- * @param reactContext The React context to get the current activity
19
- * @param topInset The top safe area inset to respect
17
+ * Tracks keyboard height and notifies delegate on changes.
18
+ * Uses WindowInsetsAnimationCompat on API 30+, ViewTreeObserver fallback on older versions.
20
19
  */
21
- class TrueSheetKeyboardHandler(
22
- private val targetView: View,
23
- private val reactContext: ThemedReactContext,
24
- private val topInset: () -> Int
25
- ) {
20
+ class TrueSheetKeyboardObserver(private val targetView: View, private val reactContext: ThemedReactContext) {
21
+
22
+ var delegate: TrueSheetKeyboardObserverDelegate? = null
23
+
24
+ var currentHeight: Int = 0
25
+ private set
26
26
 
27
27
  private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
28
28
  private var activityRootView: View? = null
29
29
 
30
- fun setup() {
30
+ fun start() {
31
31
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
32
32
  setupAnimationCallback()
33
33
  } else {
@@ -35,7 +35,7 @@ class TrueSheetKeyboardHandler(
35
35
  }
36
36
  }
37
37
 
38
- fun cleanup() {
38
+ fun stop() {
39
39
  globalLayoutListener?.let { listener ->
40
40
  activityRootView?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
41
41
  globalLayoutListener = null
@@ -44,35 +44,31 @@ class TrueSheetKeyboardHandler(
44
44
  ViewCompat.setWindowInsetsAnimationCallback(targetView, null)
45
45
  }
46
46
 
47
- private fun applyTranslation(imeHeight: Int) {
48
- // Cap translation so sheet doesn't move beyond screen bounds
49
- val maxTranslation = maxOf(0, targetView.top - topInset())
50
- val translation = minOf(imeHeight, maxTranslation)
51
- targetView.translationY = -translation.toFloat()
47
+ private fun updateHeight(height: Int) {
48
+ if (currentHeight != height) {
49
+ currentHeight = height
50
+ delegate?.keyboardHeightDidChange(height)
51
+ }
52
52
  }
53
53
 
54
- /** API 30+ smooth keyboard animation */
54
+ private fun getKeyboardHeight(insets: WindowInsetsCompat?): Int = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
55
+
55
56
  private fun setupAnimationCallback() {
56
57
  ViewCompat.setWindowInsetsAnimationCallback(
57
58
  targetView,
58
59
  object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
59
- private var startImeHeight = 0
60
- private var endImeHeight = 0
61
-
62
- private fun getKeyboardHeight(rootInsets: WindowInsetsCompat?): Int {
63
- if (rootInsets == null) return 0
64
- return rootInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
65
- }
60
+ private var startHeight = 0
61
+ private var endHeight = 0
66
62
 
67
63
  override fun onPrepare(animation: WindowInsetsAnimationCompat) {
68
- startImeHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
64
+ startHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
69
65
  }
70
66
 
71
67
  override fun onStart(
72
68
  animation: WindowInsetsAnimationCompat,
73
69
  bounds: WindowInsetsAnimationCompat.BoundsCompat
74
70
  ): WindowInsetsAnimationCompat.BoundsCompat {
75
- endImeHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
71
+ endHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
76
72
  return bounds
77
73
  }
78
74
 
@@ -82,23 +78,21 @@ class TrueSheetKeyboardHandler(
82
78
  } ?: return insets
83
79
 
84
80
  val fraction = imeAnimation.interpolatedFraction
85
- val currentImeHeight = (startImeHeight + (endImeHeight - startImeHeight) * fraction).toInt()
86
- applyTranslation(currentImeHeight)
81
+ val currentHeight = (startHeight + (endHeight - startHeight) * fraction).toInt()
82
+ updateHeight(currentHeight)
87
83
 
88
84
  return insets
89
85
  }
90
86
 
91
87
  override fun onEnd(animation: WindowInsetsAnimationCompat) {
92
- applyTranslation(getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView)))
88
+ updateHeight(getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView)))
93
89
  }
94
90
  }
95
91
  )
96
92
  }
97
93
 
98
- /** API 29 and below fallback using ViewTreeObserver on Activity's root view */
99
94
  private fun setupLegacyListener() {
100
95
  val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return
101
-
102
96
  activityRootView = rootView
103
97
 
104
98
  globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
@@ -108,11 +102,7 @@ class TrueSheetKeyboardHandler(
108
102
  val screenHeight = rootView.height
109
103
  val keyboardHeight = screenHeight - rect.bottom
110
104
 
111
- if (keyboardHeight > screenHeight * 0.15) {
112
- applyTranslation(keyboardHeight)
113
- } else {
114
- applyTranslation(0)
115
- }
105
+ updateHeight(if (keyboardHeight > screenHeight * 0.15) keyboardHeight else 0)
116
106
  }
117
107
 
118
108
  rootView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
@@ -0,0 +1,6 @@
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" />
@@ -0,0 +1,6 @@
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="1.0"
5
+ android:toAlpha="0.0"
6
+ android:interpolator="@android:interpolator/accelerate_cubic" />
@@ -6,6 +6,12 @@
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>
12
+ <item name="android:windowExitAnimation">@anim/true_sheet_fade_out</item>
13
+ </style>
14
+
9
15
  <!-- Default BottomSheetDialog style with smooth animations -->
10
16
  <style name="TrueSheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
11
17
  <item name="android:windowAnimationStyle">@style/TrueSheetAnimation</item>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.3.0-beta.2",
3
+ "version": "3.3.0-beta.4",
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",