@lodev09/react-native-true-sheet 3.4.2 → 3.5.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -12
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +255 -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 +27 -9
- package/android/src/main/res/values/styles.xml +8 -13
- 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_fade_out.xml +0 -6
- 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,18 +1,15 @@
|
|
|
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
|
|
7
6
|
import android.graphics.drawable.shapes.RoundRectShape
|
|
8
|
-
import android.util.Log
|
|
9
7
|
import android.util.TypedValue
|
|
10
8
|
import android.view.MotionEvent
|
|
11
9
|
import android.view.View
|
|
12
10
|
import android.view.WindowManager
|
|
13
11
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
14
12
|
import android.widget.FrameLayout
|
|
15
|
-
import androidx.core.view.ViewCompat
|
|
16
13
|
import androidx.core.view.isNotEmpty
|
|
17
14
|
import androidx.core.view.isVisible
|
|
18
15
|
import com.facebook.react.R
|
|
@@ -29,6 +26,11 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
|
29
26
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
30
27
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
31
28
|
import com.lodev09.truesheet.core.RNScreensFragmentObserver
|
|
29
|
+
import com.lodev09.truesheet.core.TrueSheetAnimator
|
|
30
|
+
import com.lodev09.truesheet.core.TrueSheetAnimatorProvider
|
|
31
|
+
import com.lodev09.truesheet.core.TrueSheetDetentCalculator
|
|
32
|
+
import com.lodev09.truesheet.core.TrueSheetDetentMeasurements
|
|
33
|
+
import com.lodev09.truesheet.core.TrueSheetDialogObserver
|
|
32
34
|
import com.lodev09.truesheet.core.TrueSheetDimView
|
|
33
35
|
import com.lodev09.truesheet.core.TrueSheetGrabberView
|
|
34
36
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
@@ -62,21 +64,18 @@ interface TrueSheetViewControllerDelegate {
|
|
|
62
64
|
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
|
63
65
|
class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
64
66
|
ReactViewGroup(reactContext),
|
|
65
|
-
RootView
|
|
67
|
+
RootView,
|
|
68
|
+
TrueSheetDetentMeasurements,
|
|
69
|
+
TrueSheetAnimatorProvider {
|
|
66
70
|
|
|
67
71
|
companion object {
|
|
68
72
|
const val TAG_NAME = "TrueSheet"
|
|
69
73
|
|
|
70
74
|
private const val MAX_HALF_EXPANDED_RATIO = 0.999f
|
|
71
|
-
|
|
72
75
|
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
73
76
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
74
77
|
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
|
|
78
|
+
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
// ====================================================================
|
|
@@ -99,22 +98,32 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
99
98
|
private val sheetContainer: FrameLayout?
|
|
100
99
|
get() = this.parent as? FrameLayout
|
|
101
100
|
|
|
102
|
-
|
|
101
|
+
override val bottomSheetView: FrameLayout?
|
|
103
102
|
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
104
103
|
|
|
105
104
|
private val containerView: TrueSheetContainerView?
|
|
106
105
|
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
107
106
|
|
|
108
|
-
|
|
107
|
+
override val contentHeight: Int
|
|
109
108
|
get() = containerView?.contentHeight ?: 0
|
|
110
109
|
|
|
111
|
-
|
|
110
|
+
override val headerHeight: Int
|
|
112
111
|
get() = containerView?.headerHeight ?: 0
|
|
113
112
|
|
|
113
|
+
override val keyboardHeight: Int
|
|
114
|
+
get() = keyboardObserver?.currentHeight ?: 0
|
|
115
|
+
|
|
114
116
|
// ====================================================================
|
|
115
117
|
// MARK: - State
|
|
116
118
|
// ====================================================================
|
|
117
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
|
+
|
|
118
127
|
var isPresented = false
|
|
119
128
|
private set
|
|
120
129
|
|
|
@@ -124,36 +133,42 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
124
133
|
var currentDetentIndex: Int = -1
|
|
125
134
|
private set
|
|
126
135
|
|
|
136
|
+
private var interactionState: InteractionState = InteractionState.Idle
|
|
137
|
+
private var isDismissing = false
|
|
138
|
+
private var wasHiddenByModal = false
|
|
139
|
+
private var shouldAnimatePresent = true
|
|
140
|
+
|
|
127
141
|
private var lastStateWidth: Int = 0
|
|
128
142
|
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
143
|
private var lastEmittedPositionPx: Int = -1
|
|
134
144
|
|
|
135
|
-
/** Tracks if this sheet was hidden due to a RN Screens modal (vs sheet stacking) */
|
|
136
|
-
private var wasHiddenByModal = false
|
|
137
|
-
|
|
138
145
|
var presentPromise: (() -> Unit)? = null
|
|
139
146
|
var dismissPromise: (() -> Unit)? = null
|
|
140
147
|
|
|
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
|
|
@@ -195,11 +210,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
195
210
|
|
|
196
211
|
var insetAdjustment: String = "automatic"
|
|
197
212
|
|
|
198
|
-
|
|
199
|
-
val contentBottomInset: Int
|
|
213
|
+
override val contentBottomInset: Int
|
|
200
214
|
get() = if (insetAdjustment == "automatic") bottomInset else 0
|
|
201
215
|
|
|
202
|
-
/** Edge-to-edge enabled by default on API 36+, or when explicitly configured. */
|
|
203
216
|
private val edgeToEdgeEnabled: Boolean
|
|
204
217
|
get() {
|
|
205
218
|
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
@@ -210,8 +223,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
210
223
|
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
|
211
224
|
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
|
212
225
|
|
|
213
|
-
/**
|
|
214
|
-
private
|
|
226
|
+
/** Single instance that reads current values via TrueSheetDetentMeasurements interface */
|
|
227
|
+
private val detentCalculator = TrueSheetDetentCalculator(this)
|
|
215
228
|
|
|
216
229
|
// ====================================================================
|
|
217
230
|
// MARK: - Initialization
|
|
@@ -238,8 +251,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
238
251
|
setContentView(this@TrueSheetViewController)
|
|
239
252
|
|
|
240
253
|
window?.apply {
|
|
241
|
-
|
|
242
|
-
// Disable default keyboard avoidance - sheet handles it via setupKeyboardObserver
|
|
254
|
+
setWindowAnimations(0)
|
|
243
255
|
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
|
244
256
|
}
|
|
245
257
|
|
|
@@ -272,6 +284,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
272
284
|
|
|
273
285
|
cleanupKeyboardObserver()
|
|
274
286
|
cleanupModalObserver()
|
|
287
|
+
sheetAnimator.cancel()
|
|
275
288
|
dimView?.detach()
|
|
276
289
|
dimView = null
|
|
277
290
|
parentDimView?.detach()
|
|
@@ -279,58 +292,45 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
279
292
|
sheetContainer?.removeView(this)
|
|
280
293
|
|
|
281
294
|
dialog = null
|
|
282
|
-
|
|
295
|
+
interactionState = InteractionState.Idle
|
|
283
296
|
isDismissing = false
|
|
284
297
|
isPresented = false
|
|
285
298
|
isDialogVisible = false
|
|
286
299
|
wasHiddenByModal = false
|
|
287
300
|
lastEmittedPositionPx = -1
|
|
301
|
+
shouldAnimatePresent = true
|
|
288
302
|
}
|
|
289
303
|
|
|
290
304
|
private fun setupDialogListeners(dialog: BottomSheetDialog) {
|
|
291
305
|
dialog.setOnShowListener {
|
|
306
|
+
bottomSheetView?.visibility = VISIBLE
|
|
292
307
|
isPresented = true
|
|
293
308
|
isDialogVisible = true
|
|
294
|
-
resetAnimation()
|
|
295
|
-
setupBackground()
|
|
296
|
-
setupGrabber()
|
|
297
|
-
setupKeyboardObserver()
|
|
298
309
|
|
|
299
|
-
|
|
300
|
-
setupTransitionTracker(realScreenHeight, toTop, PRESENT_ANIMATION_DURATION)
|
|
301
|
-
animateDimAlpha(show = true)
|
|
310
|
+
setupKeyboardObserver()
|
|
302
311
|
|
|
303
|
-
|
|
312
|
+
if (shouldAnimatePresent) {
|
|
313
|
+
val toTop = getExpectedSheetTop(currentDetentIndex)
|
|
314
|
+
sheetAnimator.animatePresent(
|
|
315
|
+
toTop = toTop,
|
|
316
|
+
onUpdate = { effectiveTop ->
|
|
317
|
+
emitChangePositionDelegate(effectiveTop)
|
|
318
|
+
positionFooter()
|
|
319
|
+
updateDimAmount(effectiveTop)
|
|
320
|
+
},
|
|
321
|
+
onEnd = { finishPresent() }
|
|
322
|
+
)
|
|
323
|
+
} else {
|
|
324
|
+
val toTop = getExpectedSheetTop(currentDetentIndex)
|
|
325
|
+
emitChangePositionDelegate(toTop)
|
|
304
326
|
positionFooter()
|
|
327
|
+
finishPresent()
|
|
305
328
|
}
|
|
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
329
|
}
|
|
328
330
|
|
|
329
331
|
dialog.setOnDismissListener {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
cleanupDialog()
|
|
333
|
-
}, DISMISS_ANIMATION_DURATION)
|
|
332
|
+
emitDidDismissEvents()
|
|
333
|
+
cleanupDialog()
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
|
|
@@ -350,7 +350,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
positionFooter(slideOffset)
|
|
353
|
-
updateDimAmount()
|
|
353
|
+
updateDimAmount(sheetView.top)
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
@@ -369,30 +369,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
369
369
|
|
|
370
370
|
BottomSheetBehavior.STATE_EXPANDED,
|
|
371
371
|
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
|
-
}
|
|
372
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED -> handleStateSettled(sheetView, newState)
|
|
396
373
|
|
|
397
374
|
else -> {}
|
|
398
375
|
}
|
|
@@ -401,30 +378,78 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
401
378
|
)
|
|
402
379
|
}
|
|
403
380
|
|
|
381
|
+
private fun handleStateSettled(sheetView: View, newState: Int) {
|
|
382
|
+
if (interactionState is InteractionState.Reconfiguring) return
|
|
383
|
+
|
|
384
|
+
val index = detentCalculator.getDetentIndexForState(newState) ?: return
|
|
385
|
+
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
386
|
+
val detentInfo = DetentInfo(index, position)
|
|
387
|
+
|
|
388
|
+
when (interactionState) {
|
|
389
|
+
is InteractionState.Dragging -> {
|
|
390
|
+
val draggingState = interactionState as InteractionState.Dragging
|
|
391
|
+
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
392
|
+
delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
|
|
393
|
+
|
|
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
|
+
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
|
+
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
416
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
404
422
|
private fun setupModalObserver() {
|
|
405
423
|
rnScreensObserver = RNScreensFragmentObserver(
|
|
406
424
|
reactContext = reactContext,
|
|
407
425
|
onModalPresented = {
|
|
408
426
|
if (isPresented && isDialogVisible) {
|
|
409
427
|
isDialogVisible = false
|
|
410
|
-
|
|
411
|
-
|
|
428
|
+
wasHiddenByModal = true
|
|
429
|
+
|
|
430
|
+
bottomSheetView?.animate()?.alpha(0f)?.setDuration(200)?.start()
|
|
412
431
|
dimView?.visibility = INVISIBLE
|
|
413
432
|
parentDimView?.visibility = INVISIBLE
|
|
414
|
-
|
|
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
|
+
)
|
|
415
437
|
}
|
|
416
438
|
},
|
|
417
|
-
|
|
418
|
-
// Only show if we were the one hidden by modal, not by sheet stacking
|
|
439
|
+
onModalWillDismiss = {
|
|
419
440
|
if (isPresented && wasHiddenByModal) {
|
|
420
441
|
isDialogVisible = true
|
|
421
|
-
|
|
442
|
+
|
|
443
|
+
dialog?.window?.clearFlags(
|
|
444
|
+
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
445
|
+
)
|
|
446
|
+
bottomSheetView?.alpha = 1f
|
|
422
447
|
dimView?.visibility = VISIBLE
|
|
423
448
|
parentDimView?.visibility = VISIBLE
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
onModalDidDismiss = {
|
|
452
|
+
if (isPresented && wasHiddenByModal) {
|
|
428
453
|
wasHiddenByModal = false
|
|
429
454
|
}
|
|
430
455
|
}
|
|
@@ -437,6 +462,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
437
462
|
rnScreensObserver = null
|
|
438
463
|
}
|
|
439
464
|
|
|
465
|
+
// ====================================================================
|
|
466
|
+
// MARK: - Event Emission
|
|
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
|
+
|
|
440
476
|
private fun emitWillDismissEvents() {
|
|
441
477
|
delegate?.viewControllerWillBlur()
|
|
442
478
|
delegate?.viewControllerWillDismiss()
|
|
@@ -455,11 +491,23 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
455
491
|
dismissPromise = null
|
|
456
492
|
}
|
|
457
493
|
|
|
458
|
-
|
|
494
|
+
private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
|
|
495
|
+
if (currentTop == lastEmittedPositionPx) return
|
|
496
|
+
|
|
497
|
+
lastEmittedPositionPx = currentTop
|
|
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
|
+
|
|
459
505
|
private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
|
|
460
|
-
val
|
|
461
|
-
val
|
|
462
|
-
|
|
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)
|
|
463
511
|
}
|
|
464
512
|
|
|
465
513
|
// ====================================================================
|
|
@@ -472,18 +520,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
472
520
|
return sheetTop <= topInset
|
|
473
521
|
}
|
|
474
522
|
|
|
475
|
-
val currentSheetTop: Int
|
|
476
|
-
get() = bottomSheetView?.top ?: screenHeight
|
|
477
|
-
|
|
478
523
|
val currentTranslationY: Int
|
|
479
524
|
get() = bottomSheetView?.translationY?.toInt() ?: 0
|
|
480
525
|
|
|
481
526
|
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
482
527
|
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
483
|
-
return realScreenHeight - getDetentHeight(detents[detentIndex])
|
|
528
|
+
return realScreenHeight - detentCalculator.getDetentHeight(detents[detentIndex])
|
|
484
529
|
}
|
|
485
530
|
|
|
486
|
-
/** Translates the sheet when stacking. Pass 0 to reset. */
|
|
487
531
|
fun translateDialog(translationY: Int) {
|
|
488
532
|
val bottomSheet = bottomSheetView ?: return
|
|
489
533
|
|
|
@@ -512,20 +556,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
512
556
|
if (isPresented) {
|
|
513
557
|
setStateForDetentIndex(detentIndex)
|
|
514
558
|
} else {
|
|
559
|
+
shouldAnimatePresent = animated
|
|
515
560
|
currentDetentIndex = detentIndex
|
|
516
|
-
|
|
517
|
-
setupSheetDetents()
|
|
518
|
-
setStateForDetentIndex(detentIndex)
|
|
561
|
+
interactionState = InteractionState.Idle
|
|
519
562
|
|
|
520
|
-
|
|
563
|
+
emitWillPresentEvents()
|
|
521
564
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
565
|
+
setupSheetDetents()
|
|
566
|
+
setStateForDetentIndex(detentIndex)
|
|
567
|
+
setupBackground()
|
|
568
|
+
setupGrabber()
|
|
525
569
|
|
|
526
|
-
|
|
527
|
-
dialog.window?.setWindowAnimations(0)
|
|
528
|
-
}
|
|
570
|
+
bottomSheetView?.visibility = INVISIBLE
|
|
529
571
|
|
|
530
572
|
dialog.show()
|
|
531
573
|
}
|
|
@@ -535,18 +577,33 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
535
577
|
if (isDismissing) return
|
|
536
578
|
|
|
537
579
|
isDismissing = true
|
|
538
|
-
val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
|
|
539
|
-
setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
|
|
540
580
|
emitWillDismissEvents()
|
|
541
581
|
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
582
|
+
if (animated) {
|
|
583
|
+
sheetAnimator.animateDismiss(
|
|
584
|
+
onUpdate = { effectiveTop ->
|
|
585
|
+
emitChangePositionDelegate(effectiveTop)
|
|
586
|
+
positionFooter()
|
|
587
|
+
updateDimAmount(effectiveTop)
|
|
588
|
+
},
|
|
589
|
+
onEnd = { dialog?.dismiss() }
|
|
590
|
+
)
|
|
545
591
|
} else {
|
|
592
|
+
emitChangePositionDelegate(realScreenHeight)
|
|
546
593
|
dialog?.dismiss()
|
|
547
594
|
}
|
|
548
595
|
}
|
|
549
596
|
|
|
597
|
+
private fun finishPresent() {
|
|
598
|
+
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
599
|
+
delegate?.viewControllerDidPresent(index, position, detent)
|
|
600
|
+
parentSheetView?.viewControllerDidBlur()
|
|
601
|
+
delegate?.viewControllerDidFocus()
|
|
602
|
+
|
|
603
|
+
presentPromise?.invoke()
|
|
604
|
+
presentPromise = null
|
|
605
|
+
}
|
|
606
|
+
|
|
550
607
|
// ====================================================================
|
|
551
608
|
// MARK: - Sheet Configuration
|
|
552
609
|
// ====================================================================
|
|
@@ -554,7 +611,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
554
611
|
fun setupSheetDetents() {
|
|
555
612
|
val behavior = this.behavior ?: return
|
|
556
613
|
|
|
557
|
-
|
|
614
|
+
interactionState = InteractionState.Reconfiguring
|
|
558
615
|
val edgeToEdgeTopInset: Int = if (!edgeToEdgeFullScreen) topInset else 0
|
|
559
616
|
|
|
560
617
|
behavior.apply {
|
|
@@ -563,14 +620,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
563
620
|
|
|
564
621
|
val maxAvailableHeight = realScreenHeight - edgeToEdgeTopInset
|
|
565
622
|
|
|
566
|
-
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
623
|
+
setPeekHeight(detentCalculator.getDetentHeight(detents[0]), isPresented)
|
|
567
624
|
|
|
568
625
|
val halfExpandedDetentHeight = when (detents.size) {
|
|
569
626
|
1 -> peekHeight
|
|
570
|
-
else -> getDetentHeight(detents[1])
|
|
627
|
+
else -> detentCalculator.getDetentHeight(detents[1])
|
|
571
628
|
}
|
|
572
629
|
|
|
573
|
-
val maxDetentHeight = getDetentHeight(detents.last())
|
|
630
|
+
val maxDetentHeight = detentCalculator.getDetentHeight(detents.last())
|
|
574
631
|
|
|
575
632
|
val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
|
|
576
633
|
halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
|
|
@@ -592,7 +649,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
592
649
|
setStateForDetentIndex(currentDetentIndex)
|
|
593
650
|
}
|
|
594
651
|
|
|
595
|
-
|
|
652
|
+
interactionState = InteractionState.Idle
|
|
596
653
|
}
|
|
597
654
|
}
|
|
598
655
|
|
|
@@ -617,15 +674,29 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
617
674
|
bottomSheet.addView(grabberView)
|
|
618
675
|
}
|
|
619
676
|
|
|
620
|
-
|
|
621
|
-
|
|
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
|
+
}
|
|
622
687
|
|
|
623
688
|
fun setupKeyboardObserver() {
|
|
624
689
|
val bottomSheet = bottomSheetView ?: return
|
|
625
690
|
keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
|
|
626
691
|
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
627
|
-
override fun
|
|
692
|
+
override fun keyboardWillShow(height: Int) {}
|
|
693
|
+
|
|
694
|
+
override fun keyboardWillHide() {}
|
|
695
|
+
|
|
696
|
+
override fun keyboardDidChangeHeight(height: Int) {
|
|
697
|
+
if (!shouldHandleKeyboard()) return
|
|
628
698
|
setupSheetDetents()
|
|
699
|
+
positionFooter()
|
|
629
700
|
}
|
|
630
701
|
}
|
|
631
702
|
start()
|
|
@@ -637,37 +708,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
637
708
|
keyboardObserver = null
|
|
638
709
|
}
|
|
639
710
|
|
|
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
|
-
}
|
|
711
|
+
// ====================================================================
|
|
712
|
+
// MARK: - Background & Dimming
|
|
713
|
+
// ====================================================================
|
|
671
714
|
|
|
672
715
|
fun setupBackground() {
|
|
673
716
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -719,8 +762,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
719
762
|
}
|
|
720
763
|
|
|
721
764
|
if (shouldDimAtDetent) {
|
|
722
|
-
touchOutside.setOnTouchListener
|
|
723
|
-
|
|
765
|
+
touchOutside.setOnTouchListener { _, event ->
|
|
766
|
+
if (event.action == MotionEvent.ACTION_UP && dismissible) {
|
|
767
|
+
dismiss()
|
|
768
|
+
}
|
|
769
|
+
true
|
|
770
|
+
}
|
|
724
771
|
} else {
|
|
725
772
|
touchOutside.setOnTouchListener { v, event ->
|
|
726
773
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
@@ -735,25 +782,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
735
782
|
}
|
|
736
783
|
}
|
|
737
784
|
|
|
738
|
-
fun
|
|
739
|
-
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
private fun animateDimAlpha(show: Boolean) {
|
|
743
|
-
if (!dimmed) return
|
|
744
|
-
val duration = if (show) PRESENT_ANIMATION_DURATION else DISMISS_ANIMATION_DURATION
|
|
745
|
-
dimView?.animateAlpha(show, duration, dimmedDetentIndex, currentDetentIndex)
|
|
746
|
-
parentDimView?.animateAlpha(show, duration, dimmedDetentIndex, currentDetentIndex)
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
fun updateDimAmount() {
|
|
785
|
+
fun updateDimAmount(sheetTop: Int? = null) {
|
|
750
786
|
if (!dimmed) return
|
|
751
|
-
val
|
|
752
|
-
dimView?.interpolateAlpha(
|
|
753
|
-
parentDimView?.interpolateAlpha(
|
|
787
|
+
val top = sheetTop ?: bottomSheetView?.top ?: return
|
|
788
|
+
dimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
|
|
789
|
+
parentDimView?.interpolateAlpha(top, dimmedDetentIndex, detentCalculator::getSheetTopForDetentIndex)
|
|
754
790
|
}
|
|
755
791
|
|
|
756
|
-
/** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
|
|
757
792
|
fun positionFooter(slideOffset: Float? = null) {
|
|
758
793
|
val footerView = containerView?.footerView ?: return
|
|
759
794
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -762,20 +797,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
762
797
|
val sheetHeight = bottomSheet.height
|
|
763
798
|
val sheetTop = bottomSheet.top
|
|
764
799
|
|
|
765
|
-
// Footer Y relative to sheet: place at bottom of sheet container minus footer height
|
|
766
800
|
var footerY = (sheetHeight - sheetTop - footerHeight - keyboardHeight).toFloat()
|
|
767
|
-
|
|
768
801
|
if (slideOffset != null && slideOffset < 0) {
|
|
769
802
|
footerY -= (footerHeight * slideOffset)
|
|
770
803
|
}
|
|
771
804
|
|
|
772
|
-
// Clamp to prevent footer from going above visible area
|
|
773
805
|
val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
|
|
774
806
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
775
807
|
}
|
|
776
808
|
|
|
777
809
|
fun setStateForDetentIndex(index: Int) {
|
|
778
|
-
behavior?.state = getStateForDetentIndex(index)
|
|
810
|
+
behavior?.state = detentCalculator.getStateForDetentIndex(index)
|
|
779
811
|
}
|
|
780
812
|
|
|
781
813
|
fun getDefaultBackgroundColor(): Int {
|
|
@@ -793,193 +825,65 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
793
825
|
}
|
|
794
826
|
|
|
795
827
|
// ====================================================================
|
|
796
|
-
// MARK: -
|
|
828
|
+
// MARK: - Drag Handling
|
|
797
829
|
// ====================================================================
|
|
798
830
|
|
|
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
831
|
private fun handleDragBegin(sheetView: View) {
|
|
901
|
-
val
|
|
902
|
-
val detent = getDetentValueForIndex(
|
|
903
|
-
delegate?.viewControllerDidDragBegin(
|
|
904
|
-
|
|
832
|
+
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
833
|
+
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
834
|
+
delegate?.viewControllerDidDragBegin(currentDetentIndex, position, detent)
|
|
835
|
+
interactionState = InteractionState.Dragging(
|
|
836
|
+
startTop = sheetView.top,
|
|
837
|
+
startKeyboardHeight = keyboardHeight
|
|
838
|
+
)
|
|
905
839
|
}
|
|
906
840
|
|
|
907
841
|
private fun handleDragChange(sheetView: View) {
|
|
908
|
-
|
|
909
|
-
val
|
|
910
|
-
val detent = getDetentValueForIndex(
|
|
911
|
-
delegate?.viewControllerDidDragChange(
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
} else {
|
|
925
|
-
if (detent <= 0.0 || detent > 1.0) {
|
|
926
|
-
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
842
|
+
val draggingState = interactionState as? InteractionState.Dragging ?: return
|
|
843
|
+
val position = detentCalculator.getPositionDp(detentCalculator.getVisibleSheetHeight(sheetView.top))
|
|
844
|
+
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
845
|
+
delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
|
|
846
|
+
|
|
847
|
+
// Dismiss keyboard if dragged below original position (without keyboard)
|
|
848
|
+
if (draggingState.startKeyboardHeight > 0) {
|
|
849
|
+
val detentTopWithoutKeyboard = getExpectedSheetTop(currentDetentIndex) + draggingState.startKeyboardHeight
|
|
850
|
+
val shouldDismiss = sheetView.top >= detentTopWithoutKeyboard
|
|
851
|
+
|
|
852
|
+
if (shouldDismiss != draggingState.shouldDismissKeyboard) {
|
|
853
|
+
android.util.Log.d(
|
|
854
|
+
TAG_NAME,
|
|
855
|
+
"shouldDismissKeyboard changed to: $shouldDismiss (currentTop: ${sheetView.top}, detentTop: $detentTopWithoutKeyboard)"
|
|
856
|
+
)
|
|
857
|
+
interactionState = draggingState.copy(shouldDismissKeyboard = shouldDismiss)
|
|
927
858
|
}
|
|
928
|
-
(detent * screenHeight).toInt() + contentBottomInset + keyboardHeight
|
|
929
859
|
}
|
|
930
|
-
|
|
931
|
-
val maxAllowedHeight = screenHeight + contentBottomInset
|
|
932
|
-
return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
933
860
|
}
|
|
934
861
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
|
|
939
|
-
}
|
|
862
|
+
// ====================================================================
|
|
863
|
+
// MARK: - Detent Helpers
|
|
864
|
+
// ====================================================================
|
|
940
865
|
|
|
941
|
-
/** Maps BottomSheetBehavior state to DetentInfo based on detent count. */
|
|
942
866
|
fun getDetentInfoForState(state: Int): DetentInfo? {
|
|
943
|
-
val
|
|
944
|
-
val index = stateMap[state] ?: return null
|
|
867
|
+
val index = detentCalculator.getDetentIndexForState(state) ?: return null
|
|
945
868
|
return DetentInfo(index, getPositionForDetentIndex(index))
|
|
946
869
|
}
|
|
947
870
|
|
|
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
871
|
private fun getPositionForDetentIndex(index: Int): Float {
|
|
971
872
|
if (index < 0 || index >= detents.size) return screenHeight.pxToDp()
|
|
972
873
|
|
|
973
874
|
bottomSheetView?.let {
|
|
974
|
-
val visibleSheetHeight = getVisibleSheetHeight(it)
|
|
975
|
-
if (visibleSheetHeight > 0
|
|
875
|
+
val visibleSheetHeight = detentCalculator.getVisibleSheetHeight(it.top)
|
|
876
|
+
if (visibleSheetHeight > 0 && visibleSheetHeight < realScreenHeight) {
|
|
877
|
+
return detentCalculator.getPositionDp(visibleSheetHeight)
|
|
878
|
+
}
|
|
976
879
|
}
|
|
977
880
|
|
|
978
|
-
val detentHeight = getDetentHeight(detents[index])
|
|
979
|
-
return getPositionDp(detentHeight)
|
|
881
|
+
val detentHeight = detentCalculator.getDetentHeight(detents[index])
|
|
882
|
+
return detentCalculator.getPositionDp(detentHeight)
|
|
980
883
|
}
|
|
981
884
|
|
|
982
|
-
fun getDetentInfoForIndex(index: Int)
|
|
885
|
+
fun getDetentInfoForIndex(index: Int): DetentInfo =
|
|
886
|
+
getDetentInfoForState(detentCalculator.getStateForDetentIndex(index)) ?: DetentInfo(0, 0f)
|
|
983
887
|
|
|
984
888
|
// ====================================================================
|
|
985
889
|
// MARK: - RootView Implementation
|
|
@@ -995,11 +899,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
995
899
|
|
|
996
900
|
if (w == oldw && h == oldh) return
|
|
997
901
|
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
|
-
}
|
|
902
|
+
if (h + topInset >= screenHeight && isExpanded && oldw == w) return
|
|
1003
903
|
|
|
1004
904
|
this.post {
|
|
1005
905
|
setupSheetDetents()
|
|
@@ -1016,7 +916,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1016
916
|
// MARK: - Touch Event Handling
|
|
1017
917
|
// ====================================================================
|
|
1018
918
|
|
|
1019
|
-
/** Forwards touch events to footer which is positioned outside normal hierarchy. */
|
|
1020
919
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1021
920
|
val footer = containerView?.footerView
|
|
1022
921
|
if (footer != null && footer.isVisible) {
|