@lodev09/react-native-true-sheet 3.6.11 → 3.7.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.
@@ -66,7 +66,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
66
66
  private var isSheetUpdatePending: Boolean = false
67
67
 
68
68
  // Root container for the coordinator layout (activity or Modal dialog content view)
69
- private var rootContainerView: ViewGroup? = null
69
+ internal var rootContainerView: ViewGroup? = null
70
70
 
71
71
  // ==================== Initialization ====================
72
72
 
@@ -151,7 +151,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
151
151
  // ==================== Lifecycle ====================
152
152
 
153
153
  override fun onHostResume() {
154
- viewController.reapplyHiddenState()
155
154
  finalizeUpdates()
156
155
  }
157
156
 
@@ -278,7 +277,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
278
277
  if (!viewController.isPresented) {
279
278
  // Attach coordinator to the root container
280
279
  rootContainerView = findRootContainerView()
281
- viewController.coordinatorLayout?.let { rootContainerView?.addView(it) }
280
+ viewController.coordinatorLayout?.let { coordinator ->
281
+ rootContainerView?.addView(coordinator)
282
+ coordinator.post { measureCoordinatorLayout() }
283
+ }
282
284
 
283
285
  // Register with observer to track sheet stack hierarchy
284
286
  viewController.parentSheetView = TrueSheetStackManager.onSheetWillPresent(this, detentIndex)
@@ -434,6 +436,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
434
436
 
435
437
  override fun viewControllerDidChangeSize(width: Int, height: Int) {
436
438
  updateState(width, height)
439
+ measureCoordinatorLayout()
437
440
  }
438
441
 
439
442
  override fun viewControllerWillFocus() {
@@ -478,6 +481,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
478
481
 
479
482
  // ==================== Private Helpers ====================
480
483
 
484
+ private fun measureCoordinatorLayout() {
485
+ val coordinator = viewController.coordinatorLayout ?: return
486
+ val width = viewController.screenWidth
487
+ val height = viewController.realScreenHeight
488
+
489
+ if (width > 0 && height > 0) {
490
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
491
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
492
+ coordinator.measure(widthSpec, heightSpec)
493
+ coordinator.layout(0, 0, width, height)
494
+ }
495
+ }
496
+
481
497
  /**
482
498
  * Find the root container view for presenting the sheet.
483
499
  * This traverses up the view hierarchy to find the content view (android.R.id.content)
@@ -486,14 +502,20 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
486
502
  */
487
503
  private fun findRootContainerView(): ViewGroup? {
488
504
  var current: android.view.ViewParent? = parent
505
+ var contentView: ViewGroup? = null
489
506
 
490
507
  while (current != null) {
491
- if (current is ViewGroup && current.id == android.R.id.content) {
492
- return current
508
+ if (current is ViewGroup) {
509
+ if (current.javaClass.name == "com.swmansion.rnscreens.Screen") {
510
+ return current
511
+ }
512
+ if (contentView == null && current.id == android.R.id.content) {
513
+ contentView = current
514
+ }
493
515
  }
494
516
  current = current.parent
495
517
  }
496
518
 
497
- return reactContext.currentActivity?.findViewById(android.R.id.content)
519
+ return contentView ?: reactContext.currentActivity?.findViewById(android.R.id.content)
498
520
  }
499
521
  }
@@ -23,7 +23,6 @@ import com.facebook.react.util.RNLog
23
23
  import com.facebook.react.views.view.ReactViewGroup
24
24
  import com.google.android.material.bottomsheet.BottomSheetBehavior
25
25
  import com.lodev09.truesheet.core.GrabberOptions
26
- import com.lodev09.truesheet.core.RNScreensFragmentObserver
27
26
  import com.lodev09.truesheet.core.TrueSheetBottomSheetView
28
27
  import com.lodev09.truesheet.core.TrueSheetBottomSheetViewDelegate
29
28
  import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
@@ -90,7 +89,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
90
89
  private const val DEFAULT_CORNER_RADIUS = 16 // dp
91
90
  private const val TRANSLATE_ANIMATION_DURATION = 200L
92
91
  private const val DISMISS_DURATION = 200L
93
- private const val MODAL_FADE_DURATION = 150L
94
92
  }
95
93
 
96
94
  // =============================================================================
@@ -130,7 +128,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
130
128
 
131
129
  private var interactionState: InteractionState = InteractionState.Idle
132
130
  private var isDismissing = false
133
- private var wasHiddenByModal = false
134
131
  private var shouldAnimatePresent = false
135
132
  private var isPresentAnimating = false
136
133
 
@@ -150,7 +147,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
150
147
 
151
148
  // Helper Objects
152
149
  private var keyboardObserver: TrueSheetKeyboardObserver? = null
153
- private var rnScreensObserver: RNScreensFragmentObserver? = null
154
150
  internal val detentCalculator = TrueSheetDetentCalculator(reactContext).apply {
155
151
  delegate = this@TrueSheetViewController
156
152
  }
@@ -307,7 +303,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
307
303
 
308
304
  private fun cleanupSheet() {
309
305
  cleanupKeyboardObserver()
310
- cleanupModalObserver()
311
306
  cleanupBackCallback()
312
307
  sheetView?.animate()?.cancel()
313
308
 
@@ -327,7 +322,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
327
322
  isDismissing = false
328
323
  isPresented = false
329
324
  isSheetVisible = false
330
- wasHiddenByModal = false
331
325
  isPresentAnimating = false
332
326
  lastEmittedPositionPx = -1
333
327
  detentIndexBeforeKeyboard = -1
@@ -367,6 +361,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
367
361
  }
368
362
  }
