@lodev09/react-native-true-sheet 3.4.2 → 3.5.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.
@@ -1,5 +1,6 @@
1
1
  package com.lodev09.truesheet.core
2
2
 
3
+ import android.content.Context
3
4
  import androidx.appcompat.app.AppCompatActivity
4
5
  import androidx.fragment.app.Fragment
5
6
  import androidx.fragment.app.FragmentManager
@@ -14,7 +15,8 @@ private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
14
15
  class RNScreensFragmentObserver(
15
16
  private val reactContext: ReactContext,
16
17
  private val onModalPresented: () -> Unit,
17
- private val onModalDismissed: () -> Unit
18
+ private val onModalWillDismiss: () -> Unit,
19
+ private val onModalDidDismiss: () -> Unit
18
20
  ) {
19
21
  private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
20
22
  private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
@@ -27,11 +29,11 @@ class RNScreensFragmentObserver(
27
29
  val fragmentManager = activity.supportFragmentManager
28
30
 
29
31
  fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
30
- override fun onFragmentAttached(fm: FragmentManager, fragment: Fragment, context: android.content.Context) {
31
- super.onFragmentAttached(fm, fragment, context)
32
+ override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
33
+ super.onFragmentAttached(fm, f, context)
32
34
 
33
- if (isModalFragment(fragment) && !activeModalFragments.contains(fragment)) {
34
- activeModalFragments.add(fragment)
35
+ if (isModalFragment(f) && !activeModalFragments.contains(f)) {
36
+ activeModalFragments.add(f)
35
37
 
36
38
  if (activeModalFragments.size == 1) {
37
39
  onModalPresented()
@@ -39,8 +41,8 @@ class RNScreensFragmentObserver(
39
41
  }
40
42
  }
41
43
 
42
- override fun onFragmentStopped(fm: FragmentManager, fragment: Fragment) {
43
- super.onFragmentStopped(fm, fragment)
44
+ override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
45
+ super.onFragmentStopped(fm, f)
44
46
 
45
47
  // Ignore if app is in background (fragments stop with activity)
46
48
  val activity = reactContext.currentActivity as? AppCompatActivity ?: return
@@ -48,11 +50,21 @@ class RNScreensFragmentObserver(
48
50
  return
49
51
  }
50
52
 
51
- if (activeModalFragments.contains(fragment)) {
52
- activeModalFragments.remove(fragment)
53
+ if (activeModalFragments.contains(f)) {
54
+ if (activeModalFragments.size == 1) {
55
+ onModalWillDismiss()
56
+ }
57
+ }
58
+ }
59
+
60
+ override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
61
+ super.onFragmentDestroyed(fm, f)
62
+
63
+ if (activeModalFragments.contains(f)) {
64
+ activeModalFragments.remove(f)
53
65
 
54
66
  if (activeModalFragments.isEmpty()) {
55
- onModalDismissed()
67
+ onModalDidDismiss()
56
68
  }
57
69
  }
58
70
  }
@@ -0,0 +1,134 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.ValueAnimator
6
+ import android.view.animation.AccelerateInterpolator
7
+ import android.view.animation.DecelerateInterpolator
8
+ import android.widget.FrameLayout
9
+
10
+ /**
11
+ * Provides the bottom sheet view and screen measurements for animations.
12
+ */
13
+ interface TrueSheetAnimatorProvider {
14
+ val bottomSheetView: FrameLayout?
15
+ val realScreenHeight: Int
16
+ }
17
+
18
+ /**
19
+ * Handles present and dismiss animations for the bottom sheet.
20
+ * Encapsulates animation state and provides a clean callback interface.
21
+ */
22
+ class TrueSheetAnimator(private val provider: TrueSheetAnimatorProvider) {
23
+
24
+ companion object {
25
+ const val PRESENT_DURATION = 300L
26
+ const val DISMISS_DURATION = 200L
27
+ }
28
+
29
+ private var presentAnimator: ValueAnimator? = null
30
+ private var dismissAnimator: ValueAnimator? = null
31
+
32
+ /**
33
+ * Animate the sheet presenting from bottom of screen to target position.
34
+ * @param toTop The target top position of the sheet
35
+ * @param onUpdate Called on each animation frame with the effective top position
36
+ * @param onEnd Called when animation completes or is cancelled
37
+ */
38
+ fun animatePresent(toTop: Int, onUpdate: (effectiveTop: Int) -> Unit, onEnd: () -> Unit) {
39
+ val bottomSheet = provider.bottomSheetView ?: run {
40
+ onEnd()
41
+ return
42
+ }
43
+
44
+ val fromY = (provider.realScreenHeight - toTop).toFloat()
45
+
46
+ presentAnimator?.cancel()
47
+ presentAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
48
+ duration = PRESENT_DURATION
49
+ interpolator = DecelerateInterpolator()
50
+
51
+ addUpdateListener { animator ->
52
+ val fraction = animator.animatedValue as Float
53
+ bottomSheet.translationY = fromY * fraction
54
+
55
+ val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
56
+ onUpdate(effectiveTop)
57
+ }
58
+
59
+ addListener(object : AnimatorListenerAdapter() {
60
+ override fun onAnimationEnd(animation: Animator) {
61
+ bottomSheet.translationY = 0f
62
+ presentAnimator = null
63
+ onEnd()
64
+ }
65
+
66
+ override fun onAnimationCancel(animation: Animator) {
67
+ presentAnimator = null
68
+ onEnd()
69
+ }
70
+ })
71
+
72
+ start()
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Animate the sheet dismissing from current position to bottom of screen.
78
+ * @param onUpdate Called on each animation frame with the effective top position
79
+ * @param onEnd Called when animation completes or is cancelled
80
+ */
81
+ fun animateDismiss(onUpdate: (effectiveTop: Int) -> Unit, onEnd: () -> Unit) {
82
+ val bottomSheet = provider.bottomSheetView ?: run {
83
+ onEnd()
84
+ return
85
+ }
86
+
87
+ val fromTop = bottomSheet.top + bottomSheet.translationY.toInt()
88
+ val toY = (provider.realScreenHeight - fromTop).toFloat()
89
+
90
+ dismissAnimator?.cancel()
91
+ dismissAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
92
+ duration = DISMISS_DURATION
93
+ interpolator = AccelerateInterpolator()
94
+
95
+ addUpdateListener { animator ->
96
+ val fraction = animator.animatedValue as Float
97
+ bottomSheet.translationY = toY * fraction
98
+
99
+ val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
100
+ onUpdate(effectiveTop)
101
+ }
102
+
103
+ addListener(object : AnimatorListenerAdapter() {
104
+ override fun onAnimationEnd(animation: Animator) {
105
+ dismissAnimator = null
106
+ onEnd()
107
+ }
108
+
109
+ override fun onAnimationCancel(animation: Animator) {
110
+ dismissAnimator = null
111
+ onEnd()
112
+ }
113
+ })
114
+
115
+ start()
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Cancel any running animations.
121
+ */
122
+ fun cancel() {
123
+ presentAnimator?.cancel()
124
+ presentAnimator = null
125
+ dismissAnimator?.cancel()
126
+ dismissAnimator = null
127
+ }
128
+
129
+ /**
130
+ * Check if any animation is currently running.
131
+ */
132
+ val isAnimating: Boolean
133
+ get() = presentAnimator?.isRunning == true || dismissAnimator?.isRunning == true
134
+ }
@@ -0,0 +1,208 @@
1
+ package com.lodev09.truesheet.core
2
+
3
+ import com.facebook.react.uimanager.PixelUtil.pxToDp
4
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
5
+
6
+ /**
7
+ * Provides screen dimensions and content measurements for detent calculations.
8
+ */
9
+ interface TrueSheetDetentMeasurements {
10
+ val screenHeight: Int
11
+ val realScreenHeight: Int
12
+ val detents: MutableList<Double>
13
+ val contentHeight: Int
14
+ val headerHeight: Int
15
+ val contentBottomInset: Int
16
+ val maxSheetHeight: Int?
17
+ val keyboardHeight: Int
18
+ }
19
+
20
+ /**
21
+ * Handles all detent-related calculations for the bottom sheet.
22
+ * Takes a measurements provider to always read current values.
23
+ */
24
+ class TrueSheetDetentCalculator(private val measurements: TrueSheetDetentMeasurements) {
25
+
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 keyboardHeight: Int get() = measurements.keyboardHeight
34
+
35
+ /**
36
+ * Calculate the height in pixels for a given detent value.
37
+ * @param detent The detent value: -1.0 for content-fit, or 0.0-1.0 for screen fraction
38
+ */
39
+ fun getDetentHeight(detent: Double): Int {
40
+ val baseHeight = if (detent == -1.0) {
41
+ contentHeight + headerHeight + contentBottomInset
42
+ } else {
43
+ if (detent <= 0.0 || detent > 1.0) {
44
+ throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
45
+ }
46
+ (detent * screenHeight).toInt() + contentBottomInset
47
+ }
48
+
49
+ val height = baseHeight + keyboardHeight
50
+ val maxAllowedHeight = screenHeight + contentBottomInset
51
+ return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
52
+ }
53
+
54
+ /**
55
+ * Get the expected sheet top position for a detent index.
56
+ */
57
+ fun getSheetTopForDetentIndex(index: Int): Int {
58
+ if (index < 0 || index >= detents.size) return realScreenHeight
59
+ return realScreenHeight - getDetentHeight(detents[index])
60
+ }
61
+
62
+ /**
63
+ * Calculate visible sheet height from sheet top position.
64
+ */
65
+ fun getVisibleSheetHeight(sheetTop: Int): Int = realScreenHeight - sheetTop
66
+
67
+ /**
68
+ * Convert visible sheet height to position in dp.
69
+ */
70
+ fun getPositionDp(visibleSheetHeight: Int): Float = (screenHeight - visibleSheetHeight).pxToDp()
71
+
72
+ /**
73
+ * Returns the raw screen fraction for a detent index (without bottomInset).
74
+ */
75
+ fun getDetentValueForIndex(index: Int): Float {
76
+ if (index < 0 || index >= detents.size) return 0f
77
+ val value = detents[index]
78
+ return if (value == -1.0) {
79
+ (contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
80
+ } else {
81
+ value.toFloat()
82
+ }
83
+ }
84
+
85
+ // ====================================================================
86
+ // MARK: - State Mapping
87
+ // ====================================================================
88
+
89
+ /**
90
+ * Maps detent index to BottomSheetBehavior state based on detent count.
91
+ */
92
+ fun getStateForDetentIndex(index: Int): Int {
93
+ val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
94
+ return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
95
+ }
96
+
97
+ /**
98
+ * Maps BottomSheetBehavior state to detent index.
99
+ * @return The detent index, or null if state is not mapped
100
+ */
101
+ fun getDetentIndexForState(state: Int): Int? {
102
+ val stateMap = getDetentStateMap() ?: return null
103
+ return stateMap[state]
104
+ }
105
+
106
+ /**
107
+ * Returns state-to-index mapping based on detent count.
108
+ */
109
+ private fun getDetentStateMap(): Map<Int, Int>? =
110
+ when (detents.size) {
111
+ 1 -> mapOf(
112
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
113
+ BottomSheetBehavior.STATE_EXPANDED to 0
114
+ )
115
+
116
+ 2 -> mapOf(
117
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
118
+ BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
119
+ BottomSheetBehavior.STATE_EXPANDED to 1
120
+ )
121
+
122
+ 3 -> mapOf(
123
+ BottomSheetBehavior.STATE_COLLAPSED to 0,
124
+ BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
125
+ BottomSheetBehavior.STATE_EXPANDED to 2
126
+ )
127
+
128
+ else -> null
129
+ }
130
+
131
+ // ====================================================================
132
+ // MARK: - Interpolation
133
+ // ====================================================================
134
+
135
+ /**
136
+ * Find which segment the position falls into for interpolation.
137
+ * @return Triple(fromIndex, toIndex, progress) where progress is 0-1, or null if no detents
138
+ */
139
+ fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
140
+ val count = detents.size
141
+ if (count == 0) return null
142
+
143
+ val firstPos = getSheetTopForDetentIndex(0)
144
+
145
+ // Position is below first detent (sheet is being dragged down to dismiss)
146
+ if (positionPx > firstPos) {
147
+ val range = realScreenHeight - firstPos
148
+ val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
149
+ return Triple(-1, 0, progress)
150
+ }
151
+
152
+ if (count == 1) return Triple(0, 0, 0f)
153
+
154
+ val lastPos = getSheetTopForDetentIndex(count - 1)
155
+ // Position is above last detent
156
+ if (positionPx < lastPos) {
157
+ return Triple(count - 1, count - 1, 0f)
158
+ }
159
+
160
+ // Find the segment containing this position
161
+ for (i in 0 until count - 1) {
162
+ val pos = getSheetTopForDetentIndex(i)
163
+ val nextPos = getSheetTopForDetentIndex(i + 1)
164
+
165
+ if (positionPx in nextPos..pos) {
166
+ val range = pos - nextPos
167
+ val progress = if (range > 0) (pos - positionPx).toFloat() / range else 0f
168
+ return Triple(i, i + 1, maxOf(0f, minOf(1f, progress)))
169
+ }
170
+ }
171
+
172
+ return Triple(count - 1, count - 1, 0f)
173
+ }
174
+
175
+ /**
176
+ * Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1).
177
+ */
178
+ fun getInterpolatedIndexForPosition(positionPx: Int): Float {
179
+ val count = detents.size
180
+ if (count == 0) return -1f
181
+
182
+ val segment = findSegmentForPosition(positionPx) ?: return 0f
183
+ val (fromIndex, _, progress) = segment
184
+
185
+ if (fromIndex == -1) return -progress
186
+ return fromIndex + progress
187
+ }
188
+
189
+ /**
190
+ * Returns interpolated screen fraction for position.
191
+ */
192
+ fun getInterpolatedDetentForPosition(positionPx: Int): Float {
193
+ val count = detents.size
194
+ if (count == 0) return 0f
195
+
196
+ val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
197
+ val (fromIndex, toIndex, progress) = segment
198
+
199
+ if (fromIndex == -1) {
200
+ val firstDetent = getDetentValueForIndex(0)
201
+ return maxOf(0f, firstDetent * (1 - progress))
202
+ }
203
+
204
+ val fromDetent = getDetentValueForIndex(fromIndex)
205
+ val toDetent = getDetentValueForIndex(toIndex)
206
+ return fromDetent + progress * (toDetent - fromDetent)
207
+ }
208
+ }
@@ -13,7 +13,7 @@ import com.lodev09.truesheet.utils.ScreenUtils
13
13
  class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reactContext) {
14
14
 
15
15
  companion object {
16
- private const val MAX_ALPHA = 0.4f
16
+ private const val MAX_ALPHA = 0.5f
17
17
  }
18
18
 
19
19
  private var targetView: ViewGroup? = null
@@ -48,11 +48,6 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
48
48
  targetView = null
49
49
  }
50
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
51
  fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
57
52
  val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
58
53
  val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
@@ -10,7 +10,9 @@ import androidx.core.view.WindowInsetsCompat
10
10
  import com.facebook.react.uimanager.ThemedReactContext
11
11
 
12
12
  interface TrueSheetKeyboardObserverDelegate {
13
- fun keyboardHeightDidChange(height: Int)
13
+ fun keyboardWillShow(height: Int)
14
+ fun keyboardWillHide()
15
+ fun keyboardDidChangeHeight(height: Int)
14
16
  }
15
17
 
16
18
  /**
@@ -44,10 +46,11 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
44
46
  ViewCompat.setWindowInsetsAnimationCallback(targetView, null)
45
47
  }
46
48
 
47
- private fun updateHeight(height: Int) {
48
- if (currentHeight != height) {
49
- currentHeight = height
50
- delegate?.keyboardHeightDidChange(height)
49
+ private fun updateHeight(from: Int, to: Int, fraction: Float) {
50
+ val newHeight = (from + (to - from) * fraction).toInt()
51
+ if (currentHeight != newHeight) {
52
+ currentHeight = newHeight
53
+ delegate?.keyboardDidChangeHeight(newHeight)
51
54
  }
52
55
  }
53
56
 
@@ -69,6 +72,11 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
69
72
  bounds: WindowInsetsAnimationCompat.BoundsCompat
70
73
  ): WindowInsetsAnimationCompat.BoundsCompat {
71
74
  endHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
75
+ if (endHeight > startHeight) {
76
+ delegate?.keyboardWillShow(endHeight)
77
+ } else if (endHeight < startHeight) {
78
+ delegate?.keyboardWillHide()
79
+ }
72
80
  return bounds
73
81
  }
74
82
 
@@ -78,14 +86,14 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
78
86
  } ?: return insets
79
87
 
80
88
  val fraction = imeAnimation.interpolatedFraction
81
- val currentHeight = (startHeight + (endHeight - startHeight) * fraction).toInt()
82
- updateHeight(currentHeight)
89
+ updateHeight(startHeight, endHeight, fraction)
83
90
 
84
91
  return insets
85
92
  }
86
93
 
87
94
  override fun onEnd(animation: WindowInsetsAnimationCompat) {
88
- updateHeight(getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView)))
95
+ val finalHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
96
+ updateHeight(startHeight, finalHeight, 1f)
89
97
  }
90
98
  }
91
99
  )
@@ -102,7 +110,17 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
102
110
  val screenHeight = rootView.height
103
111
  val keyboardHeight = screenHeight - rect.bottom
104
112
 
105
- updateHeight(if (keyboardHeight > screenHeight * 0.15) keyboardHeight else 0)
113
+ val newHeight = if (keyboardHeight > screenHeight * 0.15) keyboardHeight else 0
114
+ val previousHeight = currentHeight
115
+
116
+ if (previousHeight != newHeight) {
117
+ if (newHeight > previousHeight) {
118
+ delegate?.keyboardWillShow(newHeight)
119
+ } else if (newHeight < previousHeight) {
120
+ delegate?.keyboardWillHide()
121
+ }
122
+ updateHeight(previousHeight, newHeight, 1f)
123
+ }
106
124
  }
107
125
 
108
126
  rootView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
@@ -1,26 +1,21 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <resources>
3
- <!-- Smooth slide animation for BottomSheetDialog -->
4
- <style name="TrueSheetAnimation" parent="Animation.AppCompat.Dialog">
5
- <item name="android:windowEnterAnimation">@anim/true_sheet_slide_in</item>
6
- <item name="android:windowExitAnimation">@anim/true_sheet_slide_out</item>
3
+ <!-- No animation style - animations handled programmatically -->
4
+ <style name="TrueSheetNoAnimation" parent="Animation.AppCompat.Dialog">
5
+ <item name="android:windowEnterAnimation">@null</item>
6
+ <item name="android:windowExitAnimation">@null</item>
7
7
  </style>
8
8
 
9
- <!-- Fade out only - used when hiding sheet for RN Screens modal -->
10
- <style name="TrueSheetFadeOutAnimation" parent="Animation.AppCompat.Dialog">
11
- <item name="android:windowExitAnimation">@anim/true_sheet_fade_out</item>
12
- </style>
13
-
14
- <!-- Default BottomSheetDialog style with smooth animations -->
9
+ <!-- Default BottomSheetDialog style with programmatic animations -->
15
10
  <style name="TrueSheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
16
- <item name="android:windowAnimationStyle">@style/TrueSheetAnimation</item>
11
+ <item name="android:windowAnimationStyle">@style/TrueSheetNoAnimation</item>
17
12
  </style>
18
13
 
19
- <!-- BottomSheetDialog style with edge-to-edge and smooth animations -->
14
+ <!-- BottomSheetDialog style with edge-to-edge and programmatic animations -->
20
15
  <style name="TrueSheetEdgeToEdgeEnabledDialog" parent="Theme.Design.Light.BottomSheetDialog">
21
16
  <item name="android:windowIsFloating">false</item>
22
17
  <item name="enableEdgeToEdge">true</item>
23
18
  <item name="android:navigationBarColor">@android:color/transparent</item>
24
- <item name="android:windowAnimationStyle">@style/TrueSheetAnimation</item>
19
+ <item name="android:windowAnimationStyle">@style/TrueSheetNoAnimation</item>
25
20
  </style>
26
21
  </resources>
@@ -40,7 +40,10 @@ NS_ASSUME_NONNULL_BEGIN
40
40
 
41
41
  @end
42
42
 
43
- @interface TrueSheetViewController : UIViewController <UISheetPresentationControllerDelegate
43
+ @protocol TrueSheetDetentMeasurements;
44
+
45
+ @interface TrueSheetViewController : UIViewController <UISheetPresentationControllerDelegate,
46
+ TrueSheetDetentMeasurements
44
47
  #if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
45
48
  ,
46
49
  RNSDismissibleModalProtocol