@lodev09/react-native-true-sheet 3.5.0-beta.1 → 3.5.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +162 -138
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +324 -258
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetAnimator.kt +15 -6
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogObserver.kt +10 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +6 -2
- package/android/src/main/res/values/styles.xml +2 -8
- package/package.json +1 -1
|
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
|
|
4
4
|
import android.graphics.Color
|
|
5
5
|
import android.graphics.drawable.ShapeDrawable
|
|
6
6
|
import android.graphics.drawable.shapes.RoundRectShape
|
|
7
|
-
import android.util.Log
|
|
8
7
|
import android.util.TypedValue
|
|
9
8
|
import android.view.MotionEvent
|
|
10
9
|
import android.view.View
|
|
@@ -38,6 +37,10 @@ import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
|
38
37
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
39
38
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
40
39
|
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// MARK: - Data Types & Delegate Protocol
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
41
44
|
data class DetentInfo(val index: Int, val position: Float)
|
|
42
45
|
|
|
43
46
|
interface TrueSheetViewControllerDelegate {
|
|
@@ -58,9 +61,13 @@ interface TrueSheetViewControllerDelegate {
|
|
|
58
61
|
fun viewControllerDidBackPress()
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// MARK: - TrueSheetViewController
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
61
68
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
69
|
+
* Manages the bottom sheet dialog and its presentation lifecycle.
|
|
70
|
+
* Acts as a RootView to properly dispatch touch events to React Native.
|
|
64
71
|
*/
|
|
65
72
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
66
73
|
class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
@@ -72,56 +79,35 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
72
79
|
companion object {
|
|
73
80
|
const val TAG_NAME = "TrueSheet"
|
|
74
81
|
|
|
75
|
-
|
|
82
|
+
// Prevents fully expanded ratio which causes behavior issues
|
|
76
83
|
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
77
84
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
78
85
|
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
79
86
|
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
//
|
|
83
|
-
// MARK: -
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
var delegate: TrueSheetViewControllerDelegate? = null
|
|
87
|
-
|
|
88
|
-
// ====================================================================
|
|
89
|
-
// MARK: - Dialog & Views
|
|
90
|
-
// ====================================================================
|
|
91
|
-
|
|
92
|
-
private var dialog: BottomSheetDialog? = null
|
|
93
|
-
private var dimView: TrueSheetDimView? = null
|
|
94
|
-
private var parentDimView: TrueSheetDimView? = null
|
|
95
|
-
|
|
96
|
-
private val behavior: BottomSheetBehavior<FrameLayout>?
|
|
97
|
-
get() = dialog?.behavior
|
|
98
|
-
|
|
99
|
-
private val sheetContainer: FrameLayout?
|
|
100
|
-
get() = this.parent as? FrameLayout
|
|
101
|
-
|
|
102
|
-
override val bottomSheetView: FrameLayout?
|
|
103
|
-
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
104
|
-
|
|
105
|
-
private val containerView: TrueSheetContainerView?
|
|
106
|
-
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
107
|
-
|
|
108
|
-
override val contentHeight: Int
|
|
109
|
-
get() = containerView?.contentHeight ?: 0
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// MARK: - Types
|
|
91
|
+
// =============================================================================
|
|
110
92
|
|
|
111
|
-
override val headerHeight: Int
|
|
112
|
-
get() = containerView?.headerHeight ?: 0
|
|
113
|
-
|
|
114
|
-
// ====================================================================
|
|
115
|
-
// MARK: - State
|
|
116
|
-
// ====================================================================
|
|
117
|
-
|
|
118
|
-
/** Interaction state for the sheet */
|
|
119
93
|
private sealed class InteractionState {
|
|
120
94
|
data object Idle : InteractionState()
|
|
121
95
|
data class Dragging(val startTop: Int) : InteractionState()
|
|
122
96
|
data object Reconfiguring : InteractionState()
|
|
123
97
|
}
|
|
124
98
|
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// MARK: - Properties
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
var delegate: TrueSheetViewControllerDelegate? = null
|
|
104
|
+
|
|
105
|
+
// Dialog & Views
|
|
106
|
+
private var dialog: BottomSheetDialog? = null
|
|
107
|
+
private var dimView: TrueSheetDimView? = null
|
|
108
|
+
private var parentDimView: TrueSheetDimView? = null
|
|
109
|
+
|
|
110
|
+
// Presentation State
|
|
125
111
|
var isPresented = false
|
|
126
112
|
private set
|
|
127
113
|
|
|
@@ -139,48 +125,47 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
139
125
|
private var lastStateWidth: Int = 0
|
|
140
126
|
private var lastStateHeight: Int = 0
|
|
141
127
|
private var lastEmittedPositionPx: Int = -1
|
|
128
|
+
|
|
129
|
+
// Keyboard State
|
|
142
130
|
private var detentIndexBeforeKeyboard: Int = -1
|
|
143
131
|
private var isKeyboardTransitioning: Boolean = false
|
|
144
132
|
|
|
133
|
+
// Promises
|
|
145
134
|
var presentPromise: (() -> Unit)? = null
|
|
146
135
|
var dismissPromise: (() -> Unit)? = null
|
|
147
136
|
|
|
148
|
-
//
|
|
137
|
+
// For stacked sheets
|
|
149
138
|
var parentSheetView: TrueSheetView? = null
|
|
150
139
|
|
|
151
|
-
//
|
|
152
|
-
// MARK: - Helper Classes
|
|
153
|
-
// ====================================================================
|
|
154
|
-
|
|
140
|
+
// Helper Objects
|
|
155
141
|
private val sheetAnimator = TrueSheetAnimator(this)
|
|
156
142
|
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
157
143
|
private var rnScreensObserver: RNScreensFragmentObserver? = null
|
|
144
|
+
private val detentCalculator = TrueSheetDetentCalculator(this)
|
|
158
145
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
override val screenHeight: Int
|
|
164
|
-
get() = ScreenUtils.getScreenHeight(reactContext)
|
|
165
|
-
val screenWidth: Int
|
|
166
|
-
get() = ScreenUtils.getScreenWidth(reactContext)
|
|
167
|
-
override val realScreenHeight: Int
|
|
168
|
-
get() = ScreenUtils.getRealScreenHeight(reactContext)
|
|
146
|
+
// Touch Dispatchers
|
|
147
|
+
internal var eventDispatcher: EventDispatcher? = null
|
|
148
|
+
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
149
|
+
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
169
150
|
|
|
151
|
+
// Detent Configuration
|
|
170
152
|
override var maxSheetHeight: Int? = null
|
|
171
153
|
override var detents: MutableList<Double> = mutableListOf(0.5, 1.0)
|
|
172
154
|
|
|
155
|
+
// Appearance Configuration
|
|
173
156
|
var dimmed = true
|
|
174
157
|
var dimmedDetentIndex = 0
|
|
175
158
|
var grabber: Boolean = true
|
|
176
159
|
var grabberOptions: GrabberOptions? = null
|
|
160
|
+
var sheetBackgroundColor: Int? = null
|
|
161
|
+
var edgeToEdgeFullScreen: Boolean = false
|
|
162
|
+
var insetAdjustment: String = "automatic"
|
|
163
|
+
|
|
177
164
|
var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
178
165
|
set(value) {
|
|
179
166
|
field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
|
|
180
167
|
if (isPresented) setupBackground()
|
|
181
168
|
}
|
|
182
|
-
var sheetBackgroundColor: Int? = null
|
|
183
|
-
var edgeToEdgeFullScreen: Boolean = false
|
|
184
169
|
|
|
185
170
|
var dismissible: Boolean = true
|
|
186
171
|
set(value) {
|
|
@@ -198,13 +183,47 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
198
183
|
behavior?.isDraggable = value
|
|
199
184
|
}
|
|
200
185
|
|
|
201
|
-
//
|
|
186
|
+
// =============================================================================
|
|
202
187
|
// MARK: - Computed Properties
|
|
203
|
-
//
|
|
188
|
+
// =============================================================================
|
|
189
|
+
|
|
190
|
+
// Dialog
|
|
191
|
+
private val behavior: BottomSheetBehavior<FrameLayout>?
|
|
192
|
+
get() = dialog?.behavior
|
|
193
|
+
|
|
194
|
+
private val sheetContainer: FrameLayout?
|
|
195
|
+
get() = this.parent as? FrameLayout
|
|
196
|
+
|
|
197
|
+
override val bottomSheetView: FrameLayout?
|
|
198
|
+
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
199
|
+
|
|
200
|
+
private val containerView: TrueSheetContainerView?
|
|
201
|
+
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
202
|
+
|
|
203
|
+
// Screen Measurements
|
|
204
|
+
override val screenHeight: Int
|
|
205
|
+
get() = ScreenUtils.getScreenHeight(reactContext)
|
|
206
|
+
|
|
207
|
+
val screenWidth: Int
|
|
208
|
+
get() = ScreenUtils.getScreenWidth(reactContext)
|
|
209
|
+
|
|
210
|
+
// Includes system bars for accurate positioning
|
|
211
|
+
override val realScreenHeight: Int
|
|
212
|
+
get() = ScreenUtils.getRealScreenHeight(reactContext)
|
|
213
|
+
|
|
214
|
+
// Content Measurements
|
|
215
|
+
override val contentHeight: Int
|
|
216
|
+
get() = containerView?.contentHeight ?: 0
|
|
217
|
+
|
|
218
|
+
override val headerHeight: Int
|
|
219
|
+
get() = containerView?.headerHeight ?: 0
|
|
204
220
|
|
|
221
|
+
// Insets
|
|
222
|
+
// Target keyboard height used for detent calculations
|
|
205
223
|
override val keyboardInset: Int
|
|
206
224
|
get() = keyboardObserver?.targetHeight ?: 0
|
|
207
225
|
|
|
226
|
+
// Current animated keyboard height for positioning
|
|
208
227
|
private val currentKeyboardInset: Int
|
|
209
228
|
get() = keyboardObserver?.currentHeight ?: 0
|
|
210
229
|
|
|
@@ -214,8 +233,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
214
233
|
val topInset: Int
|
|
215
234
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).top else 0
|
|
216
235
|
|
|
217
|
-
var insetAdjustment: String = "automatic"
|
|
218
|
-
|
|
219
236
|
override val contentBottomInset: Int
|
|
220
237
|
get() = if (insetAdjustment == "automatic") bottomInset else 0
|
|
221
238
|
|
|
@@ -225,24 +242,33 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
225
242
|
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
226
243
|
}
|
|
227
244
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
245
|
+
// Sheet State
|
|
246
|
+
val isExpanded: Boolean
|
|
247
|
+
get() {
|
|
248
|
+
val sheetTop = bottomSheetView?.top ?: return false
|
|
249
|
+
return sheetTop <= topInset
|
|
250
|
+
}
|
|
231
251
|
|
|
232
|
-
|
|
233
|
-
|
|
252
|
+
val currentTranslationY: Int
|
|
253
|
+
get() = bottomSheetView?.translationY?.toInt() ?: 0
|
|
254
|
+
|
|
255
|
+
private val isTopmostSheet: Boolean
|
|
256
|
+
get() {
|
|
257
|
+
val hostView = delegate as? TrueSheetView ?: return true
|
|
258
|
+
return TrueSheetDialogObserver.isTopmostSheet(hostView)
|
|
259
|
+
}
|
|
234
260
|
|
|
235
|
-
//
|
|
261
|
+
// =============================================================================
|
|
236
262
|
// MARK: - Initialization
|
|
237
|
-
//
|
|
263
|
+
// =============================================================================
|
|
238
264
|
|
|
239
265
|
init {
|
|
240
266
|
jSPointerDispatcher = JSPointerDispatcher(this)
|
|
241
267
|
}
|
|
242
268
|
|
|
243
|
-
//
|
|
244
|
-
// MARK: - Dialog
|
|
245
|
-
//
|
|
269
|
+
// =============================================================================
|
|
270
|
+
// MARK: - Dialog Creation & Cleanup
|
|
271
|
+
// =============================================================================
|
|
246
272
|
|
|
247
273
|
fun createDialog() {
|
|
248
274
|
if (dialog != null) return
|
|
@@ -307,12 +333,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
307
333
|
shouldAnimatePresent = true
|
|
308
334
|
}
|
|
309
335
|
|
|
336
|
+
// =============================================================================
|
|
337
|
+
// MARK: - Dialog Listeners
|
|
338
|
+
// =============================================================================
|
|
339
|
+
|
|
310
340
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
311
341
|
dialog.setOnShowListener {
|
|
312
342
|
bottomSheetView?.visibility = VISIBLE
|
|
343
|
+
|
|
313
344
|
isPresented = true
|
|
314
345
|
isDialogVisible = true
|
|
315
346
|
|
|
347
|
+
emitWillPresentEvents()
|
|
348
|
+
|
|
349
|
+
setupSheetDetents()
|
|
350
|
+
setupBackground()
|
|
351
|
+
setupGrabber()
|
|
316
352
|
setupKeyboardObserver()
|
|
317
353
|
|
|
318
354
|
if (shouldAnimatePresent) {
|
|
@@ -346,8 +382,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
346
382
|
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
347
383
|
val behavior = behavior ?: return
|
|
348
384
|
|
|
349
|
-
emitChangePositionDelegate(sheetView.top)
|
|
350
|
-
|
|
351
385
|
when (behavior.state) {
|
|
352
386
|
BottomSheetBehavior.STATE_DRAGGING,
|
|
353
387
|
BottomSheetBehavior.STATE_SETTLING -> handleDragChange(sheetView)
|
|
@@ -355,9 +389,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
355
389
|
else -> { }
|
|
356
390
|
}
|
|
357
391
|
|
|
358
|
-
if (!
|
|
359
|
-
|
|
360
|
-
|
|
392
|
+
if (!sheetAnimator.isAnimating) {
|
|
393
|
+
emitChangePositionDelegate(sheetView.top)
|
|
394
|
+
|
|
395
|
+
if (!isKeyboardTransitioning) {
|
|
396
|
+
positionFooter(slideOffset)
|
|
397
|
+
updateDimAmount(sheetView.top)
|
|
398
|
+
}
|
|
361
399
|
}
|
|
362
400
|
}
|
|
363
401
|
|
|
@@ -421,33 +459,30 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
421
459
|
}
|
|
422
460
|
}
|
|
423
461
|
|
|
462
|
+
// =============================================================================
|
|
463
|
+
// MARK: - Modal Observer (react-native-screens)
|
|
464
|
+
// =============================================================================
|
|
465
|
+
|
|
424
466
|
private fun setupModalObserver() {
|
|
425
467
|
rnScreensObserver = RNScreensFragmentObserver(
|
|
426
468
|
reactContext = reactContext,
|
|
427
469
|
onModalPresented = {
|
|
428
|
-
if (isPresented && isDialogVisible) {
|
|
429
|
-
|
|
430
|
-
wasHiddenByModal = true
|
|
431
|
-
|
|
432
|
-
dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFastFadeOut)
|
|
433
|
-
dialog?.window?.decorView?.visibility = GONE
|
|
434
|
-
dimView?.visibility = INVISIBLE
|
|
435
|
-
parentDimView?.visibility = INVISIBLE
|
|
470
|
+
if (isPresented && isDialogVisible && isTopmostSheet) {
|
|
471
|
+
hideForModal()
|
|
436
472
|
}
|
|
437
473
|
},
|
|
438
474
|
onModalWillDismiss = {
|
|
439
|
-
if (isPresented && wasHiddenByModal) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
dialog?.window?.setWindowAnimations(0)
|
|
443
|
-
dialog?.window?.decorView?.visibility = VISIBLE
|
|
444
|
-
dimView?.visibility = VISIBLE
|
|
445
|
-
parentDimView?.visibility = VISIBLE
|
|
475
|
+
if (isPresented && wasHiddenByModal && isTopmostSheet) {
|
|
476
|
+
showAfterModal()
|
|
446
477
|
}
|
|
447
478
|
},
|
|
448
479
|
onModalDidDismiss = {
|
|
449
480
|
if (isPresented && wasHiddenByModal) {
|
|
450
481
|
wasHiddenByModal = false
|
|
482
|
+
// Restore parent sheet after this sheet is restored
|
|
483
|
+
parentSheetView?.viewController?.let { parent ->
|
|
484
|
+
post { parent.showAfterModal() }
|
|
485
|
+
}
|
|
451
486
|
}
|
|
452
487
|
}
|
|
453
488
|
)
|
|
@@ -459,88 +494,36 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
459
494
|
rnScreensObserver = null
|
|
460
495
|
}
|
|
461
496
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
private fun emitWillPresentEvents() {
|
|
467
|
-
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
468
|
-
parentSheetView?.viewControllerWillBlur()
|
|
469
|
-
delegate?.viewControllerWillPresent(index, position, detent)
|
|
470
|
-
delegate?.viewControllerWillFocus()
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
private fun emitWillDismissEvents() {
|
|
474
|
-
delegate?.viewControllerWillBlur()
|
|
475
|
-
delegate?.viewControllerWillDismiss()
|
|
476
|
-
parentSheetView?.viewControllerWillFocus()
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
private fun emitDidDismissEvents() {
|
|
480
|
-
val hadParent = parentSheetView != null
|
|
481
|
-
parentSheetView?.viewControllerDidFocus()
|
|
482
|
-
parentSheetView = null
|
|
483
|
-
|
|
484
|
-
delegate?.viewControllerDidBlur()
|
|
485
|
-
delegate?.viewControllerDidDismiss(hadParent)
|
|
486
|
-
|
|
487
|
-
dismissPromise?.invoke()
|
|
488
|
-
dismissPromise = null
|
|
489
|
-
}
|
|
497
|
+
private fun hideForModal() {
|
|
498
|
+
isDialogVisible = false
|
|
499
|
+
wasHiddenByModal = true
|
|
490
500
|
|
|
491
|
-
|
|
492
|
-
|
|
501
|
+
// Prepare for fast fade out
|
|
502
|
+
dimView?.alpha = 0f
|
|
503
|
+
parentDimView?.alpha = 0f
|
|
493
504
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
499
|
-
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
500
|
-
}
|
|
505
|
+
dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFastFadeOut)
|
|
506
|
+
dialog?.window?.decorView?.visibility = GONE
|
|
507
|
+
dimView?.visibility = INVISIBLE
|
|
508
|
+
parentDimView?.visibility = INVISIBLE
|
|
501
509
|
|
|
502
|
-
|
|
503
|
-
val state = detentCalculator.getStateForDetentIndex(index)
|
|
504
|
-
val detentIndex = detentCalculator.getDetentIndexForState(state) ?: 0
|
|
505
|
-
val position = getPositionForDetentIndex(detentIndex)
|
|
506
|
-
val detent = detentCalculator.getDetentValueForIndex(detentIndex)
|
|
507
|
-
return Triple(detentIndex, position, detent)
|
|
510
|
+
parentSheetView?.viewController?.hideForModal()
|
|
508
511
|
}
|
|
509
512
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
// ====================================================================
|
|
513
|
-
|
|
514
|
-
val isExpanded: Boolean
|
|
515
|
-
get() {
|
|
516
|
-
val sheetTop = bottomSheetView?.top ?: return false
|
|
517
|
-
return sheetTop <= topInset
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
val currentTranslationY: Int
|
|
521
|
-
get() = bottomSheetView?.translationY?.toInt() ?: 0
|
|
522
|
-
|
|
523
|
-
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
524
|
-
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
525
|
-
return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
|
|
526
|
-
}
|
|
513
|
+
private fun showAfterModal() {
|
|
514
|
+
isDialogVisible = true
|
|
527
515
|
|
|
528
|
-
|
|
529
|
-
|
|
516
|
+
dialog?.window?.setWindowAnimations(0)
|
|
517
|
+
dialog?.window?.decorView?.visibility = VISIBLE
|
|
518
|
+
dimView?.visibility = VISIBLE
|
|
519
|
+
parentDimView?.visibility = VISIBLE
|
|
530
520
|
|
|
531
|
-
|
|
532
|
-
.translationY(translationY.toFloat())
|
|
533
|
-
.setDuration(TRANSLATE_ANIMATION_DURATION)
|
|
534
|
-
.setUpdateListener {
|
|
535
|
-
val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
|
|
536
|
-
emitChangePositionDelegate(effectiveTop)
|
|
537
|
-
}
|
|
538
|
-
.start()
|
|
521
|
+
updateDimAmount(animated = true)
|
|
539
522
|
}
|
|
540
523
|
|
|
541
|
-
//
|
|
524
|
+
// =============================================================================
|
|
542
525
|
// MARK: - Presentation
|
|
543
|
-
//
|
|
526
|
+
// =============================================================================
|
|
544
527
|
|
|
545
528
|
fun present(detentIndex: Int, animated: Boolean = true) {
|
|
546
529
|
val dialog = this.dialog ?: run {
|
|
@@ -549,21 +532,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
549
532
|
}
|
|
550
533
|
|
|
551
534
|
setupDimmedBackground(detentIndex)
|
|
535
|
+
setStateForDetentIndex(detentIndex)
|
|
552
536
|
|
|
553
|
-
if (isPresented) {
|
|
554
|
-
setStateForDetentIndex(detentIndex)
|
|
555
|
-
} else {
|
|
537
|
+
if (!isPresented) {
|
|
556
538
|
shouldAnimatePresent = animated
|
|
557
539
|
currentDetentIndex = detentIndex
|
|
558
540
|
interactionState = InteractionState.Idle
|
|
559
541
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
setupSheetDetents()
|
|
563
|
-
setStateForDetentIndex(detentIndex)
|
|
564
|
-
setupBackground()
|
|
565
|
-
setupGrabber()
|
|
566
|
-
|
|
542
|
+
// Position off-screen until animation starts
|
|
543
|
+
bottomSheetView?.translationY = realScreenHeight.toFloat()
|
|
567
544
|
bottomSheetView?.visibility = INVISIBLE
|
|
568
545
|
|
|
569
546
|
dialog.show()
|
|
@@ -601,9 +578,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
601
578
|
presentPromise = null
|
|
602
579
|
}
|
|
603
580
|
|
|
604
|
-
//
|
|
581
|
+
// =============================================================================
|
|
605
582
|
// MARK: - Sheet Configuration
|
|
606
|
-
//
|
|
583
|
+
// =============================================================================
|
|
607
584
|
|
|
608
585
|
fun setupSheetDetents() {
|
|
609
586
|
val behavior = this.behavior ?: return
|
|
@@ -627,9 +604,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
627
604
|
val maxDetentHeight = detentCalculator.getDetentHeight(detents.last())
|
|
628
605
|
|
|
629
606
|
val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
|
|
630
|
-
halfExpandedRatio =
|
|
607
|
+
halfExpandedRatio = (adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat())
|
|
608
|
+
.coerceIn(0f, 0.999f)
|
|
631
609
|
|
|
632
610
|
expandedOffset = maxOf(edgeToEdgeTopInset, realScreenHeight - maxDetentHeight)
|
|
611
|
+
|
|
612
|
+
// fitToContents works better with <= 2 detents when no expanded offset
|
|
633
613
|
isFitToContents = detents.size < 3 && expandedOffset == 0
|
|
634
614
|
|
|
635
615
|
val offset = if (expandedOffset == 0) topInset else 0
|
|
@@ -655,6 +635,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
655
635
|
positionFooter()
|
|
656
636
|
}
|
|
657
637
|
|
|
638
|
+
fun setStateForDetentIndex(index: Int) {
|
|
639
|
+
behavior?.state = detentCalculator.getStateForDetentIndex(index)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// =============================================================================
|
|
643
|
+
// MARK: - Grabber
|
|
644
|
+
// =============================================================================
|
|
645
|
+
|
|
658
646
|
fun setupGrabber() {
|
|
659
647
|
val bottomSheet = bottomSheetView ?: return
|
|
660
648
|
|
|
@@ -671,61 +659,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
671
659
|
bottomSheet.addView(grabberView)
|
|
672
660
|
}
|
|
673
661
|
|
|
674
|
-
//
|
|
675
|
-
// MARK: - Keyboard Handling
|
|
676
|
-
// ====================================================================
|
|
677
|
-
|
|
678
|
-
private fun shouldHandleKeyboard(): Boolean {
|
|
679
|
-
if (wasHiddenByModal) return false
|
|
680
|
-
|
|
681
|
-
val parentView = parentSheetView ?: return true
|
|
682
|
-
return TrueSheetDialogObserver.getSheetsAbove(parentView).firstOrNull()?.viewController == this
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
fun setupKeyboardObserver() {
|
|
686
|
-
val bottomSheet = bottomSheetView ?: return
|
|
687
|
-
keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
|
|
688
|
-
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
689
|
-
override fun keyboardWillShow(height: Int) {
|
|
690
|
-
if (!shouldHandleKeyboard()) return
|
|
691
|
-
detentIndexBeforeKeyboard = currentDetentIndex
|
|
692
|
-
isKeyboardTransitioning = true
|
|
693
|
-
setupSheetDetents()
|
|
694
|
-
setStateForDetentIndex(detents.size - 1)
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
override fun keyboardWillHide() {
|
|
698
|
-
if (!shouldHandleKeyboard()) return
|
|
699
|
-
setupSheetDetents()
|
|
700
|
-
if (detentIndexBeforeKeyboard >= 0) {
|
|
701
|
-
setStateForDetentIndex(detentIndexBeforeKeyboard)
|
|
702
|
-
detentIndexBeforeKeyboard = -1
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
override fun keyboardDidHide() {
|
|
707
|
-
if (!shouldHandleKeyboard()) return
|
|
708
|
-
isKeyboardTransitioning = false
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
override fun keyboardDidChangeHeight(height: Int) {}
|
|
712
|
-
}
|
|
713
|
-
start()
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
fun cleanupKeyboardObserver() {
|
|
718
|
-
keyboardObserver?.stop()
|
|
719
|
-
keyboardObserver = null
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// ====================================================================
|
|
662
|
+
// =============================================================================
|
|
723
663
|
// MARK: - Background & Dimming
|
|
724
|
-
//
|
|
664
|
+
// =============================================================================
|
|
725
665
|
|
|
726
666
|
fun setupBackground() {
|
|
727
667
|
val bottomSheet = bottomSheetView ?: return
|
|
728
668
|
|
|
669
|
+
// Rounded corners only on top
|
|
729
670
|
val outerRadii = floatArrayOf(
|
|
730
671
|
sheetCornerRadius,
|
|
731
672
|
sheetCornerRadius,
|
|
@@ -759,6 +700,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
759
700
|
if (dimView == null) dimView = TrueSheetDimView(reactContext)
|
|
760
701
|
if (!parentDimVisible) dimView?.attach(null)
|
|
761
702
|
|
|
703
|
+
// Attach dim view to parent sheet if stacked
|
|
762
704
|
val parentController = parentSheetView?.viewController
|
|
763
705
|
val parentBottomSheet = parentController?.bottomSheetView
|
|
764
706
|
if (parentBottomSheet != null) {
|
|
@@ -780,6 +722,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
780
722
|
true
|
|
781
723
|
}
|
|
782
724
|
} else {
|
|
725
|
+
// Pass through touches to parent or activity when not dimmed
|
|
783
726
|
touchOutside.setOnTouchListener { v, event ->
|
|
784
727
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
785
728
|
(
|
|
@@ -793,13 +736,42 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
793
736
|
}
|
|
794
737
|
}
|
|
795
738
|
|
|
796
|
-
fun updateDimAmount(sheetTop: Int? = null) {
|
|
739
|
+
fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
|
|
797
740
|
if (!dimmed) return
|
|
798
741
|
val top = (sheetTop ?: bottomSheetView?.top ?: return) + currentKeyboardInset
|
|
799
|
-
|
|
800
|
-
|
|
742
|
+
|
|
743
|
+
if (animated) {
|
|
744
|
+
val targetAlpha = dimView?.calculateAlpha(
|
|
745
|
+
top,
|
|
746
|
+
dimmedDetentIndex,
|
|
747
|
+
detentCalculator::getSheetTopForDetentIndex
|
|
748
|
+
) ?: 0f
|
|
749
|
+
dimView?.animate()?.alpha(targetAlpha)?.setDuration(200)?.start()
|
|
750
|
+
parentDimView?.animate()?.alpha(targetAlpha)?.setDuration(200)?.start()
|
|
751
|
+
} else {
|
|
752
|
+
dimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
|
|
753
|
+
parentDimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
|
|
754
|
+
}
|
|
801
755
|
}
|
|
802
756
|
|
|
757
|
+
fun getDefaultBackgroundColor(): Int {
|
|
758
|
+
val typedValue = TypedValue()
|
|
759
|
+
return if (reactContext.theme.resolveAttribute(
|
|
760
|
+
com.google.android.material.R.attr.colorSurfaceContainerLow,
|
|
761
|
+
typedValue,
|
|
762
|
+
true
|
|
763
|
+
)
|
|
764
|
+
) {
|
|
765
|
+
typedValue.data
|
|
766
|
+
} else {
|
|
767
|
+
Color.WHITE
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// =============================================================================
|
|
772
|
+
// MARK: - Footer Positioning
|
|
773
|
+
// =============================================================================
|
|
774
|
+
|
|
803
775
|
fun positionFooter(slideOffset: Float? = null) {
|
|
804
776
|
val footerView = containerView?.footerView ?: return
|
|
805
777
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -809,35 +781,66 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
809
781
|
val sheetTop = bottomSheet.top
|
|
810
782
|
|
|
811
783
|
var footerY = (sheetHeight - sheetTop - footerHeight - currentKeyboardInset).toFloat()
|
|
784
|
+
|
|
785
|
+
// Adjust during dismiss animation when slideOffset is negative
|
|
812
786
|
if (slideOffset != null && slideOffset < 0) {
|
|
813
787
|
footerY -= (footerHeight * slideOffset)
|
|
814
788
|
}
|
|
815
789
|
|
|
790
|
+
// Clamp to prevent footer going above safe area
|
|
816
791
|
val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
|
|
817
792
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
818
793
|
}
|
|
819
794
|
|
|
820
|
-
|
|
821
|
-
|
|
795
|
+
// =============================================================================
|
|
796
|
+
// MARK: - Keyboard Handling
|
|
797
|
+
// =============================================================================
|
|
798
|
+
|
|
799
|
+
private fun shouldHandleKeyboard(): Boolean {
|
|
800
|
+
if (wasHiddenByModal) return false
|
|
801
|
+
return isTopmostSheet
|
|
822
802
|
}
|
|
823
803
|
|
|
824
|
-
fun
|
|
825
|
-
val
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
804
|
+
fun setupKeyboardObserver() {
|
|
805
|
+
val bottomSheet = bottomSheetView ?: return
|
|
806
|
+
keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
|
|
807
|
+
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
808
|
+
override fun keyboardWillShow(height: Int) {
|
|
809
|
+
if (!shouldHandleKeyboard()) return
|
|
810
|
+
detentIndexBeforeKeyboard = currentDetentIndex
|
|
811
|
+
isKeyboardTransitioning = true
|
|
812
|
+
setupSheetDetents()
|
|
813
|
+
setStateForDetentIndex(detents.size - 1)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
override fun keyboardWillHide() {
|
|
817
|
+
if (!shouldHandleKeyboard()) return
|
|
818
|
+
setupSheetDetents()
|
|
819
|
+
if (detentIndexBeforeKeyboard >= 0) {
|
|
820
|
+
setStateForDetentIndex(detentIndexBeforeKeyboard)
|
|
821
|
+
detentIndexBeforeKeyboard = -1
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
override fun keyboardDidHide() {
|
|
826
|
+
if (!shouldHandleKeyboard()) return
|
|
827
|
+
isKeyboardTransitioning = false
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
override fun keyboardDidChangeHeight(height: Int) {}
|
|
831
|
+
}
|
|
832
|
+
start()
|
|
835
833
|
}
|
|
836
834
|
}
|
|
837
835
|
|
|
838
|
-
|
|
836
|
+
fun cleanupKeyboardObserver() {
|
|
837
|
+
keyboardObserver?.stop()
|
|
838
|
+
keyboardObserver = null
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// =============================================================================
|
|
839
842
|
// MARK: - Drag Handling
|
|
840
|
-
//
|
|
843
|
+
// =============================================================================
|
|
841
844
|
|
|
842
845
|
private fun handleDragBegin(sheetView: View) {
|
|
843
846
|
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
@@ -853,13 +856,75 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
853
856
|
delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
|
|
854
857
|
}
|
|
855
858
|
|
|
856
|
-
//
|
|
859
|
+
// =============================================================================
|
|
860
|
+
// MARK: - Event Emission
|
|
861
|
+
// =============================================================================
|
|
862
|
+
|
|
863
|
+
private fun emitWillPresentEvents() {
|
|
864
|
+
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
865
|
+
parentSheetView?.viewControllerWillBlur()
|
|
866
|
+
delegate?.viewControllerWillPresent(index, position, detent)
|
|
867
|
+
delegate?.viewControllerWillFocus()
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private fun emitWillDismissEvents() {
|
|
871
|
+
delegate?.viewControllerWillBlur()
|
|
872
|
+
delegate?.viewControllerWillDismiss()
|
|
873
|
+
parentSheetView?.viewControllerWillFocus()
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
private fun emitDidDismissEvents() {
|
|
877
|
+
val hadParent = parentSheetView != null
|
|
878
|
+
parentSheetView?.viewControllerDidFocus()
|
|
879
|
+
parentSheetView = null
|
|
880
|
+
|
|
881
|
+
delegate?.viewControllerDidBlur()
|
|
882
|
+
delegate?.viewControllerDidDismiss(hadParent)
|
|
883
|
+
|
|
884
|
+
dismissPromise?.invoke()
|
|
885
|
+
dismissPromise = null
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
|
|
889
|
+
// Dedupe emissions for same position
|
|
890
|
+
if (currentTop == lastEmittedPositionPx) return
|
|
891
|
+
|
|
892
|
+
lastEmittedPositionPx = currentTop
|
|
893
|
+
val visibleHeight = realScreenHeight - currentTop
|
|
894
|
+
val position = detentCalculator.getPositionDp(visibleHeight)
|
|
895
|
+
val interpolatedIndex = detentCalculator.getInterpolatedIndexForPosition(currentTop)
|
|
896
|
+
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
897
|
+
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// =============================================================================
|
|
857
901
|
// MARK: - Detent Helpers
|
|
858
|
-
//
|
|
902
|
+
// =============================================================================
|
|
903
|
+
|
|
904
|
+
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
905
|
+
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
906
|
+
return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
fun translateDialog(translationY: Int) {
|
|
910
|
+
val bottomSheet = bottomSheetView ?: return
|
|
859
911
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
912
|
+
bottomSheet.animate()
|
|
913
|
+
.translationY(translationY.toFloat())
|
|
914
|
+
.setDuration(TRANSLATE_ANIMATION_DURATION)
|
|
915
|
+
.setUpdateListener {
|
|
916
|
+
val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
|
|
917
|
+
emitChangePositionDelegate(effectiveTop)
|
|
918
|
+
}
|
|
919
|
+
.start()
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
|
|
923
|
+
val state = detentCalculator.getStateForDetentIndex(index)
|
|
924
|
+
val detentIndex = detentCalculator.getDetentIndexForState(state) ?: 0
|
|
925
|
+
val position = getPositionForDetentIndex(detentIndex)
|
|
926
|
+
val detent = detentCalculator.getDetentValueForIndex(detentIndex)
|
|
927
|
+
return Triple(detentIndex, position, detent)
|
|
863
928
|
}
|
|
864
929
|
|
|
865
930
|
private fun getPositionForDetentIndex(index: Int): Float {
|
|
@@ -867,7 +932,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
867
932
|
|
|
868
933
|
bottomSheetView?.let {
|
|
869
934
|
val visibleSheetHeight = detentCalculator.getVisibleSheetHeight(it.top)
|
|
870
|
-
if (visibleSheetHeight
|
|
935
|
+
if (visibleSheetHeight in 1..<realScreenHeight) {
|
|
871
936
|
return detentCalculator.getPositionDp(visibleSheetHeight)
|
|
872
937
|
}
|
|
873
938
|
}
|
|
@@ -876,12 +941,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
876
941
|
return detentCalculator.getPositionDp(detentHeight)
|
|
877
942
|
}
|
|
878
943
|
|
|
879
|
-
|
|
880
|
-
getDetentInfoForState(detentCalculator.getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
|
|
881
|
-
|
|
882
|
-
// ====================================================================
|
|
944
|
+
// =============================================================================
|
|
883
945
|
// MARK: - RootView Implementation
|
|
884
|
-
//
|
|
946
|
+
// =============================================================================
|
|
885
947
|
|
|
886
948
|
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
|
|
887
949
|
super.onInitializeAccessibilityNodeInfo(info)
|
|
@@ -893,9 +955,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
893
955
|
|
|
894
956
|
if (w == oldw && h == oldh) return
|
|
895
957
|
if (!isPresented) return
|
|
958
|
+
|
|
959
|
+
// Skip reconfiguration if expanded and only height changed (e.g., keyboard)
|
|
896
960
|
if (h + topInset >= screenHeight && isExpanded && oldw == w) return
|
|
897
961
|
|
|
898
|
-
|
|
962
|
+
post {
|
|
899
963
|
setupSheetDetents()
|
|
900
964
|
positionFooter()
|
|
901
965
|
bottomSheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
|
|
@@ -906,17 +970,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
906
970
|
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
907
971
|
}
|
|
908
972
|
|
|
909
|
-
//
|
|
973
|
+
// =============================================================================
|
|
910
974
|
// MARK: - Touch Event Handling
|
|
911
|
-
//
|
|
975
|
+
// =============================================================================
|
|
912
976
|
|
|
913
977
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
978
|
+
// Footer needs special handling since it's positioned absolutely
|
|
914
979
|
val footer = containerView?.footerView
|
|
915
980
|
if (footer != null && footer.isVisible) {
|
|
916
981
|
val footerLocation = ScreenUtils.getScreenLocation(footer)
|
|
917
982
|
val touchScreenX = event.rawX.toInt()
|
|
918
983
|
val touchScreenY = event.rawY.toInt()
|
|
919
984
|
|
|
985
|
+
// Check if touch is within footer bounds
|
|
920
986
|
if (touchScreenX >= footerLocation[0] &&
|
|
921
987
|
touchScreenX <= footerLocation[0] + footer.width &&
|
|
922
988
|
touchScreenY >= footerLocation[1] &&
|