369
363
 
364
+ override fun coordinatorLayoutDidChangeConfiguration() {
365
+ if (!isPresented) return
366
+
367
+ updateStateDimensions()
368
+ sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
369
+ }
370
+
370
371
  // =============================================================================
371
372
  // MARK: - TrueSheetDimViewDelegate
372
373
  // =============================================================================
@@ -497,84 +498,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
497
498
  }
498
499
  }
499
500
 
500
- // =============================================================================
501
- // MARK: - Modal Observer (react-native-screens)
502
- // =============================================================================
503
-
504
- private fun setupModalObserver() {
505
- rnScreensObserver = RNScreensFragmentObserver(
506
- reactContext = reactContext,
507
- onModalPresented = {
508
- if (isPresented && isSheetVisible && isTopmostSheet) {
509
- hideForModal()
510
- }
511
- },
512
- onModalWillDismiss = {
513
- if (isPresented && wasHiddenByModal && isTopmostSheet) {
514
- showAfterModal()
515
- }
516
- },
517
- onModalDidDismiss = {
518
- if (isPresented && wasHiddenByModal) {
519
- wasHiddenByModal = false
520
- // Restore parent sheet after this sheet is restored
521
- parentSheetView?.viewController?.let { parent ->
522
- post { parent.showAfterModal() }
523
- }
524
- }
525
- }
526
- )
527
- rnScreensObserver?.start()
528
- }
529
-
530
- private fun cleanupModalObserver() {
531
- rnScreensObserver?.stop()
532
- rnScreensObserver = null
533
- }
534
-
535
- private fun setSheetVisibility(visible: Boolean) {
536
- coordinatorLayout?.visibility = if (visible) VISIBLE else GONE
537
- dimViews.forEach { it.visibility = if (visible) VISIBLE else INVISIBLE }
538
- }
539
-
540
- private fun hideForModal() {
541
- val sheet = sheetView ?: run {
542
- RNLog.e(reactContext, "TrueSheet: sheetView is null in hideForModal")
543
- return
544
- }
545
-
546
- isSheetVisible = false
547
- wasHiddenByModal = true
548
-
549
- dimViews.forEach { it.animate().alpha(0f).setDuration(MODAL_FADE_DURATION).start() }
550
- sheet.animate()
551
- .alpha(0f)
552
- .setDuration(MODAL_FADE_DURATION)
553
- .withEndAction {
554
- setSheetVisibility(false)
555
- }
556
- .start()
557
-
558
- // This will hide parent sheets first
559
- parentSheetView?.viewController?.hideForModal()
560
- }
561
-
562
- private fun showAfterModal() {
563
- isSheetVisible = true
564
- setSheetVisibility(true)
565
- sheetView?.alpha = 1f
566
- updateDimAmount(animated = true)
567
- }
568
-
569
- /**
570
- * Re-applies hidden state after returning from background.
571
- * Android may restore visibility on activity resume, so we need to hide it again.
572
- */
573
- fun reapplyHiddenState() {
574
- if (!wasHiddenByModal) return
575
- setSheetVisibility(false)
576
- }
577
-
578
501
  // =============================================================================
