@lodev09/react-native-true-sheet 3.0.0-beta.12 → 3.0.0-beta.14
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 +41 -57
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +182 -25
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +5 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogObserver.kt +67 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetDragEvents.kt +71 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetFocusEvents.kt +65 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +94 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetStateEvents.kt +56 -0
- 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 +49 -21
- package/ios/TrueSheetViewController.h +9 -1
- package/ios/TrueSheetViewController.mm +130 -42
- package/ios/events/TrueSheetDragEvents.h +39 -0
- package/ios/events/TrueSheetDragEvents.mm +62 -0
- package/ios/events/{OnPositionChangeEvent.h → TrueSheetFocusEvents.h} +8 -5
- package/ios/events/TrueSheetFocusEvents.mm +49 -0
- package/ios/events/TrueSheetLifecycleEvents.h +40 -0
- package/ios/events/TrueSheetLifecycleEvents.mm +71 -0
- package/ios/events/TrueSheetStateEvents.h +35 -0
- package/ios/events/TrueSheetStateEvents.mm +49 -0
- package/lib/module/TrueSheet.js +20 -0
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +7 -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 +4 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +24 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +7 -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 +28 -0
- package/src/TrueSheet.types.ts +28 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +7 -1
- package/src/reanimated/ReanimatedTrueSheet.tsx +9 -4
- package/src/reanimated/ReanimatedTrueSheetProvider.tsx +11 -3
- package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +0 -32
- package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +0 -26
- package/ios/events/OnDetentChangeEvent.h +0 -28
- package/ios/events/OnDetentChangeEvent.mm +0 -30
- package/ios/events/OnDidDismissEvent.h +0 -26
- package/ios/events/OnDidDismissEvent.mm +0 -25
- package/ios/events/OnDidPresentEvent.h +0 -28
- package/ios/events/OnDidPresentEvent.mm +0 -30
- package/ios/events/OnDragBeginEvent.h +0 -28
- package/ios/events/OnDragBeginEvent.mm +0 -30
- package/ios/events/OnDragChangeEvent.h +0 -28
- package/ios/events/OnDragChangeEvent.mm +0 -30
- package/ios/events/OnDragEndEvent.h +0 -28
- package/ios/events/OnDragEndEvent.mm +0 -30
- package/ios/events/OnMountEvent.h +0 -26
- package/ios/events/OnMountEvent.mm +0 -25
- package/ios/events/OnPositionChangeEvent.mm +0 -32
- package/ios/events/OnWillDismissEvent.h +0 -26
- package/ios/events/OnWillDismissEvent.mm +0 -25
- package/ios/events/OnWillPresentEvent.h +0 -28
- package/ios/events/OnWillPresentEvent.mm +0 -30
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,16 +13,8 @@ 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
|
|
17
|
-
import com.lodev09.truesheet.
|
|
18
|
-
import com.lodev09.truesheet.events
|
|
19
|
-
import com.lodev09.truesheet.events.DidPresentEvent
|
|
20
|
-
import com.lodev09.truesheet.events.DragBeginEvent
|
|
21
|
-
import com.lodev09.truesheet.events.DragChangeEvent
|
|
22
|
-
import com.lodev09.truesheet.events.DragEndEvent
|
|
23
|
-
import com.lodev09.truesheet.events.MountEvent
|
|
24
|
-
import com.lodev09.truesheet.events.PositionChangeEvent
|
|
25
|
-
import com.lodev09.truesheet.events.WillDismissEvent
|
|
26
|
-
import com.lodev09.truesheet.events.WillPresentEvent
|
|
16
|
+
import com.lodev09.truesheet.core.TrueSheetDialogObserver
|
|
17
|
+
import com.lodev09.truesheet.events.*
|
|
27
18
|
|
|
28
19
|
/**
|
|
29
20
|
* Main TrueSheet host view that manages the sheet dialog and dispatches events to JavaScript.
|
|
@@ -36,7 +27,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
36
27
|
TrueSheetViewControllerDelegate,
|
|
37
28
|
TrueSheetContainerViewDelegate {
|
|
38
29
|
|
|
39
|
-
|
|
30
|
+
internal val viewController: TrueSheetViewController = TrueSheetViewController(reactContext)
|
|
40
31
|
|
|
41
32
|
private val containerView: TrueSheetContainerView?
|
|
42
33
|
get() = viewController.getChildAt(0) as? TrueSheetContainerView
|
|
@@ -63,9 +54,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
63
54
|
// Flag to prevent multiple pending sheet updates
|
|
64
55
|
private var isSheetUpdatePending: Boolean = false
|
|
65
56
|
|
|
66
|
-
// Reference to parent sheet's controller (for stacking support)
|
|
67
|
-
private var parentViewController: TrueSheetViewController? = null
|
|
68
|
-
|
|
69
57
|
init {
|
|
70
58
|
reactContext.addLifecycleEventListener(this)
|
|
71
59
|
viewController.delegate = this
|
|
@@ -144,6 +132,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
144
132
|
fun onDropInstance() {
|
|
145
133
|
reactContext.removeLifecycleEventListener(this)
|
|
146
134
|
TrueSheetModule.unregisterView(id)
|
|
135
|
+
TrueSheetDialogObserver.removeSheet(this)
|
|
147
136
|
|
|
148
137
|
if (viewController.isPresented) {
|
|
149
138
|
viewController.dismiss()
|
|
@@ -164,14 +153,14 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
164
153
|
|
|
165
154
|
// ==================== TrueSheetViewControllerDelegate Implementation ====================
|
|
166
155
|
|
|
167
|
-
override fun viewControllerWillPresent(index: Int, position: Float) {
|
|
156
|
+
override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
|
|
168
157
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
169
|
-
eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position))
|
|
158
|
+
eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position, detent))
|
|
170
159
|
}
|
|
171
160
|
|
|
172
|
-
override fun viewControllerDidPresent(index: Int, position: Float) {
|
|
161
|
+
override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
|
|
173
162
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
174
|
-
eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position))
|
|
163
|
+
eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position, detent))
|
|
175
164
|
|
|
176
165
|
// Enable touch event dispatching to React Native
|
|
177
166
|
viewController.eventDispatcher = eventDispatcher
|
|
@@ -187,44 +176,62 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
187
176
|
containerView?.footerView?.eventDispatcher = null
|
|
188
177
|
}
|
|
189
178
|
|
|
190
|
-
override fun viewControllerDidDismiss() {
|
|
179
|
+
override fun viewControllerDidDismiss(hadParent: Boolean) {
|
|
191
180
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
192
181
|
eventDispatcher?.dispatchEvent(DidDismissEvent(surfaceId, id))
|
|
193
182
|
|
|
194
|
-
|
|
195
|
-
parentViewController?.showDialog()
|
|
196
|
-
parentViewController = null
|
|
183
|
+
TrueSheetDialogObserver.onSheetDidDismiss(this, hadParent)
|
|
197
184
|
}
|
|
198
185
|
|
|
199
|
-
override fun viewControllerDidChangeDetent(index: Int, position: Float) {
|
|
186
|
+
override fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float) {
|
|
200
187
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
201
|
-
eventDispatcher?.dispatchEvent(DetentChangeEvent(surfaceId, id, index, position))
|
|
188
|
+
eventDispatcher?.dispatchEvent(DetentChangeEvent(surfaceId, id, index, position, detent))
|
|
202
189
|
}
|
|
203
190
|
|
|
204
|
-
override fun viewControllerDidDragBegin(index: Int, position: Float) {
|
|
191
|
+
override fun viewControllerDidDragBegin(index: Int, position: Float, detent: Float) {
|
|
205
192
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
206
|
-
eventDispatcher?.dispatchEvent(DragBeginEvent(surfaceId, id, index, position))
|
|
193
|
+
eventDispatcher?.dispatchEvent(DragBeginEvent(surfaceId, id, index, position, detent))
|
|
207
194
|
}
|
|
208
195
|
|
|
209
|
-
override fun viewControllerDidDragChange(index: Int, position: Float) {
|
|
196
|
+
override fun viewControllerDidDragChange(index: Int, position: Float, detent: Float) {
|
|
210
197
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
211
|
-
eventDispatcher?.dispatchEvent(DragChangeEvent(surfaceId, id, index, position))
|
|
198
|
+
eventDispatcher?.dispatchEvent(DragChangeEvent(surfaceId, id, index, position, detent))
|
|
212
199
|
}
|
|
213
200
|
|
|
214
|
-
override fun viewControllerDidDragEnd(index: Int, position: Float) {
|
|
201
|
+
override fun viewControllerDidDragEnd(index: Int, position: Float, detent: Float) {
|
|
215
202
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
216
|
-
eventDispatcher?.dispatchEvent(DragEndEvent(surfaceId, id, index, position))
|
|
203
|
+
eventDispatcher?.dispatchEvent(DragEndEvent(surfaceId, id, index, position, detent))
|
|
217
204
|
}
|
|
218
205
|
|
|
219
|
-
override fun viewControllerDidChangePosition(index:
|
|
206
|
+
override fun viewControllerDidChangePosition(index: Float, position: Float, detent: Float, transitioning: Boolean) {
|
|
220
207
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
221
|
-
eventDispatcher?.dispatchEvent(PositionChangeEvent(surfaceId, id, index, position, transitioning))
|
|
208
|
+
eventDispatcher?.dispatchEvent(PositionChangeEvent(surfaceId, id, index, position, detent, transitioning))
|
|
222
209
|
}
|
|
223
210
|
|
|
224
211
|
override fun viewControllerDidChangeSize(width: Int, height: Int) {
|
|
225
212
|
updateState(width, height)
|
|
226
213
|
}
|
|
227
214
|
|
|
215
|
+
override fun viewControllerWillFocus() {
|
|
216
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
217
|
+
eventDispatcher?.dispatchEvent(WillFocusEvent(surfaceId, id))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
override fun viewControllerDidFocus() {
|
|
221
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
222
|
+
eventDispatcher?.dispatchEvent(FocusEvent(surfaceId, id))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
override fun viewControllerWillBlur() {
|
|
226
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
227
|
+
eventDispatcher?.dispatchEvent(WillBlurEvent(surfaceId, id))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
override fun viewControllerDidBlur() {
|
|
231
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
232
|
+
eventDispatcher?.dispatchEvent(BlurEvent(surfaceId, id))
|
|
233
|
+
}
|
|
234
|
+
|
|
228
235
|
// ==================== Property Setters (forward to controller) ====================
|
|
229
236
|
|
|
230
237
|
fun setMaxHeight(height: Int) {
|
|
@@ -300,17 +307,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
300
307
|
|
|
301
308
|
@UiThread
|
|
302
309
|
fun present(detentIndex: Int, animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
303
|
-
// Find and hide parent sheet if this sheet is nested inside another TrueSheet
|
|
304
|
-
// Only hide if parent is not expanded (otherwise it's already covering the screen)
|
|
305
310
|
if (!viewController.isPresented) {
|
|
306
|
-
|
|
307
|
-
if (parentViewController?.isExpanded == false) {
|
|
308
|
-
parentViewController?.hideDialog()
|
|
309
|
-
} else {
|
|
310
|
-
parentViewController = null
|
|
311
|
-
}
|
|
311
|
+
viewController.parentSheetView = TrueSheetDialogObserver.onSheetWillPresent(this, detentIndex)
|
|
312
312
|
}
|
|
313
|
-
|
|
314
313
|
viewController.presentPromise = promiseCallback
|
|
315
314
|
viewController.present(detentIndex, animated)
|
|
316
315
|
}
|
|
@@ -321,21 +320,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
321
320
|
viewController.dismiss()
|
|
322
321
|
}
|
|
323
322
|
|
|
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
323
|
/**
|
|
340
324
|
* Debounced sheet update to handle rapid content/header size changes.
|
|
341
325
|
* Uses post to ensure all layout passes complete before reconfiguring.
|
|
@@ -31,16 +31,20 @@ 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
|
-
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:
|
|
37
|
+
fun viewControllerDidDismiss(hadParent: Boolean)
|
|
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 viewControllerWillFocus()
|
|
45
|
+
fun viewControllerDidFocus()
|
|
46
|
+
fun viewControllerWillBlur()
|
|
47
|
+
fun viewControllerDidBlur()
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -107,15 +111,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
107
111
|
var isPresented = false
|
|
108
112
|
private set
|
|
109
113
|
|
|
114
|
+
var isDialogVisible = false
|
|
115
|
+
private set
|
|
116
|
+
|
|
110
117
|
var currentDetentIndex: Int = -1
|
|
111
118
|
private set
|
|
112
119
|
|
|
113
120
|
private var isDragging = false
|
|
114
121
|
private var windowAnimation: Int = 0
|
|
122
|
+
private var lastEmittedPositionPx: Int = -1
|
|
115
123
|
|
|
116
124
|
var presentPromise: (() -> Unit)? = null
|
|
117
125
|
var dismissPromise: (() -> Unit)? = null
|
|
118
126
|
|
|
127
|
+
// Reference to parent TrueSheetView (if presented from another sheet)
|
|
128
|
+
var parentSheetView: TrueSheetView? = null
|
|
129
|
+
|
|
119
130
|
// ====================================================================
|
|
120
131
|
// MARK: - Configuration Properties
|
|
121
132
|
// ====================================================================
|
|
@@ -206,7 +217,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
206
217
|
val style = if (edgeToEdgeEnabled) {
|
|
207
218
|
com.lodev09.truesheet.R.style.TrueSheetEdgeToEdgeEnabledDialog
|
|
208
219
|
} else {
|
|
209
|
-
|
|
220
|
+
com.lodev09.truesheet.R.style.TrueSheetDialog
|
|
210
221
|
}
|
|
211
222
|
|
|
212
223
|
dialog = BottomSheetDialog(reactContext, style).apply {
|
|
@@ -239,19 +250,27 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
239
250
|
dialog = null
|
|
240
251
|
isDragging = false
|
|
241
252
|
isPresented = false
|
|
253
|
+
isDialogVisible = false
|
|
254
|
+
lastEmittedPositionPx = -1
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
245
258
|
dialog.setOnShowListener {
|
|
246
259
|
isPresented = true
|
|
260
|
+
isDialogVisible = true
|
|
247
261
|
resetAnimation()
|
|
248
262
|
setupBackground()
|
|
249
263
|
setupGrabber()
|
|
250
264
|
|
|
251
265
|
sheetContainer?.post {
|
|
252
266
|
val detentInfo = getDetentInfoForIndex(currentDetentIndex)
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
268
|
+
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
269
|
+
delegate?.viewControllerDidPresent(detentInfo.index, detentInfo.position, detent)
|
|
270
|
+
emitChangePositionDelegate(detentInfo.index, positionPx, transitioning = true)
|
|
271
|
+
|
|
272
|
+
// Notify parent sheet that it has lost focus (after this sheet appeared)
|
|
273
|
+
parentSheetView?.viewControllerDidBlur()
|
|
255
274
|
|
|
256
275
|
presentPromise?.invoke()
|
|
257
276
|
presentPromise = null
|
|
@@ -261,13 +280,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
261
280
|
}
|
|
262
281
|
|
|
263
282
|
dialog.setOnCancelListener {
|
|
283
|
+
// Notify parent sheet that it is about to regain focus
|
|
284
|
+
parentSheetView?.viewControllerWillFocus()
|
|
285
|
+
|
|
264
286
|
delegate?.viewControllerWillDismiss()
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
dialog.setOnDismissListener {
|
|
290
|
+
val hadParent = parentSheetView != null
|
|
291
|
+
|
|
292
|
+
// Notify parent sheet that it has regained focus
|
|
293
|
+
parentSheetView?.viewControllerDidFocus()
|
|
294
|
+
parentSheetView = null
|
|
295
|
+
|
|
268
296
|
dismissPromise?.invoke()
|
|
269
297
|
dismissPromise = null
|
|
270
|
-
delegate?.viewControllerDidDismiss()
|
|
298
|
+
delegate?.viewControllerDidDismiss(hadParent)
|
|
271
299
|
cleanupDialog()
|
|
272
300
|
}
|
|
273
301
|
}
|
|
@@ -277,8 +305,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
277
305
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
|
278
306
|
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
279
307
|
val behavior = behavior ?: return
|
|
280
|
-
val
|
|
281
|
-
|
|
308
|
+
val positionPx = getCurrentPositionPx(sheetView)
|
|
309
|
+
val detentIndex = getDetentIndexForPosition(positionPx)
|
|
310
|
+
|
|
311
|
+
emitChangePositionDelegate(detentIndex, positionPx, transitioning = false)
|
|
282
312
|
|
|
283
313
|
when (behavior.state) {
|
|
284
314
|
BottomSheetBehavior.STATE_DRAGGING,
|
|
@@ -347,12 +377,33 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
347
377
|
return sheetTop <= statusBarHeight
|
|
348
378
|
}
|
|
349
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Returns the current top position of the sheet (Y coordinate from screen top).
|
|
382
|
+
* Used for comparing sheet positions during stacking.
|
|
383
|
+
*/
|
|
384
|
+
val currentSheetTop: Int
|
|
385
|
+
get() = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Returns the expected top position of the sheet when presented at the given detent index.
|
|
389
|
+
* Used for comparing sheet positions before presentation.
|
|
390
|
+
*/
|
|
391
|
+
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
392
|
+
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
393
|
+
val detentHeight = getDetentHeight(detents[detentIndex])
|
|
394
|
+
return screenHeight - detentHeight
|
|
395
|
+
}
|
|
396
|
+
|
|
350
397
|
/**
|
|
351
398
|
* Hides the dialog without dismissing it.
|
|
352
|
-
* Used when another TrueSheet presents on top.
|
|
399
|
+
* Used when another TrueSheet presents on top or when RN screen is presented.
|
|
353
400
|
*/
|
|
354
401
|
fun hideDialog() {
|
|
402
|
+
isDialogVisible = false
|
|
355
403
|
dialog?.window?.decorView?.visibility = View.INVISIBLE
|
|
404
|
+
|
|
405
|
+
// Emit off-screen position (detent = 0 since sheet is fully hidden)
|
|
406
|
+
emitChangePositionDelegate(currentDetentIndex, screenHeight, transitioning = true)
|
|
356
407
|
}
|
|
357
408
|
|
|
358
409
|
/**
|
|
@@ -360,7 +411,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
360
411
|
* Used when the sheet on top dismisses.
|
|
361
412
|
*/
|
|
362
413
|
fun showDialog() {
|
|
414
|
+
isDialogVisible = true
|
|
363
415
|
dialog?.window?.decorView?.visibility = View.VISIBLE
|
|
416
|
+
|
|
417
|
+
// Emit current position
|
|
418
|
+
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
419
|
+
emitChangePositionDelegate(currentDetentIndex, positionPx, transitioning = true)
|
|
364
420
|
}
|
|
365
421
|
|
|
366
422
|
// ====================================================================
|
|
@@ -378,7 +434,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
378
434
|
|
|
379
435
|
if (isPresented) {
|
|
380
436
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
381
|
-
|
|
437
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
438
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
382
439
|
setStateForDetentIndex(detentIndex)
|
|
383
440
|
} else {
|
|
384
441
|
isDragging = false
|
|
@@ -386,7 +443,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
386
443
|
setStateForDetentIndex(detentIndex)
|
|
387
444
|
|
|
388
445
|
val detentInfo = getDetentInfoForIndex(detentIndex)
|
|
389
|
-
|
|
446
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
447
|
+
|
|
448
|
+
// Notify parent sheet that it is about to lose focus (before this sheet appears)
|
|
449
|
+
parentSheetView?.viewControllerWillBlur()
|
|
450
|
+
|
|
451
|
+
delegate?.viewControllerWillPresent(detentInfo.index, detentInfo.position, detent)
|
|
390
452
|
|
|
391
453
|
if (!animated) {
|
|
392
454
|
dialog.window?.setWindowAnimations(0)
|
|
@@ -398,8 +460,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
398
460
|
|
|
399
461
|
fun dismiss() {
|
|
400
462
|
this.post {
|
|
401
|
-
|
|
402
|
-
|
|
463
|
+
// Emit off-screen position (detent = 0 since sheet is fully hidden)
|
|
464
|
+
emitChangePositionDelegate(currentDetentIndex, screenHeight, transitioning = true)
|
|
403
465
|
}
|
|
404
466
|
dialog?.dismiss()
|
|
405
467
|
}
|
|
@@ -565,6 +627,76 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
565
627
|
}
|
|
566
628
|
}
|
|
567
629
|
|
|
630
|
+
// ====================================================================
|
|
631
|
+
// MARK: - Position Change Delegate
|
|
632
|
+
// ====================================================================
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Emits position change to the delegate if the position has changed.
|
|
636
|
+
* @param index The current detent index (discrete, used as fallback)
|
|
637
|
+
* @param positionPx The current position in pixels (screen Y coordinate)
|
|
638
|
+
* @param transitioning Whether the sheet is transitioning (programmatic) vs dragging
|
|
639
|
+
*/
|
|
640
|
+
private fun emitChangePositionDelegate(index: Int, positionPx: Int, transitioning: Boolean) {
|
|
641
|
+
if (positionPx == lastEmittedPositionPx) return
|
|
642
|
+
|
|
643
|
+
lastEmittedPositionPx = positionPx
|
|
644
|
+
val position = positionPx.pxToDp()
|
|
645
|
+
val interpolatedIndex = getInterpolatedIndexForPosition(positionPx)
|
|
646
|
+
val detent = getDetentValueForIndex(kotlin.math.round(interpolatedIndex).toInt())
|
|
647
|
+
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, transitioning)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Calculates the interpolated index based on position.
|
|
652
|
+
* Returns a continuous value (e.g., 0.5 means halfway between detent 0 and 1).
|
|
653
|
+
*/
|
|
654
|
+
private fun getInterpolatedIndexForPosition(positionPx: Int): Float {
|
|
655
|
+
val count = detents.size
|
|
656
|
+
if (count == 0) return -1f
|
|
657
|
+
if (count == 1) return 0f
|
|
658
|
+
|
|
659
|
+
// Convert position to detent fraction
|
|
660
|
+
val currentDetent = (screenHeight - positionPx).toFloat() / screenHeight.toFloat()
|
|
661
|
+
|
|
662
|
+
// Handle below first detent (interpolate from -1 to 0)
|
|
663
|
+
val firstDetentValue = getDetentValueForIndex(0)
|
|
664
|
+
if (currentDetent < firstDetentValue) {
|
|
665
|
+
if (firstDetentValue <= 0) return 0f
|
|
666
|
+
val progress = currentDetent / firstDetentValue
|
|
667
|
+
return progress - 1f
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Find which segment the current detent falls into and interpolate
|
|
671
|
+
for (i in 0 until count - 1) {
|
|
672
|
+
val detentValue = getDetentValueForIndex(i)
|
|
673
|
+
val nextDetentValue = getDetentValueForIndex(i + 1)
|
|
674
|
+
|
|
675
|
+
if (currentDetent <= nextDetentValue) {
|
|
676
|
+
val range = nextDetentValue - detentValue
|
|
677
|
+
if (range <= 0) return i.toFloat()
|
|
678
|
+
val progress = (currentDetent - detentValue) / range
|
|
679
|
+
return i + maxOf(0f, minOf(1f, progress))
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return (count - 1).toFloat()
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Gets the detent value (fraction) for a given index.
|
|
688
|
+
* For auto (-1), calculates the actual fraction from content + header height.
|
|
689
|
+
*/
|
|
690
|
+
private fun getDetentValueForIndex(index: Int): Float {
|
|
691
|
+
if (index < 0 || index >= detents.size) return 0f
|
|
692
|
+
val value = detents[index]
|
|
693
|
+
return if (value == -1.0) {
|
|
694
|
+
(contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
|
|
695
|
+
} else {
|
|
696
|
+
value.toFloat()
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
568
700
|
// ====================================================================
|
|
569
701
|
// MARK: - Drag Handling
|
|
570
702
|
// ====================================================================
|
|
@@ -574,16 +706,40 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
574
706
|
return DetentInfo(currentDetentIndex, screenY.pxToDp())
|
|
575
707
|
}
|
|
576
708
|
|
|
709
|
+
private fun getCurrentPositionPx(sheetView: View): Int = ScreenUtils.getScreenY(sheetView)
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Returns the detent index for the current position.
|
|
713
|
+
* Only reports a higher index when the sheet has reached that detent's height.
|
|
714
|
+
*/
|
|
715
|
+
private fun getDetentIndexForPosition(positionPx: Int): Int {
|
|
716
|
+
if (detents.isEmpty()) return 0
|
|
717
|
+
|
|
718
|
+
val sheetHeight = screenHeight - positionPx
|
|
719
|
+
|
|
720
|
+
// Find the highest detent index that the sheet has reached
|
|
721
|
+
for (i in detents.indices.reversed()) {
|
|
722
|
+
val detentHeight = getDetentHeight(detents[i])
|
|
723
|
+
if (sheetHeight >= detentHeight) {
|
|
724
|
+
return i
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return 0
|
|
729
|
+
}
|
|
730
|
+
|
|
577
731
|
private fun handleDragBegin(sheetView: View) {
|
|
578
732
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
579
|
-
|
|
733
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
734
|
+
delegate?.viewControllerDidDragBegin(detentInfo.index, detentInfo.position, detent)
|
|
580
735
|
isDragging = true
|
|
581
736
|
}
|
|
582
737
|
|
|
583
738
|
private fun handleDragChange(sheetView: View) {
|
|
584
739
|
if (!isDragging) return
|
|
585
740
|
val detentInfo = getCurrentDetentInfo(sheetView)
|
|
586
|
-
|
|
741
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
742
|
+
delegate?.viewControllerDidDragChange(detentInfo.index, detentInfo.position, detent)
|
|
587
743
|
}
|
|
588
744
|
|
|
589
745
|
private fun handleDragEnd(state: Int) {
|
|
@@ -591,7 +747,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
591
747
|
|
|
592
748
|
val detentInfo = getDetentInfoForState(state)
|
|
593
749
|
detentInfo?.let {
|
|
594
|
-
|
|
750
|
+
val detent = getDetentValueForIndex(it.index)
|
|
751
|
+
delegate?.viewControllerDidDragEnd(it.index, it.position, detent)
|
|
595
752
|
|
|
596
753
|
if (it.index != currentDetentIndex) {
|
|
597
754
|
presentPromise?.invoke()
|
|
@@ -599,7 +756,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
599
756
|
|
|
600
757
|
currentDetentIndex = it.index
|
|
601
758
|
setupDimmedBackground(it.index)
|
|
602
|
-
delegate?.viewControllerDidChangeDetent(it.index, it.position)
|
|
759
|
+
delegate?.viewControllerDidChangeDetent(it.index, it.position, detent)
|
|
603
760
|
}
|
|
604
761
|
}
|
|
605
762
|
|
|
@@ -705,8 +862,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
705
862
|
setupSheetDetents()
|
|
706
863
|
this.post {
|
|
707
864
|
positionFooter()
|
|
708
|
-
val
|
|
709
|
-
|
|
865
|
+
val positionPx = bottomSheetView?.let { ScreenUtils.getScreenY(it) } ?: screenHeight
|
|
866
|
+
emitChangePositionDelegate(currentDetentIndex, positionPx, transitioning = true)
|
|
710
867
|
}
|
|
711
868
|
}
|
|
712
869
|
}
|
|
@@ -67,7 +67,11 @@ class TrueSheetViewManager :
|
|
|
67
67
|
DragBeginEvent.EVENT_NAME to hashMapOf("registrationName" to DragBeginEvent.REGISTRATION_NAME),
|
|
68
68
|
DragChangeEvent.EVENT_NAME to hashMapOf("registrationName" to DragChangeEvent.REGISTRATION_NAME),
|
|
69
69
|
DragEndEvent.EVENT_NAME to hashMapOf("registrationName" to DragEndEvent.REGISTRATION_NAME),
|
|
70
|
-
PositionChangeEvent.EVENT_NAME to hashMapOf("registrationName" to PositionChangeEvent.REGISTRATION_NAME)
|
|
70
|
+
PositionChangeEvent.EVENT_NAME to hashMapOf("registrationName" to PositionChangeEvent.REGISTRATION_NAME),
|
|
71
|
+
WillFocusEvent.EVENT_NAME to hashMapOf("registrationName" to WillFocusEvent.REGISTRATION_NAME),
|
|
72
|
+
FocusEvent.EVENT_NAME to hashMapOf("registrationName" to FocusEvent.REGISTRATION_NAME),
|
|
73
|
+
WillBlurEvent.EVENT_NAME to hashMapOf("registrationName" to WillBlurEvent.REGISTRATION_NAME),
|
|
74
|
+
BlurEvent.EVENT_NAME to hashMapOf("registrationName" to BlurEvent.REGISTRATION_NAME)
|
|
71
75
|
)
|
|
72
76
|
|
|
73
77
|
// ==================== Props ====================
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import com.lodev09.truesheet.TrueSheetView
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manages TrueSheet stacking behavior.
|
|
7
|
+
* Tracks presented sheets and handles visibility when sheets stack on top of each other.
|
|
8
|
+
*/
|
|
9
|
+
object TrueSheetDialogObserver {
|
|
10
|
+
|
|
11
|
+
private val presentedSheetStack = mutableListOf<TrueSheetView>()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Called when a sheet is about to be presented.
|
|
15
|
+
* Returns the visible parent sheet to stack on, or null if none.
|
|
16
|
+
*/
|
|
17
|
+
@JvmStatic
|
|
18
|
+
fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
|
|
19
|
+
synchronized(presentedSheetStack) {
|
|
20
|
+
val parentSheet = presentedSheetStack.lastOrNull()
|
|
21
|
+
?.takeIf { it.viewController.isPresented && it.viewController.isDialogVisible }
|
|
22
|
+
|
|
23
|
+
// Hide parent if the new sheet would cover it
|
|
24
|
+
parentSheet?.let {
|
|
25
|
+
val parentTop = it.viewController.currentSheetTop
|
|
26
|
+
val newSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
|
|
27
|
+
if (!it.viewController.isExpanded && parentTop <= newSheetTop) {
|
|
28
|
+
it.viewController.hideDialog()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!presentedSheetStack.contains(sheetView)) {
|
|
33
|
+
presentedSheetStack.add(sheetView)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return parentSheet
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Called when a sheet has been dismissed.
|
|
42
|
+
* Shows the parent sheet if this sheet was stacked on it.
|
|
43
|
+
*/
|
|
44
|
+
@JvmStatic
|
|
45
|
+
fun onSheetDidDismiss(sheetView: TrueSheetView, hadParent: Boolean) {
|
|
46
|
+
synchronized(presentedSheetStack) {
|
|
47
|
+
presentedSheetStack.remove(sheetView)
|
|
48
|
+
if (hadParent) {
|
|
49
|
+
presentedSheetStack.lastOrNull()?.viewController?.showDialog()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@JvmStatic
|
|
55
|
+
fun removeSheet(sheetView: TrueSheetView) {
|
|
56
|
+
synchronized(presentedSheetStack) {
|
|
57
|
+
presentedSheetStack.remove(sheetView)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@JvmStatic
|
|
62
|
+
fun clear() {
|
|
63
|
+
synchronized(presentedSheetStack) {
|
|
64
|
+
presentedSheetStack.clear()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|