@lodev09/react-native-true-sheet 3.5.0-beta.0 → 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.
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +327 -265
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +3 -3
- 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/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +16 -1
- package/android/src/main/res/anim/fast_fade_out.xml +6 -0
- package/android/src/main/res/values/styles.xml +6 -0
- package/package.json +1 -1
|
@@ -37,6 +37,10 @@ import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
|
37
37
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
38
38
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
39
39
|
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// MARK: - Data Types & Delegate Protocol
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
40
44
|
data class DetentInfo(val index: Int, val position: Float)
|
|
41
45
|
|
|
42
46
|
interface TrueSheetViewControllerDelegate {
|
|
@@ -57,9 +61,13 @@ interface TrueSheetViewControllerDelegate {
|
|
|
57
61
|
fun viewControllerDidBackPress()
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// MARK: - TrueSheetViewController
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
60
68
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
69
|
+
* Manages the bottom sheet dialog and its presentation lifecycle.
|
|
70
|
+
* Acts as a RootView to properly dispatch touch events to React Native.
|
|
63
71
|
*/
|
|
64
72
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
65
73
|
class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
@@ -71,6 +79,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
71
79
|
companion object {
|
|
72
80
|
const val TAG_NAME = "TrueSheet"
|
|
73
81
|
|
|
82
|
+
// Prevents fully expanded ratio which causes behavior issues
|
|
74
83
|
private const val MAX_HALF_EXPANDED_RATIO = 0.999f
|
|
75
84
|
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
76
85
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
@@ -78,52 +87,28 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
78
87
|
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
79
88
|
}
|
|
80
89
|
|
|
81
|
-
//
|
|
82
|
-
// MARK: -
|
|
83
|
-
//
|
|
90
|
+
// =============================================================================
|
|
91
|
+
// MARK: - Types
|
|
92
|
+
// =============================================================================
|
|
84
93
|
|
|
85
|
-
|
|
94
|
+
private sealed class InteractionState {
|
|
95
|
+
data object Idle : InteractionState()
|
|
96
|
+
data class Dragging(val startTop: Int) : InteractionState()
|
|
97
|
+
data object Reconfiguring : InteractionState()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// MARK: - Properties
|
|
102
|
+
// =============================================================================
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
// MARK: - Dialog & Views
|
|
89
|
-
// ====================================================================
|
|
104
|
+
var delegate: TrueSheetViewControllerDelegate? = null
|
|
90
105
|
|
|
106
|
+
// Dialog & Views
|
|
91
107
|
private var dialog: BottomSheetDialog? = null
|
|
92
108
|
private var dimView: TrueSheetDimView? = null
|
|
93
109
|
private var parentDimView: TrueSheetDimView? = null
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
get() = dialog?.behavior
|
|
97
|
-
|
|
98
|
-
private val sheetContainer: FrameLayout?
|
|
99
|
-
get() = this.parent as? FrameLayout
|
|
100
|
-
|
|
101
|
-
override val bottomSheetView: FrameLayout?
|
|
102
|
-
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
103
|
-
|
|
104
|
-
private val containerView: TrueSheetContainerView?
|
|
105
|
-
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
106
|
-
|
|
107
|
-
override val contentHeight: Int
|
|
108
|
-
get() = containerView?.contentHeight ?: 0
|
|
109
|
-
|
|
110
|
-
override val headerHeight: Int
|
|
111
|
-
get() = containerView?.headerHeight ?: 0
|
|
112
|
-
|
|
113
|
-
override val keyboardHeight: Int
|
|
114
|
-
get() = keyboardObserver?.currentHeight ?: 0
|
|
115
|
-
|
|
116
|
-
// ====================================================================
|
|
117
|
-
// MARK: - State
|
|
118
|
-
// ====================================================================
|
|
119
|
-
|
|
120
|
-
/** Interaction state for the sheet */
|
|
121
|
-
private sealed class InteractionState {
|
|
122
|
-
data object Idle : InteractionState()
|
|
123
|
-
data class Dragging(val startTop: Int, val startKeyboardHeight: Int, val shouldDismissKeyboard: Boolean = false) : InteractionState()
|
|
124
|
-
data object Reconfiguring : InteractionState()
|
|
125
|
-
}
|
|
126
|
-
|
|
111
|
+
// Presentation State
|
|
127
112
|
var isPresented = false
|
|
128
113
|
private set
|
|
129
114
|
|
|
@@ -142,45 +127,46 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
142
127
|
private var lastStateHeight: Int = 0
|
|
143
128
|
private var lastEmittedPositionPx: Int = -1
|
|
144
129
|
|
|
130
|
+
// Keyboard State
|
|
131
|
+
private var detentIndexBeforeKeyboard: Int = -1
|
|
132
|
+
private var isKeyboardTransitioning: Boolean = false
|
|
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,9 +184,49 @@ 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
|
|
218
|
+
|
|
219
|
+
override val headerHeight: Int
|
|
220
|
+
get() = containerView?.headerHeight ?: 0
|
|
221
|
+
|
|
222
|
+
// Insets
|
|
223
|
+
// Target keyboard height used for detent calculations
|
|
224
|
+
override val keyboardInset: Int
|
|
225
|
+
get() = keyboardObserver?.targetHeight ?: 0
|
|
226
|
+
|
|
227
|
+
// Current animated keyboard height for positioning
|
|
228
|
+
private val currentKeyboardInset: Int
|
|
229
|
+
get() = keyboardObserver?.currentHeight ?: 0
|
|
204
230
|
|
|
205
231
|
val bottomInset: Int
|
|
206
232
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).bottom else 0
|
|
@@ -208,8 +234,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
208
234
|
val topInset: Int
|
|
209
235
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).top else 0
|
|
210
236
|
|
|
211
|
-
var insetAdjustment: String = "automatic"
|
|
212
|
-
|
|
213
237
|
override val contentBottomInset: Int
|
|
214
238
|
get() = if (insetAdjustment == "automatic") bottomInset else 0
|
|
215
239
|
|
|
@@ -219,24 +243,33 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
219
243
|
return BuildConfig.EDGE_TO_EDGE_ENABLED || dialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
220
244
|
}
|
|
221
245
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
246
|
+
// Sheet State
|
|
247
|
+
val isExpanded: Boolean
|
|
248
|
+
get() {
|
|
249
|
+
val sheetTop = bottomSheetView?.top ?: return false
|
|
250
|
+
return sheetTop <= topInset
|
|
251
|
+
}
|
|
225
252
|
|
|
226
|
-
|
|
227
|
-
|
|
253
|
+
val currentTranslationY: Int
|
|
254
|
+
get() = bottomSheetView?.translationY?.toInt() ?: 0
|
|
255
|
+
|
|
256
|
+
private val isTopmostSheet: Boolean
|
|
257
|
+
get() {
|
|
258
|
+
val hostView = delegate as? TrueSheetView ?: return true
|
|
259
|
+
return TrueSheetDialogObserver.isTopmostSheet(hostView)
|
|
260
|
+
}
|
|
228
261
|
|
|
229
|
-
//
|
|
262
|
+
// =============================================================================
|
|
230
263
|
// MARK: - Initialization
|
|
231
|
-
//
|
|
264
|
+
// =============================================================================
|
|
232
265
|
|
|
233
266
|
init {
|
|
234
267
|
jSPointerDispatcher = JSPointerDispatcher(this)
|
|
235
268
|
}
|
|
236
269
|
|
|
237
|
-
//
|
|
238
|
-
// MARK: - Dialog
|
|
239
|
-
//
|
|
270
|
+
// =============================================================================
|
|
271
|
+
// MARK: - Dialog Creation & Cleanup
|
|
272
|
+
// =============================================================================
|
|
240
273
|
|
|
241
274
|
fun createDialog() {
|
|
242
275
|
if (dialog != null) return
|
|
@@ -301,6 +334,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
301
334
|
shouldAnimatePresent = true
|
|
302
335
|
}
|
|
303
336
|
|
|
337
|
+
// =============================================================================
|
|
338
|
+
// MARK: - Dialog Listeners
|
|
339
|
+
// =============================================================================
|
|
340
|
+
|
|
304
341
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
305
342
|
dialog.setOnShowListener {
|
|
306
343
|
bottomSheetView?.visibility = VISIBLE
|
|
@@ -349,8 +386,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
349
386
|
else -> { }
|
|
350
387
|
}
|
|
351
388
|
|
|
352
|
-
|
|
353
|
-
|
|
389
|
+
if (!isKeyboardTransitioning) {
|
|
390
|
+
positionFooter(slideOffset)
|
|
391
|
+
updateDimAmount(sheetView.top)
|
|
392
|
+
}
|
|
354
393
|
}
|
|
355
394
|
|
|
356
395
|
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
@@ -387,17 +426,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
387
426
|
|
|
388
427
|
when (interactionState) {
|
|
389
428
|
is InteractionState.Dragging -> {
|
|
390
|
-
val draggingState = interactionState as InteractionState.Dragging
|
|
391
429
|
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
392
430
|
delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
|
|
393
431
|
|
|
394
|
-
// Dismiss keyboard if dragged past threshold
|
|
395
|
-
if (draggingState.shouldDismissKeyboard) {
|
|
396
|
-
val imm = reactContext.getSystemService(android.content.Context.INPUT_METHOD_SERVICE)
|
|
397
|
-
as? android.view.inputmethod.InputMethodManager
|
|
398
|
-
imm?.hideSoftInputFromWindow((dialog?.currentFocus ?: bottomSheetView)?.windowToken, 0)
|
|
399
|
-
}
|
|
400
|
-
|
|
401
432
|
if (detentInfo.index != currentDetentIndex) {
|
|
402
433
|
presentPromise?.invoke()
|
|
403
434
|
presentPromise = null
|
|
@@ -412,45 +443,39 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
412
443
|
else -> {
|
|
413
444
|
if (detentInfo.index != currentDetentIndex) {
|
|
414
445
|
currentDetentIndex = detentInfo.index
|
|
415
|
-
|
|
416
|
-
|
|
446
|
+
if (!isKeyboardTransitioning) {
|
|
447
|
+
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
448
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
449
|
+
}
|
|
417
450
|
}
|
|
418
451
|
}
|
|
419
452
|
}
|
|
420
453
|
}
|
|
421
454
|
|
|
455
|
+
// =============================================================================
|
|
456
|
+
// MARK: - Modal Observer (react-native-screens)
|
|
457
|
+
// =============================================================================
|
|
458
|
+
|
|
422
459
|
private fun setupModalObserver() {
|
|
423
460
|
rnScreensObserver = RNScreensFragmentObserver(
|
|
424
461
|
reactContext = reactContext,
|
|
425
462
|
onModalPresented = {
|
|
426
|
-
if (isPresented && isDialogVisible) {
|
|
427
|
-
|
|
428
|
-
wasHiddenByModal = true
|
|
429
|
-
|
|
430
|
-
bottomSheetView?.animate()?.alpha(0f)?.setDuration(200)?.start()
|
|
431
|
-
dimView?.visibility = INVISIBLE
|
|
432
|
-
parentDimView?.visibility = INVISIBLE
|
|
433
|
-
dialog?.window?.setFlags(
|
|
434
|
-
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
|
435
|
-
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
436
|
-
)
|
|
463
|
+
if (isPresented && isDialogVisible && isTopmostSheet) {
|
|
464
|
+
hideForModal()
|
|
437
465
|
}
|
|
438
466
|
},
|
|
439
467
|
onModalWillDismiss = {
|
|
440
|
-
if (isPresented && wasHiddenByModal) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
dialog?.window?.clearFlags(
|
|
444
|
-
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
445
|
-
)
|
|
446
|
-
bottomSheetView?.alpha = 1f
|
|
447
|
-
dimView?.visibility = VISIBLE
|
|
448
|
-
parentDimView?.visibility = VISIBLE
|
|
468
|
+
if (isPresented && wasHiddenByModal && isTopmostSheet) {
|
|
469
|
+
showAfterModal()
|
|
449
470
|
}
|
|
450
471
|
},
|
|
451
472
|
onModalDidDismiss = {
|
|
452
473
|
if (isPresented && wasHiddenByModal) {
|
|
453
474
|
wasHiddenByModal = false
|
|
475
|
+
// Restore parent sheet after this sheet is restored
|
|
476
|
+
parentSheetView?.viewController?.let { parent ->
|
|
477
|
+
post { parent.showAfterModal() }
|
|
478
|
+
}
|
|
454
479
|
}
|
|
455
480
|
}
|
|
456
481
|
)
|
|
@@ -462,88 +487,36 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
462
487
|
rnScreensObserver = null
|
|
463
488
|
}
|
|
464
489
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
private fun emitWillPresentEvents() {
|
|
470
|
-
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
471
|
-
parentSheetView?.viewControllerWillBlur()
|
|
472
|
-
delegate?.viewControllerWillPresent(index, position, detent)
|
|
473
|
-
delegate?.viewControllerWillFocus()
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
private fun emitWillDismissEvents() {
|
|
477
|
-
delegate?.viewControllerWillBlur()
|
|
478
|
-
delegate?.viewControllerWillDismiss()
|
|
479
|
-
parentSheetView?.viewControllerWillFocus()
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
private fun emitDidDismissEvents() {
|
|
483
|
-
val hadParent = parentSheetView != null
|
|
484
|
-
parentSheetView?.viewControllerDidFocus()
|
|
485
|
-
parentSheetView = null
|
|
486
|
-
|
|
487
|
-
delegate?.viewControllerDidBlur()
|
|
488
|
-
delegate?.viewControllerDidDismiss(hadParent)
|
|
490
|
+
private fun hideForModal() {
|
|
491
|
+
isDialogVisible = false
|
|
492
|
+
wasHiddenByModal = true
|
|
489
493
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
494
|
+
// Prepare for fast fade out
|
|
495
|
+
dimView?.alpha = 0f
|
|
496
|
+
parentDimView?.alpha = 0f
|
|
493
497
|
|
|
494
|
-
|
|
495
|
-
|
|
498
|
+
dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFastFadeOut)
|
|
499
|
+
dialog?.window?.decorView?.visibility = GONE
|
|
500
|
+
dimView?.visibility = INVISIBLE
|
|
501
|
+
parentDimView?.visibility = INVISIBLE
|
|
496
502
|
|
|
497
|
-
|
|
498
|
-
val visibleHeight = realScreenHeight - currentTop
|
|
499
|
-
val position = detentCalculator.getPositionDp(visibleHeight)
|
|
500
|
-
val interpolatedIndex = detentCalculator.getInterpolatedIndexForPosition(currentTop)
|
|
501
|
-
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
502
|
-
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
|
|
506
|
-
val state = detentCalculator.getStateForDetentIndex(index)
|
|
507
|
-
val detentIndex = detentCalculator.getDetentIndexForState(state) ?: 0
|
|
508
|
-
val position = getPositionForDetentIndex(detentIndex)
|
|
509
|
-
val detent = detentCalculator.getDetentValueForIndex(detentIndex)
|
|
510
|
-
return Triple(detentIndex, position, detent)
|
|
503
|
+
parentSheetView?.viewController?.hideForModal()
|
|
511
504
|
}
|
|
512
505
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// ====================================================================
|
|
506
|
+
private fun showAfterModal() {
|
|
507
|
+
isDialogVisible = true
|
|
516
508
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
509
|
+
dialog?.window?.setWindowAnimations(0)
|
|
510
|
+
dialog?.window?.decorView?.visibility = VISIBLE
|
|
511
|
+
dimView?.visibility = VISIBLE
|
|
512
|
+
parentDimView?.visibility = VISIBLE
|
|
522
513
|
|
|
523
|
-
|
|
524
|
-
get() = bottomSheetView?.translationY?.toInt() ?: 0
|
|
525
|
-
|
|
526
|
-
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
527
|
-
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
528
|
-
return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
|
|
514
|
+
updateDimAmount(animated = true)
|
|
529
515
|
}
|
|
530
516
|
|
|
531
|
-
|
|
532
|
-
val bottomSheet = bottomSheetView ?: return
|
|
533
|
-
|
|
534
|
-
bottomSheet.animate()
|
|
535
|
-
.translationY(translationY.toFloat())
|
|
536
|
-
.setDuration(TRANSLATE_ANIMATION_DURATION)
|
|
537
|
-
.setUpdateListener {
|
|
538
|
-
val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
|
|
539
|
-
emitChangePositionDelegate(effectiveTop)
|
|
540
|
-
}
|
|
541
|
-
.start()
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// ====================================================================
|
|
517
|
+
// =============================================================================
|
|
545
518
|
// MARK: - Presentation
|
|
546
|
-
//
|
|
519
|
+
// =============================================================================
|
|
547
520
|
|
|
548
521
|
fun present(detentIndex: Int, animated: Boolean = true) {
|
|
549
522
|
val dialog = this.dialog ?: run {
|
|
@@ -567,6 +540,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
567
540
|
setupBackground()
|
|
568
541
|
setupGrabber()
|
|
569
542
|
|
|
543
|
+
// Hide until animation starts
|
|
570
544
|
bottomSheetView?.visibility = INVISIBLE
|
|
571
545
|
|
|
572
546
|
dialog.show()
|
|
@@ -604,9 +578,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
604
578
|
presentPromise = null
|
|
605
579
|
}
|
|
606
580
|
|
|
607
|
-
//
|
|
581
|
+
// =============================================================================
|
|
608
582
|
// MARK: - Sheet Configuration
|
|
609
|
-
//
|
|
583
|
+
// =============================================================================
|
|
610
584
|
|
|
611
585
|
fun setupSheetDetents() {
|
|
612
586
|
val behavior = this.behavior ?: return
|
|
@@ -630,9 +604,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
630
604
|
val maxDetentHeight = detentCalculator.getDetentHeight(detents.last())
|
|
631
605
|
|
|
632
606
|
val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
|
|
633
|
-
halfExpandedRatio = minOf(
|
|
607
|
+
halfExpandedRatio = minOf(
|
|
608
|
+
adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(),
|
|
609
|
+
MAX_HALF_EXPANDED_RATIO
|
|
610
|
+
)
|
|
634
611
|
|
|
635
612
|
expandedOffset = maxOf(edgeToEdgeTopInset, realScreenHeight - maxDetentHeight)
|
|
613
|
+
|
|
614
|
+
// fitToContents works better with <= 2 detents when no expanded offset
|
|
636
615
|
isFitToContents = detents.size < 3 && expandedOffset == 0
|
|
637
616
|
|
|
638
617
|
val offset = if (expandedOffset == 0) topInset else 0
|
|
@@ -658,6 +637,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
658
637
|
positionFooter()
|
|
659
638
|
}
|
|
660
639
|
|
|
640
|
+
fun setStateForDetentIndex(index: Int) {
|
|
641
|
+
behavior?.state = detentCalculator.getStateForDetentIndex(index)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// =============================================================================
|
|
645
|
+
// MARK: - Grabber
|
|
646
|
+
// =============================================================================
|
|
647
|
+
|
|
661
648
|
fun setupGrabber() {
|
|
662
649
|
val bottomSheet = bottomSheetView ?: return
|
|
663
650
|
|
|
@@ -674,47 +661,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
674
661
|
bottomSheet.addView(grabberView)
|
|
675
662
|
}
|
|
676
663
|
|
|
677
|
-
//
|
|
678
|
-
// MARK: - Keyboard Handling
|
|
679
|
-
// ====================================================================
|
|
680
|
-
|
|
681
|
-
private fun shouldHandleKeyboard(): Boolean {
|
|
682
|
-
if (wasHiddenByModal) return false
|
|
683
|
-
|
|
684
|
-
val parentView = parentSheetView ?: return true
|
|
685
|
-
return TrueSheetDialogObserver.getSheetsAbove(parentView).firstOrNull()?.viewController == this
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
fun setupKeyboardObserver() {
|
|
689
|
-
val bottomSheet = bottomSheetView ?: return
|
|
690
|
-
keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
|
|
691
|
-
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
692
|
-
override fun keyboardWillShow(height: Int) {}
|
|
693
|
-
|
|
694
|
-
override fun keyboardWillHide() {}
|
|
695
|
-
|
|
696
|
-
override fun keyboardDidChangeHeight(height: Int) {
|
|
697
|
-
if (!shouldHandleKeyboard()) return
|
|
698
|
-
setupSheetDetents()
|
|
699
|
-
positionFooter()
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
start()
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
fun cleanupKeyboardObserver() {
|
|
707
|
-
keyboardObserver?.stop()
|
|
708
|
-
keyboardObserver = null
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// ====================================================================
|
|
664
|
+
// =============================================================================
|
|
712
665
|
// MARK: - Background & Dimming
|
|
713
|
-
//
|
|
666
|
+
// =============================================================================
|
|
714
667
|
|
|
715
668
|
fun setupBackground() {
|
|
716
669
|
val bottomSheet = bottomSheetView ?: return
|
|
717
670
|
|
|
671
|
+
// Rounded corners only on top
|
|
718
672
|
val outerRadii = floatArrayOf(
|
|
719
673
|
sheetCornerRadius,
|
|
720
674
|
sheetCornerRadius,
|
|
@@ -748,6 +702,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
748
702
|
if (dimView == null) dimView = TrueSheetDimView(reactContext)
|
|
749
703
|
if (!parentDimVisible) dimView?.attach(null)
|
|
750
704
|
|
|
705
|
+
// Attach dim view to parent sheet if stacked
|
|
751
706
|
val parentController = parentSheetView?.viewController
|
|
752
707
|
val parentBottomSheet = parentController?.bottomSheetView
|
|
753
708
|
if (parentBottomSheet != null) {
|
|
@@ -769,6 +724,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
769
724
|
true
|
|
770
725
|
}
|
|
771
726
|
} else {
|
|
727
|
+
// Pass through touches to parent or activity when not dimmed
|
|
772
728
|
touchOutside.setOnTouchListener { v, event ->
|
|
773
729
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
774
730
|
(
|
|
@@ -782,13 +738,42 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
782
738
|
}
|
|
783
739
|
}
|
|
784
740
|
|
|
785
|
-
fun updateDimAmount(sheetTop: Int? = null) {
|
|
741
|
+
fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
|
|
786
742
|
if (!dimmed) return
|
|
787
|
-
val top = sheetTop ?: bottomSheetView?.top ?: return
|
|
788
|
-
|
|
789
|
-
|
|
743
|
+
val top = (sheetTop ?: bottomSheetView?.top ?: return) + currentKeyboardInset
|
|
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
|
+
}
|
|
790
757
|
}
|
|
791
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
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// =============================================================================
|
|
774
|
+
// MARK: - Footer Positioning
|
|
775
|
+
// =============================================================================
|
|
776
|
+
|
|
792
777
|
fun positionFooter(slideOffset: Float? = null) {
|
|
793
778
|
val footerView = containerView?.footerView ?: return
|
|
794
779
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -797,75 +782,151 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
797
782
|
val sheetHeight = bottomSheet.height
|
|
798
783
|
val sheetTop = bottomSheet.top
|
|
799
784
|
|
|
800
|
-
var footerY = (sheetHeight - sheetTop - footerHeight -
|
|
785
|
+
var footerY = (sheetHeight - sheetTop - footerHeight - currentKeyboardInset).toFloat()
|
|
786
|
+
|
|
787
|
+
// Adjust during dismiss animation when slideOffset is negative
|
|
801
788
|
if (slideOffset != null && slideOffset < 0) {
|
|
802
789
|
footerY -= (footerHeight * slideOffset)
|
|
803
790
|
}
|
|
804
791
|
|
|
792
|
+
// Clamp to prevent footer going above safe area
|
|
805
793
|
val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
|
|
806
794
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
807
795
|
}
|
|
808
796
|
|
|
809
|
-
|
|
810
|
-
|
|
797
|
+
// =============================================================================
|
|
798
|
+
// MARK: - Keyboard Handling
|
|
799
|
+
// =============================================================================
|
|
800
|
+
|
|
801
|
+
private fun shouldHandleKeyboard(): Boolean {
|
|
802
|
+
if (wasHiddenByModal) return false
|
|
803
|
+
return isTopmostSheet
|
|
811
804
|
}
|
|
812
805
|
|
|
813
|
-
fun
|
|
814
|
-
val
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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()
|
|
824
835
|
}
|
|
825
836
|
}
|
|
826
837
|
|
|
827
|
-
|
|
838
|
+
fun cleanupKeyboardObserver() {
|
|
839
|
+
keyboardObserver?.stop()
|
|
840
|
+
keyboardObserver = null
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// =============================================================================
|
|
828
844
|
// MARK: - Drag Handling
|
|
829
|
-
//
|
|
845
|
+
// =============================================================================
|
|
830
846
|
|
|
831
847
|
private fun handleDragBegin(sheetView: View) {
|
|
832
848
|
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
833
849
|
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
834
850
|
delegate?.viewControllerDidDragBegin(currentDetentIndex, position, detent)
|
|
835
|
-
interactionState = InteractionState.Dragging(
|
|
836
|
-
startTop = sheetView.top,
|
|
837
|
-
startKeyboardHeight = keyboardHeight
|
|
838
|
-
)
|
|
851
|
+
interactionState = InteractionState.Dragging(startTop = sheetView.top)
|
|
839
852
|
}
|
|
840
853
|
|
|
841
854
|
private fun handleDragChange(sheetView: View) {
|
|
842
|
-
|
|
855
|
+
if (interactionState !is InteractionState.Dragging) return
|
|
843
856
|
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
844
857
|
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
845
858
|
delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
|
|
859
|
+
}
|
|
846
860
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
val shouldDismiss = sheetView.top >= detentTopWithoutKeyboard
|
|
861
|
+
// =============================================================================
|
|
862
|
+
// MARK: - Event Emission
|
|
863
|
+
// =============================================================================
|
|
851
864
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
interactionState = draggingState.copy(shouldDismissKeyboard = shouldDismiss)
|
|
858
|
-
}
|
|
859
|
-
}
|
|
865
|
+
private fun emitWillPresentEvents() {
|
|
866
|
+
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
867
|
+
parentSheetView?.viewControllerWillBlur()
|
|
868
|
+
delegate?.viewControllerWillPresent(index, position, detent)
|
|
869
|
+
delegate?.viewControllerWillFocus()
|
|
860
870
|
}
|
|
861
871
|
|
|
862
|
-
|
|
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
|
+
// =============================================================================
|
|
863
903
|
// MARK: - Detent Helpers
|
|
864
|
-
//
|
|
904
|
+
// =============================================================================
|
|
865
905
|
|
|
866
|
-
fun
|
|
867
|
-
|
|
868
|
-
return
|
|
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
|
+
}
|
|
923
|
+
|
|
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)
|
|
869
930
|
}
|
|
870
931
|
|
|
871
932
|
private fun getPositionForDetentIndex(index: Int): Float {
|
|
@@ -873,7 +934,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
873
934
|
|
|
874
935
|
bottomSheetView?.let {
|
|
875
936
|
val visibleSheetHeight = detentCalculator.getVisibleSheetHeight(it.top)
|
|
876
|
-
if (visibleSheetHeight
|
|
937
|
+
if (visibleSheetHeight in 1..<realScreenHeight) {
|
|
877
938
|
return detentCalculator.getPositionDp(visibleSheetHeight)
|
|
878
939
|
}
|
|
879
940
|
}
|
|
@@ -882,12 +943,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
882
943
|
return detentCalculator.getPositionDp(detentHeight)
|
|
883
944
|
}
|
|
884
945
|
|
|
885
|
-
|
|
886
|
-
getDetentInfoForState(detentCalculator.getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
|
|
887
|
-
|
|
888
|
-
// ====================================================================
|
|
946
|
+
// =============================================================================
|
|
889
947
|
// MARK: - RootView Implementation
|
|
890
|
-
//
|
|
948
|
+
// =============================================================================
|
|
891
949
|
|
|
892
950
|
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
|
|
893
951
|
super.onInitializeAccessibilityNodeInfo(info)
|
|
@@ -899,6 +957,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
899
957
|
|
|
900
958
|
if (w == oldw && h == oldh) return
|
|
901
959
|
if (!isPresented) return
|
|
960
|
+
|
|
961
|
+
// Skip reconfiguration if expanded and only height changed (e.g., keyboard)
|
|
902
962
|
if (h + topInset >= screenHeight && isExpanded && oldw == w) return
|
|
903
963
|
|
|
904
964
|
this.post {
|
|
@@ -912,17 +972,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
912
972
|
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
913
973
|
}
|
|
914
974
|
|
|
915
|
-
//
|
|
975
|
+
// =============================================================================
|
|
916
976
|
// MARK: - Touch Event Handling
|
|
917
|
-
//
|
|
977
|
+
// =============================================================================
|
|
918
978
|
|
|
919
979
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
980
|
+
// Footer needs special handling since it's positioned absolutely
|
|
920
981
|
val footer = containerView?.footerView
|
|
921
982
|
if (footer != null && footer.isVisible) {
|
|
922
983
|
val footerLocation = ScreenUtils.getScreenLocation(footer)
|
|
923
984
|
val touchScreenX = event.rawX.toInt()
|
|
924
985
|
val touchScreenY = event.rawY.toInt()
|
|
925
986
|
|
|
987
|
+
// Check if touch is within footer bounds
|
|
926
988
|
if (touchScreenX >= footerLocation[0] &&
|
|
927
989
|
touchScreenX <= footerLocation[0] + footer.width &&
|
|
928
990
|
touchScreenY >= footerLocation[1] &&
|
|
@@ -14,7 +14,7 @@ interface TrueSheetDetentMeasurements {
|
|
|
14
14
|
val headerHeight: Int
|
|
15
15
|
val contentBottomInset: Int
|
|
16
16
|
val maxSheetHeight: Int?
|
|
17
|
-
val
|
|
17
|
+
val keyboardInset: Int
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -30,7 +30,7 @@ class TrueSheetDetentCalculator(private val measurements: TrueSheetDetentMeasure
|
|
|
30
30
|
private val headerHeight: Int get() = measurements.headerHeight
|
|
31
31
|
private val contentBottomInset: Int get() = measurements.contentBottomInset
|
|
32
32
|
private val maxSheetHeight: Int? get() = measurements.maxSheetHeight
|
|
33
|
-
private val
|
|
33
|
+
private val keyboardInset: Int get() = measurements.keyboardInset
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Calculate the height in pixels for a given detent value.
|
|
@@ -46,7 +46,7 @@ class TrueSheetDetentCalculator(private val measurements: TrueSheetDetentMeasure
|
|
|
46
46
|
(detent * screenHeight).toInt() + contentBottomInset
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
val height = baseHeight +
|
|
49
|
+
val height = baseHeight + keyboardInset
|
|
50
50
|
val maxAllowedHeight = screenHeight + contentBottomInset
|
|
51
51
|
return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
52
52
|
}
|
|
@@ -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
|
}
|
|
@@ -12,6 +12,7 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
12
12
|
interface TrueSheetKeyboardObserverDelegate {
|
|
13
13
|
fun keyboardWillShow(height: Int)
|
|
14
14
|
fun keyboardWillHide()
|
|
15
|
+
fun keyboardDidHide()
|
|
15
16
|
fun keyboardDidChangeHeight(height: Int)
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -26,6 +27,10 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
26
27
|
var currentHeight: Int = 0
|
|
27
28
|
private set
|
|
28
29
|
|
|
30
|
+
var targetHeight: Int = 0
|
|
31
|
+
private set
|
|
32
|
+
|
|
33
|
+
private var isHiding: Boolean = false
|
|
29
34
|
private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
|
30
35
|
private var activityRootView: View? = null
|
|
31
36
|
|
|
@@ -72,9 +77,11 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
72
77
|
bounds: WindowInsetsAnimationCompat.BoundsCompat
|
|
73
78
|
): WindowInsetsAnimationCompat.BoundsCompat {
|
|
74
79
|
endHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
|
|
80
|
+
targetHeight = endHeight
|
|
81
|
+
isHiding = endHeight < startHeight
|
|
75
82
|
if (endHeight > startHeight) {
|
|
76
83
|
delegate?.keyboardWillShow(endHeight)
|
|
77
|
-
} else if (
|
|
84
|
+
} else if (isHiding) {
|
|
78
85
|
delegate?.keyboardWillHide()
|
|
79
86
|
}
|
|
80
87
|
return bounds
|
|
@@ -94,6 +101,10 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
94
101
|
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
95
102
|
val finalHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
|
|
96
103
|
updateHeight(startHeight, finalHeight, 1f)
|
|
104
|
+
if (isHiding) {
|
|
105
|
+
delegate?.keyboardDidHide()
|
|
106
|
+
isHiding = false
|
|
107
|
+
}
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
)
|
|
@@ -114,12 +125,16 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
114
125
|
val previousHeight = currentHeight
|
|
115
126
|
|
|
116
127
|
if (previousHeight != newHeight) {
|
|
128
|
+
targetHeight = newHeight
|
|
117
129
|
if (newHeight > previousHeight) {
|
|
118
130
|
delegate?.keyboardWillShow(newHeight)
|
|
119
131
|
} else if (newHeight < previousHeight) {
|
|
120
132
|
delegate?.keyboardWillHide()
|
|
121
133
|
}
|
|
122
134
|
updateHeight(previousHeight, newHeight, 1f)
|
|
135
|
+
if (newHeight == 0 && previousHeight > 0) {
|
|
136
|
+
delegate?.keyboardDidHide()
|
|
137
|
+
}
|
|
123
138
|
}
|
|
124
139
|
}
|
|
125
140
|
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
<item name="android:windowExitAnimation">@null</item>
|
|
7
7
|
</style>
|
|
8
8
|
|
|
9
|
+
<!-- Fast fade out animation for hiding sheet when rn-screen is presented -->
|
|
10
|
+
<style name="TrueSheetFastFadeOut" parent="Animation.AppCompat.Dialog">
|
|
11
|
+
<item name="android:windowEnterAnimation">@null</item>
|
|
12
|
+
<item name="android:windowExitAnimation">@anim/fast_fade_out</item>
|
|
13
|
+
</style>
|
|
14
|
+
|
|
9
15
|
<!-- Default BottomSheetDialog style with programmatic animations -->
|
|
10
16
|
<style name="TrueSheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
|
|
11
17
|
<item name="android:windowAnimationStyle">@style/TrueSheetNoAnimation</item>
|
package/package.json
CHANGED