579
502
  // MARK: - Presentation
580
503
  // =============================================================================
@@ -606,7 +529,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
606
529
  setupSheetDetents()
607
530
  setupDimmedBackground(currentDetentIndex)
608
531
  setupKeyboardObserver()
609
- setupModalObserver()
610
532
  setupBackCallback()
611
533
 
612
534
  sheet.setupBackground()
@@ -749,15 +671,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
749
671
  animate = isPresented
750
672
  )
751
673
 
752
- val offset = if (expandedOffset == 0) topInset else 0
753
- val newHeight = realScreenHeight - expandedOffset - offset
754
- val newWidth = minOf(screenWidth, DEFAULT_MAX_WIDTH.dpToPx().toInt())
755
-
756
- if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
757
- lastStateWidth = newWidth
758
- lastStateHeight = newHeight
759
- delegate?.viewControllerDidChangeSize(newWidth, newHeight)
760
- }
674
+ updateStateDimensions(expandedOffset)
761
675
 
762
676
  if (isPresented) {
763
677
  setStateForDetentIndex(currentDetentIndex)
@@ -880,7 +794,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
880
794
  // =============================================================================
881
795
 
882
796
  private fun shouldHandleKeyboard(checkFocus: Boolean = true): Boolean {
883
- if (wasHiddenByModal) return false
884
797
  if (!isTopmostSheet) return false
885
798
  if (checkFocus && !isFocusedViewWithinSheet()) return false
886
799
  return true
@@ -907,14 +820,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
907
820
  setupSheetDetents()
908
821
  if (!isDismissing && detentIndexBeforeKeyboard >= 0) {
909
822
  setStateForDetentIndex(detentIndexBeforeKeyboard)
910
- detentIndexBeforeKeyboard = -1
911
823
  }
912
824
  }
913
825
 
914
- override fun keyboardDidHide() {}
826
+ override fun keyboardDidHide() {
827
+ if (!shouldHandleKeyboard(checkFocus = false)) return
828
+ detentIndexBeforeKeyboard = -1
829
+ positionFooter()
830
+ }
915
831
 
916
832
  override fun keyboardDidChangeHeight(height: Int) {
917
- if (!shouldHandleKeyboard()) return
833
+ // Skip focus check if already handling keyboard (focus may be lost during hide)
834
+ val isHandlingKeyboard = detentIndexBeforeKeyboard >= 0
835
+ if (!shouldHandleKeyboard(checkFocus = !isHandlingKeyboard)) return
918
836
  positionFooter()
919
837
  }
920
838
  }
@@ -1003,6 +921,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1003
921
  // MARK: - Detent Helpers
1004
922
  // =============================================================================
1005
923
 
