@lodev09/react-native-true-sheet 3.0.0-beta.12 → 3.0.0-beta.13
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 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +3 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +33 -45
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +151 -22
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogObserver.kt +93 -0
- package/android/src/main/java/com/lodev09/truesheet/events/BlurEvent.kt +20 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/events/FocusEvent.kt +20 -0
- package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +5 -3
- package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +2 -1
- package/android/src/main/res/anim/true_sheet_slide_in.xml +13 -0
- package/android/src/main/res/anim/true_sheet_slide_out.xml +13 -0
- package/android/src/main/res/values/styles.xml +13 -1
- package/ios/TrueSheetView.mm +26 -8
- package/ios/TrueSheetViewController.h +7 -1
- package/ios/TrueSheetViewController.mm +110 -40
- package/ios/events/OnDetentChangeEvent.h +2 -1
- package/ios/events/OnDetentChangeEvent.mm +3 -1
- package/ios/events/OnDidBlurEvent.h +26 -0
- package/ios/events/OnDidBlurEvent.mm +25 -0
- package/ios/events/OnDidFocusEvent.h +26 -0
- package/ios/events/OnDidFocusEvent.mm +25 -0
- package/ios/events/OnDidPresentEvent.h +2 -1
- package/ios/events/OnDidPresentEvent.mm +3 -1
- package/ios/events/OnDragBeginEvent.h +2 -1
- package/ios/events/OnDragBeginEvent.mm +3 -1
- package/ios/events/OnDragChangeEvent.h +2 -1
- package/ios/events/OnDragChangeEvent.mm +3 -1
- package/ios/events/OnDragEndEvent.h +2 -1
- package/ios/events/OnDragEndEvent.mm +3 -1
- package/ios/events/OnPositionChangeEvent.h +2 -1
- package/ios/events/OnPositionChangeEvent.mm +4 -2
- package/ios/events/OnWillPresentEvent.h +2 -1
- package/ios/events/OnWillPresentEvent.mm +3 -1
- package/lib/module/TrueSheet.js +10 -0
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -1
- package/lib/module/reanimated/ReanimatedTrueSheet.js +9 -4
- package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +4 -2
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +2 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +14 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +8 -2
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +14 -0
- package/src/TrueSheet.types.ts +16 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +5 -1
- package/src/reanimated/ReanimatedTrueSheet.tsx +9 -4
- package/src/reanimated/ReanimatedTrueSheetProvider.tsx +11 -3
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The true native bottom sheet experience for your React Native Apps. 💩
|
|
|
13
13
|
* 🚀 **Native** - Implemented in the native realm
|
|
14
14
|
* 🎯 **Type-safe** - Full TypeScript support with Codegen-generated native interfaces
|
|
15
15
|
* ♿ **Accessible** - Native accessibility and screen reader support out of the box
|
|
16
|
-
* 🔄 **
|
|
16
|
+
* 🔄 **Flexible API** - Use [imperative methods](https://sheet.lodev09.com/reference/methods#ref-methods) or [lifecycle events](https://sheet.lodev09.com/reference/props#events)
|
|
17
17
|
* 🪟 **Liquid Glass** - iOS 26+ Liquid Glass support out of the box. Featured in [Expo Blog](https://expo.dev/blog/how-to-create-apple-maps-style-liquid-glass-sheets)
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
@@ -8,6 +8,7 @@ import com.facebook.react.bridge.ReactMethod
|
|
|
8
8
|
import com.facebook.react.module.annotations.ReactModule
|
|
9
9
|
import com.facebook.react.turbomodule.core.interfaces.TurboModule
|
|
10
10
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
11
|
+
import com.lodev09.truesheet.core.TrueSheetDialogObserver
|
|
11
12
|
import java.util.concurrent.ConcurrentHashMap
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -27,10 +28,11 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
|
|
|
27
28
|
|
|
28
29
|
override fun invalidate() {
|
|
29
30
|
super.invalidate()
|
|
30
|
-
// Clear all registered views on module invalidation
|
|
31
|
+
// Clear all registered views and observer on module invalidation
|
|
31
32
|
synchronized(viewRegistry) {
|
|
32
33
|
viewRegistry.clear()
|
|
33
34
|
}
|
|
35
|
+
TrueSheetDialogObserver.clear()
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -2,7 +2,6 @@ package com.lodev09.truesheet
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.view.View
|
|
5
|
-
import android.view.ViewGroup
|
|
6
5
|
import android.view.ViewStructure
|
|
7
6
|
import android.view.accessibility.AccessibilityEvent
|
|
8
7
|
import androidx.annotation.UiThread
|
|
@@ -14,12 +13,15 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
14
13
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
15
14
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
16
15
|
import com.facebook.react.views.view.ReactViewGroup
|
|
16
|
+
import com.lodev09.truesheet.core.TrueSheetDialogObserver
|
|
17
|
+
import com.lodev09.truesheet.events.BlurEvent
|
|
17
18
|
import com.lodev09.truesheet.events.DetentChangeEvent
|
|
18
19
|
import com.lodev09.truesheet.events.DidDismissEvent
|
|
19
20
|
import com.lodev09.truesheet.events.DidPresentEvent
|
|
20
21
|
import com.lodev09.truesheet.events.DragBeginEvent
|
|
21
22
|
import com.lodev09.truesheet.events.DragChangeEvent
|
|
22
23
|
import com.lodev09.truesheet.events.DragEndEvent
|
|
24
|
+
import com.lodev09.truesheet.events.FocusEvent
|
|
23
25
|
import com.lodev09.truesheet.events.MountEvent
|
|
24
26
|
import com.lodev09.truesheet.events.PositionChangeEvent
|
|
25
27
|
import com.lodev09.truesheet.events.WillDismissEvent
|
|
@@ -36,7 +38,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
36
38
|
TrueSheetViewControllerDelegate,
|
|
37
39
|
TrueSheetContainerViewDelegate {
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
internal val viewController: TrueSheetViewController = TrueSheetViewController(reactContext)
|
|
40
42
|
|
|
41
43
|
private val containerView: TrueSheetContainerView?
|
|
42
44
|
get() = viewController.getChildAt(0) as? TrueSheetContainerView
|
|
@@ -63,9 +65,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
63
65
|
// Flag to prevent multiple pending sheet updates
|
|
64
66
|
private var isSheetUpdatePending: Boolean = false
|
|
65
67
|
|
|
66
|
-
// Reference to parent sheet's controller (for stacking support)
|
|
67
|
-
private var parentViewController: TrueSheetViewController? = null
|
|
68
|
-
|
|
69
68
|
init {
|
|
70
69
|
reactContext.addLifecycleEventListener(this)
|
|
71
70
|
viewController.delegate = this
|
|
@@ -144,6 +143,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
144
143
|
fun onDropInstance() {
|
|
145
144
|
reactContext.removeLifecycleEventListener(this)
|
|
146
145
|
TrueSheetModule.unregisterView(id)
|
|
146
|
+
TrueSheetDialogObserver.removeSheet(this)
|
|
147
147
|
|
|
148
148
|
if (viewController.isPresented) {
|
|
149
149
|
viewController.dismiss()
|
|
@@ -164,14 +164,14 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
164
164
|
|
|
165
165
|
// ==================== TrueSheetViewControllerDelegate Implementation ====================
|
|
166
166
|
|
|
167
|
-
override fun viewControllerWillPresent(index: Int, position: Float) {
|
|
167
|
+
override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
|
|
168
168
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
169
|
-
eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position))
|
|
169
|
+
eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position, detent))
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
override fun viewControllerDidPresent(index: Int, position: Float) {
|
|
172
|
+
override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
|
|
173
173
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
174
|
-
eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position))
|
|
174
|
+
eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position, detent))
|
|
175
175
|
|
|
176
176
|
// Enable touch event dispatching to React Native
|
|
177
177
|
viewController.eventDispatcher = eventDispatcher
|
|
@@ -191,40 +191,49 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
191
191
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
192
192
|
eventDispatcher?.dispatchEvent(DidDismissEvent(surfaceId, id))
|
|
193
193
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
parentViewController = null
|
|
194
|
+
// Notify observer that this sheet was dismissed (will show/focus parent sheet)
|
|
195
|
+
TrueSheetDialogObserver.onSheetDidDismiss(this)
|
|
197
196
|
}
|
|
198
197
|
|
|
199
|
-
override fun viewControllerDidChangeDetent(index: Int, position: Float) {
|
|
198
|
+
override fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float) {
|
|
200
199
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
201
|
-
eventDispatcher?.dispatchEvent(DetentChangeEvent(surfaceId, id, index, position))
|
|
200
|
+
eventDispatcher?.dispatchEvent(DetentChangeEvent(surfaceId, id, index, position, detent))
|
|
202
201
|
}
|
|
203
202
|
|
|
204
|
-
override fun viewControllerDidDragBegin(index: Int, position: Float) {
|
|
203
|
+
override fun viewControllerDidDragBegin(index: Int, position: Float, detent: Float) {
|
|
205
204
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
206
|
-
eventDispatcher?.dispatchEvent(DragBeginEvent(surfaceId, id, index, position))
|
|
205
|
+
eventDispatcher?.dispatchEvent(DragBeginEvent(surfaceId, id, index, position, detent))
|
|
207
206
|
}
|
|
208
207
|
|
|
209
|
-
override fun viewControllerDidDragChange(index: Int, position: Float) {
|
|
208
|
+
override fun viewControllerDidDragChange(index: Int, position: Float, detent: Float) {
|
|
210
209
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
211
|
-
eventDispatcher?.dispatchEvent(DragChangeEvent(surfaceId, id, index, position))
|
|
210
|
+
eventDispatcher?.dispatchEvent(DragChangeEvent(surfaceId, id, index, position, detent))
|
|
212
211
|
}
|
|
213
212
|
|
|
214
|
-
override fun viewControllerDidDragEnd(index: Int, position: Float) {
|
|
213
|
+
override fun viewControllerDidDragEnd(index: Int, position: Float, detent: Float) {
|
|
215
214
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
216
|
-
eventDispatcher?.dispatchEvent(DragEndEvent(surfaceId, id, index, position))
|
|
215
|
+
eventDispatcher?.dispatchEvent(DragEndEvent(surfaceId, id, index, position, detent))
|
|
217
216
|
}
|
|
218
217
|
|
|
219
|
-
override fun viewControllerDidChangePosition(index:
|
|
218
|
+
override fun viewControllerDidChangePosition(index: Float, position: Float, detent: Float, transitioning: Boolean) {
|
|
220
219
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
221
|
-
eventDispatcher?.dispatchEvent(PositionChangeEvent(surfaceId, id, index, position, transitioning))
|
|
220
|
+
eventDispatcher?.dispatchEvent(PositionChangeEvent(surfaceId, id, index, position, detent, transitioning))
|
|
222
221
|
}
|
|
223
222
|
|
|
224
223
|
override fun viewControllerDidChangeSize(width: Int, height: Int) {
|
|
225
224
|
updateState(width, height)
|
|
226
225
|
}
|
|
227
226
|
|
|
227
|
+
override fun viewControllerDidFocus() {
|
|
228
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
229
|
+
eventDispatcher?.dispatchEvent(FocusEvent(surfaceId, id))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
override fun viewControllerDidBlur() {
|
|
233
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
234
|
+
eventDispatcher?.dispatchEvent(BlurEvent(surfaceId, id))
|
|
235
|
+
}
|
|
236
|
+
|
|
228
237
|
// ==================== Property Setters (forward to controller) ====================
|
|
229
238
|
|
|
230
239
|
fun setMaxHeight(height: Int) {
|
|
@@ -300,15 +309,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
300
309
|
|
|
301
310
|
@UiThread
|
|
302
311
|
fun present(detentIndex: Int, animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
303
|
-
//
|
|
304
|
-
// Only hide if parent is not expanded (otherwise it's already covering the screen)
|
|
312
|
+
// Notify observer that this sheet will present (will hide/blur topmost sheet)
|
|
305
313
|
if (!viewController.isPresented) {
|
|
306
|
-
|
|
307
|
-
if (parentViewController?.isExpanded == false) {
|
|
308
|
-
parentViewController?.hideDialog()
|
|
309
|
-
} else {
|
|
310
|
-
parentViewController = null
|
|
311
|
-
}
|
|
314
|
+
TrueSheetDialogObserver.onSheetWillPresent(this, detentIndex)
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
viewController.presentPromise = promiseCallback
|
|
@@ -321,21 +324,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
321
324
|
viewController.dismiss()
|
|
322
325
|
}
|
|
323
326
|
|
|
324
|
-
/**
|
|
325
|
-
* Traverses up the view hierarchy to find a parent TrueSheetViewController.
|
|
326
|
-
* This is used to detect if this sheet is nested inside another TrueSheet's content.
|
|
327
|
-
*/
|
|
328
|
-
private fun findParentViewController(): TrueSheetViewController? {
|
|
329
|
-
var current: ViewGroup? = parent as? ViewGroup
|
|
330
|
-
while (current != null) {
|
|
331
|
-
if (current is TrueSheetViewController) {
|
|
332
|
-
return current
|
|
333
|
-
}
|
|
334
|
-
current = current.parent as? ViewGroup
|
|
335
|
-
}
|
|
336
|
-
return null
|
|
337
|
-
}
|
|
338
|
-
|
|
339
327
|
/**
|
|
340
328
|
* Debounced sheet update to handle rapid content/header size changes.
|
|
341
329
|
* Uses post to ensure all layout passes complete before reconfiguring.
|
|
@@ -31,16 +31,18 @@ import com.lodev09.truesheet.utils.ScreenUtils
|
|
|
31
31
|
data class DetentInfo(val index: Int, val position: Float)
|
|
32
32
|
|
|
33
33
|
interface TrueSheetViewControllerDelegate {
|
|
34
|
-
fun viewControllerWillPresent(index: Int, position: Float)
|
|
35
|
-
fun viewControllerDidPresent(index: Int, position: Float)
|
|
34
|
+
fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
|
|
35
|
+
fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
|
|
36
36
|
fun viewControllerWillDismiss()
|
|
37
37
|
fun viewControllerDidDismiss()
|
|
38
|
-
fun viewControllerDidChangeDetent(index: Int, position: Float)
|
|
39
|
-
fun viewControllerDidDragBegin(index: Int, position: Float)
|
|
40
|
-
fun viewControllerDidDragChange(index: Int, position: Float)
|
|
41
|
-
fun viewControllerDidDragEnd(index: Int, position: Float)
|
|
42
|
-
fun viewControllerDidChangePosition(index:
|
|
38
|
+
fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float)
|
|
39
|
+
fun viewControllerDidDragBegin(index: Int, position: Float, detent: Float)
|
|
40
|
+
fun viewControllerDidDragChange(index: Int, position: Float, detent: Float)
|
|
41
|
+
fun viewControllerDidDragEnd(index: Int, position: Float, detent: Float)
|
|
42
|
+
fun viewControllerDidChangePosition(index: Float, position: Float, detent: Float, transitioning: Boolean)
|
|
43
43
|
fun viewControllerDidChangeSize(width: Int, height: Int)
|
|
44
|
+
fun viewControllerDidFocus()
|
|
45
|
+
fun viewControllerDidBlur()
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -112,6 +114,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
112
114
|
|
|
113
115
|
private var isDragging = false
|
|
114
116
|
private var windowAnimation: Int = 0
|
|
117
|
+
private var lastEmittedPositionPx: Int = -1
|
|
115
118
|
|
|
116
119
|
var presentPromise: (() -> Unit)? = null
|
|
117
120
|
var dismissPromise: (() -> Unit)? = null
|
|
@@ -206,7 +209,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
206
209
|
val style = if (edgeToEdgeEnabled) {
|
|
207
210
|
com.lodev09.truesheet.R.style.TrueSheetEdgeToEdgeEnabledDialog
|
|
208
211
|
} else {
|
|
209
|
-
|
|
212
|
+
com.lodev09.truesheet.R.style.TrueSheetDialog
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
dialog = BottomSheetDialog(reactContext, style).apply {
|
|
@@ -239,6 +242,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
239
242
|
dialog = null
|
|
240
243
|
isDragging = false
|
|
241
244
|
isPresented = false
|
|
245
|
+
lastEmittedPositionPx = -1
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
@@ -250,8 +254,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
250
254
|
|
|
251
255
|
sheetContainer?.post {
|
|
252
256
|
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
258
|
+
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
259
|
+
delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position, detent)
|
|
260
|
+
emitChangePositionDelegate(detentInfo.index, positionPx, transitioning = true)
|
|
255
261
|
|
|
256
262
|
presentPromise?.invoke()
|
|
257
263
|
presentPromise = null
|
|
@@ -277,8 +283,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
277
283
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
|
278
284
|
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
279
285
|
val behavior = behavior ?: return
|
|
280
|
-
val
|
|
281
|
-
|
|
286
|
+
val positionPx = getCurrentPositionPx(sheetView)
|
|
287
|
+
val detentIndex = getDetentIndexForPosition(positionPx)
|
|
288
|
+
|
|
289
|
+
emitChangePositionDelegate(detentIndex, positionPx, transitioning = false)
|
|
282
290
|
|
|
283
291
|
when (behavior.state) {
|
|
284
292
|
BottomSheetBehavior.STATE_DRAGGING,
|
|
@@ -347,12 +355,32 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
347
355
|
return sheetTop <= statusBarHeight
|
|
348
356
|
}
|
|
349
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Returns the current top position of the sheet (Y coordinate from screen top).
|
|
360
|
+
* Used for comparing sheet positions during stacking.
|
|
361
|
+
*/
|
|
362
|
+
val currentSheetTop: Int
|
|
363
|
+
get() = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Returns the expected top position of the sheet when presented at the given detent index.
|
|
367
|
+
* Used for comparing sheet positions before presentation.
|
|
368
|
+
*/
|
|
369
|
+
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
370
|
+
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
371
|
+
val detentHeight = getDetentHeight(detents[detentIndex])
|
|
372
|
+
return screenHeight - detentHeight
|
|
373
|
+
}
|
|
374
|
+
|
|
350
375
|
/**
|
|
351
376
|
* Hides the dialog without dismissing it.
|
|
352
377
|
* Used when another TrueSheet presents on top.
|
|
353
378
|
*/
|
|
354
379
|
fun hideDialog() {
|
|
355
380
|
dialog?.window?.decorView?.visibility = View.INVISIBLE
|
|
381
|
+
|
|
382
|
+
// Emit off-screen position (detent = 0 since sheet is fully hidden)
|
|
383
|
+
emitChangePositionDelegate(currentDetentIndex, screenHeight, transitioning = true)
|
|
356
384
|
}
|
|
357
385
|
|
|
358
386
|
/**
|
|
@@ -361,6 +389,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
361
389
|
*/
|
|
362
390
|
fun showDialog() {
|
|
363
391
|
dialog?.window?.decorView?.visibility = View.VISIBLE
|
|
392
|
+
|
|
393
|
+
// Emit current position
|
|
394
|
+
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
395
|
+
emitChangePositionDelegate(currentDetentIndex, positionPx, transitioning = true)
|
|
364
396
|
}
|
|
365
397
|
|
|
366
398
|
// ====================================================================
|
|
@@ -378,7 +410,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
378
410
|
|
|
379
411
|
if (isPresented) {
|
|
380
412
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
381
|
-
|
|
413
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
414
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
382
415
|
setStateForDetentIndex(detentIndex)
|
|
383
416
|
} else {
|
|
384
417
|
isDragging = false
|
|
@@ -386,7 +419,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
386
419
|
setStateForDetentIndex(detentIndex)
|
|
387
420
|
|
|
388
421
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
389
|
-
|
|
422
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
423
|
+
delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position, detent)
|
|
390
424
|
|
|
391
425
|
if (!animated) {
|
|
392
426
|
dialog.window?.setWindowAnimations(0)
|
|
@@ -398,8 +432,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
398
432
|
|
|
399
433
|
fun dismiss() {
|
|
400
434
|
this.post {
|
|
401
|
-
|
|
402
|
-
|
|
435
|
+
// Emit off-screen position (detent = 0 since sheet is fully hidden)
|
|
436
|
+
emitChangePositionDelegate(currentDetentIndex, screenHeight, transitioning = true)
|
|
403
437
|
}
|
|
404
438
|
dialog?.dismiss()
|
|
405
439
|
}
|
|
@@ -565,6 +599,76 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
565
599
|
}
|
|
566
600
|
}
|
|
567
601
|
|
|
602
|
+
// ====================================================================
|
|
603
|
+
// MARK: - Position Change Delegate
|
|
604
|
+
// ====================================================================
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Emits position change to the delegate if the position has changed.
|
|
608
|
+
* @param index The current detent index (discrete, used as fallback)
|
|
609
|
+
* @param positionPx The current position in pixels (screen Y coordinate)
|
|
610
|
+
* @param transitioning Whether the sheet is transitioning (programmatic) vs dragging
|
|
611
|
+
*/
|
|
612
|
+
private fun emitChangePositionDelegate(index: Int, positionPx: Int, transitioning: Boolean) {
|
|
613
|
+
if (positionPx == lastEmittedPositionPx) return
|
|
614
|
+
|
|
615
|
+
lastEmittedPositionPx = positionPx
|
|
616
|
+
val position = positionPx.pxToDp()
|
|
617
|
+
val interpolatedIndex = getInterpolatedIndexForPosition(positionPx)
|
|
618
|
+
val detent = getDetentValueForIndex(kotlin.math.round(interpolatedIndex).toInt())
|
|
619
|
+
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, transitioning)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Calculates the interpolated index based on position.
|
|
624
|
+
* Returns a continuous value (e.g., 0.5 means halfway between detent 0 and 1).
|
|
625
|
+
*/
|
|
626
|
+
private fun getInterpolatedIndexForPosition(positionPx: Int): Float {
|
|
627
|
+
val count = detents.size
|
|
628
|
+
if (count == 0) return -1f
|
|
629
|
+
if (count == 1) return 0f
|
|
630
|
+
|
|
631
|
+
// Convert position to detent fraction
|
|
632
|
+
val currentDetent = (screenHeight - positionPx).toFloat() / screenHeight.toFloat()
|
|
633
|
+
|
|
634
|
+
// Handle below first detent (interpolate from -1 to 0)
|
|
635
|
+
val firstDetentValue = getDetentValueForIndex(0)
|
|
636
|
+
if (currentDetent < firstDetentValue) {
|
|
637
|
+
if (firstDetentValue <= 0) return 0f
|
|
638
|
+
val progress = currentDetent / firstDetentValue
|
|
639
|
+
return progress - 1f
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Find which segment the current detent falls into and interpolate
|
|
643
|
+
for (i in 0 until count - 1) {
|
|
644
|
+
val detentValue = getDetentValueForIndex(i)
|
|
645
|
+
val nextDetentValue = getDetentValueForIndex(i + 1)
|
|
646
|
+
|
|
647
|
+
if (currentDetent <= nextDetentValue) {
|
|
648
|
+
val range = nextDetentValue - detentValue
|
|
649
|
+
if (range <= 0) return i.toFloat()
|
|
650
|
+
val progress = (currentDetent - detentValue) / range
|
|
651
|
+
return i + maxOf(0f, minOf(1f, progress))
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return (count - 1).toFloat()
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Gets the detent value (fraction) for a given index.
|
|
660
|
+
* For auto (-1), calculates the actual fraction from content + header height.
|
|
661
|
+
*/
|
|
662
|
+
private fun getDetentValueForIndex(index: Int): Float {
|
|
663
|
+
if (index < 0 || index >= detents.size) return 0f
|
|
664
|
+
val value = detents[index]
|
|
665
|
+
return if (value == -1.0) {
|
|
666
|
+
(contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
|
|
667
|
+
} else {
|
|
668
|
+
value.toFloat()
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
568
672
|
// ====================================================================
|
|
569
673
|
// MARK: - Drag Handling
|
|
570
674
|
// ====================================================================
|
|
@@ -574,16 +678,40 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
574
678
|
return DetentInfo(currentDetentIndex, screenY.pxToDp())
|
|
575
679
|
}
|
|
576
680
|
|
|
681
|
+
private fun getCurrentPositionPx(sheetView: View): Int = ScreenUtils.getScreenY(sheetView)
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Returns the detent index for the current position.
|
|
685
|
+
* Only reports a higher index when the sheet has reached that detent's height.
|
|
686
|
+
*/
|
|
687
|
+
private fun getDetentIndexForPosition(positionPx: Int): Int {
|
|
688
|
+
if (detents.isEmpty()) return 0
|
|
689
|
+
|
|
690
|
+
val sheetHeight = screenHeight - positionPx
|
|
691
|
+
|
|
692
|
+
// Find the highest detent index that the sheet has reached
|
|
693
|
+
for (i in detents.indices.reversed()) {
|
|
694
|
+
val detentHeight = getDetentHeight(detents[i])
|
|
695
|
+
if (sheetHeight >= detentHeight) {
|
|
696
|
+
return i
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return 0
|
|
701
|
+
}
|
|
702
|
+
|
|
577
703
|
private fun handleDragBegin(sheetView: View) {
|
|
578
704
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
579
|
-
|
|
705
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
706
|
+
delegate?.viewControllerDidDragBegin(detentInfo.index, detentInfo.position, detent)
|
|
580
707
|
isDragging = true
|
|
581
708
|
}
|
|
582
709
|
|
|
583
710
|
private fun handleDragChange(sheetView: View) {
|
|
584
711
|
if (!isDragging) return
|
|
585
712
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
586
|
-
|
|
713
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
714
|
+
delegate?.viewControllerDidDragChange(detentInfo.index, detentInfo.position, detent)
|
|
587
715
|
}
|
|
588
716
|
|
|
589
717
|
private fun handleDragEnd(state: Int) {
|
|
@@ -591,7 +719,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
591
719
|
|
|
592
720
|
val detentInfo = getDetentInfoForState(state)
|
|
593
721
|
detentInfo?.let {
|
|
594
|
-
|
|
722
|
+
val detent = getDetentValueForIndex(it.index)
|
|
723
|
+
delegate?.viewControllerDidDragEnd(it.index, it.position, detent)
|
|
595
724
|
|
|
596
725
|
if (it.index != currentDetentIndex) {
|
|
597
726
|
presentPromise?.invoke()
|
|
@@ -599,7 +728,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
599
728
|
|
|
600
729
|
currentDetentIndex = it.index
|
|
601
730
|
setupDimmedBackground(it.index)
|
|
602
|
-
delegate?.viewControllerDidChangeDetent(it.index, it.position)
|
|
731
|
+
delegate?.viewControllerDidChangeDetent(it.index, it.position, detent)
|
|
603
732
|
}
|
|
604
733
|
}
|
|
605
734
|
|
|
@@ -705,8 +834,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
705
834
|
setupSheetDetents()
|
|
706
835
|
this.post {
|
|
707
836
|
positionFooter()
|
|
708
|
-
val
|
|
709
|
-
|
|
837
|
+
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
838
|
+
emitChangePositionDelegate(currentDetentIndex, positionPx, transitioning = true)
|
|
710
839
|
}
|
|
711
840
|
}
|
|
712
841
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import com.lodev09.truesheet.TrueSheetView
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Observes TrueSheet dialog lifecycle to manage sheet stacking.
|
|
7
|
+
* Automatically hides/shows sheets and dispatches focus/blur events
|
|
8
|
+
* when sheets are presented on top of each other.
|
|
9
|
+
*/
|
|
10
|
+
object TrueSheetDialogObserver {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Stack of currently presented sheet views (most recent on top)
|
|
14
|
+
*/
|
|
15
|
+
private val presentedSheetStack = mutableListOf<TrueSheetView>()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Called when a sheet is about to be presented.
|
|
19
|
+
* Hides and blurs the current topmost sheet if exists.
|
|
20
|
+
*
|
|
21
|
+
* @param sheetView The sheet that is about to be presented
|
|
22
|
+
* @param detentIndex The detent index the sheet will be presented at
|
|
23
|
+
*/
|
|
24
|
+
@JvmStatic
|
|
25
|
+
fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int) {
|
|
26
|
+
synchronized(presentedSheetStack) {
|
|
27
|
+
// Get the current topmost sheet
|
|
28
|
+
val topSheet = presentedSheetStack.lastOrNull()
|
|
29
|
+
|
|
30
|
+
// Hide and blur the topmost sheet if it exists
|
|
31
|
+
topSheet?.let {
|
|
32
|
+
// Don't hide if the top sheet is fully expanded (covers the screen)
|
|
33
|
+
// or if the top sheet is smaller than the presenting sheet
|
|
34
|
+
// A smaller topSheetTop value means the sheet is taller (closer to top of screen)
|
|
35
|
+
val topSheetTop = it.viewController.currentSheetTop
|
|
36
|
+
val presentingSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
|
|
37
|
+
|
|
38
|
+
if (!it.viewController.isExpanded && topSheetTop <= presentingSheetTop) {
|
|
39
|
+
it.viewController.hideDialog()
|
|
40
|
+
}
|
|
41
|
+
it.viewControllerDidBlur()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add new sheet to stack
|
|
45
|
+
if (!presentedSheetStack.contains(sheetView)) {
|
|
46
|
+
presentedSheetStack.add(sheetView)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Called when a sheet has been dismissed.
|
|
53
|
+
* Shows and focuses the sheet below it (if any).
|
|
54
|
+
*
|
|
55
|
+
* @param sheetView The sheet that was dismissed
|
|
56
|
+
*/
|
|
57
|
+
@JvmStatic
|
|
58
|
+
fun onSheetDidDismiss(sheetView: TrueSheetView) {
|
|
59
|
+
synchronized(presentedSheetStack) {
|
|
60
|
+
presentedSheetStack.remove(sheetView)
|
|
61
|
+
|
|
62
|
+
// Show and focus the new topmost sheet
|
|
63
|
+
presentedSheetStack.lastOrNull()?.let {
|
|
64
|
+
it.viewController.showDialog()
|
|
65
|
+
it.viewControllerDidFocus()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Removes a sheet from the stack without triggering focus events.
|
|
72
|
+
* Used when a sheet is being destroyed/cleaned up.
|
|
73
|
+
*
|
|
74
|
+
* @param sheetView The sheet to remove
|
|
75
|
+
*/
|
|
76
|
+
@JvmStatic
|
|
77
|
+
fun removeSheet(sheetView: TrueSheetView) {
|
|
78
|
+
synchronized(presentedSheetStack) {
|
|
79
|
+
presentedSheetStack.remove(sheetView)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clears all tracked sheets.
|
|
85
|
+
* Used when the module is invalidated.
|
|
86
|
+
*/
|
|
87
|
+
@JvmStatic
|
|
88
|
+
fun clear() {
|
|
89
|
+
synchronized(presentedSheetStack) {
|
|
90
|
+
presentedSheetStack.clear()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.lodev09.truesheet.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fired when the sheet loses focus because another sheet is presented on top of it
|
|
9
|
+
*/
|
|
10
|
+
class BlurEvent(surfaceId: Int, viewId: Int) : Event<BlurEvent>(surfaceId, viewId) {
|
|
11
|
+
|
|
12
|
+
override fun getEventName(): String = EVENT_NAME
|
|
13
|
+
|
|
14
|
+
override fun getEventData(): WritableMap = Arguments.createMap()
|
|
15
|
+
|
|
16
|
+
companion object {
|
|
17
|
+
const val EVENT_NAME = "topDidBlur"
|
|
18
|
+
const val REGISTRATION_NAME = "onDidBlur"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -8,7 +8,7 @@ import com.facebook.react.uimanager.events.Event
|
|
|
8
8
|
* Fired when the active detent changes
|
|
9
9
|
* Payload: { index: number, position: number }
|
|
10
10
|
*/
|
|
11
|
-
class DetentChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float) :
|
|
11
|
+
class DetentChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
|
|
12
12
|
Event<DetentChangeEvent>(surfaceId, viewId) {
|
|
13
13
|
|
|
14
14
|
override fun getEventName(): String = EVENT_NAME
|
|
@@ -17,6 +17,7 @@ class DetentChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, pri
|
|
|
17
17
|
Arguments.createMap().apply {
|
|
18
18
|
putInt("index", index)
|
|
19
19
|
putDouble("position", position.toDouble())
|
|
20
|
+
putDouble("detent", detent.toDouble())
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
companion object {
|
|
@@ -8,7 +8,7 @@ import com.facebook.react.uimanager.events.Event
|
|
|
8
8
|
* Fired after the sheet presentation is complete
|
|
9
9
|
* Payload: { index: number, position: number }
|
|
10
10
|
*/
|
|
11
|
-
class DidPresentEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float) :
|
|
11
|
+
class DidPresentEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
|
|
12
12
|
Event<DidPresentEvent>(surfaceId, viewId) {
|
|
13
13
|
|
|
14
14
|
override fun getEventName(): String = EVENT_NAME
|
|
@@ -17,6 +17,7 @@ class DidPresentEvent(surfaceId: Int, viewId: Int, private val index: Int, priva
|
|
|
17
17
|
Arguments.createMap().apply {
|
|
18
18
|
putInt("index", index)
|
|
19
19
|
putDouble("position", position.toDouble())
|
|
20
|
+
putDouble("detent", detent.toDouble())
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
companion object {
|
|
@@ -8,7 +8,7 @@ import com.facebook.react.uimanager.events.Event
|
|
|
8
8
|
* Fired when user starts dragging the sheet
|
|
9
9
|
* Payload: { index: number, position: number }
|
|
10
10
|
*/
|
|
11
|
-
class DragBeginEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float) :
|
|
11
|
+
class DragBeginEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
|
|
12
12
|
Event<DragBeginEvent>(surfaceId, viewId) {
|
|
13
13
|
|
|
14
14
|
override fun getEventName(): String = EVENT_NAME
|
|
@@ -17,6 +17,7 @@ class DragBeginEvent(surfaceId: Int, viewId: Int, private val index: Int, privat
|
|
|
17
17
|
Arguments.createMap().apply {
|
|
18
18
|
putInt("index", index)
|
|
19
19
|
putDouble("position", position.toDouble())
|
|
20
|
+
putDouble("detent", detent.toDouble())
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
companion object {
|