@lodev09/react-native-true-sheet 3.5.1-beta.2 → 3.5.1-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.
Files changed (34) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +2 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +32 -28
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +378 -253
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +0 -5
  5. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +36 -14
  6. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +150 -0
  7. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +55 -0
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +18 -12
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +91 -2
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +21 -11
  11. package/android/src/main/java/com/lodev09/truesheet/core/{TrueSheetDialogObserver.kt → TrueSheetStackManager.kt} +7 -5
  12. package/ios/TrueSheetViewController.h +2 -3
  13. package/ios/TrueSheetViewController.mm +11 -4
  14. package/ios/core/TrueSheetDetentCalculator.h +2 -3
  15. package/ios/core/TrueSheetDetentCalculator.mm +7 -9
  16. package/lib/module/TrueSheet.js +1 -16
  17. package/lib/module/TrueSheet.js.map +1 -1
  18. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +0 -1
  19. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  20. package/lib/typescript/src/TrueSheet.types.d.ts +0 -8
  21. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  22. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +0 -1
  23. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  24. package/lib/typescript/src/navigation/types.d.ts +1 -1
  25. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/TrueSheet.tsx +1 -16
  28. package/src/TrueSheet.types.ts +0 -9
  29. package/src/fabric/TrueSheetViewNativeComponent.ts +0 -1
  30. package/src/navigation/types.ts +0 -1
  31. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetAnimator.kt +0 -145
  32. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogFragment.kt +0 -320
  33. package/android/src/main/res/anim/fast_fade_out.xml +0 -6
  34. package/android/src/main/res/values/styles.xml +0 -21
@@ -183,11 +183,6 @@ class TrueSheetViewManager :
183
183
  // iOS-specific prop - no-op on Android
184
184
  }
185
185
 
186
- @ReactProp(name = "edgeToEdgeFullScreen", defaultBoolean = false)
187
- override fun setEdgeToEdgeFullScreen(view: TrueSheetView, edgeToEdgeFullScreen: Boolean) {
188
- view.setEdgeToEdgeFullScreen(edgeToEdgeFullScreen)
189
- }
190
-
191
186
  @ReactProp(name = "insetAdjustment")