924
+ private fun updateStateDimensions(expandedOffset: Int? = null) {
925
+ val offset = expandedOffset ?: (realScreenHeight - detentCalculator.getDetentHeight(detents.last()))
926
+ val topOffset = if (offset == 0) topInset else 0
927
+ val newHeight = realScreenHeight - offset - topOffset
928
+ val newWidth = minOf(screenWidth, DEFAULT_MAX_WIDTH.dpToPx().toInt())
929
+
930
+ if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
931
+ lastStateWidth = newWidth
932
+ lastStateHeight = newHeight
933
+ delegate?.viewControllerDidChangeSize(newWidth, newHeight)
934
+ }
935
+ }
936
+
1006
937
  fun translateSheet(translationY: Int) {
1007
938
  val sheet = sheetView ?: return
1008
939
 
@@ -1047,22 +978,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1047
978
  (getTag(R.id.react_test_id) as? String)?.let { info.viewIdResourceName = it }
1048
979
  }
1049
980
 
1050
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
1051
- super.onSizeChanged(w, h, oldw, oldh)
1052
-
1053
- if (w == oldw && h == oldh) return
1054
- if (!isPresented) return
1055
-
1056
- // Skip reconfiguration if expanded and only height changed (e.g., keyboard)
1057
- if (h + topInset >= screenHeight && isExpanded && oldw == w) return
1058
-
1059
- post {
1060
- setupSheetDetents()
1061
- positionFooter()
1062
- sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
1063
- }
1064
- }
1065
-
1066
981
  // =============================================================================
1067
982
  // MARK: - RootView Touch Handling
1068
983
  // =============================================================================
@@ -2,6 +2,7 @@ package com.lodev09.truesheet.core
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
+ import android.content.res.Configuration
5
6
  import android.view.MotionEvent
6
7
  import android.view.ViewConfiguration
7
8
  import android.view.ViewGroup
@@ -12,6 +13,7 @@ import com.facebook.react.uimanager.ReactPointerEventsView
12
13
 
13
14
  interface TrueSheetCoordinatorLayoutDelegate {
14
15
  fun coordinatorLayoutDidLayout(changed: Boolean)
16
+ fun coordinatorLayoutDidChangeConfiguration()
15
17
  }
16
18
 
17
19
  /**
@@ -53,6 +55,11 @@ class TrueSheetCoordinatorLayout(context: Context) :
53
55
  delegate?.coordinatorLayoutDidLayout(changed)
54
56
  }
55
57
 
58
+ override fun onConfigurationChanged(newConfig: Configuration?) {
59
+ super.onConfigurationChanged(newConfig)
60
+ delegate?.coordinatorLayoutDidChangeConfiguration()
61
+ }
62
+
56
63
  override val pointerEvents: PointerEvents
57
64
  get() = PointerEvents.BOX_NONE
58
65
 
@@ -68,12 +68,11 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
68
68
 
69
69
  /**
70
70
  * Attaches this dim view to a target view group.
71
- * For CoordinatorLayout usage, pass null to use the default (activity's decor view).
72
71
  * For stacked sheets, pass the parent sheet's bottom sheet view with corner radius.
73
72
  */
