@lodev09/react-native-true-sheet 3.4.2 → 3.5.0-beta.1
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/TrueSheetModule.kt +13 -12
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +249 -356
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +22 -10
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetAnimator.kt +134 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +208 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +1 -6
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +42 -9
- package/android/src/main/res/anim/{true_sheet_fade_out.xml → fast_fade_out.xml} +2 -2
- package/android/src/main/res/values/styles.xml +12 -11
- package/ios/TrueSheetViewController.h +4 -1
- package/ios/TrueSheetViewController.mm +11 -115
- package/ios/core/TrueSheetDetentCalculator.h +87 -0
- package/ios/core/TrueSheetDetentCalculator.mm +170 -0
- package/package.json +1 -1
- package/android/src/main/res/anim/true_sheet_slide_in.xml +0 -13
- package/android/src/main/res/anim/true_sheet_slide_out.xml +0 -14
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
|
-
import android.animation.ValueAnimator
|
|
4
3
|
import android.annotation.SuppressLint
|
|
5
4
|
import android.graphics.Color
|
|
6
5
|
import android.graphics.drawable.ShapeDrawable
|
|
@@ -12,7 +11,6 @@ import android.view.View
|
|
|
12
11
|
import android.view.WindowManager
|
|
13
12
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
14
13
|
import android.widget.FrameLayout
|
|
15
|
-
import androidx.core.view.ViewCompat
|
|
16
14
|
import androidx.core.view.isNotEmpty
|
|
17
15
|
import androidx.core.view.isVisible
|
|
18
16
|
import com.facebook.react.R
|
|
@@ -29,6 +27,11 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
|
29
27
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
30
28
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
31
29
|
import com.lodev09.truesheet.core.RNScreensFragmentObserver
|
|
30
|
+
import com.lodev09.truesheet.core.TrueSheetAnimator
|
|
31
|
+
import com.lodev09.truesheet.core.TrueSheetAnimatorProvider
|
|
32
|
+
import com.lodev09.truesheet.core.TrueSheetDetentCalculator
|
|
33
|
+
import com.lodev09.truesheet.core.TrueSheetDetentMeasurements
|
|
34
|
+
import com.lodev09.truesheet.core.TrueSheetDialogObserver
|
|
32
35
|
import com.lodev09.truesheet.core.TrueSheetDimView
|
|
33
36
|
import com.lodev09.truesheet.core.TrueSheetGrabberView
|
|
34
37
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
@@ -62,21 +65,18 @@ interface TrueSheetViewControllerDelegate {
|
|
|
62
65
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
63
66
|
class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
64
67
|
ReactViewGroup(reactContext),
|
|
65
|
-
RootView
|
|
68
|
+
RootView,
|
|
69
|
+
TrueSheetDetentMeasurements,
|
|
70
|
+
TrueSheetAnimatorProvider {
|
|
66
71
|
|
|
67
72
|
companion object {
|
|
68
73
|
const val TAG_NAME = "TrueSheet"
|
|
69
74
|
|
|
70
75
|
private const val MAX_HALF_EXPANDED_RATIO = 0.999f
|
|
71
|
-
|
|
72
76
|
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
73
77
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
74
78
|
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
75
|
-
|
|
76
|
-
// Animation durations from res/anim/true_sheet_slide_in.xml and true_sheet_slide_out.xml
|
|
77
|
-
private const val PRESENT_ANIMATION_DURATION = 250L
|
|
78
|
-
private const val DISMISS_ANIMATION_DURATION = 150L
|
|
79
|
-
private const val TRANSLATE_ANIMATION_DURATION = 100L
|
|
79
|
+
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// ====================================================================
|
|
@@ -99,22 +99,29 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
99
99
|
private val sheetContainer: FrameLayout?
|
|
100
100
|
get() = this.parent as? FrameLayout
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
override val bottomSheetView: FrameLayout?
|
|
103
103
|
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
104
104
|
|
|
105
105
|
private val containerView: TrueSheetContainerView?
|
|
106
106
|
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
override val contentHeight: Int
|
|
109
109
|
get() = containerView?.contentHeight ?: 0
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
override val headerHeight: Int
|
|
112
112
|
get() = containerView?.headerHeight ?: 0
|
|
113
113
|
|
|
114
114
|
// ====================================================================
|
|
115
115
|
// MARK: - State
|
|
116
116
|
// ====================================================================
|
|
117
117
|
|
|
118
|
+
/** Interaction state for the sheet */
|
|
119
|
+
private sealed class InteractionState {
|
|
120
|
+
data object Idle : InteractionState()
|
|
121
|
+
data class Dragging(val startTop: Int) : InteractionState()
|
|
122
|
+
data object Reconfiguring : InteractionState()
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
var isPresented = false
|
|
119
126
|
private set
|
|
120
127
|
|
|
@@ -124,16 +131,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
124
131
|
var currentDetentIndex: Int = -1
|
|
125
132
|
private set
|
|
126
133
|
|
|
134
|
+
private var interactionState: InteractionState = InteractionState.Idle
|
|
135
|
+
private var isDismissing = false
|
|
136
|
+
private var wasHiddenByModal = false
|
|
137
|
+
private var shouldAnimatePresent = true
|
|
138
|
+
|
|
127
139
|
private var lastStateWidth: Int = 0
|
|
128
140
|
private var lastStateHeight: Int = 0
|
|
129
|
-
private var isDragging = false
|
|
130
|
-
private var isDismissing = false
|
|
131
|
-
private var isReconfiguring = false
|
|
132
|
-
private var windowAnimation: Int = 0
|
|
133
141
|
private var lastEmittedPositionPx: Int = -1
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
private var wasHiddenByModal = false
|
|
142
|
+
private var detentIndexBeforeKeyboard: Int = -1
|
|
143
|
+
private var isKeyboardTransitioning: Boolean = false
|
|
137
144
|
|
|
138
145
|
var presentPromise: (() -> Unit)? = null
|
|
139
146
|
var dismissPromise: (() -> Unit)? = null
|
|
@@ -141,19 +148,27 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
141
148
|
// Reference to parent TrueSheetView (if presented from another sheet)
|
|
142
149
|
var parentSheetView: TrueSheetView? = null
|
|
143
150
|
|
|
151
|
+
// ====================================================================
|
|
152
|
+
// MARK: - Helper Classes
|
|
153
|
+
// ====================================================================
|
|
154
|
+
|
|
155
|
+
private val sheetAnimator = TrueSheetAnimator(this)
|
|
156
|
+
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
157
|
+
private var rnScreensObserver: RNScreensFragmentObserver? = null
|
|
158
|
+
|
|
144
159
|
// ====================================================================
|
|
145
160
|
// MARK: - Configuration Properties
|
|
146
161
|
// ====================================================================
|
|
147
162
|
|
|
148
|
-
val screenHeight: Int
|
|
163
|
+
override val screenHeight: Int
|
|
149
164
|
get() = ScreenUtils.getScreenHeight(reactContext)
|
|
150
165
|
val screenWidth: Int
|
|
151
166
|
get() = ScreenUtils.getScreenWidth(reactContext)
|
|
152
|
-
val realScreenHeight: Int
|
|
167
|
+
override val realScreenHeight: Int
|
|
153
168
|
get() = ScreenUtils.getRealScreenHeight(reactContext)
|
|
154
169
|
|
|
155
|
-
var maxSheetHeight: Int? = null
|
|
156
|
-
var detents = mutableListOf(0.5, 1.0)
|
|
170
|
+
override var maxSheetHeight: Int? = null
|
|
171
|
+
override var detents: MutableList<Double> = mutableListOf(0.5, 1.0)
|
|
157
172
|
|
|
158
173
|
var dimmed = true
|
|
159
174
|
var dimmedDetentIndex = 0
|
|
@@ -162,7 +177,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
162
177
|
var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
163
178
|
set(value) {
|
|
164
179
|
field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
|
|
165
|
-
setupBackground()
|
|
180
|
+
if (isPresented) setupBackground()
|
|
166
181
|
}
|
|
167
182
|
var sheetBackgroundColor: Int? = null
|
|
168
183
|
var edgeToEdgeFullScreen: Boolean = false
|
|
@@ -187,6 +202,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
187
202
|
// MARK: - Computed Properties
|
|
188
203
|
// ====================================================================
|
|
189
204
|
|
|
205
|
+
override val keyboardInset: Int
|
|
206
|
+
get() = keyboardObserver?.targetHeight ?: 0
|
|
207
|
+
|
|
208
|
+
private val currentKeyboardInset: Int
|
|
209
|
+
get() = keyboardObserver?.currentHeight ?: 0
|
|
210
|
+
|
|
190
211
|
val bottomInset: Int
|
|
191
212
|
get() = if (edgeToEdgeEnabled) ScreenUtils.getInsets(reactContext).bottom else 0
|
|
192
213
|
|
|
@@ -195,11 +216,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
195
216
|
|
|
196
217
|
var insetAdjustment: String = "automatic"
|
|
197
218
|
|
|
198
|
-
|
|
199
|
-
val contentBottomInset: Int
|
|
219
|
+
override val contentBottomInset: Int
|
|
200
220
|
get() = if (insetAdjustment == "automatic") bottomInset else 0
|
|
201
221
|
|
|
202
|
-
/** Edge-to-edge enabled by default on API 36+, or when explicitly configured. */
|
|
203
222
|
private val edgeToEdgeEnabled: Boolean
|
|
204
223
|
get() {
|
|
205
224
|
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
@@ -210,8 +229,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
210
229
|
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
211
230
|
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
212
231
|
|
|
213
|
-
/**
|
|
214
|
-
private
|
|
232
|
+
/** Single instance that reads current values via TrueSheetDetentMeasurements interface */
|
|
233
|
+
private val detentCalculator = TrueSheetDetentCalculator(this)
|
|
215
234
|
|
|
216
235
|
// ====================================================================
|
|
217
236
|
// MARK: - Initialization
|
|
@@ -238,8 +257,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
238
257
|
setContentView(this@TrueSheetViewController)
|
|
239
258
|
|
|
240
259
|
window?.apply {
|
|
241
|
-
|
|
242
|
-
// Disable default keyboard avoidance - sheet handles it via setupKeyboardObserver
|
|
260
|
+
setWindowAnimations(0)
|
|
243
261
|
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
|
244
262
|
}
|
|
245
263
|
|
|
@@ -272,6 +290,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
272
290
|
|
|
273
291
|
cleanupKeyboardObserver()
|
|
274
292
|
cleanupModalObserver()
|
|
293
|
+
sheetAnimator.cancel()
|
|
275
294
|
dimView?.detach()
|
|
276
295
|
dimView = null
|
|
277
296
|
parentDimView?.detach()
|
|
@@ -279,58 +298,45 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
279
298
|
sheetContainer?.removeView(this)
|
|
280
299
|
|
|
281
300
|
dialog = null
|
|
282
|
-
|
|
301
|
+
interactionState = InteractionState.Idle
|
|
283
302
|
isDismissing = false
|
|
284
303
|
isPresented = false
|
|
285
304
|
isDialogVisible = false
|
|
286
305
|
wasHiddenByModal = false
|
|
287
306
|
lastEmittedPositionPx = -1
|
|
307
|
+
shouldAnimatePresent = true
|
|
288
308
|
}
|
|
289
309
|
|
|
290
310
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
291
311
|
dialog.setOnShowListener {
|
|
312
|
+
bottomSheetView?.visibility = VISIBLE
|
|
292
313
|
isPresented = true
|
|
293
314
|
isDialogVisible = true
|
|
294
|
-
resetAnimation()
|
|
295
|
-
setupBackground()
|
|
296
|
-
setupGrabber()
|
|
297
|
-
setupKeyboardObserver()
|
|
298
315
|
|
|
299
|
-
|
|
300
|
-
setupTransitionTracker(realScreenHeight, toTop, PRESENT_ANIMATION_DURATION)
|
|
301
|
-
animateDimAlpha(show = true)
|
|
316
|
+
setupKeyboardObserver()
|
|
302
317
|
|
|
303
|
-
|
|
318
|
+
if (shouldAnimatePresent) {
|
|
319
|
+
val toTop = getExpectedSheetTop(currentDetentIndex)
|
|
320
|
+
sheetAnimator.animatePresent(
|
|
321
|
+
toTop = toTop,
|
|
322
|
+
onUpdate = { effectiveTop ->
|
|
323
|
+
emitChangePositionDelegate(effectiveTop)
|
|
324
|
+
positionFooter()
|
|
325
|
+
updateDimAmount(effectiveTop)
|
|
326
|
+
},
|
|
327
|
+
onEnd = { finishPresent() }
|
|
328
|
+
)
|
|
329
|
+
} else {
|
|
330
|
+
val toTop = getExpectedSheetTop(currentDetentIndex)
|
|
331
|
+
emitChangePositionDelegate(toTop)
|
|
304
332
|
positionFooter()
|
|
333
|
+
finishPresent()
|
|
305
334
|
}
|
|
306
|
-
|
|
307
|
-
sheetContainer?.postDelayed({
|
|
308
|
-
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
309
|
-
|
|
310
|
-
delegate?.viewControllerDidPresent(index, position, detent)
|
|
311
|
-
parentSheetView?.viewControllerDidBlur()
|
|
312
|
-
delegate?.viewControllerDidFocus()
|
|
313
|
-
|
|
314
|
-
presentPromise?.invoke()
|
|
315
|
-
presentPromise = null
|
|
316
|
-
}, PRESENT_ANIMATION_DURATION)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
dialog.setOnCancelListener {
|
|
320
|
-
if (isDismissing) return@setOnCancelListener
|
|
321
|
-
|
|
322
|
-
isDismissing = true
|
|
323
|
-
val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
|
|
324
|
-
setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
|
|
325
|
-
animateDimAlpha(show = false)
|
|
326
|
-
emitWillDismissEvents()
|
|
327
335
|
}
|
|
328
336
|
|
|
329
337
|
dialog.setOnDismissListener {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
cleanupDialog()
|
|
333
|
-
}, DISMISS_ANIMATION_DURATION)
|
|
338
|
+
emitDidDismissEvents()
|
|
339
|
+
cleanupDialog()
|
|
334
340
|
}
|
|
335
341
|
}
|
|
336
342
|
|
|
@@ -349,8 +355,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
349
355
|
else -> { }
|
|
350
356
|
}
|
|
351
357
|
|
|
352
|
-
|
|
353
|
-
|
|
358
|
+
if (!isKeyboardTransitioning) {
|
|
359
|
+
positionFooter(slideOffset)
|
|
360
|
+
updateDimAmount(sheetView.top)
|
|
361
|
+
}
|
|
354
362
|
}
|
|
355
363
|
|
|
356
364
|
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
@@ -369,30 +377,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
369
377
|
|
|
370
378
|
BottomSheetBehavior.STATE_EXPANDED,
|
|
371
379
|
BottomSheetBehavior.STATE_COLLAPSED,
|
|
372
|
-
BottomSheetBehavior.STATE_HALF_EXPANDED ->
|
|
373
|
-
if (isReconfiguring) return
|
|
374
|
-
|
|
375
|
-
getDetentInfoForState(newState)?.let { detentInfo ->
|
|
376
|
-
if (isDragging) {
|
|
377
|
-
val detent = getDetentValueForIndex(detentInfo.index)
|
|
378
|
-
delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
|
|
379
|
-
|
|
380
|
-
if (detentInfo.index != currentDetentIndex) {
|
|
381
|
-
presentPromise?.invoke()
|
|
382
|
-
presentPromise = null
|
|
383
|
-
currentDetentIndex = detentInfo.index
|
|
384
|
-
setupDimmedBackground(detentInfo.index)
|
|
385
|
-
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
isDragging = false
|
|
389
|
-
} else if (detentInfo.index != currentDetentIndex) {
|
|
390
|
-
val detent = getDetentValueForIndex(detentInfo.index)
|
|
391
|
-
currentDetentIndex = detentInfo.index
|
|
392
|
-
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
380
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED -> handleStateSettled(sheetView, newState)
|
|
396
381
|
|
|
397
382
|
else -> {}
|
|
398
383
|
}
|
|
@@ -401,30 +386,67 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
401
386
|
)
|
|
402
387
|
}
|
|
403
388
|
|
|
389
|
+
private fun handleStateSettled(sheetView: View, newState: Int) {
|
|
390
|
+
if (interactionState is InteractionState.Reconfiguring) return
|
|
391
|
+
|
|
392
|
+
val index = detentCalculator.getDetentIndexForState(newState) ?: return
|
|
393
|
+
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
394
|
+
val detentInfo = DetentInfo(index, position)
|
|
395
|
+
|
|
396
|
+
when (interactionState) {
|
|
397
|
+
is InteractionState.Dragging -> {
|
|
398
|
+
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
399
|
+
delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
|
|
400
|
+
|
|
401
|
+
if (detentInfo.index != currentDetentIndex) {
|
|
402
|
+
presentPromise?.invoke()
|
|
403
|
+
presentPromise = null
|
|
404
|
+
currentDetentIndex = detentInfo.index
|
|
405
|
+
setupDimmedBackground(detentInfo.index)
|
|
406
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
interactionState = InteractionState.Idle
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
else -> {
|
|
413
|
+
if (detentInfo.index != currentDetentIndex) {
|
|
414
|
+
currentDetentIndex = detentInfo.index
|
|
415
|
+
if (!isKeyboardTransitioning) {
|
|
416
|
+
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
417
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
404
424
|
private fun setupModalObserver() {
|
|
405
425
|
rnScreensObserver = RNScreensFragmentObserver(
|
|
406
426
|
reactContext = reactContext,
|
|
407
427
|
onModalPresented = {
|
|
408
428
|
if (isPresented && isDialogVisible) {
|
|
409
429
|
isDialogVisible = false
|
|
410
|
-
|
|
411
|
-
|
|
430
|
+
wasHiddenByModal = true
|
|
431
|
+
|
|
432
|
+
dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFastFadeOut)
|
|
433
|
+
dialog?.window?.decorView?.visibility = GONE
|
|
412
434
|
dimView?.visibility = INVISIBLE
|
|
413
435
|
parentDimView?.visibility = INVISIBLE
|
|
414
|
-
wasHiddenByModal = true
|
|
415
436
|
}
|
|
416
437
|
},
|
|
417
|
-
|
|
418
|
-
// Only show if we were the one hidden by modal, not by sheet stacking
|
|
438
|
+
onModalWillDismiss = {
|
|
419
439
|
if (isPresented && wasHiddenByModal) {
|
|
420
440
|
isDialogVisible = true
|
|
441
|
+
|
|
442
|
+
dialog?.window?.setWindowAnimations(0)
|
|
421
443
|
dialog?.window?.decorView?.visibility = VISIBLE
|
|
422
444
|
dimView?.visibility = VISIBLE
|
|
423
445
|
parentDimView?.visibility = VISIBLE
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
onModalDidDismiss = {
|
|
449
|
+
if (isPresented && wasHiddenByModal) {
|
|
428
450
|
wasHiddenByModal = false
|
|
429
451
|
}
|
|
430
452
|
}
|
|
@@ -437,6 +459,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
437
459
|
rnScreensObserver = null
|
|
438
460
|
}
|
|
439
461
|
|
|
462
|
+
// ====================================================================
|
|
463
|
+
// MARK: - Event Emission
|
|
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
|
+
|
|
440
473
|
private fun emitWillDismissEvents() {
|
|
441
474
|
delegate?.viewControllerWillBlur()
|
|
442
475
|
delegate?.viewControllerWillDismiss()
|
|
@@ -455,11 +488,23 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
455
488
|
dismissPromise = null
|
|
456
489
|
}
|
|
457
490
|
|
|
458
|
-
|
|
491
|
+
private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
|
|
492
|
+
if (currentTop == lastEmittedPositionPx) return
|
|
493
|
+
|
|
494
|
+
lastEmittedPositionPx = currentTop
|
|
495
|
+
val visibleHeight = realScreenHeight - currentTop
|
|
496
|
+
val position = detentCalculator.getPositionDp(visibleHeight)
|
|
497
|
+
val interpolatedIndex = detentCalculator.getInterpolatedIndexForPosition(currentTop)
|
|
498
|
+
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
499
|
+
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
500
|
+
}
|
|
501
|
+
|
|
459
502
|
private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
|
|
460
|
-
val
|
|
461
|
-
val
|
|
462
|
-
|
|
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)
|
|
463
508
|
}
|
|
464
509
|
|
|
465
510
|
// ====================================================================
|
|
@@ -472,18 +517,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
472
517
|
return sheetTop <= topInset
|
|
473
518
|
}
|
|
474
519
|
|
|
475
|
-
val currentSheetTop: Int
|
|
476
|
-
get() = bottomSheetView?.top ?: screenHeight
|
|
477
|
-
|
|
478
520
|
val currentTranslationY: Int
|
|
479
521
|
get() = bottomSheetView?.translationY?.toInt() ?: 0
|
|
480
522
|
|
|
481
523
|
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
482
524
|
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
483
|
-
return realScreenHeight - getDetentHeight(detents[detentIndex])
|
|
525
|
+
return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
|
|
484
526
|
}
|
|
485
527
|
|
|
486
|
-
/** Translates the sheet when stacking. Pass 0 to reset. */
|
|
487
528
|
fun translateDialog(translationY: Int) {
|
|
488
529
|
val bottomSheet = bottomSheetView ?: return
|
|
489
530
|
|
|
@@ -512,20 +553,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
512
553
|
if (isPresented) {
|
|
513
554
|
setStateForDetentIndex(detentIndex)
|
|
514
555
|
} else {
|
|
556
|
+
shouldAnimatePresent = animated
|
|
515
557
|
currentDetentIndex = detentIndex
|
|
516
|
-
|
|
517
|
-
setupSheetDetents()
|
|
518
|
-
setStateForDetentIndex(detentIndex)
|
|
558
|
+
interactionState = InteractionState.Idle
|
|
519
559
|
|
|
520
|
-
|
|
560
|
+
emitWillPresentEvents()
|
|
521
561
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
562
|
+
setupSheetDetents()
|
|
563
|
+
setStateForDetentIndex(detentIndex)
|
|
564
|
+
setupBackground()
|
|
565
|
+
setupGrabber()
|
|
525
566
|
|
|
526
|
-
|
|
527
|
-
dialog.window?.setWindowAnimations(0)
|
|
528
|
-
}
|
|
567
|
+
bottomSheetView?.visibility = INVISIBLE
|
|
529
568
|
|
|
530
569
|
dialog.show()
|
|
531
570
|
}
|
|
@@ -535,18 +574,33 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
535
574
|
if (isDismissing) return
|
|
536
575
|
|
|
537
576
|
isDismissing = true
|
|
538
|
-
val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
|
|
539
|
-
setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
|
|
540
577
|
emitWillDismissEvents()
|
|
541
578
|
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
579
|
+
if (animated) {
|
|
580
|
+
sheetAnimator.animateDismiss(
|
|
581
|
+
onUpdate = { effectiveTop ->
|
|
582
|
+
emitChangePositionDelegate(effectiveTop)
|
|
583
|
+
positionFooter()
|
|
584
|
+
updateDimAmount(effectiveTop)
|
|
585
|
+
},
|
|
586
|
+
onEnd = { dialog?.dismiss() }
|
|
587
|
+
)
|
|
545
588
|
} else {
|
|
589
|
+
emitChangePositionDelegate(realScreenHeight)
|
|
546
590
|
dialog?.dismiss()
|
|
547
591
|
}
|
|
548
592
|
}
|
|
549
593
|
|
|
594
|
+
private fun finishPresent() {
|
|
595
|
+
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
596
|
+
delegate?.viewControllerDidPresent(index, position, detent)
|
|
597
|
+
parentSheetView?.viewControllerDidBlur()
|
|
598
|
+
delegate?.viewControllerDidFocus()
|
|
599
|
+
|
|
600
|
+
presentPromise?.invoke()
|
|
601
|
+
presentPromise = null
|
|
602
|
+
}
|
|
603
|
+
|
|
550
604
|
// ====================================================================
|
|
551
605
|
// MARK: - Sheet Configuration
|
|
552
606
|
// ====================================================================
|
|
@@ -554,7 +608,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
554
608
|
fun setupSheetDetents() {
|
|
555
609
|
val behavior = this.behavior ?: return
|
|
556
610
|
|
|
557
|
-
|
|
611
|
+
interactionState = InteractionState.Reconfiguring
|
|
558
612
|
val edgeToEdgeTopInset: Int = if (!edgeToEdgeFullScreen) topInset else 0
|
|
559
613
|
|
|
560
614
|
behavior.apply {
|
|
@@ -563,14 +617,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
563
617
|
|
|
564
618
|
val maxAvailableHeight = realScreenHeight - edgeToEdgeTopInset
|
|
565
619
|
|
|
566
|
-
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
620
|
+
setPeekHeight(detentCalculator.getDetentHeight(detents[0]), isPresented)
|
|
567
621
|
|
|
568
622
|
val halfExpandedDetentHeight = when (detents.size) {
|
|
569
623
|
1 -> peekHeight
|
|
570
|
-
else -> getDetentHeight(detents[1])
|
|
624
|
+
else -> detentCalculator.getDetentHeight(detents[1])
|
|
571
625
|
}
|
|
572
626
|
|
|
573
|
-
val maxDetentHeight = getDetentHeight(detents.last())
|
|
627
|
+
val maxDetentHeight = detentCalculator.getDetentHeight(detents.last())
|
|
574
628
|
|
|
575
629
|
val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
|
|
576
630
|
halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
|
|
@@ -592,7 +646,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
592
646
|
setStateForDetentIndex(currentDetentIndex)
|
|
593
647
|
}
|
|
594
648
|
|
|
595
|
-
|
|
649
|
+
interactionState = InteractionState.Idle
|
|
596
650
|
}
|
|
597
651
|
}
|
|
598
652
|
|
|
@@ -617,16 +671,44 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
617
671
|
bottomSheet.addView(grabberView)
|
|
618
672
|
}
|
|
619
673
|
|
|
620
|
-
|
|
621
|
-
|
|
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
|
+
}
|
|
622
684
|
|
|
623
685
|
fun setupKeyboardObserver() {
|
|
624
686
|
val bottomSheet = bottomSheetView ?: return
|
|
625
687
|
keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
|
|
626
688
|
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
627
|
-
override fun
|
|
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
|
|
628
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
|
|
629
709
|
}
|
|
710
|
+
|
|
711
|
+
override fun keyboardDidChangeHeight(height: Int) {}
|
|
630
712
|
}
|
|
631
713
|
start()
|
|
632
714
|
}
|
|
@@ -637,37 +719,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
637
719
|
keyboardObserver = null
|
|
638
720
|
}
|
|
639
721
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
this.duration = duration
|
|
644
|
-
interpolator = if (fromTop > toTop) {
|
|
645
|
-
android.view.animation.DecelerateInterpolator(2f) // present
|
|
646
|
-
} else {
|
|
647
|
-
android.view.animation.AccelerateInterpolator(2f) // dismiss
|
|
648
|
-
}
|
|
649
|
-
addUpdateListener { animator ->
|
|
650
|
-
emitChangePositionDelegate(animator.animatedValue as Int)
|
|
651
|
-
}
|
|
652
|
-
addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
653
|
-
override fun onAnimationEnd(animation: android.animation.Animator) {
|
|
654
|
-
positionAnimator = null
|
|
655
|
-
}
|
|
656
|
-
})
|
|
657
|
-
start()
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
|
|
662
|
-
if (currentTop == lastEmittedPositionPx) return
|
|
663
|
-
|
|
664
|
-
lastEmittedPositionPx = currentTop
|
|
665
|
-
val visibleHeight = realScreenHeight - currentTop
|
|
666
|
-
val position = getPositionDp(visibleHeight)
|
|
667
|
-
val interpolatedIndex = getInterpolatedIndexForPosition(currentTop)
|
|
668
|
-
val detent = getInterpolatedDetentForPosition(currentTop)
|
|
669
|
-
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
670
|
-
}
|
|
722
|
+
// ====================================================================
|
|
723
|
+
// MARK: - Background & Dimming
|
|
724
|
+
// ====================================================================
|
|
671
725
|
|
|
672
726
|
fun setupBackground() {
|
|
673
727
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -719,8 +773,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
719
773
|
}
|
|
720
774
|
|
|
721
775
|
if (shouldDimAtDetent) {
|
|
722
|
-
touchOutside.setOnTouchListener
|
|
723
|
-
|
|
776
|
+
touchOutside.setOnTouchListener { _, event ->
|
|
777
|
+
if (event.action == MotionEvent.ACTION_UP && dismissible) {
|
|
778
|
+
dismiss()
|
|
779
|
+
}
|
|
780
|
+
true
|
|
781
|
+
}
|
|
724
782
|
} else {
|
|
725
783
|
touchOutside.setOnTouchListener { v, event ->
|
|
726
784
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
@@ -735,25 +793,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
735
793
|
}
|
|
736
794
|
}
|
|
737
795
|
|
|
738
|
-
fun
|
|
739
|
-
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
private fun animateDimAlpha(show: Boolean) {
|
|
796
|
+
fun updateDimAmount(sheetTop: Int? = null) {
|
|
743
797
|
if (!dimmed) return
|
|
744
|
-
val
|
|
745
|
-
dimView?.
|
|
746
|
-
parentDimView?.
|
|
798
|
+
val top = (sheetTop ?: bottomSheetView?.top ?: return) + currentKeyboardInset
|
|
799
|
+
dimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
|
|
800
|
+
parentDimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
|
|
747
801
|
}
|
|
748
802
|
|
|
749
|
-
fun updateDimAmount() {
|
|
750
|
-
if (!dimmed) return
|
|
751
|
-
val sheetTop = bottomSheetView?.top ?: return
|
|
752
|
-
dimView?.interpolateAlpha(sheetTop, dimmedDetentIndex, ::getSheetTopForDetentIndex)
|
|
753
|
-
parentDimView?.interpolateAlpha(sheetTop, dimmedDetentIndex, ::getSheetTopForDetentIndex)
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
|
|
757
803
|
fun positionFooter(slideOffset: Float? = null) {
|
|
758
804
|
val footerView = containerView?.footerView ?: return
|
|
759
805
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -762,20 +808,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
762
808
|
val sheetHeight = bottomSheet.height
|
|
763
809
|
val sheetTop = bottomSheet.top
|
|
764
810
|
|
|
765
|
-
|
|
766
|
-
var footerY = (sheetHeight - sheetTop - footerHeight - keyboardHeight).toFloat()
|
|
767
|
-
|
|
811
|
+
var footerY = (sheetHeight - sheetTop - footerHeight - currentKeyboardInset).toFloat()
|
|
768
812
|
if (slideOffset != null && slideOffset < 0) {
|
|
769
813
|
footerY -= (footerHeight * slideOffset)
|
|
770
814
|
}
|
|
771
815
|
|
|
772
|
-
// Clamp to prevent footer from going above visible area
|
|
773
816
|
val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
|
|
774
817
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
775
818
|
}
|
|
776
819
|
|
|
777
820
|
fun setStateForDetentIndex(index: Int) {
|
|
778
|
-
behavior?.state = getStateForDetentIndex(index)
|
|
821
|
+
behavior?.state = detentCalculator.getStateForDetentIndex(index)
|
|
779
822
|
}
|
|
780
823
|
|
|
781
824
|
fun getDefaultBackgroundColor(): Int {
|
|
@@ -793,193 +836,48 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
793
836
|
}
|
|
794
837
|
|
|
795
838
|
// ====================================================================
|
|
796
|
-
// MARK: -
|
|
839
|
+
// MARK: - Drag Handling
|
|
797
840
|
// ====================================================================
|
|
798
841
|
|
|
799
|
-
/**
|
|
800
|
-
* Calculate the visible sheet height from a sheet view.
|
|
801
|
-
* Uses real screen height for consistency across API levels.
|
|
802
|
-
*/
|
|
803
|
-
private fun getVisibleSheetHeight(sheetView: View): Int = realScreenHeight - sheetView.top
|
|
804
|
-
|
|
805
|
-
private fun getPositionDp(visibleSheetHeight: Int): Float = (screenHeight - visibleSheetHeight).pxToDp()
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Get the expected sheetTop position for a detent index.
|
|
809
|
-
*/
|
|
810
|
-
private fun getSheetTopForDetentIndex(index: Int): Int {
|
|
811
|
-
if (index < 0 || index >= detents.size) return realScreenHeight
|
|
812
|
-
return realScreenHeight - getDetentHeight(detents[index])
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
/** Returns (fromIndex, toIndex, progress) for interpolation, or null if < 2 detents. */
|
|
816
|
-
private fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
|
|
817
|
-
val count = detents.size
|
|
818
|
-
if (count == 0) return null
|
|
819
|
-
|
|
820
|
-
val firstPos = getSheetTopForDetentIndex(0)
|
|
821
|
-
|
|
822
|
-
// Above first detent - interpolating toward closed
|
|
823
|
-
if (positionPx > firstPos) {
|
|
824
|
-
val range = realScreenHeight - firstPos
|
|
825
|
-
val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
|
|
826
|
-
return Triple(-1, 0, progress)
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Single detent - at or above the detent
|
|
830
|
-
if (count == 1) return Triple(0, 0, 0f)
|
|
831
|
-
|
|
832
|
-
val lastPos = getSheetTopForDetentIndex(count - 1)
|
|
833
|
-
|
|
834
|
-
// Below last detent
|
|
835
|
-
if (positionPx < lastPos) {
|
|
836
|
-
return Triple(count - 1, count - 1, 0f)
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Between detents
|
|
840
|
-
for (i in 0 until count - 1) {
|
|
841
|
-
val pos = getSheetTopForDetentIndex(i)
|
|
842
|
-
val nextPos = getSheetTopForDetentIndex(i + 1)
|
|
843
|
-
|
|
844
|
-
if (positionPx in nextPos..pos) {
|
|
845
|
-
val range = pos - nextPos
|
|
846
|
-
val progress = if (range > 0) (pos - positionPx).toFloat() / range else 0f
|
|
847
|
-
return Triple(i, i + 1, maxOf(0f, minOf(1f, progress)))
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
return Triple(count - 1, count - 1, 0f)
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/** Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1). */
|
|
855
|
-
private fun getInterpolatedIndexForPosition(positionPx: Int): Float {
|
|
856
|
-
val count = detents.size
|
|
857
|
-
if (count == 0) return -1f
|
|
858
|
-
|
|
859
|
-
val segment = findSegmentForPosition(positionPx) ?: return 0f
|
|
860
|
-
val (fromIndex, _, progress) = segment
|
|
861
|
-
|
|
862
|
-
if (fromIndex == -1) return -progress
|
|
863
|
-
return fromIndex + progress
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
/** Returns interpolated screen fraction for position. */
|
|
867
|
-
private fun getInterpolatedDetentForPosition(positionPx: Int): Float {
|
|
868
|
-
val count = detents.size
|
|
869
|
-
if (count == 0) return 0f
|
|
870
|
-
|
|
871
|
-
val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
|
|
872
|
-
val (fromIndex, toIndex, progress) = segment
|
|
873
|
-
|
|
874
|
-
if (fromIndex == -1) {
|
|
875
|
-
val firstDetent = getDetentValueForIndex(0)
|
|
876
|
-
return maxOf(0f, firstDetent * (1 - progress))
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
val fromDetent = getDetentValueForIndex(fromIndex)
|
|
880
|
-
val toDetent = getDetentValueForIndex(toIndex)
|
|
881
|
-
return fromDetent + progress * (toDetent - fromDetent)
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/** Returns raw screen fraction for index (without bottomInset). */
|
|
885
|
-
private fun getDetentValueForIndex(index: Int): Float {
|
|
886
|
-
if (index < 0 || index >= detents.size) return 0f
|
|
887
|
-
val value = detents[index]
|
|
888
|
-
return if (value == -1.0) {
|
|
889
|
-
(contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
|
|
890
|
-
} else {
|
|
891
|
-
value.toFloat()
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
private fun getCurrentDetentInfo(sheetView: View): DetentInfo {
|
|
896
|
-
val position = getPositionDp(getVisibleSheetHeight(sheetView))
|
|
897
|
-
return DetentInfo(currentDetentIndex, position)
|
|
898
|
-
}
|
|
899
|
-
|
|
900
842
|
private fun handleDragBegin(sheetView: View) {
|
|
901
|
-
val
|
|
902
|
-
val detent = getDetentValueForIndex(
|
|
903
|
-
delegate?.viewControllerDidDragBegin(
|
|
904
|
-
|
|
843
|
+
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
844
|
+
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
845
|
+
delegate?.viewControllerDidDragBegin(currentDetentIndex, position, detent)
|
|
846
|
+
interactionState = InteractionState.Dragging(startTop = sheetView.top)
|
|
905
847
|
}
|
|
906
848
|
|
|
907
849
|
private fun handleDragChange(sheetView: View) {
|
|
908
|
-
if (!
|
|
909
|
-
val
|
|
910
|
-
val detent = getDetentValueForIndex(
|
|
911
|
-
delegate?.viewControllerDidDragChange(
|
|
850
|
+
if (interactionState !is InteractionState.Dragging) return
|
|
851
|
+
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
852
|
+
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
853
|
+
delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
|
|
912
854
|
}
|
|
913
855
|
|
|
914
856
|
// ====================================================================
|
|
915
|
-
// MARK: - Detent
|
|
857
|
+
// MARK: - Detent Helpers
|
|
916
858
|
// ====================================================================
|
|
917
859
|
|
|
918
|
-
private val keyboardHeight: Int
|
|
919
|
-
get() = keyboardObserver?.currentHeight ?: 0
|
|
920
|
-
|
|
921
|
-
private fun getDetentHeight(detent: Double): Int {
|
|
922
|
-
val height = if (detent == -1.0) {
|
|
923
|
-
contentHeight + headerHeight + contentBottomInset + keyboardHeight
|
|
924
|
-
} else {
|
|
925
|
-
if (detent <= 0.0 || detent > 1.0) {
|
|
926
|
-
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
927
|
-
}
|
|
928
|
-
(detent * screenHeight).toInt() + contentBottomInset + keyboardHeight
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
val maxAllowedHeight = screenHeight + contentBottomInset
|
|
932
|
-
return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
/** Maps detent index to BottomSheetBehavior state based on detent count. */
|
|
936
|
-
private fun getStateForDetentIndex(index: Int): Int {
|
|
937
|
-
val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
|
|
938
|
-
return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
/** Maps BottomSheetBehavior state to DetentInfo based on detent count. */
|
|
942
860
|
fun getDetentInfoForState(state: Int): DetentInfo? {
|
|
943
|
-
val
|
|
944
|
-
val index = stateMap[state] ?: return null
|
|
861
|
+
val index = detentCalculator.getDetentIndexForState(state) ?: return null
|
|
945
862
|
return DetentInfo(index, getPositionForDetentIndex(index))
|
|
946
863
|
}
|
|
947
864
|
|
|
948
|
-
/** Returns state-to-index mapping based on detent count. */
|
|
949
|
-
private fun getDetentStateMap(): Map<Int, Int>? =
|
|
950
|
-
when (detents.size) {
|
|
951
|
-
1 -> mapOf(
|
|
952
|
-
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
953
|
-
BottomSheetBehavior.STATE_EXPANDED to 0
|
|
954
|
-
)
|
|
955
|
-
|
|
956
|
-
2 -> mapOf(
|
|
957
|
-
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
958
|
-
BottomSheetBehavior.STATE_EXPANDED to 1
|
|
959
|
-
)
|
|
960
|
-
|
|
961
|
-
3 -> mapOf(
|
|
962
|
-
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
963
|
-
BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
|
|
964
|
-
BottomSheetBehavior.STATE_EXPANDED to 2
|
|
965
|
-
)
|
|
966
|
-
|
|
967
|
-
else -> null
|
|
968
|
-
}
|
|
969
|
-
|
|
970
865
|
private fun getPositionForDetentIndex(index: Int): Float {
|
|
971
866
|
if (index < 0 || index >= detents.size) return screenHeight.pxToDp()
|
|
972
867
|
|
|
973
868
|
bottomSheetView?.let {
|
|
974
|
-
val visibleSheetHeight = getVisibleSheetHeight(it)
|
|
975
|
-
if (visibleSheetHeight > 0
|
|
869
|
+
val visibleSheetHeight = detentCalculator.getVisibleSheetHeight(it.top)
|
|
870
|
+
if (visibleSheetHeight > 0 && visibleSheetHeight < realScreenHeight) {
|
|
871
|
+
return detentCalculator.getPositionDp(visibleSheetHeight)
|
|
872
|
+
}
|
|
976
873
|
}
|
|
977
874
|
|
|
978
|
-
val detentHeight = getDetentHeight(detents[index])
|
|
979
|
-
return getPositionDp(detentHeight)
|
|
875
|
+
val detentHeight = detentCalculator.getDetentHeight(detents[index])
|
|
876
|
+
return detentCalculator.getPositionDp(detentHeight)
|
|
980
877
|
}
|
|
981
878
|
|
|
982
|
-
fun getDetentInfoForIndex(index: Int)
|
|
879
|
+
fun getDetentInfoForIndex(index: Int): DetentInfo =
|
|
880
|
+
getDetentInfoForState(detentCalculator.getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
|
|
983
881
|
|
|
984
882
|
// ====================================================================
|
|
985
883
|
// MARK: - RootView Implementation
|
|
@@ -995,11 +893,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
995
893
|
|
|
996
894
|
if (w == oldw && h == oldh) return
|
|
997
895
|
if (!isPresented) return
|
|
998
|
-
|
|
999
|
-
// Skip continuous size changes when fullScreen + edge-to-edge
|
|
1000
|
-
if (h + topInset >= screenHeight && isExpanded && oldw == w) {
|
|
1001
|
-
return
|
|
1002
|
-
}
|
|
896
|
+
if (h + topInset >= screenHeight && isExpanded && oldw == w) return
|
|
1003
897
|
|
|
1004
898
|
this.post {
|
|
1005
899
|
setupSheetDetents()
|
|
@@ -1016,7 +910,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1016
910
|
// MARK: - Touch Event Handling
|
|
1017
911
|
// ====================================================================
|
|
1018
912
|
|
|
1019
|
-
/** Forwards touch events to footer which is positioned outside normal hierarchy. */
|
|
1020
913
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1021
914
|
val footer = containerView?.footerView
|
|
1022
915
|
if (footer != null && footer.isVisible) {
|