@lodev09/react-native-true-sheet 3.5.0-beta.1 → 3.5.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.
|
@@ -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,6 +79,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
72
79
|
companion object {
|
|
73
80
|
const val TAG_NAME = "TrueSheet"
|
|
74
81
|
|
|
82
|
+
// Prevents fully expanded ratio which causes behavior issues
|
|
75
83
|
private const val MAX_HALF_EXPANDED_RATIO = 0.999f
|
|
76
84
|
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
77
85
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
@@ -79,49 +87,28 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
79
87
|
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
80
88
|
}
|
|
81
89
|
|
|
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
|
|
90
|
+
// =============================================================================
|
|
91
|
+
// MARK: - Types
|
|
92
|
+
// =============================================================================
|
|
98
93
|
|
|
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
|
|
110
|
-
|
|
111
|
-
override val headerHeight: Int
|
|
112
|
-
get() = containerView?.headerHeight ?: 0
|
|
113
|
-
|
|
114
|
-
// ====================================================================
|
|
115
|
-
// MARK: - State
|
|
116
|
-
// ====================================================================
|
|
117
|
-
|
|
118
|
-
/** Interaction state for the sheet */
|
|
119
94
|
private sealed class InteractionState {
|
|
120
95
|
data object Idle : InteractionState()
|
|
121
96
|
data class Dragging(val startTop: Int) : InteractionState()
|
|
122
97
|
data object Reconfiguring : InteractionState()
|
|
123
98
|
}
|
|
124
99
|
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// MARK: - Properties
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
var delegate: TrueSheetViewControllerDelegate? = null
|
|
105
|
+
|
|
106
|
+
// Dialog & Views
|
|
107
|
+
private var dialog: BottomSheetDialog? = null
|
|
108
|
+
private var dimView: TrueSheetDimView? = null
|
|
109
|
+
private var parentDimView: TrueSheetDimView? = null
|
|
110
|
+
|
|
111
|
+
// Presentation State
|
|
125
112
|
var isPresented = false
|
|
126
113
|
private set
|
|
127
114
|
|
|
@@ -139,48 +126,47 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
139
126
|
private var lastStateWidth: Int = 0
|
|
140
127
|
private var lastStateHeight: Int = 0
|
|
141
128
|
private var lastEmittedPositionPx: Int = -1
|
|
129
|
+
|
|
130
|
+
// Keyboard State
|
|
142
131
|
private var detentIndexBeforeKeyboard: Int = -1
|
|
143
132
|
private var isKeyboardTransitioning: Boolean = false
|
|
144
133
|
|
|
134
|
+
// Promises
|
|
145
135
|
var presentPromise: (() -> Unit)? = null
|
|
146
136
|
var dismissPromise: (() -> Unit)? = null
|
|
147
137
|
|
|
148
|
-
//
|
|
138
|
+
// For stacked sheets
|
|
149
139
|
var parentSheetView: TrueSheetView? = null
|
|
150
140
|
|
|
151
|
-
//
|
|
152
|
-
// MARK: - Helper Classes
|
|
153
|
-
// ====================================================================
|
|
154
|
-
|
|
141
|
+
// Helper Objects
|
|
155
142
|
private val sheetAnimator = TrueSheetAnimator(this)
|
|
156
143
|
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
157
144
|
private var rnScreensObserver: RNScreensFragmentObserver? = null
|
|
145
|
+
private val detentCalculator = TrueSheetDetentCalculator(this)
|
|
158
146
|
|
|
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)
|
|
147
|
+
// Touch Dispatchers
|
|
148
|
+
internal var eventDispatcher: EventDispatcher? = null
|
|
149
|
+
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
150
|
+
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
169
151
|
|
|
152
|
+
// Detent Configuration
|
|
170
153
|
override var maxSheetHeight: Int? = null
|
|
171
154
|
override var detents: MutableList<Double> = mutableListOf(0.5, 1.0)
|
|
172
155
|
|
|
156
|
+
// Appearance Configuration
|
|
173
157
|
var dimmed = true
|
|
174
158
|
var dimmedDetentIndex = 0
|
|
175
159
|
var grabber: Boolean = true
|
|
176
160
|
var grabberOptions: GrabberOptions? = null
|
|
161
|
+
var sheetBackgroundColor: Int? = null
|
|
162
|
+
var edgeToEdgeFullScreen: Boolean = false
|
|
163
|
+
var insetAdjustment: String = "automatic"
|
|
164
|
+
|
|
177
165
|
var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
178
166
|
set(value) {
|
|
179
167
|
field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
|
|
180
168
|
if (isPresented) setupBackground()
|
|
181
169
|
}
|
|
182
|
-
var sheetBackgroundColor: Int? = null
|
|
183
|
-
var edgeToEdgeFullScreen: Boolean = false
|
|
184
170
|
|
|
185
171
|
var dismissible: Boolean = true
|
|
186
172
|
set(value) {
|
|
@@ -198,13 +184,47 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
198
184
|
behavior?.isDraggable = value
|
|
199
185
|
}
|
|
200
186
|
|
|
201
|
-
//
|
|
187
|
+
// =============================================================================
|
|
202
188
|
// MARK: - Computed Properties
|
|
203
|
-
//
|
|
189
|
+
// =============================================================================
|
|
190
|
+
|
|
191
|
+
// Dialog
|
|
192
|
+
private val behavior: BottomSheetBehavior<FrameLayout>?
|
|
193
|
+
get() = dialog?.behavior
|
|
194
|
+
|
|
195
|
+
private val sheetContainer: FrameLayout?
|
|
196
|
+
get() = this.parent as? FrameLayout
|
|
197
|
+
|
|
198
|
+
override val bottomSheetView: FrameLayout?
|
|
199
|
+
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
200
|
+
|
|
201
|
+
private val containerView: TrueSheetContainerView?
|
|
202
|
+
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
203
|
+
|
|
204
|
+
// Screen Measurements
|
|
205
|
+
override val screenHeight: Int
|
|
206
|
+
get() = ScreenUtils.getScreenHeight(reactContext)
|
|
207
|
+
|
|
208
|
+
val screenWidth: Int
|
|
209
|
+
get() = ScreenUtils.getScreenWidth(reactContext)
|
|
210
|
+
|
|
211
|
+
// Includes system bars for accurate positioning
|
|
212
|
+
override val realScreenHeight: Int
|
|
213
|
+
get() = ScreenUtils.getRealScreenHeight(reactContext)
|
|
214
|
+
|
|
215
|
+
// Content Measurements
|
|
216
|
+
override val contentHeight: Int
|
|
217
|
+
get() = containerView?.contentHeight ?: 0
|
|
204
218
|
|
|
219
|
+
override val headerHeight: Int
|
|
220
|
+
get() = containerView?.headerHeight ?: 0
|
|
221
|
+
|
|
222
|
+
// Insets
|
|
223
|
+
// Target keyboard height used for detent calculations
|
|
205
224
|
override val keyboardInset: Int
|
|
206
225
|
get() = keyboardObserver?.targetHeight ?: 0
|
|
207
226
|
|
|
227
|
+
// Current animated keyboard height for positioning
|
|
208
228
|
private val currentKeyboardInset: Int
|
|
209
229
|
get() = keyboardObserver?.currentHeight ?: 0
|
|
210
230
|
|
|
@@ -214,8 +234,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
214
234
|
val topInset: Int
|
|
215
235
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).top else 0
|
|
216
236
|
|
|
217
|
-
var insetAdjustment: String = "automatic"
|
|
218
|
-
|
|
219
237
|
override val contentBottomInset: Int
|
|
220
238
|
get() = if (insetAdjustment == "automatic") bottomInset else 0
|
|
221
239
|
|
|
@@ -225,24 +243,33 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
225
243
|
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
226
244
|
}
|
|
227
245
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
246
|
+
// Sheet State
|
|
247
|
+
val isExpanded: Boolean
|
|
248
|
+
get() {
|
|
249
|
+
val sheetTop = bottomSheetView?.top ?: return false
|
|
250
|
+
return sheetTop <= topInset
|
|
251
|
+
}
|
|
231
252
|
|
|
232
|
-
|
|
233
|
-
|
|
253
|
+
val currentTranslationY: Int
|
|
254
|
+
get() = bottomSheetView?.translationY?.toInt() ?: 0
|
|
234
255
|
|
|
235
|
-
|
|
256
|
+
private val isTopmostSheet: Boolean
|
|
257
|
+
get() {
|
|
258
|
+
val hostView = delegate as? TrueSheetView ?: return true
|
|
259
|
+
return TrueSheetDialogObserver.isTopmostSheet(hostView)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// =============================================================================
|
|
236
263
|
// MARK: - Initialization
|
|
237
|
-
//
|
|
264
|
+
// =============================================================================
|
|
238
265
|
|
|
239
266
|
init {
|
|
240
267
|
jSPointerDispatcher = JSPointerDispatcher(this)
|
|
241
268
|
}
|
|
242
269
|
|
|
243
|
-
//
|
|
244
|
-
// MARK: - Dialog
|
|
245
|
-
//
|
|
270
|
+
// =============================================================================
|
|
271
|
+
// MARK: - Dialog Creation & Cleanup
|
|
272
|
+
// =============================================================================
|
|
246
273
|
|
|
247
274
|
fun createDialog() {
|
|
248
275
|
if (dialog != null) return
|
|
@@ -307,6 +334,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
307
334
|
shouldAnimatePresent = true
|
|
308
335
|
}
|
|
309
336
|
|
|
337
|
+
// =============================================================================
|
|
338
|
+
// MARK: - Dialog Listeners
|
|
339
|
+
// =============================================================================
|
|
340
|
+
|
|
310
341
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
311
342
|
dialog.setOnShowListener {
|
|
312
343
|
bottomSheetView?.visibility = VISIBLE
|
|
@@ -421,33 +452,30 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
421
452
|
}
|
|
422
453
|
}
|
|
423
454
|
|
|
455
|
+
// =============================================================================
|
|
456
|
+
// MARK: - Modal Observer (react-native-screens)
|
|
457
|
+
// =============================================================================
|
|
458
|
+
|
|
424
459
|
private fun setupModalObserver() {
|
|
425
460
|
rnScreensObserver = RNScreensFragmentObserver(
|
|
426
461
|
reactContext = reactContext,
|
|
427
462
|
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
|
|
463
|
+
if (isPresented && isDialogVisible && isTopmostSheet) {
|
|
464
|
+
hideForModal()
|
|
436
465
|
}
|
|
437
466
|
},
|
|
438
467
|
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
|
|
468
|
+
if (isPresented && wasHiddenByModal && isTopmostSheet) {
|
|
469
|
+
showAfterModal()
|
|
446
470
|
}
|
|
447
471
|
},
|
|
448
472
|
onModalDidDismiss = {
|
|
449
473
|
if (isPresented && wasHiddenByModal) {
|
|
450
474
|
wasHiddenByModal = false
|
|
475
|
+
// Restore parent sheet after this sheet is restored
|
|
476
|
+
parentSheetView?.viewController?.let { parent ->
|
|
477
|
+
post { parent.showAfterModal() }
|
|
478
|
+
}
|
|
451
479
|
}
|
|
452
480
|
}
|
|
453
481
|
)
|
|
@@ -459,88 +487,36 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
459
487
|
rnScreensObserver = null
|
|
460
488
|
}
|
|
461
489
|
|
|
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
|
-
}
|
|
490
|
+
private fun hideForModal() {
|
|
491
|
+
isDialogVisible = false
|
|
492
|
+
wasHiddenByModal = true
|
|
490
493
|
|
|
491
|
-
|
|
492
|
-
|
|
494
|
+
// Prepare for fast fade out
|
|
495
|
+
dimView?.alpha = 0f
|
|
496
|
+
parentDimView?.alpha = 0f
|
|
493
497
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
499
|
-
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
500
|
-
}
|
|
498
|
+
dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFastFadeOut)
|
|
499
|
+
dialog?.window?.decorView?.visibility = GONE
|
|
500
|
+
dimView?.visibility = INVISIBLE
|
|
501
|
+
parentDimView?.visibility = INVISIBLE
|
|
501
502
|
|
|
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)
|
|
503
|
+
parentSheetView?.viewController?.hideForModal()
|
|
508
504
|
}
|
|
509
505
|
|
|
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
|
-
}
|
|
506
|
+
private fun showAfterModal() {
|
|
507
|
+
isDialogVisible = true
|
|
527
508
|
|
|
528
|
-
|
|
529
|
-
|
|
509
|
+
dialog?.window?.setWindowAnimations(0)
|
|
510
|
+
dialog?.window?.decorView?.visibility = VISIBLE
|
|
511
|
+
dimView?.visibility = VISIBLE
|
|
512
|
+
parentDimView?.visibility = VISIBLE
|
|
530
513
|
|
|
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()
|
|
514
|
+
updateDimAmount(animated = true)
|
|
539
515
|
}
|
|
540
516
|
|
|
541
|
-
//
|
|
517
|
+
// =============================================================================
|
|
542
518
|
// MARK: - Presentation
|
|
543
|
-
//
|
|
519
|
+
// =============================================================================
|
|
544
520
|
|
|
545
521
|
fun present(detentIndex: Int, animated: Boolean = true) {
|
|
546
522
|
val dialog = this.dialog ?: run {
|
|
@@ -564,6 +540,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
564
540
|
setupBackground()
|
|
565
541
|
setupGrabber()
|
|
566
542
|
|
|
543
|
+
// Hide until animation starts
|
|
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,14 @@ 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 = minOf(
|
|
607
|
+
halfExpandedRatio = minOf(
|
|
608
|
+
adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(),
|
|
609
|
+
MAX_HALF_EXPANDED_RATIO
|
|
610
|
+
)
|
|
631
611
|
|
|
632
612
|
expandedOffset = maxOf(edgeToEdgeTopInset, realScreenHeight - maxDetentHeight)
|
|
613
|
+
|
|
614
|
+
// fitToContents works better with <= 2 detents when no expanded offset
|
|
633
615
|
isFitToContents = detents.size < 3 && expandedOffset == 0
|
|
634
616
|
|
|
635
617
|
val offset = if (expandedOffset == 0) topInset else 0
|
|
@@ -655,6 +637,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
655
637
|
positionFooter()
|
|
656
638
|
}
|
|
657
639
|
|
|
640
|
+
fun setStateForDetentIndex(index: Int) {
|
|
641
|
+
behavior?.state = detentCalculator.getStateForDetentIndex(index)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// =============================================================================
|
|
645
|
+
// MARK: - Grabber
|
|
646
|
+
// =============================================================================
|
|
647
|
+
|
|
658
648
|
fun setupGrabber() {
|
|
659
649
|
val bottomSheet = bottomSheetView ?: return
|
|
660
650
|
|
|
@@ -671,61 +661,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
671
661
|
bottomSheet.addView(grabberView)
|
|
672
662
|
}
|
|
673
663
|
|
|
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
|
-
// ====================================================================
|
|
664
|
+
// =============================================================================
|
|
723
665
|
// MARK: - Background & Dimming
|
|
724
|
-
//
|
|
666
|
+
// =============================================================================
|
|
725
667
|
|
|
726
668
|
fun setupBackground() {
|
|
727
669
|
val bottomSheet = bottomSheetView ?: return
|
|
728
670
|
|
|
671
|
+
// Rounded corners only on top
|
|
729
672
|
val outerRadii = floatArrayOf(
|
|
730
673
|
sheetCornerRadius,
|
|
731
674
|
sheetCornerRadius,
|
|
@@ -759,6 +702,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
759
702
|
if (dimView == null) dimView = TrueSheetDimView(reactContext)
|
|
760
703
|
if (!parentDimVisible) dimView?.attach(null)
|
|
761
704
|
|
|
705
|
+
// Attach dim view to parent sheet if stacked
|
|
762
706
|
val parentController = parentSheetView?.viewController
|
|
763
707
|
val parentBottomSheet = parentController?.bottomSheetView
|
|
764
708
|
if (parentBottomSheet != null) {
|
|
@@ -780,6 +724,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
780
724
|
true
|
|
781
725
|
}
|
|
782
726
|
} else {
|
|
727
|
+
// Pass through touches to parent or activity when not dimmed
|
|
783
728
|
touchOutside.setOnTouchListener { v, event ->
|
|
784
729
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
785
730
|
(
|
|
@@ -793,13 +738,42 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
793
738
|
}
|
|
794
739
|
}
|
|
795
740
|
|
|
796
|
-
fun updateDimAmount(sheetTop: Int? = null) {
|
|
741
|
+
fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
|
|
797
742
|
if (!dimmed) return
|
|
798
743
|
val top = (sheetTop ?: bottomSheetView?.top ?: return) + currentKeyboardInset
|
|
799
|
-
|
|
800
|
-
|
|
744
|
+
|
|
745
|
+
if (animated) {
|
|
746
|
+
val targetAlpha = dimView?.calculateAlpha(
|
|
747
|
+
top,
|
|
748
|
+
dimmedDetentIndex,
|
|
749
|
+
detentCalculator::getSheetTopForDetentIndex
|
|
750
|
+
) ?: 0f
|
|
751
|
+
dimView?.animate()?.alpha(targetAlpha)?.setDuration(200)?.start()
|
|
752
|
+
parentDimView?.animate()?.alpha(targetAlpha)?.setDuration(200)?.start()
|
|
753
|
+
} else {
|
|
754
|
+
dimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
|
|
755
|
+
parentDimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
fun getDefaultBackgroundColor(): Int {
|
|
760
|
+
val typedValue = TypedValue()
|
|
761
|
+
return if (reactContext.theme.resolveAttribute(
|
|
762
|
+
com.google.android.material.R.attr.colorSurfaceContainerLow,
|
|
763
|
+
typedValue,
|
|
764
|
+
true
|
|
765
|
+
)
|
|
766
|
+
) {
|
|
767
|
+
typedValue.data
|
|
768
|
+
} else {
|
|
769
|
+
Color.WHITE
|
|
770
|
+
}
|
|
801
771
|
}
|
|
802
772
|
|
|
773
|
+
// =============================================================================
|
|
774
|
+
// MARK: - Footer Positioning
|
|
775
|
+
// =============================================================================
|
|
776
|
+
|
|
803
777
|
fun positionFooter(slideOffset: Float? = null) {
|
|
804
778
|
val footerView = containerView?.footerView ?: return
|
|
805
779
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -809,35 +783,66 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
809
783
|
val sheetTop = bottomSheet.top
|
|
810
784
|
|
|
811
785
|
var footerY = (sheetHeight - sheetTop - footerHeight - currentKeyboardInset).toFloat()
|
|
786
|
+
|
|
787
|
+
// Adjust during dismiss animation when slideOffset is negative
|
|
812
788
|
if (slideOffset != null && slideOffset < 0) {
|
|
813
789
|
footerY -= (footerHeight * slideOffset)
|
|
814
790
|
}
|
|
815
791
|
|
|
792
|
+
// Clamp to prevent footer going above safe area
|
|
816
793
|
val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
|
|
817
794
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
818
795
|
}
|
|
819
796
|
|
|
820
|
-
|
|
821
|
-
|
|
797
|
+
// =============================================================================
|
|
798
|
+
// MARK: - Keyboard Handling
|
|
799
|
+
// =============================================================================
|
|
800
|
+
|
|
801
|
+
private fun shouldHandleKeyboard(): Boolean {
|
|
802
|
+
if (wasHiddenByModal) return false
|
|
803
|
+
return isTopmostSheet
|
|
822
804
|
}
|
|
823
805
|
|
|
824
|
-
fun
|
|
825
|
-
val
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
806
|
+
fun setupKeyboardObserver() {
|
|
807
|
+
val bottomSheet = bottomSheetView ?: return
|
|
808
|
+
keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
|
|
809
|
+
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
810
|
+
override fun keyboardWillShow(height: Int) {
|
|
811
|
+
if (!shouldHandleKeyboard()) return
|
|
812
|
+
detentIndexBeforeKeyboard = currentDetentIndex
|
|
813
|
+
isKeyboardTransitioning = true
|
|
814
|
+
setupSheetDetents()
|
|
815
|
+
setStateForDetentIndex(detents.size - 1)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
override fun keyboardWillHide() {
|
|
819
|
+
if (!shouldHandleKeyboard()) return
|
|
820
|
+
setupSheetDetents()
|
|
821
|
+
if (detentIndexBeforeKeyboard >= 0) {
|
|
822
|
+
setStateForDetentIndex(detentIndexBeforeKeyboard)
|
|
823
|
+
detentIndexBeforeKeyboard = -1
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
override fun keyboardDidHide() {
|
|
828
|
+
if (!shouldHandleKeyboard()) return
|
|
829
|
+
isKeyboardTransitioning = false
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
override fun keyboardDidChangeHeight(height: Int) {}
|
|
833
|
+
}
|
|
834
|
+
start()
|
|
835
835
|
}
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
-
|
|
838
|
+
fun cleanupKeyboardObserver() {
|
|
839
|
+
keyboardObserver?.stop()
|
|
840
|
+
keyboardObserver = null
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// =============================================================================
|
|
839
844
|
// MARK: - Drag Handling
|
|
840
|
-
//
|
|
845
|
+
// =============================================================================
|
|
841
846
|
|
|
842
847
|
private fun handleDragBegin(sheetView: View) {
|
|
843
848
|
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
@@ -853,13 +858,75 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
853
858
|
delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
|
|
854
859
|
}
|
|
855
860
|
|
|
856
|
-
//
|
|
861
|
+
// =============================================================================
|
|
862
|
+
// MARK: - Event Emission
|
|
863
|
+
// =============================================================================
|
|
864
|
+
|
|
865
|
+
private fun emitWillPresentEvents() {
|
|
866
|
+
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
867
|
+
parentSheetView?.viewControllerWillBlur()
|
|
868
|
+
delegate?.viewControllerWillPresent(index, position, detent)
|
|
869
|
+
delegate?.viewControllerWillFocus()
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
private fun emitWillDismissEvents() {
|
|
873
|
+
delegate?.viewControllerWillBlur()
|
|
874
|
+
delegate?.viewControllerWillDismiss()
|
|
875
|
+
parentSheetView?.viewControllerWillFocus()
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private fun emitDidDismissEvents() {
|
|
879
|
+
val hadParent = parentSheetView != null
|
|
880
|
+
parentSheetView?.viewControllerDidFocus()
|
|
881
|
+
parentSheetView = null
|
|
882
|
+
|
|
883
|
+
delegate?.viewControllerDidBlur()
|
|
884
|
+
delegate?.viewControllerDidDismiss(hadParent)
|
|
885
|
+
|
|
886
|
+
dismissPromise?.invoke()
|
|
887
|
+
dismissPromise = null
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
|
|
891
|
+
// Dedupe emissions for same position
|
|
892
|
+
if (currentTop == lastEmittedPositionPx) return
|
|
893
|
+
|
|
894
|
+
lastEmittedPositionPx = currentTop
|
|
895
|
+
val visibleHeight = realScreenHeight - currentTop
|
|
896
|
+
val position = detentCalculator.getPositionDp(visibleHeight)
|
|
897
|
+
val interpolatedIndex = detentCalculator.getInterpolatedIndexForPosition(currentTop)
|
|
898
|
+
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
899
|
+
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// =============================================================================
|
|
857
903
|
// MARK: - Detent Helpers
|
|
858
|
-
//
|
|
904
|
+
// =============================================================================
|
|
905
|
+
|
|
906
|
+
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
907
|
+
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
908
|
+
return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
fun translateDialog(translationY: Int) {
|
|
912
|
+
val bottomSheet = bottomSheetView ?: return
|
|
913
|
+
|
|
914
|
+
bottomSheet.animate()
|
|
915
|
+
.translationY(translationY.toFloat())
|
|
916
|
+
.setDuration(TRANSLATE_ANIMATION_DURATION)
|
|
917
|
+
.setUpdateListener {
|
|
918
|
+
val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
|
|
919
|
+
emitChangePositionDelegate(effectiveTop)
|
|
920
|
+
}
|
|
921
|
+
.start()
|
|
922
|
+
}
|
|
859
923
|
|
|
860
|
-
fun
|
|
861
|
-
val
|
|
862
|
-
|
|
924
|
+
private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
|
|
925
|
+
val state = detentCalculator.getStateForDetentIndex(index)
|
|
926
|
+
val detentIndex = detentCalculator.getDetentIndexForState(state) ?: 0
|
|
927
|
+
val position = getPositionForDetentIndex(detentIndex)
|
|
928
|
+
val detent = detentCalculator.getDetentValueForIndex(detentIndex)
|
|
929
|
+
return Triple(detentIndex, position, detent)
|
|
863
930
|
}
|
|
864
931
|
|
|
865
932
|
private fun getPositionForDetentIndex(index: Int): Float {
|
|
@@ -867,7 +934,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
867
934
|
|
|
868
935
|
bottomSheetView?.let {
|
|
869
936
|
val visibleSheetHeight = detentCalculator.getVisibleSheetHeight(it.top)
|
|
870
|
-
if (visibleSheetHeight
|
|
937
|
+
if (visibleSheetHeight in 1..<realScreenHeight) {
|
|
871
938
|
return detentCalculator.getPositionDp(visibleSheetHeight)
|
|
872
939
|
}
|
|
873
940
|
}
|
|
@@ -876,12 +943,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
876
943
|
return detentCalculator.getPositionDp(detentHeight)
|
|
877
944
|
}
|
|
878
945
|
|
|
879
|
-
|
|
880
|
-
getDetentInfoForState(detentCalculator.getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
|
|
881
|
-
|
|
882
|
-
// ====================================================================
|
|
946
|
+
// =============================================================================
|
|
883
947
|
// MARK: - RootView Implementation
|
|
884
|
-
//
|
|
948
|
+
// =============================================================================
|
|
885
949
|
|
|
886
950
|
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
|
|
887
951
|
super.onInitializeAccessibilityNodeInfo(info)
|
|
@@ -893,6 +957,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
893
957
|
|
|
894
958
|
if (w == oldw && h == oldh) return
|
|
895
959
|
if (!isPresented) return
|
|
960
|
+
|
|
961
|
+
// Skip reconfiguration if expanded and only height changed (e.g., keyboard)
|
|
896
962
|
if (h + topInset >= screenHeight && isExpanded && oldw == w) return
|
|
897
963
|
|
|
898
964
|
this.post {
|
|
@@ -906,17 +972,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
906
972
|
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
907
973
|
}
|
|
908
974
|
|
|
909
|
-
//
|
|
975
|
+
// =============================================================================
|
|
910
976
|
// MARK: - Touch Event Handling
|
|
911
|
-
//
|
|
977
|
+
// =============================================================================
|
|
912
978
|
|
|
913
979
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
980
|
+
// Footer needs special handling since it's positioned absolutely
|
|
914
981
|
val footer = containerView?.footerView
|
|
915
982
|
if (footer != null && footer.isVisible) {
|
|
916
983
|
val footerLocation = ScreenUtils.getScreenLocation(footer)
|
|
917
984
|
val touchScreenX = event.rawX.toInt()
|
|
918
985
|
val touchScreenY = event.rawY.toInt()
|
|
919
986
|
|
|
987
|
+
// Check if touch is within footer bounds
|
|
920
988
|
if (touchScreenX >= footerLocation[0] &&
|
|
921
989
|
touchScreenX <= footerLocation[0] + footer.width &&
|
|
922
990
|
touchScreenY >= footerLocation[1] &&
|
|
@@ -106,4 +106,14 @@ object TrueSheetDialogObserver {
|
|
|
106
106
|
return presentedSheetStack[index - 1]
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns true if the given sheet is the topmost presented sheet.
|
|
112
|
+
*/
|
|
113
|
+
@JvmStatic
|
|
114
|
+
fun isTopmostSheet(sheetView: TrueSheetView): Boolean {
|
|
115
|
+
synchronized(presentedSheetStack) {
|
|
116
|
+
return presentedSheetStack.lastOrNull() == sheetView
|
|
117
|
+
}
|
|
118
|
+
}
|
|
109
119
|
}
|
|
@@ -48,12 +48,12 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
|
|
|
48
48
|
targetView = null
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
fun
|
|
51
|
+
fun calculateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
|
|
52
52
|
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
53
53
|
val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
|
|
54
54
|
val belowDimmedTop = if (dimmedDetentIndex > 0) getSheetTopForDetentIndex(dimmedDetentIndex - 1) else realHeight
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
return when {
|
|
57
57
|
sheetTop <= dimmedDetentTop -> MAX_ALPHA
|
|
58
58
|
|
|
59
59
|
sheetTop >= belowDimmedTop -> 0f
|
|
@@ -64,4 +64,8 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
|
|
69
|
+
alpha = calculateAlpha(sheetTop, dimmedDetentIndex, getSheetTopForDetentIndex)
|
|
70
|
+
}
|
|
67
71
|
}
|
package/package.json
CHANGED