74
- fun attach(view: ViewGroup? = null, cornerRadius: Float = 0f) {
73
+ fun attach(view: ViewGroup, cornerRadius: Float = 0f) {
75
74
  if (parent != null) return
76
- targetView = view ?: reactContext.currentActivity?.window?.decorView as? ViewGroup
75
+ targetView = view
77
76
 
78
77
  if (cornerRadius > 0f) {
79
78
  outlineProvider = object : ViewOutlineProvider() {
@@ -87,7 +86,19 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
87
86
  clipToOutline = false
88
87
  }
89
88
 
90
- targetView?.addView(this)
89
+ view.addView(this)
90
+
91
+ // Manually measure and layout for React Native views (they don't layout native children)
92
+ view.post {
93
+ val width = view.width
94
+ val height = view.height
95
+ if (width > 0 && height > 0) {
96
+ val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
97
+ val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
98
+ measure(widthSpec, heightSpec)
99
+ layout(0, 0, width, height)
100
+ }
101
+ }
91
102
  }
92
103
 
93
104
  /**
@@ -13,12 +13,18 @@ object TrueSheetStackManager {
13
13
  /**
14
14
  * Called when a sheet is about to be presented.
15
15
  * Returns the visible parent sheet to stack on, or null if none.
16
+ * Only returns a parent if it's in the same container (e.g., same Screen).
16
17
  */
17
18
  @JvmStatic
18
19
  fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
19
20
  synchronized(presentedSheetStack) {
21
+ val rootContainer = sheetView.rootContainerView
20
22
  val parentSheet = presentedSheetStack.lastOrNull()
21
- ?.takeIf { it.viewController.isPresented && it.viewController.isSheetVisible }
23
+ ?.takeIf {
24
+ it.viewController.isPresented &&
25
+ it.viewController.isSheetVisible &&
26
+ it.rootContainerView == rootContainer
27
+ }
22
28
 
23
29
  val childSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(detentIndex)
24
30
  parentSheet?.updateTranslationForChild(childSheetTop)
@@ -48,6 +54,7 @@ object TrueSheetStackManager {
48
54
  /**
49
55
  * Called when a presented sheet's size changes (e.g., after setupSheetDetents).
50
56
  * Updates parent sheet translations to match the new sheet position.
57
+ * Only affects parent sheets in the same container.
51
58
  */
52
59
  @JvmStatic
53
60
  fun onSheetSizeChanged(sheetView: TrueSheetView) {
@@ -55,7 +62,9 @@ object TrueSheetStackManager {
55
62
  val index = presentedSheetStack.indexOf(sheetView)
56
63
  if (index <= 0) return
57
64
 
65
+ val rootContainer = sheetView.rootContainerView
58
66
  val parentSheet = presentedSheetStack[index - 1]
67
+ .takeIf { it.rootContainerView == rootContainer } ?: return
59
68
 
60
69
  // Post to ensure layout is complete before reading position
61
70
  sheetView.viewController.post {
@@ -71,15 +80,18 @@ object TrueSheetStackManager {
71
80
  }
72
81
 
73
82
  /**
74
- * Returns all sheets presented on top of the given sheet (children/descendants).
75
- * Returns them in reverse order (top-most first) for proper dismissal.
83
+ * Returns all sheets presented on top of the given sheet (children/descendants)
84
+ * that are in the same container. Returns them in reverse order (top-most first) for proper dismissal.
76
85
  */
77
86
  @JvmStatic
78
87
  fun getSheetsAbove(sheetView: TrueSheetView): List<TrueSheetView> {
79
88
  synchronized(presentedSheetStack) {
80
89
  val index = presentedSheetStack.indexOf(sheetView)
81
90
  if (index < 0 || index >= presentedSheetStack.size - 1) return emptyList()
82
- return presentedSheetStack.subList(index + 1, presentedSheetStack.size).reversed()
91
+ val rootContainer = sheetView.rootContainerView
92
+ return presentedSheetStack.subList(index + 1, presentedSheetStack.size)
93
+ .filter { it.rootContainerView == rootContainer }
94
+ .reversed()
83
95
  }
84
96
  }
85
97
 
@@ -99,23 +111,26 @@ object TrueSheetStackManager {
99
111
 
100
112
  /**
101
113
  * Gets the parent sheet of the given sheet, if any.
114
+ * Only returns a parent if it's in the same container.
102
115
  */
103
116
  @JvmStatic
104
117
  fun getParentSheet(sheetView: TrueSheetView): TrueSheetView? {
105
118
  synchronized(presentedSheetStack) {
106
119
  val index = presentedSheetStack.indexOf(sheetView)
107
120
  if (index <= 0) return null
108
- return presentedSheetStack[index - 1]
121
+ val rootContainer = sheetView.rootContainerView
122
+ return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
109
123
  }
110
124
  }
111
125
 
112
126
  /**
113
- * Returns true if the given sheet is the topmost presented sheet.
127
+ * Returns true if the given sheet is the topmost presented sheet in its container.
114
128
  */
115
129
  @JvmStatic
116
130
  fun isTopmostSheet(sheetView: TrueSheetView): Boolean {
117
131
  synchronized(presentedSheetStack) {
118
- return presentedSheetStack.lastOrNull() == sheetView
132
+ val rootContainer = sheetView.rootContainerView
133
+ return presentedSheetStack.lastOrNull { it.rootContainerView == rootContainer } == sheetView
119
134
  }
120
135
  }
121
136
  }
@@ -22,6 +22,12 @@ class JSI_EXPORT TrueSheetViewShadowNode final
22
22
  using ConcreteViewShadowNode::ConcreteViewShadowNode;
23
23
 
24
24
  public:
25
+ static ShadowNodeTraits BaseTraits() {
26
+ auto traits = ConcreteViewShadowNode::BaseTraits();
27
+ traits.set(ShadowNodeTraits::Trait::RootNodeKind);
28
+ return traits;
29
+ }
30
+
25
31
  void adjustLayoutWithState();
26
32
  };
27
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "3.6.11",
3
+ "version": "3.7.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",
@@ -1,181 +0,0 @@
1
- package com.lodev09.truesheet.core
2
-
3
- import android.content.Context
4
- import androidx.appcompat.app.AppCompatActivity
5
- import androidx.fragment.app.Fragment
6
- import androidx.fragment.app.FragmentManager
7
- import androidx.lifecycle.DefaultLifecycleObserver
8
- import androidx.lifecycle.LifecycleOwner
9
- import com.facebook.react.bridge.ReactContext
10
-
11
- private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
12
-
13
- /**
14
- * Observes fragment lifecycle to detect react-native-screens modal presentation.
15
- * Automatically notifies when modals are presented/dismissed.
16
- */
17
- class RNScreensFragmentObserver(
18
- private val reactContext: ReactContext,
19
- private val onModalPresented: () -> Unit,
20
- private val onModalWillDismiss: () -> Unit,
21
- private val onModalDidDismiss: () -> Unit
22
- ) {
23
- private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
24
- private var activityLifecycleObserver: DefaultLifecycleObserver? = null
25
- private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
26
- private var isActivityInForeground = true
27
- private var pendingDismissRunnable: Runnable? = null
28
-
29
- /**
30
- * Start observing fragment lifecycle events.
31
- */
32
- fun start() {
33
- val activity = reactContext.currentActivity as? AppCompatActivity ?: return
34
- val fragmentManager = activity.supportFragmentManager
35
-
36
- // Track activity foreground state to ignore fragment lifecycle events during background/foreground transitions
37
- activityLifecycleObserver = object : DefaultLifecycleObserver {
38
- override fun onResume(owner: LifecycleOwner) {
39
- isActivityInForeground = true
40
- }
41
-
42
- override fun onPause(owner: LifecycleOwner) {
43
- isActivityInForeground = false
44
- }
45
- }
46
- activity.lifecycle.addObserver(activityLifecycleObserver!!)
47
-
48
- fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
49
- override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
50
- super.onFragmentAttached(fm, f, context)
51
-
52
- // Ignore if app is resuming from background
53
- if (!isActivityInForeground) return
54
-
55
- if (isModalFragment(f) && !activeModalFragments.contains(f)) {
56
- // Cancel any pending dismiss since a modal is being presented
57
- cancelPendingDismiss()
58
-
59
- activeModalFragments.add(f)
60
-
61
- if (activeModalFragments.size == 1) {
62
- onModalPresented()
63
- }
64
- }
65
- }
66
-
67
- override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
68
- super.onFragmentStopped(fm, f)
69
-
70
- // Ignore if app is going to background (fragments stop with activity)
71
- if (!isActivityInForeground) return
72
-
73
- // Only trigger when fragment is being removed (not just stopped for navigation)
74
- if (activeModalFragments.contains(f) && f.isRemoving) {
75
- activeModalFragments.remove(f)
76
-
77
- if (activeModalFragments.isEmpty()) {
78
- // Post dismiss to allow fragment attach to cancel if navigation is happening
79
- schedulePendingDismiss()
80
- }
81
- }
82
- }
83
-
84
- override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
85
- super.onFragmentDestroyed(fm, f)
86
-
87
- if (activeModalFragments.isEmpty() && pendingDismissRunnable == null) {
88
- onModalDidDismiss()
89
- }
90
- }
91
- }
92
-
93
- fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallback!!, true)
94
- }
95
-
96
- /**
97
- * Stop observing and cleanup.
98
- */
99
- fun stop() {
100
- val activity = reactContext.currentActivity as? AppCompatActivity
101
-
102
- cancelPendingDismiss()
103
-
104
- fragmentLifecycleCallback?.let { callback ->
105
- activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(callback)
106
- }
107
- fragmentLifecycleCallback = null
108
-
109
- activityLifecycleObserver?.let { observer ->
110
- activity?.lifecycle?.removeObserver(observer)
111
- }
112
- activityLifecycleObserver = null
113
-
114
- activeModalFragments.clear()
115
- }
116
-
117
- private fun schedulePendingDismiss() {
118
- val activity = reactContext.currentActivity ?: return
119
- val decorView = activity.window?.decorView ?: return
120
-
121
- cancelPendingDismiss()
122
-
123
- pendingDismissRunnable = Runnable {
124
- pendingDismissRunnable = null
125
- if (activeModalFragments.isEmpty()) {
126
- onModalWillDismiss()
127
- }
128
- }
129
- decorView.post(pendingDismissRunnable)
130
- }
131
-
132
- private fun cancelPendingDismiss() {
133
- val activity = reactContext.currentActivity ?: return
134
- val decorView = activity.window?.decorView ?: return
135
-
136
- pendingDismissRunnable?.let {
137
- decorView.removeCallbacks(it)
138
- pendingDismissRunnable = null
139
- }
140
- }
141
-
142
- companion object {
143
- /**
144
- * Check if fragment is from react-native-screens.
145
- */
146
- private fun isScreensFragment(fragment: Fragment): Boolean = fragment.javaClass.name.startsWith(RN_SCREENS_PACKAGE)
147
-
148
- /**
149
- * Check if fragment is a react-native-screens modal (fullScreenModal, transparentModal, or formSheet).
150
- * Uses reflection to check the fragment's screen.stackPresentation property.
151
- */
152
- private fun isModalFragment(fragment: Fragment): Boolean {
153
- val className = fragment.javaClass.name
154
-
155
- if (!isScreensFragment(fragment)) {
156
- return false
157
- }
158
-
159
- // ScreenModalFragment is always a modal (used for formSheet with BottomSheetDialog)
160
- if (className.contains("ScreenModalFragment")) {
161
- return true
162
- }
163
-
164
- // For ScreenStackFragment, check the screen's stackPresentation via reflection
165
- try {
166
- val getScreenMethod = fragment.javaClass.getMethod("getScreen")
167
- val screen = getScreenMethod.invoke(fragment) ?: return false
168
-
169
- val getStackPresentationMethod = screen.javaClass.getMethod("getStackPresentation")
170
- val stackPresentation = getStackPresentationMethod.invoke(screen) ?: return false
171
-
172
- val presentationName = stackPresentation.toString()
173
- return presentationName == "MODAL" ||
174
- presentationName == "TRANSPARENT_MODAL" ||
175
- presentationName == "FORM_SHEET"
176
- } catch (e: Exception) {
177
- return false
178
- }
179
- }
180
- }
181
- }