192
187
  override fun setInsetAdjustment(view: TrueSheetView, insetAdjustment: String?) {
193
188
  view.setInsetAdjustment(insetAdjustment ?: "automatic")
@@ -4,6 +4,8 @@ import android.content.Context
4
4
  import androidx.appcompat.app.AppCompatActivity
5
5
  import androidx.fragment.app.Fragment
6
6
  import androidx.fragment.app.FragmentManager
7
+ import androidx.lifecycle.DefaultLifecycleObserver
8
+ import androidx.lifecycle.LifecycleOwner
7
9
  import com.facebook.react.bridge.ReactContext
8
10
 
9
11
  private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
@@ -19,7 +21,9 @@ class RNScreensFragmentObserver(
19
21
  private val onModalDidDismiss: () -> Unit
20
22
  ) {
21
23
  private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
24
+ private var activityLifecycleObserver: DefaultLifecycleObserver? = null
22
25
  private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
26
+ private var isActivityInForeground = true
23
27
 
24
28
  /**
25
29
  * Start observing fragment lifecycle events.
@@ -28,10 +32,25 @@ class RNScreensFragmentObserver(
28
32
  val activity = reactContext.currentActivity as? AppCompatActivity ?: return
29
33
  val fragmentManager = activity.supportFragmentManager
30
34
 
35
+ // Track activity foreground state to ignore fragment lifecycle events during background/foreground transitions
36
+ activityLifecycleObserver = object : DefaultLifecycleObserver {
37
+ override fun onResume(owner: LifecycleOwner) {
38
+ isActivityInForeground = true
39
+ }
40
+
41
+ override fun onPause(owner: LifecycleOwner) {
42
+ isActivityInForeground = false
43
+ }
44
+ }
45
+ activity.lifecycle.addObserver(activityLifecycleObserver!!)
46
+
31
47
  fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
32
48
  override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
33
49
  super.onFragmentAttached(fm, f, context)
34
50
 
51
+ // Ignore if app is resuming from background
52
+ if (!isActivityInForeground) return
53
+
35
54
  if (isModalFragment(f) && !activeModalFragments.contains(f)) {
36
55
  activeModalFragments.add(f)
37
56
 
@@ -44,14 +63,14 @@ class RNScreensFragmentObserver(
44
63
  override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
45
64
  super.onFragmentStopped(fm, f)
46
65
 
47
- // Ignore if app is in background (fragments stop with activity)
48
- val activity = reactContext.currentActivity as? AppCompatActivity ?: return
49
- if (!activity.lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) {
50
- return
51
- }
66
+ // Ignore if app is going to background (fragments stop with activity)
67
+ if (!isActivityInForeground) return
52
68
 
53
- if (activeModalFragments.contains(f)) {
54
- if (activeModalFragments.size == 1) {
69
+ // Only trigger when fragment is being removed (not just stopped for navigation)
70
+ if (activeModalFragments.contains(f) && f.isRemoving) {
71
+ activeModalFragments.remove(f)
72
+
73
+ if (activeModalFragments.isEmpty()) {
55
74
  onModalWillDismiss()
56
75
  }
57
76
  }
@@ -60,12 +79,8 @@ class RNScreensFragmentObserver(
60
79
  override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
61
80
  super.onFragmentDestroyed(fm, f)
62
81
 
63
- if (activeModalFragments.contains(f)) {
64
- activeModalFragments.remove(f)
65
-
66
- if (activeModalFragments.isEmpty()) {
67
- onModalDidDismiss()
68
- }
82
+ if (activeModalFragments.isEmpty()) {
83
+ onModalDidDismiss()
69
84
  }
70
85
  }
71
86
  }
@@ -77,11 +92,18 @@ class RNScreensFragmentObserver(
77
92
  * Stop observing and cleanup.
78
93
  */
79
94
  fun stop() {
95
+ val activity = reactContext.currentActivity as? AppCompatActivity
96
+
80
97
  fragmentLifecycleCallback?.let { callback ->
81
- val activity = reactContext.currentActivity as? AppCompatActivity
82
98
  activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(callback)
83
99
  }
84
100
  fragmentLifecycleCallback = null
101
+
102
+ activityLifecycleObserver?.let { observer ->
103
+ activity?.lifecycle?.removeObserver(observer)
104
+ }
105
+ activityLifecycleObserver = null
106
+
85
107
  activeModalFragments.clear()
86
108
  }
87
109
 
@@ -0,0 +1,150 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.graphics.Color
5
+ import android.graphics.drawable.ShapeDrawable
6
+ import android.graphics.drawable.shapes.RoundRectShape
7
+ import android.util.TypedValue
8
+ import android.view.Gravity
9
+ import android.view.View
10
+ import android.widget.FrameLayout
11
+ import androidx.coordinatorlayout.widget.CoordinatorLayout
12
+ import com.facebook.react.uimanager.PixelUtil.dpToPx
13
+ import com.facebook.react.uimanager.ThemedReactContext
14
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
15
+
16
+ interface TrueSheetBottomSheetViewDelegate {
17
+ val isTopmostSheet: Boolean
18
+ val sheetCornerRadius: Float
19
+ val sheetBackgroundColor: Int?
20
+ val grabber: Boolean
21
+ val grabberOptions: GrabberOptions?
22
+ }
23
+
24
+ /**
25
+ * The bottom sheet view that holds the content.
26
+ * This view has BottomSheetBehavior attached via CoordinatorLayout.LayoutParams.
27
+ *
28
+ * Touch dispatching to React Native is handled by TrueSheetViewController,
29
+ * which is the actual RootView containing the React content.
30
+ */
31
+ @SuppressLint("ViewConstructor")
32
+ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : FrameLayout(reactContext) {
33
+
34
+ companion object {
35
+ private const val GRABBER_TAG = "TrueSheetGrabber"
36
+ private const val DEFAULT_CORNER_RADIUS = 16f // dp
37
+ private const val DEFAULT_MAX_WIDTH = 640 // dp
38
+ }
39
+
40
+ // =============================================================================
41
+ // MARK: - Properties
42
+ // =============================================================================
43
+
44
+ var delegate: TrueSheetBottomSheetViewDelegate? = null
45
+
46
+ // Behavior reference (set after adding to CoordinatorLayout)
47
+ val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
48
+ get() = (layoutParams as? CoordinatorLayout.LayoutParams)
49
+ ?.behavior as? BottomSheetBehavior<TrueSheetBottomSheetView>
50
+
51
+ // =============================================================================
52
+ // MARK: - Initialization
53
+ // =============================================================================
54
+
55
+ init {
56
+ // Allow content to extend beyond bounds (for footer positioning)
57
+ clipChildren = false
58
+ clipToPadding = false
59
+ }
60
+
61
+ override fun setTranslationY(translationY: Float) {
62
+ // Skip resetting translation to 0 for parent sheets (non-topmost)
63
+ // This prevents keyboard inset animations from resetting parent sheet translation
64
+ if (delegate?.isTopmostSheet == false && translationY == 0f && this.translationY != 0f) {
65
+ return
66
+ }
67
+ super.setTranslationY(translationY)
68
+ }
69
+
70
+ // =============================================================================
71
+ // MARK: - Layout
72
+ // =============================================================================
73
+
74
+ /**
75
+ * Creates layout params with BottomSheetBehavior attached.
76
+ */
77
+ fun createLayoutParams(): CoordinatorLayout.LayoutParams {
78
+ val behavior = BottomSheetBehavior<TrueSheetBottomSheetView>().apply {
79
+ isHideable = true
80
+ maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
81
+ }
82
+
83
+ return CoordinatorLayout.LayoutParams(
84
+ CoordinatorLayout.LayoutParams.MATCH_PARENT,
85
+ CoordinatorLayout.LayoutParams.MATCH_PARENT
86
+ ).apply {
87
+ this.behavior = behavior
88
+ this.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
89
+ }
90
+ }
91
+
92
+ // =============================================================================
93
+ // MARK: - Background & Styling
94
+ // =============================================================================
95
+
96
+ fun setupBackground() {
97
+ val radius = delegate?.sheetCornerRadius ?: DEFAULT_CORNER_RADIUS.dpToPx()
98
+ val effectiveRadius = if (radius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else radius
99
+
100
+ val outerRadii = floatArrayOf(
101
+ effectiveRadius,
102
+ effectiveRadius, // top-left
103
+ effectiveRadius,
104
+ effectiveRadius, // top-right
105
+ 0f,
106
+ 0f, // bottom-right
107
+ 0f,
108
+ 0f // bottom-left
109
+ )
110
+
111
+ val color = delegate?.sheetBackgroundColor ?: getDefaultBackgroundColor()
112
+
113
+ background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
114
+ paint.color = color
115
+ }
116
+ clipToOutline = true
117
+ }
118
+
119
+ private fun getDefaultBackgroundColor(): Int {
120
+ val typedValue = TypedValue()
121
+ return if (reactContext.theme.resolveAttribute(
122
+ com.google.android.material.R.attr.colorSurfaceContainerLow,
123
+ typedValue,
124
+ true
125
+ )
126
+ ) {
127
+ typedValue.data
128
+ } else {
129
+ Color.WHITE
130
+ }
131
+ }
132
+
133
+ // =============================================================================
134
+ // MARK: - Grabber
135
+ // =============================================================================
136
+
137
+ fun setupGrabber() {
138
+ findViewWithTag<View>(GRABBER_TAG)?.let { removeView(it) }
139
+
140
+ val isEnabled = delegate?.grabber ?: true
141
+ val isDraggable = behavior?.isDraggable ?: true
142
+ if (!isEnabled || !isDraggable) return
143
+
144
+ val grabberView = TrueSheetGrabberView(reactContext, delegate?.grabberOptions).apply {
145
+ tag = GRABBER_TAG
146
+ }
147
+
148
+ addView(grabberView, 0)
149
+ }
150
+ }
@@ -0,0 +1,55 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.view.View
6
+ import androidx.coordinatorlayout.widget.CoordinatorLayout
7
+ import com.facebook.react.uimanager.PointerEvents
8
+ import com.facebook.react.uimanager.ReactPointerEventsView
9
+
10
+ interface TrueSheetCoordinatorLayoutDelegate {
11
+ fun coordinatorLayoutDidLayout(changed: Boolean)
12
+ }
13
+
14
+ /**
15
+ * Custom CoordinatorLayout that hosts the bottom sheet and dim view.
16
+ * Implements ReactPointerEventsView to allow touch events to pass through
17
+ * to underlying React Native views when appropriate.
18
+ */
19
+ @SuppressLint("ViewConstructor")
20
+ class TrueSheetCoordinatorLayout(context: Context) :
21
+ CoordinatorLayout(context),
22
+ ReactPointerEventsView {
23
+
24
+ var delegate: TrueSheetCoordinatorLayoutDelegate? = null
25
+
26
+ init {
27
+ // Fill the entire screen
28
+ layoutParams = LayoutParams(
29
+ LayoutParams.MATCH_PARENT,
30
+ LayoutParams.MATCH_PARENT
31
+ )
32
+
33
+ // Ensure we don't clip the sheet during animations
34
+ clipChildren = false
35
+ clipToPadding = false
36
+ }
37
+
38
+ override fun onLayout(
39
+ changed: Boolean,
40
+ l: Int,
41
+ t: Int,
42
+ r: Int,
43
+ b: Int
44
+ ) {
45
+ super.onLayout(changed, l, t, r, b)
46
+ delegate?.coordinatorLayoutDidLayout(changed)
47
+ }
48
+
49
+ /**
50
+ * Allow pointer events to pass through to underlying views.
51
+ * The DimView and BottomSheetView handle their own touch interception.
52
+ */
53
+ override val pointerEvents: PointerEvents
54
+ get() = PointerEvents.BOX_NONE
55
+ }
@@ -1,12 +1,14 @@
1
1
  package com.lodev09.truesheet.core
2
2
 
3
3
  import com.facebook.react.uimanager.PixelUtil.pxToDp
4
+ import com.facebook.react.uimanager.ThemedReactContext
5
+ import com.facebook.react.util.RNLog
4
6
  import com.google.android.material.bottomsheet.BottomSheetBehavior
5
7
 
6
8
  /**
7
9
  * Provides screen dimensions and content measurements for detent calculations.
8
10
  */
9
- interface TrueSheetDetentMeasurements {
11
+ interface TrueSheetDetentCalculatorDelegate {
10
12
  val screenHeight: Int
11
13
  val realScreenHeight: Int
12
14
  val detents: MutableList<Double>
@@ -19,18 +21,19 @@ interface TrueSheetDetentMeasurements {
19
21
 
20
22
  /**
21
23
  * Handles all detent-related calculations for the bottom sheet.
22
- * Takes a measurements provider to always read current values.
23
24
  */
24
- class TrueSheetDetentCalculator(private val measurements: TrueSheetDetentMeasurements) {
25
+ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
25
26
 
26
- private val screenHeight: Int get() = measurements.screenHeight
27
- private val realScreenHeight: Int get() = measurements.realScreenHeight
28
- private val detents: List<Double> get() = measurements.detents
29
- private val contentHeight: Int get() = measurements.contentHeight
30
- private val headerHeight: Int get() = measurements.headerHeight
31
- private val contentBottomInset: Int get() = measurements.contentBottomInset
32
- private val maxSheetHeight: Int? get() = measurements.maxSheetHeight
33
- private val keyboardInset: Int get() = measurements.keyboardInset
27
+ var delegate: TrueSheetDetentCalculatorDelegate? = null
28
+
29
+ private val screenHeight: Int get() = delegate?.screenHeight ?: 0
30
+ private val realScreenHeight: Int get() = delegate?.realScreenHeight ?: 0
31
+ private val detents: List<Double> get() = delegate?.detents ?: emptyList()
32
+ private val contentHeight: Int get() = delegate?.contentHeight ?: 0
33
+ private val headerHeight: Int get() = delegate?.headerHeight ?: 0
34
+ private val contentBottomInset: Int get() = delegate?.contentBottomInset ?: 0
35
+ private val maxSheetHeight: Int? get() = delegate?.maxSheetHeight
36
+ private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
34
37
 
35
38
  /**
36
39
  * Calculate the height in pixels for a given detent value.
@@ -55,7 +58,10 @@ class TrueSheetDetentCalculator(private val measurements: TrueSheetDetentMeasure
55
58
  * Get the expected sheet top position for a detent index.
56
59
  */
57
60
  fun getSheetTopForDetentIndex(index: Int): Int {
58
- if (index < 0 || index >= detents.size) return realScreenHeight
61
+ if (index < 0 || index >= detents.size) {
62
+ RNLog.w(reactContext, "TrueSheet: Detent index ($index) is out of bounds (0..${detents.size - 1})")
63
+ return realScreenHeight
64
+ }
59
65
  return realScreenHeight - getDetentHeight(detents[index])
60
66
  }
61
67
 
@@ -3,21 +3,51 @@ package com.lodev09.truesheet.core
3
3
  import android.annotation.SuppressLint
4
4
  import android.graphics.Color
5
5
  import android.graphics.Outline
6
+ import android.view.MotionEvent
6
7
  import android.view.View
7
8
  import android.view.ViewGroup
8
9
  import android.view.ViewOutlineProvider
10
+ import com.facebook.react.uimanager.PointerEvents
11
+ import com.facebook.react.uimanager.ReactPointerEventsView
9
12
  import com.facebook.react.uimanager.ThemedReactContext
10
13
  import com.lodev09.truesheet.utils.ScreenUtils
11
14
 
12
- @SuppressLint("ViewConstructor")
13
- class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reactContext) {
15
+ /**
16
+ * Delegate for handling dim view interactions.
17
+ */
18
+ interface TrueSheetDimViewDelegate {
19
+ fun dimViewDidTap()
20
+ }
21
+
22
+ /**
23
+ * Dim view that sits behind the bottom sheet in the CoordinatorLayout.
24
+ *
25
+ * Key behaviors:
26
+ * - When alpha > 0 (dimmed): blocks touches and calls delegate on tap
27
+ * - When alpha == 0 (not dimmed): passes touches through to views below
28
+ *
29
+ * This implements the "dimmedDetentIndex" equivalent functionality:
30
+ * the view only becomes interactive when the sheet is at or above the dimmed detent.
31
+ */
32
+ @SuppressLint("ViewConstructor", "ClickableViewAccessibility")
33
+ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
34
+ View(reactContext),
35
+ ReactPointerEventsView {
14
36
 
15
37
  companion object {
16
38
  private const val MAX_ALPHA = 0.5f
17
39
  }
18
40
 
41
+ var delegate: TrueSheetDimViewDelegate? = null
42
+
19
43
  private var targetView: ViewGroup? = null
20
44
 
45
+ /**
46
+ * Whether this view should block gestures (when dimmed).
47
+ */
48
+ private val blockGestures: Boolean
49
+ get() = alpha > 0f
50
+
21
51
  init {
22
52
  layoutParams = ViewGroup.LayoutParams(
23
53
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -25,8 +55,22 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
25
55
  )
26
56
  setBackgroundColor(Color.BLACK)
27
57
  alpha = 0f
58
+
59
+ // Handle taps on the dim view
60
+ setOnClickListener {
61
+ delegate?.dimViewDidTap()
62
+ }
28
63
  }
29
64
 
65
+ // =============================================================================
66
+ // MARK: - Attachment
67
+ // =============================================================================
68
+
69
+ /**
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
+ * For stacked sheets, pass the parent sheet's bottom sheet view with corner radius.
73
+ */
30
74
  fun attach(view: ViewGroup? = null, cornerRadius: Float = 0f) {
31
75
  if (parent != null) return
32
76
  targetView = view ?: reactContext.currentActivity?.window?.decorView as? ViewGroup
@@ -38,16 +82,34 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
38
82
  }
39
83
  }
40
84
  clipToOutline = true
85
+ } else {
86
+ outlineProvider = null
87
+ clipToOutline = false
41
88
  }
42
89
 
43
90
  targetView?.addView(this)
44
91
  }
45
92
 
93
+ /**
94
+ * Attaches this dim view to a CoordinatorLayout at index 0 (behind the sheet).
95
+ */
96
+ fun attachToCoordinator(coordinator: TrueSheetCoordinatorLayout) {
97
+ if (parent != null) return
98
+ targetView = coordinator
99
+ outlineProvider = null
100
+ clipToOutline = false
101
+ coordinator.addView(this, 0)
102
+ }
103
+
46
104
  fun detach() {
47
105
  targetView?.removeView(this)
48
106
  targetView = null
49
107
  }
50
108
 
109
+ // =============================================================================
110
+ // MARK: - Alpha Calculation
111
+ // =============================================================================
112
+
51
113
  fun calculateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
52
114
  val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
53
115
  val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
@@ -68,4 +130,31 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
68
130
  fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
69
131
  alpha = calculateAlpha(sheetTop, dimmedDetentIndex, getSheetTopForDetentIndex)
70
132
  }
133
+
134
+ // =============================================================================
135
+ // MARK: - Touch Handling
136
+ // =============================================================================
137
+
138
+ override fun onTouchEvent(event: MotionEvent): Boolean {
139
+ if (blockGestures) {
140
+ // When dimmed, consume touch and trigger click on ACTION_UP
141
+ if (event.action == MotionEvent.ACTION_UP) {
142
+ performClick()
143
+ }
144
+ return true
145
+ }
146
+ // When not dimmed, let touches pass through
147
+ return false
148
+ }
149
+
150
+ // =============================================================================
151
+ // MARK: - ReactPointerEventsView
152
+ // =============================================================================
153
+
154
+ /**
155
+ * When dimmed (alpha > 0), intercept touches (AUTO).
156
+ * When not dimmed (alpha == 0), pass through (NONE).
157
+ */
158
+ override val pointerEvents: PointerEvents
159
+ get() = if (blockGestures) PointerEvents.AUTO else PointerEvents.NONE
71
160
  }
@@ -111,6 +111,9 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
111
111
  }
112
112
 
113
113
  private fun setupLegacyListener() {
114
+ // Ensure we don't add duplicate listeners
115
+ if (globalLayoutListener != null) return
116
+
114
117
  val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return
115
118
  activityRootView = rootView
116
119
 
@@ -122,19 +125,26 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
122
125
  val keyboardHeight = screenHeight - rect.bottom
123
126
 
124
127
  val newHeight = if (keyboardHeight > screenHeight * 0.15) keyboardHeight else 0
128
+
129
+ // Skip if already at this height
130
+ if (targetHeight == newHeight) return@OnGlobalLayoutListener
131
+
125
132
  val previousHeight = currentHeight
133
+ targetHeight = newHeight
134
+ isHiding = newHeight < previousHeight
126
135
 
127
- if (previousHeight != newHeight) {
128
- targetHeight = newHeight
129
- if (newHeight > previousHeight) {
130
- delegate?.keyboardWillShow(newHeight)
131
- } else if (newHeight < previousHeight) {
132
- delegate?.keyboardWillHide()
133
- }
134
- updateHeight(previousHeight, newHeight, 1f)
135
- if (newHeight == 0 && previousHeight > 0) {
136
- delegate?.keyboardDidHide()
137
- }
136
+ if (newHeight > previousHeight) {
137
+ delegate?.keyboardWillShow(newHeight)
138
+ } else if (isHiding) {
139
+ delegate?.keyboardWillHide()
140
+ }
141
+
142
+ // On legacy API, keyboard has already animated - just update immediately
143
+ updateHeight(previousHeight, newHeight, 1f)
144
+
145
+ if (isHiding && newHeight == 0) {
146
+ delegate?.keyboardDidHide()
147
+ isHiding = false
138
148
  }
139
149
  }
140
150
 
@@ -6,7 +6,7 @@ import com.lodev09.truesheet.TrueSheetView
6
6
  * Manages TrueSheet stacking behavior.
7
7
  * Tracks presented sheets and handles visibility when sheets stack on top of each other.
8
8
  */
9
- object TrueSheetDialogObserver {
9
+ object TrueSheetStackManager {
10
10
 
11
11
  private val presentedSheetStack = mutableListOf<TrueSheetView>()
12
12
 
@@ -18,9 +18,9 @@ object TrueSheetDialogObserver {
18
18
  fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
19
19
  synchronized(presentedSheetStack) {
20
20
  val parentSheet = presentedSheetStack.lastOrNull()
21
- ?.takeIf { it.viewController.isPresented && it.viewController.isDialogVisible }
21
+ ?.takeIf { it.viewController.isPresented && it.viewController.isSheetVisible }
22
22
 
23
- val childSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
23
+ val childSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(detentIndex)
24
24
  parentSheet?.updateTranslationForChild(childSheetTop)
25
25
 
26
26
  if (!presentedSheetStack.contains(sheetView)) {
@@ -59,8 +59,10 @@ object TrueSheetDialogObserver {
59
59
 
60
60
  // Post to ensure layout is complete before reading position
61
61
  sheetView.viewController.post {
62
- val childMinSheetTop = sheetView.viewController.getExpectedSheetTop(0)
63
- val childCurrentSheetTop = sheetView.viewController.getExpectedSheetTop(sheetView.viewController.currentDetentIndex)
62
+ val childMinSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(0)
63
+ val childCurrentSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(
64
+ sheetView.viewController.currentDetentIndex
65
+ )
64
66
  // Cap to minimum detent position
65
67
  val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
66
68
  parentSheet.updateTranslationForChild(childSheetTop)
@@ -7,6 +7,7 @@
7
7
  //
8
8
 
9
9
  #import <UIKit/UIKit.h>
10
+ #import "core/TrueSheetDetentCalculator.h"
10
11
 
11
12
  #if __has_include(<RNScreens/RNSDismissibleModalProtocol.h>)
12
13
  #import <RNScreens/RNSDismissibleModalProtocol.h>
@@ -40,10 +41,8 @@ NS_ASSUME_NONNULL_BEGIN
40
41
 
41
42
  @end
42
43
 
43
- @protocol TrueSheetDetentMeasurements;
44
-
45
44
  @interface TrueSheetViewController : UIViewController <UISheetPresentationControllerDelegate,
46
- TrueSheetDetentMeasurements
45
+ TrueSheetDetentCalculatorDelegate
47
46
  #if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
48
47
  ,
49
48
  RNSDismissibleModalProtocol
@@ -65,7 +65,8 @@
65
65
 
66
66
  _blurInteraction = YES;
67
67
  _insetAdjustment = @"automatic";
68
- _detentCalculator = [[TrueSheetDetentCalculator alloc] initWithMeasurements:self];
68
+ _detentCalculator = [[TrueSheetDetentCalculator alloc] init];
69
+ _detentCalculator.delegate = self;
69
70
  }
70
71
  return self;
71
72
  }
@@ -503,8 +504,10 @@
503
504
 
504
505
  - (void)setupSheetDetents {
505
506
  UISheetPresentationController *sheet = self.sheetPresentationController;
506
- if (!sheet)
507
+ if (!sheet) {
508
+ RCTLogError(@"TrueSheet: sheetPresentationController is nil in setupSheetDetents");
507
509
  return;
510
+ }
508
511
 
509
512
  NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
510
513
  [_detentCalculator clearResolvedPositions];
@@ -591,8 +594,10 @@
591
594
 
592
595
  - (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
593
596
  UISheetPresentationController *sheet = self.sheetPresentationController;
594
- if (!sheet)
597
+ if (!sheet) {
598
+ RCTLogError(@"TrueSheet: sheetPresentationController is nil in detentIdentifierForIndex");
595
599
  return UISheetPresentationControllerDetentIdentifierMedium;
600
+ }
596
601
 
597
602
  UISheetPresentationControllerDetentIdentifier identifier = UISheetPresentationControllerDetentIdentifierMedium;
598
603
  if (index >= 0 && index < (NSInteger)sheet.detents.count) {
@@ -611,8 +616,10 @@
611
616
 
612
617
  - (void)applyActiveDetent {
613
618
  UISheetPresentationController *sheet = self.sheetPresentationController;
614
- if (!sheet)
619
+ if (!sheet) {
620
+ RCTLogError(@"TrueSheet: sheetPresentationController is nil in applyActiveDetent");
615
621
  return;
622
+ }
616
623
 
617
624
  NSInteger detentCount = _detents.count;
618
625
  if (detentCount == 0)