@lodev09/react-native-true-sheet 3.0.0-beta.6 → 3.0.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +51 -99
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerViewManager.kt +0 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +10 -18
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +76 -20
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +21 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +106 -136
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +305 -419
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +9 -4
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +116 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +33 -5
- package/android/src/main/jni/TrueSheetSpec.h +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/{TrueSheetContainerViewComponentDescriptor.h → TrueSheetViewComponentDescriptor.h} +5 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/{TrueSheetContainerViewShadowNode.cpp → TrueSheetViewShadowNode.cpp} +4 -4
- package/common/cpp/react/renderer/components/TrueSheetSpec/{TrueSheetContainerViewShadowNode.h → TrueSheetViewShadowNode.h} +8 -8
- package/common/cpp/react/renderer/components/TrueSheetSpec/{TrueSheetContainerViewState.cpp → TrueSheetViewState.cpp} +2 -2
- package/common/cpp/react/renderer/components/TrueSheetSpec/{TrueSheetContainerViewState.h → TrueSheetViewState.h} +6 -6
- package/ios/TrueSheetContainerView.h +20 -2
- package/ios/TrueSheetContainerView.mm +62 -62
- package/ios/TrueSheetContentView.h +4 -2
- package/ios/TrueSheetContentView.mm +29 -72
- package/ios/TrueSheetFooterView.mm +2 -2
- package/ios/TrueSheetHeaderView.h +29 -0
- package/ios/TrueSheetHeaderView.mm +60 -0
- package/ios/TrueSheetView.mm +195 -214
- package/ios/TrueSheetViewController.h +2 -2
- package/ios/TrueSheetViewController.mm +126 -231
- package/ios/utils/LayoutUtil.h +2 -1
- package/ios/utils/LayoutUtil.mm +14 -1
- package/lib/module/TrueSheet.js +9 -2
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +1 -3
- package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +3 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +9 -9
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/package.json +6 -3
- package/react-native.config.js +1 -1
- package/src/TrueSheet.tsx +9 -0
- package/src/TrueSheet.types.ts +10 -11
- package/src/fabric/TrueSheetContainerViewNativeComponent.ts +1 -3
- package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +3 -1
|
@@ -2,9 +2,11 @@ package com.lodev09.truesheet
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.graphics.Color
|
|
5
|
+
import android.graphics.drawable.GradientDrawable
|
|
5
6
|
import android.graphics.drawable.ShapeDrawable
|
|
6
7
|
import android.graphics.drawable.shapes.RoundRectShape
|
|
7
8
|
import android.util.TypedValue
|
|
9
|
+
import android.view.Gravity
|
|
8
10
|
import android.view.MotionEvent
|
|
9
11
|
import android.view.View
|
|
10
12
|
import android.view.WindowManager
|
|
@@ -12,7 +14,6 @@ import android.view.accessibility.AccessibilityNodeInfo
|
|
|
12
14
|
import android.widget.FrameLayout
|
|
13
15
|
import androidx.core.view.isNotEmpty
|
|
14
16
|
import com.facebook.react.R
|
|
15
|
-
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
16
17
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
17
18
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
18
19
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
@@ -20,17 +21,15 @@ import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
|
20
21
|
import com.facebook.react.uimanager.RootView
|
|
21
22
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
22
23
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
24
|
+
import com.facebook.react.util.RNLog
|
|
23
25
|
import com.facebook.react.views.view.ReactViewGroup
|
|
24
26
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
25
27
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
28
|
+
import com.lodev09.truesheet.core.RNScreensFragmentObserver
|
|
26
29
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
27
30
|
|
|
28
31
|
data class DetentInfo(val index: Int, val position: Float)
|
|
29
32
|
|
|
30
|
-
/**
|
|
31
|
-
* Delegate protocol for TrueSheetViewController lifecycle and interaction events.
|
|
32
|
-
* Similar to iOS TrueSheetViewControllerDelegate pattern.
|
|
33
|
-
*/
|
|
34
33
|
interface TrueSheetViewControllerDelegate {
|
|
35
34
|
fun viewControllerWillPresent(index: Int, position: Float)
|
|
36
35
|
fun viewControllerDidPresent(index: Int, position: Float)
|
|
@@ -41,13 +40,12 @@ interface TrueSheetViewControllerDelegate {
|
|
|
41
40
|
fun viewControllerDidDragChange(index: Int, position: Float)
|
|
42
41
|
fun viewControllerDidDragEnd(index: Int, position: Float)
|
|
43
42
|
fun viewControllerDidChangePosition(index: Int, position: Float, transitioning: Boolean)
|
|
43
|
+
fun viewControllerDidChangeSize(width: Int, height: Int)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* TrueSheetViewController manages the bottom sheet dialog
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* This view acts as both the RootView (handles touch events) and the controller (manages dialog).
|
|
47
|
+
* TrueSheetViewController manages the bottom sheet dialog and its presentation lifecycle.
|
|
48
|
+
* This view also acts as a RootView to properly handle and dispatch touch events to React Native.
|
|
51
49
|
*/
|
|
52
50
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
53
51
|
class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
@@ -55,135 +53,84 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
55
53
|
RootView {
|
|
56
54
|
|
|
57
55
|
companion object {
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ==================== RootView Touch Handling ====================
|
|
56
|
+
const val TAG_NAME = "TrueSheet"
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
59
|
+
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
60
|
+
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
61
|
+
private const val GRABBER_WIDTH = 32f // dp
|
|
62
|
+
private const val GRABBER_HEIGHT = 4f // dp
|
|
63
|
+
private const val GRABBER_TOP_MARGIN = 16f // dp
|
|
64
|
+
private val GRABBER_COLOR = Color.argb((0.4 * 255).toInt(), 73, 69, 79) // #49454F @ 40%
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Gets the effective sheet height by subtracting headerHeight * 2.
|
|
68
|
+
* This is needed because both native layout and Yoga layout account for the header separately.
|
|
69
|
+
*/
|
|
70
|
+
fun getEffectiveSheetHeight(sheetHeight: Int, headerHeight: Int): Int = sheetHeight - headerHeight * 2
|
|
71
|
+
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
// ====================================================================
|
|
74
|
+
// MARK: - Delegate
|
|
75
|
+
// ====================================================================
|
|
70
76
|
|
|
71
|
-
/**
|
|
72
|
-
* Delegate for handling view controller events
|
|
73
|
-
*/
|
|
74
77
|
var delegate: TrueSheetViewControllerDelegate? = null
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// ====================================================================
|
|
80
|
+
// MARK: - Dialog & Views
|
|
81
|
+
// ====================================================================
|
|
82
|
+
|
|
79
83
|
private var dialog: BottomSheetDialog? = null
|
|
80
84
|
|
|
81
|
-
/**
|
|
82
|
-
* The sheet behavior from the dialog
|
|
83
|
-
*/
|
|
84
85
|
private val behavior: BottomSheetBehavior<FrameLayout>?
|
|
85
86
|
get() = dialog?.behavior
|
|
86
87
|
|
|
87
|
-
/**
|
|
88
|
-
* The sheet container view from Material BottomSheetDialog (our parent)
|
|
89
|
-
*/
|
|
90
88
|
private val sheetContainer: FrameLayout?
|
|
91
89
|
get() = this.parent as? FrameLayout
|
|
92
90
|
|
|
93
|
-
/**
|
|
94
|
-
* The actual bottom sheet view used by Material BottomSheetBehavior
|
|
95
|
-
* This is the view whose position changes during drag
|
|
96
|
-
*/
|
|
97
91
|
private val bottomSheetView: FrameLayout?
|
|
98
92
|
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
99
93
|
|
|
100
|
-
/**
|
|
101
|
-
* Our sheet container view from this root view's only child
|
|
102
|
-
*/
|
|
103
94
|
private val containerView: TrueSheetContainerView?
|
|
104
|
-
get() = if (this.isNotEmpty())
|
|
105
|
-
this.getChildAt(0) as? TrueSheetContainerView
|
|
106
|
-
} else {
|
|
107
|
-
null
|
|
108
|
-
}
|
|
95
|
+
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
109
96
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*/
|
|
113
|
-
private val footerView: TrueSheetFooterView?
|
|
114
|
-
get() = containerView?.footerView
|
|
97
|
+
private val contentHeight: Int
|
|
98
|
+
get() = containerView?.contentHeight ?: 0
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
100
|
+
private val headerHeight: Int
|
|
101
|
+
get() = containerView?.headerHeight ?: 0
|
|
102
|
+
|
|
103
|
+
// ====================================================================
|
|
104
|
+
// MARK: - State
|
|
105
|
+
// ====================================================================
|
|
120
106
|
|
|
121
|
-
/**
|
|
122
|
-
* Track if the sheet has been presented (after onShow callback)
|
|
123
|
-
*/
|
|
124
107
|
var isPresented = false
|
|
125
108
|
private set
|
|
126
109
|
|
|
127
|
-
private val edgeToEdgeEnabled: Boolean
|
|
128
|
-
get() {
|
|
129
|
-
// Auto-enable edge-to-edge for Android 16+ (API level 36) if not explicitly set
|
|
130
|
-
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
131
|
-
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Whether to allow the sheet to extend behind the status bar in edge-to-edge mode
|
|
136
|
-
*/
|
|
137
|
-
var edgeToEdgeFullScreen: Boolean = false
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Top inset to apply to sheet max height calculation (only when not edgeToEdgeFullScreen)
|
|
141
|
-
*/
|
|
142
|
-
private val sheetTopInset: Int
|
|
143
|
-
get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) ScreenUtils.getStatusBarHeight(reactContext) else 0
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Current active detent index
|
|
147
|
-
*/
|
|
148
110
|
var currentDetentIndex: Int = -1
|
|
149
111
|
private set
|
|
150
112
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
*/
|
|
154
|
-
var presentPromise: (() -> Unit)? = null
|
|
113
|
+
private var isDragging = false
|
|
114
|
+
private var windowAnimation: Int = 0
|
|
155
115
|
|
|
156
|
-
|
|
157
|
-
* Promise callback to be invoked after dismiss is called
|
|
158
|
-
*/
|
|
116
|
+
var presentPromise: (() -> Unit)? = null
|
|
159
117
|
var dismissPromise: (() -> Unit)? = null
|
|
160
118
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
* Specify whether the sheet background is dimmed.
|
|
165
|
-
* Set to `false` to allow interaction with the background components.
|
|
166
|
-
*/
|
|
167
|
-
var dimmed = true
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* The detent index that the sheet should start to dim the background.
|
|
171
|
-
* This is ignored if `dimmed` is set to `false`.
|
|
172
|
-
*/
|
|
173
|
-
var dimmedDetentIndex = 0
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* The maximum window height
|
|
177
|
-
*/
|
|
178
|
-
var maxScreenHeight = 0
|
|
119
|
+
// ====================================================================
|
|
120
|
+
// MARK: - Configuration Properties
|
|
121
|
+
// ====================================================================
|
|
179
122
|
|
|
123
|
+
var screenHeight = 0
|
|
124
|
+
var screenWidth = 0
|
|
180
125
|
var maxSheetHeight: Int? = null
|
|
126
|
+
var detents = mutableListOf(0.5, 1.0)
|
|
181
127
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
var
|
|
128
|
+
var dimmed = true
|
|
129
|
+
var dimmedDetentIndex = 0
|
|
130
|
+
var grabber: Boolean = true
|
|
131
|
+
var sheetCornerRadius: Float = -1f
|
|
132
|
+
var sheetBackgroundColor: Int = 0
|
|
133
|
+
var edgeToEdgeFullScreen: Boolean = false
|
|
187
134
|
|
|
188
135
|
var dismissible: Boolean = true
|
|
189
136
|
set(value) {
|
|
@@ -195,40 +142,66 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
195
142
|
}
|
|
196
143
|
}
|
|
197
144
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
private var windowAnimation: Int = 0
|
|
145
|
+
// ====================================================================
|
|
146
|
+
// MARK: - Computed Properties
|
|
147
|
+
// ====================================================================
|
|
203
148
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
jSPointerDispatcher = JSPointerDispatcher(this)
|
|
207
|
-
}
|
|
149
|
+
val statusBarHeight: Int
|
|
150
|
+
get() = ScreenUtils.getStatusBarHeight(reactContext)
|
|
208
151
|
|
|
209
152
|
/**
|
|
210
|
-
*
|
|
211
|
-
* Uses colorSurfaceContainerLow which adapts to light/dark mode.
|
|
153
|
+
* Edge-to-edge is enabled by default on API 36+, or when explicitly configured.
|
|
212
154
|
*/
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
typedValue,
|
|
218
|
-
true
|
|
219
|
-
)
|
|
220
|
-
) {
|
|
221
|
-
typedValue.data
|
|
222
|
-
} else {
|
|
223
|
-
Color.WHITE
|
|
155
|
+
private val edgeToEdgeEnabled: Boolean
|
|
156
|
+
get() {
|
|
157
|
+
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
158
|
+
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
224
159
|
}
|
|
225
|
-
}
|
|
226
160
|
|
|
227
|
-
|
|
161
|
+
/**
|
|
162
|
+
* The top inset to apply when edge-to-edge is enabled but not full-screen.
|
|
163
|
+
* This prevents the sheet from going under the status bar.
|
|
164
|
+
*/
|
|
165
|
+
private val sheetTopInset: Int
|
|
166
|
+
get() = if (edgeToEdgeEnabled && !edgeToEdgeFullScreen) statusBarHeight else 0
|
|
167
|
+
|
|
168
|
+
// ====================================================================
|
|
169
|
+
// MARK: - Touch Dispatchers
|
|
170
|
+
// ====================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Touch dispatchers are required for RootView to properly forward touch events to React Native.
|
|
174
|
+
*/
|
|
175
|
+
internal var eventDispatcher: EventDispatcher? = null
|
|
176
|
+
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
177
|
+
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
178
|
+
|
|
179
|
+
// ====================================================================
|
|
180
|
+
// MARK: - Modal Observer
|
|
181
|
+
// ====================================================================
|
|
228
182
|
|
|
229
183
|
/**
|
|
230
|
-
*
|
|
184
|
+
* Observes react-native-screens modal fragments to hide/show the sheet appropriately.
|
|
185
|
+
* This prevents the sheet from rendering on top of modals.
|
|
231
186
|
*/
|
|
187
|
+
private var rnScreensObserver: RNScreensFragmentObserver? = null
|
|
188
|
+
|
|
189
|
+
fun hasActiveModals(): Boolean = rnScreensObserver?.hasActiveModals() ?: false
|
|
190
|
+
|
|
191
|
+
// ====================================================================
|
|
192
|
+
// MARK: - Initialization
|
|
193
|
+
// ====================================================================
|
|
194
|
+
|
|
195
|
+
init {
|
|
196
|
+
screenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
197
|
+
screenWidth = ScreenUtils.getScreenWidth(reactContext)
|
|
198
|
+
jSPointerDispatcher = JSPointerDispatcher(this)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ====================================================================
|
|
202
|
+
// MARK: - Dialog Lifecycle
|
|
203
|
+
// ====================================================================
|
|
204
|
+
|
|
232
205
|
fun createDialog() {
|
|
233
206
|
if (dialog != null) return
|
|
234
207
|
|
|
@@ -241,31 +214,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
241
214
|
dialog = BottomSheetDialog(reactContext, style).apply {
|
|
242
215
|
setContentView(this@TrueSheetViewController)
|
|
243
216
|
|
|
244
|
-
// Setup window params
|
|
245
217
|
window?.apply {
|
|
246
|
-
// Store current windowAnimation value to toggle later
|
|
247
218
|
windowAnimation = attributes.windowAnimations
|
|
248
219
|
}
|
|
249
220
|
|
|
250
|
-
|
|
221
|
+
setupModalObserver()
|
|
251
222
|
setupDialogListeners(this)
|
|
252
|
-
|
|
253
|
-
// Setup bottom sheet behavior callbacks
|
|
254
223
|
setupBottomSheetBehavior(this)
|
|
255
224
|
|
|
256
|
-
// Apply initial properties
|
|
257
225
|
setCanceledOnTouchOutside(dismissible)
|
|
258
226
|
setCancelable(dismissible)
|
|
259
227
|
behavior.isHideable = dismissible
|
|
260
|
-
|
|
261
|
-
// Apply background color and corner radius
|
|
262
|
-
setupBackground()
|
|
263
228
|
}
|
|
264
229
|
}
|
|
265
230
|
|
|
266
|
-
/**
|
|
267
|
-
* Cleans up the dialog instance. Called when dismissed to ensure clean state.
|
|
268
|
-
*/
|
|
269
231
|
private fun cleanupDialog() {
|
|
270
232
|
dialog?.apply {
|
|
271
233
|
setOnShowListener(null)
|
|
@@ -273,7 +235,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
273
235
|
setOnDismissListener(null)
|
|
274
236
|
}
|
|
275
237
|
|
|
276
|
-
|
|
238
|
+
cleanupModalObserver()
|
|
277
239
|
sheetContainer?.removeView(this)
|
|
278
240
|
|
|
279
241
|
dialog = null
|
|
@@ -281,89 +243,56 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
281
243
|
isPresented = false
|
|
282
244
|
}
|
|
283
245
|
|
|
284
|
-
/**
|
|
285
|
-
* Setup dialog lifecycle listeners
|
|
286
|
-
*/
|
|
287
246
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
288
|
-
// Setup listener when the dialog has been presented
|
|
289
247
|
dialog.setOnShowListener {
|
|
290
248
|
isPresented = true
|
|
291
|
-
|
|
292
|
-
// Re-enable animation
|
|
293
249
|
resetAnimation()
|
|
250
|
+
setupBackground()
|
|
251
|
+
setupGrabber()
|
|
294
252
|
|
|
295
|
-
// Wait for the sheet to settle before notifying didPresent
|
|
296
|
-
// The sheet animates to its final position after onShow fires
|
|
297
253
|
sheetContainer?.post {
|
|
298
|
-
// Notify delegate with the settled position
|
|
299
254
|
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
300
255
|
delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position)
|
|
301
|
-
|
|
302
|
-
// Emit position change with transitioning=true so Reanimated can animate it
|
|
303
256
|
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = true)
|
|
304
257
|
|
|
305
|
-
|
|
306
|
-
presentPromise
|
|
307
|
-
promise()
|
|
308
|
-
presentPromise = null
|
|
309
|
-
}
|
|
258
|
+
presentPromise?.invoke()
|
|
259
|
+
presentPromise = null
|
|
310
260
|
|
|
311
|
-
// Initialize footer position after layout is complete
|
|
312
261
|
positionFooter()
|
|
313
262
|
}
|
|
314
263
|
}
|
|
315
264
|
|
|
316
|
-
// Setup listener when the dialog is about to be dismissed
|
|
317
265
|
dialog.setOnCancelListener {
|
|
318
|
-
// Notify delegate
|
|
319
266
|
delegate?.viewControllerWillDismiss()
|
|
320
267
|
}
|
|
321
268
|
|
|
322
|
-
// Setup listener when the dialog has been dismissed
|
|
323
269
|
dialog.setOnDismissListener {
|
|
324
|
-
|
|
325
|
-
dismissPromise
|
|
326
|
-
promise()
|
|
327
|
-
dismissPromise = null
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Notify delegate
|
|
270
|
+
dismissPromise?.invoke()
|
|
271
|
+
dismissPromise = null
|
|
331
272
|
delegate?.viewControllerDidDismiss()
|
|
332
|
-
|
|
333
|
-
// Clean up the dialog for next presentation
|
|
334
273
|
cleanupDialog()
|
|
335
274
|
}
|
|
336
275
|
}
|
|
337
276
|
|
|
338
|
-
/**
|
|
339
|
-
* Setup bottom sheet behavior callbacks
|
|
340
|
-
*/
|
|
341
277
|
private fun setupBottomSheetBehavior(dialog: BottomSheetDialog) {
|
|
342
278
|
dialog.behavior.addBottomSheetCallback(
|
|
343
279
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
|
344
280
|
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
345
281
|
val behavior = behavior ?: return
|
|
346
|
-
|
|
347
|
-
// Emit position change event continuously during slide
|
|
348
|
-
// Set transitioning=false during drag to get real-time position updates
|
|
349
282
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
350
283
|
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = false)
|
|
351
284
|
|
|
352
285
|
when (behavior.state) {
|
|
353
|
-
// For consistency with iOS, we consider SETTLING as dragging change
|
|
354
286
|
BottomSheetBehavior.STATE_DRAGGING,
|
|
355
287
|
BottomSheetBehavior.STATE_SETTLING -> handleDragChange(sheetView)
|
|
356
288
|
|
|
357
289
|
else -> { }
|
|
358
290
|
}
|
|
359
291
|
|
|
360
|
-
// Update footer position during slide
|
|
361
292
|
positionFooter(slideOffset)
|
|
362
293
|
}
|
|
363
294
|
|
|
364
295
|
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
365
|
-
// Handle STATE_HIDDEN before checking isPresented
|
|
366
|
-
// This ensures we can dismiss even if dialog state gets out of sync
|
|
367
296
|
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
|
368
297
|
dismiss()
|
|
369
298
|
return
|
|
@@ -372,10 +301,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
372
301
|
if (!isPresented) return
|
|
373
302
|
|
|
374
303
|
when (newState) {
|
|
375
|
-
// When changed to dragging, we know that the drag has started
|
|
376
304
|
BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
|
|
377
305
|
|
|
378
|
-
// Either of the following state determines drag end
|
|
379
306
|
BottomSheetBehavior.STATE_EXPANDED,
|
|
380
307
|
BottomSheetBehavior.STATE_COLLAPSED,
|
|
381
308
|
BottomSheetBehavior.STATE_HALF_EXPANDED -> handleDragEnd(newState)
|
|
@@ -387,14 +314,35 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
387
314
|
)
|
|
388
315
|
}
|
|
389
316
|
|
|
390
|
-
|
|
317
|
+
private fun setupModalObserver() {
|
|
318
|
+
rnScreensObserver = RNScreensFragmentObserver(
|
|
319
|
+
reactContext = reactContext,
|
|
320
|
+
onModalPresented = {
|
|
321
|
+
if (isPresented) {
|
|
322
|
+
dialog?.window?.decorView?.visibility = View.INVISIBLE
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
onModalDismissed = {
|
|
326
|
+
if (isPresented) {
|
|
327
|
+
dialog?.window?.decorView?.visibility = View.VISIBLE
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
rnScreensObserver?.start()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private fun cleanupModalObserver() {
|
|
335
|
+
rnScreensObserver?.stop()
|
|
336
|
+
rnScreensObserver = null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ====================================================================
|
|
340
|
+
// MARK: - Presentation
|
|
341
|
+
// ====================================================================
|
|
391
342
|
|
|
392
|
-
/**
|
|
393
|
-
* Present the sheet.
|
|
394
|
-
*/
|
|
395
343
|
fun present(detentIndex: Int, animated: Boolean = true) {
|
|
396
344
|
val dialog = this.dialog ?: run {
|
|
397
|
-
|
|
345
|
+
RNLog.w(reactContext, "TrueSheet: No dialog available. Ensure the sheet is mounted before presenting.")
|
|
398
346
|
return
|
|
399
347
|
}
|
|
400
348
|
|
|
@@ -402,26 +350,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
402
350
|
setupDimmedBackground(detentIndex)
|
|
403
351
|
|
|
404
352
|
if (isPresented) {
|
|
405
|
-
// For consistency with iOS, we notify detent change immediately
|
|
406
|
-
// when already presented (not waiting for state to change)
|
|
407
353
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
408
354
|
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position)
|
|
409
|
-
|
|
410
|
-
// Note: onSlide will be called during resize animation, no need to emit position change here
|
|
411
355
|
setStateForDetentIndex(detentIndex)
|
|
412
356
|
} else {
|
|
413
|
-
// Reset drag state before presenting
|
|
414
357
|
isDragging = false
|
|
415
|
-
|
|
416
358
|
setupSheetDetents()
|
|
417
359
|
setStateForDetentIndex(detentIndex)
|
|
418
360
|
|
|
419
|
-
// Notify delegate before showing
|
|
420
361
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
421
362
|
delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position)
|
|
422
363
|
|
|
423
364
|
if (!animated) {
|
|
424
|
-
// Disable animation
|
|
425
365
|
dialog.window?.setWindowAnimations(0)
|
|
426
366
|
}
|
|
427
367
|
|
|
@@ -429,67 +369,46 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
429
369
|
}
|
|
430
370
|
}
|
|
431
371
|
|
|
432
|
-
/**
|
|
433
|
-
* Dismiss the sheet.
|
|
434
|
-
*/
|
|
435
372
|
fun dismiss() {
|
|
436
373
|
this.post {
|
|
437
|
-
|
|
438
|
-
// Use maxScreenHeight as the off-screen position (sheet slides down off screen)
|
|
439
|
-
val offScreenPosition = maxScreenHeight.pxToDp()
|
|
374
|
+
val offScreenPosition = screenHeight.pxToDp()
|
|
440
375
|
delegate?.viewControllerDidChangePosition(currentDetentIndex, offScreenPosition, transitioning = true)
|
|
441
376
|
}
|
|
442
|
-
|
|
443
377
|
dialog?.dismiss()
|
|
444
378
|
}
|
|
445
379
|
|
|
446
|
-
//
|
|
380
|
+
// ====================================================================
|
|
381
|
+
// MARK: - Sheet Configuration
|
|
382
|
+
// ====================================================================
|
|
447
383
|
|
|
448
|
-
/**
|
|
449
|
-
* Setup sheet detents based on the detent preference.
|
|
450
|
-
*/
|
|
451
384
|
fun setupSheetDetents() {
|
|
452
385
|
val behavior = this.behavior ?: return
|
|
453
386
|
|
|
454
|
-
// Configure sheet sizes
|
|
455
387
|
behavior.apply {
|
|
456
388
|
skipCollapsed = false
|
|
457
389
|
isFitToContents = true
|
|
458
|
-
|
|
459
|
-
// m3 max width 640dp
|
|
460
|
-
maxWidth = 640.0.dpToPx().toInt()
|
|
390
|
+
maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
461
391
|
|
|
462
392
|
when (detents.size) {
|
|
463
393
|
1 -> {
|
|
464
|
-
|
|
465
|
-
maxHeight = detentHeight
|
|
394
|
+
maxHeight = getDetentHeight(detents[0])
|
|
466
395
|
skipCollapsed = true
|
|
467
396
|
}
|
|
468
397
|
|
|
469
398
|
2 -> {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
maxHeight = maxHeightValue
|
|
473
|
-
setPeekHeight(peekHeight, isPresented)
|
|
399
|
+
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
400
|
+
maxHeight = getDetentHeight(detents[1])
|
|
474
401
|
}
|
|
475
402
|
|
|
476
403
|
3 -> {
|
|
477
|
-
// Enables half expanded
|
|
478
404
|
isFitToContents = false
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
val middleDetentHeight = getDetentHeight(detents[1])
|
|
482
|
-
val maxHeightValue = getDetentHeight(detents[2])
|
|
483
|
-
|
|
484
|
-
setPeekHeight(peekHeightValue, isPresented)
|
|
485
|
-
maxHeight = maxHeightValue
|
|
405
|
+
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
406
|
+
maxHeight = getDetentHeight(detents[2])
|
|
486
407
|
expandedOffset = sheetTopInset
|
|
487
|
-
halfExpandedRatio = minOf(
|
|
408
|
+
halfExpandedRatio = minOf(getDetentHeight(detents[1]).toFloat() / screenHeight.toFloat(), 1.0f)
|
|
488
409
|
}
|
|
489
410
|
}
|
|
490
411
|
|
|
491
|
-
// Force a layout update when sheet is presented (e.g., during rotation)
|
|
492
|
-
// This ensures the container respects the new maxHeight constraint
|
|
493
412
|
if (isPresented) {
|
|
494
413
|
sheetContainer?.apply {
|
|
495
414
|
val params = layoutParams
|
|
@@ -500,36 +419,51 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
500
419
|
}
|
|
501
420
|
}
|
|
502
421
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
)
|
|
422
|
+
fun setupGrabber() {
|
|
423
|
+
val bottomSheet = bottomSheetView ?: return
|
|
424
|
+
|
|
425
|
+
bottomSheet.findViewWithTag<View>(GRABBER_TAG)?.let {
|
|
426
|
+
bottomSheet.removeView(it)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!grabber) return
|
|
430
|
+
|
|
431
|
+
val grabberView = View(reactContext).apply {
|
|
432
|
+
tag = GRABBER_TAG
|
|
433
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
434
|
+
GRABBER_WIDTH.dpToPx().toInt(),
|
|
435
|
+
GRABBER_HEIGHT.dpToPx().toInt()
|
|
436
|
+
).apply {
|
|
437
|
+
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
|
438
|
+
topMargin = GRABBER_TOP_MARGIN.dpToPx().toInt()
|
|
439
|
+
}
|
|
440
|
+
background = GradientDrawable().apply {
|
|
441
|
+
shape = GradientDrawable.RECTANGLE
|
|
442
|
+
cornerRadius = (GRABBER_HEIGHT / 2).dpToPx()
|
|
443
|
+
setColor(GRABBER_COLOR)
|
|
444
|
+
}
|
|
445
|
+
elevation = 1f
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
bottomSheet.addView(grabberView)
|
|
449
|
+
}
|
|
518
450
|
|
|
519
|
-
|
|
520
|
-
|
|
451
|
+
fun setupBackground() {
|
|
452
|
+
val bottomSheet = bottomSheetView ?: return
|
|
521
453
|
|
|
522
|
-
|
|
523
|
-
|
|
454
|
+
val cornerRadius = if (sheetCornerRadius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else sheetCornerRadius
|
|
455
|
+
val outerRadii = floatArrayOf(cornerRadius, cornerRadius, cornerRadius, cornerRadius, 0f, 0f, 0f, 0f)
|
|
456
|
+
val backgroundColor = if (sheetBackgroundColor != 0) sheetBackgroundColor else getDefaultBackgroundColor()
|
|
524
457
|
|
|
525
|
-
|
|
526
|
-
|
|
458
|
+
bottomSheet.background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
|
|
459
|
+
paint.color = backgroundColor
|
|
527
460
|
}
|
|
461
|
+
bottomSheet.clipToOutline = true
|
|
528
462
|
}
|
|
529
463
|
|
|
530
464
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
465
|
+
* Configures the dimmed background based on the current detent index.
|
|
466
|
+
* When not dimmed, touch events pass through to the activity behind the sheet.
|
|
533
467
|
*/
|
|
534
468
|
fun setupDimmedBackground(detentIndex: Int) {
|
|
535
469
|
val dialog = this.dialog ?: return
|
|
@@ -537,47 +471,39 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
537
471
|
val view = findViewById<View>(com.google.android.material.R.id.touch_outside)
|
|
538
472
|
|
|
539
473
|
if (dimmed && detentIndex >= dimmedDetentIndex) {
|
|
540
|
-
// Remove touch listener
|
|
541
474
|
view.setOnTouchListener(null)
|
|
542
|
-
|
|
543
|
-
// Add the dimmed background
|
|
544
|
-
setFlags(
|
|
545
|
-
WindowManager.LayoutParams.FLAG_DIM_BEHIND,
|
|
546
|
-
WindowManager.LayoutParams.FLAG_DIM_BEHIND
|
|
547
|
-
)
|
|
548
|
-
|
|
475
|
+
setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
549
476
|
dialog.setCanceledOnTouchOutside(dismissible)
|
|
550
477
|
} else {
|
|
551
|
-
//
|
|
478
|
+
// Forward touch events to the activity when not dimmed
|
|
552
479
|
view.setOnTouchListener { v, event ->
|
|
553
480
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
554
481
|
reactContext.currentActivity?.dispatchTouchEvent(event)
|
|
555
482
|
false
|
|
556
483
|
}
|
|
557
|
-
|
|
558
|
-
// Remove the dimmed background
|
|
559
484
|
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
560
|
-
|
|
561
485
|
dialog.setCanceledOnTouchOutside(false)
|
|
562
486
|
}
|
|
563
487
|
}
|
|
564
488
|
}
|
|
565
489
|
|
|
566
490
|
fun resetAnimation() {
|
|
567
|
-
dialog?.window?.
|
|
568
|
-
setWindowAnimations(windowAnimation)
|
|
569
|
-
}
|
|
491
|
+
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
570
492
|
}
|
|
571
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Positions the footer view at the bottom of the sheet.
|
|
496
|
+
* The footer stays fixed at the bottom edge of the visible sheet area,
|
|
497
|
+
* adjusting during drag gestures via slideOffset.
|
|
498
|
+
*/
|
|
572
499
|
fun positionFooter(slideOffset: Float? = null) {
|
|
573
|
-
val
|
|
500
|
+
val footerView = containerView?.footerView ?: return
|
|
574
501
|
val bottomSheet = bottomSheetView ?: return
|
|
575
|
-
val footerHeight = footer.height
|
|
576
502
|
|
|
503
|
+
val footerHeight = footerView.height
|
|
577
504
|
val bottomSheetY = ScreenUtils.getScreenY(bottomSheet)
|
|
578
505
|
|
|
579
|
-
|
|
580
|
-
var footerY = (maxScreenHeight - bottomSheetY - footerHeight).toFloat()
|
|
506
|
+
var footerY = (screenHeight - bottomSheetY - footerHeight).toFloat()
|
|
581
507
|
|
|
582
508
|
// Animate footer down with sheet when below peek height
|
|
583
509
|
if (slideOffset != null && slideOffset < 0) {
|
|
@@ -586,14 +512,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
586
512
|
|
|
587
513
|
// Clamp footer position to prevent it from going off screen when positioning at the top
|
|
588
514
|
// This happens when fullScreen is enabled in edge-to-edge mode
|
|
589
|
-
val
|
|
590
|
-
|
|
591
|
-
footer.y = minOf(footerY, maxAllowedY)
|
|
515
|
+
val maxAllowedY = (screenHeight - statusBarHeight - footerHeight).toFloat()
|
|
516
|
+
footerView.y = minOf(footerY, maxAllowedY)
|
|
592
517
|
}
|
|
593
518
|
|
|
594
|
-
/**
|
|
595
|
-
* Set the state based for the given detent index.
|
|
596
|
-
*/
|
|
597
519
|
fun setStateForDetentIndex(index: Int) {
|
|
598
520
|
behavior?.state = getStateForDetentIndex(index)
|
|
599
521
|
}
|
|
@@ -602,62 +524,54 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
602
524
|
dialog?.window?.setSoftInputMode(mode)
|
|
603
525
|
}
|
|
604
526
|
|
|
605
|
-
|
|
527
|
+
fun getDefaultBackgroundColor(): Int {
|
|
528
|
+
val typedValue = TypedValue()
|
|
529
|
+
return if (reactContext.theme.resolveAttribute(
|
|
530
|
+
com.google.android.material.R.attr.colorSurfaceContainerLow,
|
|
531
|
+
typedValue,
|
|
532
|
+
true
|
|
533
|
+
)
|
|
534
|
+
) {
|
|
535
|
+
typedValue.data
|
|
536
|
+
} else {
|
|
537
|
+
Color.WHITE
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ====================================================================
|
|
542
|
+
// MARK: - Drag Handling
|
|
543
|
+
// ====================================================================
|
|
606
544
|
|
|
607
|
-
/**
|
|
608
|
-
* Get current detent info from sheet view position
|
|
609
|
-
*/
|
|
610
545
|
private fun getCurrentDetentInfo(sheetView: View): DetentInfo {
|
|
611
|
-
// Get the Y position in screen coordinates (like iOS presentedView.frame.origin.y)
|
|
612
546
|
val screenY = ScreenUtils.getScreenY(sheetView)
|
|
613
|
-
|
|
614
|
-
return DetentInfo(currentDetentIndex, position)
|
|
547
|
+
return DetentInfo(currentDetentIndex, screenY.pxToDp())
|
|
615
548
|
}
|
|
616
549
|
|
|
617
|
-
/**
|
|
618
|
-
* Handle drag begin
|
|
619
|
-
*/
|
|
620
550
|
private fun handleDragBegin(sheetView: View) {
|
|
621
551
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
622
552
|
delegate?.viewControllerDidDragBegin(detentInfo.index, detentInfo.position)
|
|
623
553
|
isDragging = true
|
|
624
554
|
}
|
|
625
555
|
|
|
626
|
-
/**
|
|
627
|
-
* Handle drag change
|
|
628
|
-
*/
|
|
629
556
|
private fun handleDragChange(sheetView: View) {
|
|
630
557
|
if (!isDragging) return
|
|
631
|
-
|
|
632
558
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
633
559
|
delegate?.viewControllerDidDragChange(detentInfo.index, detentInfo.position)
|
|
634
560
|
}
|
|
635
561
|
|
|
636
|
-
/**
|
|
637
|
-
* Handle drag end
|
|
638
|
-
*/
|
|
639
562
|
private fun handleDragEnd(state: Int) {
|
|
640
563
|
if (!isDragging) return
|
|
641
564
|
|
|
642
|
-
// For consistency with iOS,
|
|
643
|
-
// we only handle state changes after dragging.
|
|
644
|
-
//
|
|
645
|
-
// Changing detent programmatically is handled via the present method.
|
|
646
565
|
val detentInfo = getDetentInfoForState(state)
|
|
647
566
|
detentInfo?.let {
|
|
648
|
-
// Notify delegate of drag end
|
|
649
567
|
delegate?.viewControllerDidDragEnd(it.index, it.position)
|
|
650
568
|
|
|
651
569
|
if (it.index != currentDetentIndex) {
|
|
652
|
-
presentPromise?.
|
|
653
|
-
|
|
654
|
-
presentPromise = null
|
|
655
|
-
}
|
|
570
|
+
presentPromise?.invoke()
|
|
571
|
+
presentPromise = null
|
|
656
572
|
|
|
657
573
|
currentDetentIndex = it.index
|
|
658
574
|
setupDimmedBackground(it.index)
|
|
659
|
-
|
|
660
|
-
// Notify delegate of detent change
|
|
661
575
|
delegate?.viewControllerDidChangeDetent(it.index, it.position)
|
|
662
576
|
}
|
|
663
577
|
}
|
|
@@ -665,162 +579,105 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
665
579
|
isDragging = false
|
|
666
580
|
}
|
|
667
581
|
|
|
668
|
-
//
|
|
582
|
+
// ====================================================================
|
|
583
|
+
// MARK: - Detent Calculations
|
|
584
|
+
// ====================================================================
|
|
669
585
|
|
|
670
|
-
/**
|
|
671
|
-
* Get the height value based on the detent config value.
|
|
672
|
-
*/
|
|
673
586
|
private fun getDetentHeight(detent: Double): Int {
|
|
674
587
|
val height: Int = if (detent == -1.0) {
|
|
675
|
-
|
|
676
|
-
contentHeight
|
|
588
|
+
contentHeight + headerHeight
|
|
677
589
|
} else {
|
|
678
590
|
if (detent <= 0.0 || detent > 1.0) {
|
|
679
591
|
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
680
592
|
}
|
|
681
|
-
(detent *
|
|
593
|
+
(detent * screenHeight).toInt()
|
|
682
594
|
}
|
|
683
595
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
val finalHeight = maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
687
|
-
return finalHeight
|
|
596
|
+
val maxAllowedHeight = screenHeight - sheetTopInset
|
|
597
|
+
return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
688
598
|
}
|
|
689
599
|
|
|
690
|
-
/**
|
|
691
|
-
* Determines the state based from the given detent index.
|
|
692
|
-
*/
|
|
693
600
|
private fun getStateForDetentIndex(index: Int): Int =
|
|
694
601
|
when (detents.size) {
|
|
695
|
-
1 ->
|
|
696
|
-
BottomSheetBehavior.STATE_EXPANDED
|
|
697
|
-
}
|
|
602
|
+
1 -> BottomSheetBehavior.STATE_EXPANDED
|
|
698
603
|
|
|
699
|
-
2 -> {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
704
|
-
}
|
|
604
|
+
2 -> when (index) {
|
|
605
|
+
0 -> BottomSheetBehavior.STATE_COLLAPSED
|
|
606
|
+
1 -> BottomSheetBehavior.STATE_EXPANDED
|
|
607
|
+
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
705
608
|
}
|
|
706
609
|
|
|
707
|
-
3 -> {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
713
|
-
}
|
|
610
|
+
3 -> when (index) {
|
|
611
|
+
0 -> BottomSheetBehavior.STATE_COLLAPSED
|
|
612
|
+
1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
613
|
+
2 -> BottomSheetBehavior.STATE_EXPANDED
|
|
614
|
+
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
714
615
|
}
|
|
715
616
|
|
|
716
617
|
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
717
618
|
}
|
|
718
619
|
|
|
719
|
-
/**
|
|
720
|
-
* Get the DetentInfo data by state.
|
|
721
|
-
*/
|
|
722
620
|
fun getDetentInfoForState(state: Int): DetentInfo? =
|
|
723
621
|
when (detents.size) {
|
|
724
|
-
1 -> {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
622
|
+
1 -> when (state) {
|
|
623
|
+
BottomSheetBehavior.STATE_COLLAPSED,
|
|
624
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
625
|
+
|
|
626
|
+
else -> null
|
|
730
627
|
}
|
|
731
628
|
|
|
732
|
-
2 -> {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
else -> null
|
|
737
|
-
}
|
|
629
|
+
2 -> when (state) {
|
|
630
|
+
BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
631
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
|
|
632
|
+
else -> null
|
|
738
633
|
}
|
|
739
634
|
|
|
740
|
-
3 -> {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
else -> null
|
|
746
|
-
}
|
|
635
|
+
3 -> when (state) {
|
|
636
|
+
BottomSheetBehavior.STATE_COLLAPSED -> DetentInfo(0, getPositionForDetentIndex(0))
|
|
637
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED -> DetentInfo(1, getPositionForDetentIndex(1))
|
|
638
|
+
BottomSheetBehavior.STATE_EXPANDED -> DetentInfo(2, getPositionForDetentIndex(2))
|
|
639
|
+
else -> null
|
|
747
640
|
}
|
|
748
641
|
|
|
749
642
|
else -> null
|
|
750
643
|
}
|
|
751
644
|
|
|
752
|
-
/**
|
|
753
|
-
* Calculate the expected Y position for a given detent index.
|
|
754
|
-
* Uses actual screen position if available, otherwise calculates based on screen height.
|
|
755
|
-
*/
|
|
756
645
|
private fun getPositionForDetentIndex(index: Int): Float {
|
|
757
|
-
if (index < 0 || index >= detents.size)
|
|
758
|
-
return 0f
|
|
759
|
-
}
|
|
646
|
+
if (index < 0 || index >= detents.size) return 0f
|
|
760
647
|
|
|
761
|
-
// Try to get actual position from bottom sheet view first (same view used in behavior callbacks)
|
|
762
648
|
bottomSheetView?.let {
|
|
763
|
-
it
|
|
764
649
|
val screenY = ScreenUtils.getScreenY(it)
|
|
765
|
-
|
|
766
|
-
if (screenY > 0) {
|
|
767
|
-
return screenY.pxToDp()
|
|
768
|
-
}
|
|
650
|
+
if (screenY > 0) return screenY.pxToDp()
|
|
769
651
|
}
|
|
770
652
|
|
|
771
|
-
// Fallback: calculate expected position
|
|
772
653
|
val detentHeight = getDetentHeight(detents[index])
|
|
773
|
-
|
|
774
|
-
// Position calculation is simple: screen height - sheet height
|
|
775
|
-
// In both edge-to-edge and non-edge-to-edge modes, getScreenY returns
|
|
776
|
-
// coordinates in screen space, and maxScreenHeight represents the available height
|
|
777
|
-
// for the sheet, so the calculation is the same
|
|
778
|
-
val positionPx = maxScreenHeight - detentHeight
|
|
779
|
-
|
|
780
|
-
return positionPx.pxToDp()
|
|
654
|
+
return (screenHeight - detentHeight).pxToDp()
|
|
781
655
|
}
|
|
782
656
|
|
|
783
|
-
/**
|
|
784
|
-
* Get DetentInfo data for given detent index.
|
|
785
|
-
*/
|
|
786
657
|
fun getDetentInfoForIndex(index: Int) = getDetentInfoForState(getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
|
|
787
658
|
|
|
788
|
-
//
|
|
659
|
+
// ====================================================================
|
|
660
|
+
// MARK: - RootView Implementation
|
|
661
|
+
// ====================================================================
|
|
789
662
|
|
|
790
663
|
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
|
|
791
664
|
super.onInitializeAccessibilityNodeInfo(info)
|
|
792
|
-
|
|
793
|
-
val testId = getTag(R.id.react_test_id) as String?
|
|
794
|
-
if (testId != null) {
|
|
795
|
-
info.viewIdResourceName = testId
|
|
796
|
-
}
|
|
665
|
+
(getTag(R.id.react_test_id) as? String)?.let { info.viewIdResourceName = it }
|
|
797
666
|
}
|
|
798
667
|
|
|
799
668
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
800
669
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
801
|
-
viewWidth = w
|
|
802
|
-
viewHeight = h
|
|
803
|
-
|
|
804
|
-
// Update container state with new dimensions
|
|
805
|
-
containerView?.updateState(viewWidth, viewHeight)
|
|
806
|
-
|
|
807
|
-
// Only proceed if size actually changed
|
|
808
670
|
if (w == oldw && h == oldh) return
|
|
809
671
|
|
|
810
|
-
|
|
811
|
-
val oldMaxScreenHeight = maxScreenHeight
|
|
812
|
-
maxScreenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
672
|
+
delegate?.viewControllerDidChangeSize(w, getEffectiveSheetHeight(h, headerHeight))
|
|
813
673
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
// Recalculate sheet detents with new screen dimensions
|
|
817
|
-
setupSheetDetents()
|
|
674
|
+
val oldScreenHeight = screenHeight
|
|
675
|
+
screenHeight = ScreenUtils.getScreenHeight(reactContext, edgeToEdgeEnabled)
|
|
818
676
|
|
|
677
|
+
if (isPresented && oldScreenHeight != screenHeight && oldScreenHeight > 0) {
|
|
678
|
+
setupSheetDetents()
|
|
819
679
|
this.post {
|
|
820
|
-
// Update footer position after rotation
|
|
821
680
|
positionFooter()
|
|
822
|
-
|
|
823
|
-
// Notify JS about position change after rotation settles
|
|
824
681
|
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
825
682
|
delegate?.viewControllerDidChangePosition(detentInfo.index, detentInfo.position, transitioning = true)
|
|
826
683
|
}
|
|
@@ -831,22 +688,54 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
831
688
|
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
832
689
|
}
|
|
833
690
|
|
|
691
|
+
// ====================================================================
|
|
692
|
+
// MARK: - Touch Event Handling
|
|
693
|
+
// ====================================================================
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Custom touch dispatch to handle footer touch events.
|
|
697
|
+
* The footer is positioned outside the normal view hierarchy, so we need to
|
|
698
|
+
* manually check if touches fall within its bounds and forward them.
|
|
699
|
+
*/
|
|
700
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
701
|
+
val footer = containerView?.footerView
|
|
702
|
+
if (footer != null && footer.visibility == View.VISIBLE) {
|
|
703
|
+
val footerLocation = ScreenUtils.getScreenLocation(footer)
|
|
704
|
+
val touchScreenX = event.rawX.toInt()
|
|
705
|
+
val touchScreenY = event.rawY.toInt()
|
|
706
|
+
|
|
707
|
+
if (touchScreenX >= footerLocation[0] &&
|
|
708
|
+
touchScreenX <= footerLocation[0] + footer.width &&
|
|
709
|
+
touchScreenY >= footerLocation[1] &&
|
|
710
|
+
touchScreenY <= footerLocation[1] + footer.height
|
|
711
|
+
) {
|
|
712
|
+
val localEvent = MotionEvent.obtain(event)
|
|
713
|
+
localEvent.setLocation(
|
|
714
|
+
(touchScreenX - footerLocation[0]).toFloat(),
|
|
715
|
+
(touchScreenY - footerLocation[1]).toFloat()
|
|
716
|
+
)
|
|
717
|
+
val handled = footer.dispatchTouchEvent(localEvent)
|
|
718
|
+
localEvent.recycle()
|
|
719
|
+
if (handled) return true
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return super.dispatchTouchEvent(event)
|
|
723
|
+
}
|
|
724
|
+
|
|
834
725
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
835
|
-
eventDispatcher?.let {
|
|
836
|
-
jSTouchDispatcher.handleTouchEvent(event,
|
|
837
|
-
jSPointerDispatcher?.handleMotionEvent(event,
|
|
726
|
+
eventDispatcher?.let {
|
|
727
|
+
jSTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
728
|
+
jSPointerDispatcher?.handleMotionEvent(event, it, true)
|
|
838
729
|
}
|
|
839
730
|
return super.onInterceptTouchEvent(event)
|
|
840
731
|
}
|
|
841
732
|
|
|
842
733
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
843
|
-
eventDispatcher?.let {
|
|
844
|
-
jSTouchDispatcher.handleTouchEvent(event,
|
|
845
|
-
jSPointerDispatcher?.handleMotionEvent(event,
|
|
734
|
+
eventDispatcher?.let {
|
|
735
|
+
jSTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
736
|
+
jSPointerDispatcher?.handleMotionEvent(event, it, false)
|
|
846
737
|
}
|
|
847
738
|
super.onTouchEvent(event)
|
|
848
|
-
// In case when there is no children interested in handling touch event, we return true from
|
|
849
|
-
// the root view in order to receive subsequent events related to that gesture
|
|
850
739
|
return true
|
|
851
740
|
}
|
|
852
741
|
|
|
@@ -860,12 +749,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
860
749
|
return super.onHoverEvent(event)
|
|
861
750
|
}
|
|
862
751
|
|
|
863
|
-
@OptIn(UnstableReactNativeAPI::class)
|
|
864
|
-
@Suppress("DEPRECATION")
|
|
865
752
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
866
|
-
eventDispatcher?.let {
|
|
867
|
-
jSTouchDispatcher.onChildStartedNativeGesture(ev,
|
|
868
|
-
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev,
|
|
753
|
+
eventDispatcher?.let {
|
|
754
|
+
jSTouchDispatcher.onChildStartedNativeGesture(ev, it)
|
|
755
|
+
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
|
|
869
756
|
}
|
|
870
757
|
}
|
|
871
758
|
|
|
@@ -875,7 +762,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
875
762
|
}
|
|
876
763
|
|
|
877
764
|
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
878
|
-
// Allow the request to propagate to parent
|
|
879
765
|
super.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
880
766
|
}
|
|
881
767
|
}
|