@lodev09/react-native-true-sheet 4.0.0-beta.3 → 4.0.0-beta.30
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/README.md +28 -5
- package/android/build.gradle +23 -1
- package/android/proguard-rules.pro +3 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +303 -184
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +38 -109
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +127 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +29 -3
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +234 -66
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +491 -186
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +64 -10
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +135 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +23 -23
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +130 -6
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +14 -17
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +148 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +33 -40
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
- package/android/src/main/jni/TrueSheetSpec.h +1 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +37 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +114 -30
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +14 -3
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +5 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +10 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
- package/ios/TrueSheetContainerView.h +23 -7
- package/ios/TrueSheetContainerView.mm +45 -25
- package/ios/TrueSheetContentView.h +44 -15
- package/ios/TrueSheetContentView.mm +221 -190
- package/ios/TrueSheetFooterView.mm +133 -28
- package/ios/TrueSheetModule.h +2 -7
- package/ios/TrueSheetModule.mm +18 -18
- package/ios/TrueSheetOverlayView.h +42 -0
- package/ios/TrueSheetOverlayView.mm +175 -0
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +268 -52
- package/ios/TrueSheetViewController.h +36 -3
- package/ios/TrueSheetViewController.mm +797 -102
- package/ios/core/RNScreensEventObserver.mm +31 -28
- package/ios/core/TrueSheetBlurView.h +0 -2
- package/ios/core/TrueSheetBlurView.mm +3 -36
- package/ios/core/TrueSheetDetentCalculator.h +18 -1
- package/ios/core/TrueSheetDetentCalculator.mm +44 -9
- package/ios/core/TrueSheetDimView.h +27 -0
- package/ios/core/TrueSheetDimView.mm +41 -0
- package/ios/core/TrueSheetGrabberView.mm +13 -3
- package/ios/core/TrueSheetKeyboardObserver.h +7 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +107 -15
- package/ios/core/TrueSheetOverlayContainerView.h +31 -0
- package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
- package/ios/events/TrueSheetLifecycleEvents.h +2 -0
- package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
- package/ios/tvos/TrueSheetStubs.mm +12 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
- package/lib/module/TrueSheet.js +222 -54
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +364 -275
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetBackground.js +15 -0
- package/lib/module/TrueSheetBackground.js.map +1 -0
- package/lib/module/TrueSheetOverlay.js +30 -0
- package/lib/module/TrueSheetOverlay.js.map +1 -0
- package/lib/module/TrueSheetOverlay.web.js +35 -0
- package/lib/module/TrueSheetOverlay.web.js.map +1 -0
- package/lib/module/TrueSheetPeek.web.js +20 -15
- package/lib/module/TrueSheetPeek.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +13 -9
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/detentBackgrounds.js +21 -0
- package/lib/module/detentBackgrounds.js.map +1 -0
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
- package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +89 -73
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/mocks/index.js +18 -2
- package/lib/module/mocks/index.js.map +1 -1
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
- package/lib/module/navigation/base-types.js +2 -0
- package/lib/module/navigation/base-types.js.map +1 -0
- package/lib/module/navigation/createTrueSheetRouter.js +158 -100
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
- package/lib/module/navigation/expo-router/base-types.js +2 -0
- package/lib/module/navigation/expo-router/base-types.js.map +1 -0
- package/lib/module/navigation/expo-router/extendTrueSheetRouter.js +71 -0
- package/lib/module/navigation/expo-router/extendTrueSheetRouter.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +18 -7
- package/lib/module/navigation/expo-router/index.js.map +1 -1
- package/lib/module/navigation/expo-router/types.js +4 -0
- package/lib/module/navigation/expo-router/types.js.map +1 -0
- package/lib/module/navigation/index.js.map +1 -1
- package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
- package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
- package/lib/module/scrollable.js +28 -0
- package/lib/module/scrollable.js.map +1 -0
- package/lib/module/web/constants.js +1 -1
- package/lib/module/web/constants.js.map +1 -1
- package/lib/module/web/keyboard.js +69 -0
- package/lib/module/web/keyboard.js.map +1 -0
- package/lib/module/web/layout.js +168 -0
- package/lib/module/web/layout.js.map +1 -0
- package/lib/module/web/vaul/constants.js +9 -0
- package/lib/module/web/vaul/constants.js.map +1 -1
- package/lib/module/web/vaul/context.js +4 -0
- package/lib/module/web/vaul/context.js.map +1 -1
- package/lib/module/web/vaul/helpers.js +1 -0
- package/lib/module/web/vaul/helpers.js.map +1 -1
- package/lib/module/web/vaul/index.js +83 -21
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +57 -28
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +18 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +209 -35
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
- package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
- package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
- package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
- package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -3
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +76 -65
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +4 -0
- package/lib/typescript/src/mocks/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/base-types.d.ts +9 -0
- package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +36 -10
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts +33 -0
- package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +9 -6
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
- package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +3 -4
- package/lib/typescript/src/navigation/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +43 -6
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/scrollable.d.ts +2 -0
- package/lib/typescript/src/scrollable.d.ts.map +1 -0
- package/lib/typescript/src/web/constants.d.ts +1 -1
- package/lib/typescript/src/web/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/keyboard.d.ts +15 -0
- package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
- package/lib/typescript/src/web/layout.d.ts +20 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts +3 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/context.d.ts +5 -1
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +20 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/types.d.ts +9 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +6 -4
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +18 -8
- package/react-native.config.js +1 -0
- package/src/TrueSheet.tsx +257 -62
- package/src/TrueSheet.types.ts +225 -35
- package/src/TrueSheet.web.tsx +445 -333
- package/src/TrueSheetBackground.tsx +17 -0
- package/src/TrueSheetOverlay.tsx +24 -0
- package/src/TrueSheetOverlay.web.tsx +33 -0
- package/src/TrueSheetPeek.web.tsx +25 -23
- package/src/TrueSheetProvider.web.tsx +22 -13
- package/src/detentBackgrounds.ts +24 -0
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
- package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +89 -73
- package/src/index.ts +1 -0
- package/src/mocks/index.ts +17 -2
- package/src/mocks/navigationExpoRouter.ts +6 -5
- package/src/navigation/base-types.ts +20 -0
- package/src/navigation/createTrueSheetRouter.ts +220 -144
- package/src/navigation/expo-router/base-types.ts +18 -0
- package/src/navigation/expo-router/extendTrueSheetRouter.ts +99 -0
- package/src/navigation/expo-router/index.ts +41 -30
- package/src/navigation/expo-router/types.ts +116 -0
- package/src/navigation/index.ts +6 -7
- package/src/navigation/screen/useSheetScreenState.ts +2 -0
- package/src/navigation/types.ts +81 -23
- package/src/scrollable.ts +25 -0
- package/src/web/constants.ts +1 -1
- package/src/web/keyboard.ts +79 -0
- package/src/web/layout.ts +196 -0
- package/src/web/vaul/constants.ts +9 -0
- package/src/web/vaul/context.ts +7 -1
- package/src/web/vaul/helpers.ts +5 -0
- package/src/web/vaul/index.tsx +113 -16
- package/src/web/vaul/types.ts +10 -0
- package/src/web/vaul/use-snap-points.ts +86 -33
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
|
+
import android.animation.ValueAnimator
|
|
3
4
|
import android.annotation.SuppressLint
|
|
4
5
|
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
5
7
|
import android.os.Build
|
|
6
8
|
import android.view.MotionEvent
|
|
7
9
|
import android.view.View
|
|
@@ -10,6 +12,7 @@ import android.view.accessibility.AccessibilityEvent
|
|
|
10
12
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
11
13
|
import android.widget.ImageView
|
|
12
14
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
15
|
+
import androidx.core.graphics.ColorUtils
|
|
13
16
|
import androidx.core.graphics.createBitmap
|
|
14
17
|
import androidx.core.view.isNotEmpty
|
|
15
18
|
import com.facebook.react.R
|
|
@@ -34,12 +37,15 @@ import com.lodev09.truesheet.core.TrueSheetDetentCalculator
|
|
|
34
37
|
import com.lodev09.truesheet.core.TrueSheetDetentCalculatorDelegate
|
|
35
38
|
import com.lodev09.truesheet.core.TrueSheetDimView
|
|
36
39
|
import com.lodev09.truesheet.core.TrueSheetDimViewDelegate
|
|
40
|
+
import com.lodev09.truesheet.core.TrueSheetDismissThreshold
|
|
37
41
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
38
42
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
39
43
|
import com.lodev09.truesheet.core.TrueSheetStackManager
|
|
44
|
+
import com.lodev09.truesheet.utils.Insets
|
|
40
45
|
import com.lodev09.truesheet.utils.KeyboardUtils
|
|
41
46
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
42
47
|
import com.lodev09.truesheet.utils.TouchEventDeduper
|
|
48
|
+
import kotlin.math.roundToInt
|
|
43
49
|
|
|
44
50
|
// =============================================================================
|
|
45
51
|
// MARK: - Data Types & Delegate Protocol
|
|
@@ -54,6 +60,7 @@ interface TrueSheetViewControllerDelegate {
|
|
|
54
60
|
fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
|
|
55
61
|
fun viewControllerWillDismiss()
|
|
56
62
|
fun viewControllerDidDismiss(parent: TrueSheetView?)
|
|
63
|
+
fun viewControllerDidAttemptDismiss()
|
|
57
64
|
fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float)
|
|
58
65
|
fun viewControllerDidDragBegin(index: Int, position: Float, detent: Float)
|
|
59
66
|
fun viewControllerDidDragChange(index: Int, position: Float, detent: Float)
|
|
@@ -78,17 +85,19 @@ interface TrueSheetViewControllerDelegate {
|
|
|
78
85
|
* enabling touch pass-through to underlying views. Handles detent configuration, drag interactions,
|
|
79
86
|
* keyboard avoidance, dimmed backgrounds, back button, and lifecycle events for stacked sheets.
|
|
80
87
|
*/
|
|
81
|
-
enum class
|
|
82
|
-
|
|
88
|
+
enum class TrueSheetPlacement {
|
|
89
|
+
AUTOMATIC,
|
|
90
|
+
LEADING,
|
|
83
91
|
CENTER,
|
|
84
|
-
|
|
92
|
+
TRAILING;
|
|
85
93
|
|
|
86
94
|
companion object {
|
|
87
|
-
fun fromString(value: String?):
|
|
95
|
+
fun fromString(value: String?): TrueSheetPlacement =
|
|
88
96
|
when (value) {
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
|
|
97
|
+
"leading" -> LEADING
|
|
98
|
+
"center" -> CENTER
|
|
99
|
+
"trailing" -> TRAILING
|
|
100
|
+
else -> AUTOMATIC
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
}
|
|
@@ -116,12 +125,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
116
125
|
TrueSheetBottomSheetViewDelegate {
|
|
117
126
|
|
|
118
127
|
companion object {
|
|
128
|
+
const val DEFAULT_DIM_OPACITY = 0.5f
|
|
119
129
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
120
130
|
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
121
|
-
private const val
|
|
131
|
+
private const val DEFAULT_PLACEMENT_OFFSET = 16 // dp
|
|
122
132
|
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
123
133
|
private const val DISMISS_DURATION = 200L
|
|
124
134
|
private const val SCREEN_FADE_DURATION = 150L
|
|
135
|
+
|
|
136
|
+
// Mirrors BottomSheetBehavior.HIDE_THRESHOLD — how far past the lowest stop,
|
|
137
|
+
// as a fraction of its height, a release hides a dismissible sheet
|
|
138
|
+
private const val HIDE_THRESHOLD = 0.5f
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
// =============================================================================
|
|
@@ -161,7 +175,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
161
175
|
// animation doesn't snap the sheet back to the stale currentDetentIndex.
|
|
162
176
|
private var pendingDetentIndex: Int = -1
|
|
163
177
|
|
|
178
|
+
private val isSettlingInPlace: Boolean
|
|
179
|
+
get() = !isBeingDismissed && pendingDetentIndex >= 0 && pendingDetentIndex == currentDetentIndex
|
|
180
|
+
|
|
164
181
|
private var interactionState: InteractionState = InteractionState.Idle
|
|
182
|
+
|
|
183
|
+
// A content/header/footer size change arrived mid-drag — reconfiguring then
|
|
184
|
+
// would reset behavior state and kill the gesture, so it's applied on settle.
|
|
185
|
+
private var hasPendingSizeChange = false
|
|
165
186
|
internal var isBeingDismissed = false
|
|
166
187
|
private set
|
|
167
188
|
var wasHiddenByScreen = false
|
|
@@ -175,6 +196,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
175
196
|
// Keyboard State
|
|
176
197
|
private var detentIndexBeforeKeyboard: Int = -1
|
|
177
198
|
private var isKeyboardDismissProgrammatic = false
|
|
199
|
+
|
|
200
|
+
// The keyboard hid under a touch (e.g. keyboardDismissMode="on-drag") — the
|
|
201
|
+
// keyboard stops stay until it lifts. Settling or moving the stops under a
|
|
202
|
+
// scrolling finger makes the behavior's nested pre-scroll snap the sheet and
|
|
203
|
+
// jump the content.
|
|
204
|
+
private var isKeyboardSettleDeferred = false
|
|
205
|
+
|
|
206
|
+
// Sheet top when a keyboard-floor drag was released; -1 once the settle target is known
|
|
207
|
+
private var keyboardSettleStartTop = -1
|
|
178
208
|
private var focusedViewBeforeBlur: View? = null
|
|
179
209
|
|
|
180
210
|
// Promises
|
|
@@ -194,12 +224,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
194
224
|
// Touch Dispatchers
|
|
195
225
|
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
196
226
|
private val jsPointerDispatcher = JSPointerDispatcher(this)
|
|
197
|
-
|
|
198
|
-
// True when the active touch stream began inside the footer. Latched on
|
|
199
|
-
// ACTION_DOWN so subsequent MOVE events keep routing to the footer even if
|
|
200
|
-
// the finger drifts outside the footer's rect mid-gesture.
|
|
201
|
-
private var footerOwnsTouchStream = false
|
|
202
|
-
|
|
203
227
|
private val touchDeduper = TouchEventDeduper()
|
|
204
228
|
|
|
205
229
|
private val eventDispatcher
|
|
@@ -208,17 +232,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
208
232
|
// Detent Configuration
|
|
209
233
|
override var maxContentHeight: Int? = null
|
|
210
234
|
override var maxContentWidth: Int? = null
|
|
211
|
-
override var
|
|
212
|
-
override var
|
|
235
|
+
override var placement: TrueSheetPlacement = TrueSheetPlacement.AUTOMATIC
|
|
236
|
+
override var placementOffset: Int = DEFAULT_PLACEMENT_OFFSET.dpToPx().toInt()
|
|
213
237
|
override var detents: MutableList<Double> = mutableListOf(0.5, 1.0)
|
|
214
238
|
|
|
215
239
|
// Appearance Configuration
|
|
216
240
|
var dimmed = true
|
|
217
241
|
var dimmedDetentIndex = 0
|
|
242
|
+
var dimColor: Int = Color.BLACK
|
|
243
|
+
var dimOpacity: Float = DEFAULT_DIM_OPACITY
|
|
244
|
+
private var dimProgress = 0f
|
|
218
245
|
override var grabber: Boolean = true
|
|
219
246
|
override var grabberOptions: GrabberOptions? = null
|
|
220
247
|
override var accessibilityOptions: AccessibilityOptions? = null
|
|
248
|
+
var backgroundColor: Int? = null
|
|
249
|
+
var detentBackgrounds: List<Int?> = emptyList()
|
|
250
|
+
private var displayedBackgroundIndex = 0
|
|
251
|
+
private var backgroundAnimator: ValueAnimator? = null
|
|
252
|
+
private var targetBackgroundColor: Int? = null
|
|
221
253
|
override var sheetBackgroundColor: Int? = null
|
|
254
|
+
private set
|
|
222
255
|
var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
|
|
223
256
|
|
|
224
257
|
var scrollableOptions: ScrollableOptions? = null
|
|
@@ -227,6 +260,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
227
260
|
behavior?.scrollingExpandsSheet = value?.scrollingExpandsSheet ?: true
|
|
228
261
|
}
|
|
229
262
|
|
|
263
|
+
// React tag of the user-provided scrollable within the content (see the scrollableRef prop)
|
|
264
|
+
var scrollableHandle: Int = -1
|
|
265
|
+
|
|
230
266
|
override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
231
267
|
set(value) {
|
|
232
268
|
field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
|
|
@@ -252,6 +288,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
252
288
|
if (isPresented) sheetView?.setupGrabber()
|
|
253
289
|
}
|
|
254
290
|
|
|
291
|
+
var dismissThreshold: TrueSheetDismissThreshold = TrueSheetDismissThreshold.HALF
|
|
292
|
+
set(value) {
|
|
293
|
+
field = value
|
|
294
|
+
behavior?.dismissThreshold = value
|
|
295
|
+
}
|
|
296
|
+
|
|
255
297
|
// =============================================================================
|
|
256
298
|
// MARK: - Computed Properties
|
|
257
299
|
// =============================================================================
|
|
@@ -270,9 +312,29 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
270
312
|
val screenWidth: Int
|
|
271
313
|
get() = ScreenUtils.getScreenWidth(reactContext)
|
|
272
314
|
|
|
315
|
+
// Cached per layout/configuration pass — the WindowManager and
|
|
316
|
+
// rootWindowInsets queries behind these allocate, and they're read many
|
|
317
|
+
// times per animation frame (position emission, detent interpolation,
|
|
318
|
+
// stacked sheet sync)
|
|
319
|
+
private var cachedRealScreenHeight: Int = 0
|
|
320
|
+
private var cachedInsets: Insets? = null
|
|
321
|
+
|
|
322
|
+
private fun invalidateScreenMetrics() {
|
|
323
|
+
cachedRealScreenHeight = 0
|
|
324
|
+
cachedInsets = null
|
|
325
|
+
}
|
|
326
|
+
|
|
273
327
|
// Includes system bars for accurate positioning
|
|
274
328
|
override val realScreenHeight: Int
|
|
275
|
-
get()
|
|
329
|
+
get() {
|
|
330
|
+
if (cachedRealScreenHeight == 0) {
|
|
331
|
+
cachedRealScreenHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
332
|
+
}
|
|
333
|
+
return cachedRealScreenHeight
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private val insets: Insets
|
|
337
|
+
get() = cachedInsets ?: ScreenUtils.getInsets(reactContext).also { cachedInsets = it }
|
|
276
338
|
|
|
277
339
|
// Content Measurements
|
|
278
340
|
// Cached values used during dismiss when container is unmounted
|
|
@@ -294,6 +356,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
294
356
|
|
|
295
357
|
override var absoluteFooter: Boolean = false
|
|
296
358
|
|
|
359
|
+
override val footerInsetAdjustment: Boolean
|
|
360
|
+
get() = absoluteFooter && scrollableHandle > 0 && scrollableOptions?.contentInsetAdjustment?.footer != false
|
|
361
|
+
|
|
297
362
|
override val peekContentHeight: Int
|
|
298
363
|
get() = containerView?.peekContentHeight ?: cachedPeekContentHeight
|
|
299
364
|
|
|
@@ -309,16 +374,37 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
309
374
|
private val isKeyboardTransitioning: Boolean
|
|
310
375
|
get() = keyboardObserver?.isTransitioning ?: false
|
|
311
376
|
|
|
377
|
+
// The footer slides every frame until the sheet and keyboard come to rest
|
|
378
|
+
private val isSheetMoving: Boolean
|
|
379
|
+
get() = isPresentAnimating ||
|
|
380
|
+
isKeyboardTransitioning ||
|
|
381
|
+
behavior?.state == BottomSheetBehavior.STATE_DRAGGING ||
|
|
382
|
+
behavior?.state == BottomSheetBehavior.STATE_SETTLING
|
|
383
|
+
|
|
384
|
+
// Keyboard is up or animating — detents are (or are about to be) keyboard-shifted
|
|
385
|
+
val isKeyboardActive: Boolean
|
|
386
|
+
get() = keyboardInset > 0 || isKeyboardTransitioning
|
|
387
|
+
|
|
388
|
+
// While the keyboard holds the sheet up, the keyboard-free first detent is the
|
|
389
|
+
// collapsed stop — landing there dismisses the keyboard, and (when dismissible)
|
|
390
|
+
// dragging more than half its height below it hides the sheet too, like iOS.
|
|
391
|
+
override val hasKeyboardFloor: Boolean
|
|
392
|
+
get() = keyboardInset > 0
|
|
393
|
+
|
|
394
|
+
// The detent JS last heard about — the keyboard expansion is never reported
|
|
395
|
+
private val knownDetentIndex: Int
|
|
396
|
+
get() = if (detentIndexBeforeKeyboard >= 0) detentIndexBeforeKeyboard else currentDetentIndex
|
|
397
|
+
|
|
312
398
|
fun isFocusedViewWithinSheet(): Boolean {
|
|
313
399
|
val sheet = sheetView ?: return false
|
|
314
400
|
return keyboardObserver?.isFocusedViewWithinSheet(sheet) ?: false
|
|
315
401
|
}
|
|
316
402
|
|
|
317
403
|
val bottomInset: Int
|
|
318
|
-
get() = if (edgeToEdgeEnabled)
|
|
404
|
+
get() = if (edgeToEdgeEnabled) insets.bottom else 0
|
|
319
405
|
|
|
320
406
|
override val topInset: Int
|
|
321
|
-
get() = if (edgeToEdgeEnabled)
|
|
407
|
+
get() = if (edgeToEdgeEnabled) insets.top else 0
|
|
322
408
|
|
|
323
409
|
override val contentBottomInset: Int
|
|
324
410
|
get() = if (insetAdjustment == TrueSheetInsetAdjustment.AUTOMATIC) bottomInset else 0
|
|
@@ -331,6 +417,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
331
417
|
}
|
|
332
418
|
|
|
333
419
|
// Sheet State
|
|
420
|
+
val isSettling: Boolean
|
|
421
|
+
get() = behavior?.state == BottomSheetBehavior.STATE_SETTLING
|
|
422
|
+
|
|
334
423
|
val isExpanded: Boolean
|
|
335
424
|
get() {
|
|
336
425
|
val sheetTop = sheetView?.top ?: return false
|
|
@@ -373,6 +462,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
373
462
|
|
|
374
463
|
private fun cleanupSheet() {
|
|
375
464
|
cleanupKeyboardObserver()
|
|
465
|
+
backgroundAnimator?.cancel()
|
|
466
|
+
backgroundAnimator = null
|
|
467
|
+
targetBackgroundColor = null
|
|
376
468
|
sheetView?.animate()?.cancel()
|
|
377
469
|
|
|
378
470
|
// Cleanup dim views
|
|
@@ -392,6 +484,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
392
484
|
sheetView = null
|
|
393
485
|
|
|
394
486
|
interactionState = InteractionState.Idle
|
|
487
|
+
hasPendingSizeChange = false
|
|
395
488
|
isBeingDismissed = false
|
|
396
489
|
isPresented = false
|
|
397
490
|
isSheetVisible = false
|
|
@@ -403,6 +496,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
403
496
|
detentIndexBeforeKeyboard = -1
|
|
404
497
|
pendingDetentIndex = -1
|
|
405
498
|
isKeyboardDismissProgrammatic = false
|
|
499
|
+
isKeyboardSettleDeferred = false
|
|
406
500
|
focusedViewBeforeBlur = null
|
|
407
501
|
shouldAnimatePresent = true
|
|
408
502
|
}
|
|
@@ -440,13 +534,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
440
534
|
// =============================================================================
|
|
441
535
|
|
|
442
536
|
override fun coordinatorLayoutDidLayout(changed: Boolean) {
|
|
443
|
-
|
|
537
|
+
if (changed) invalidateScreenMetrics()
|
|
538
|
+
|
|
444
539
|
if (isPresented && changed) {
|
|
445
|
-
|
|
540
|
+
setupSheetDetentsForSizeChange()
|
|
446
541
|
}
|
|
447
542
|
}
|
|
448
543
|
|
|
449
544
|
override fun coordinatorLayoutDidChangeConfiguration() {
|
|
545
|
+
invalidateScreenMetrics()
|
|
450
546
|
if (!isPresented) return
|
|
451
547
|
|
|
452
548
|
sheetView?.updateGravity()
|
|
@@ -458,6 +554,34 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
458
554
|
override fun findScrollView(): ViewGroup? = containerView?.contentView?.findScrollView()
|
|
459
555
|
override fun findSheetView(): TrueSheetBottomSheetView? = sheetView
|
|
460
556
|
|
|
557
|
+
override fun coordinatorLayoutDidPullDown(overdragPx: Int, keyboardWasVisible: Boolean) {
|
|
558
|
+
if (!isPresented || dismissible || !draggable) return
|
|
559
|
+
val sheet = sheetView ?: return
|
|
560
|
+
|
|
561
|
+
// Only a pull clamped at the lowest stop is a blocked drag-to-dismiss — a
|
|
562
|
+
// child that swallowed the gesture higher up isn't
|
|
563
|
+
val lowestTop = detentCalculator.getLowestSheetTop()
|
|
564
|
+
if (sheet.top < lowestTop) return
|
|
565
|
+
|
|
566
|
+
if (keyboardWasVisible) {
|
|
567
|
+
// A sheet that couldn't move at all (first detent already full height) never
|
|
568
|
+
// began a drag, so the keyboard is still up — dismiss it here
|
|
569
|
+
if (hasKeyboardFloor && !isKeyboardDismissProgrammatic) commitDetentAndDismissKeyboard(0, sheet)
|
|
570
|
+
|
|
571
|
+
// Past the floor by the distance that would hide a dismissible sheet, like iOS
|
|
572
|
+
val floorHeight = realScreenHeight - lowestTop
|
|
573
|
+
if (overdragPx < floorHeight * HIDE_THRESHOLD) return
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
delegate?.viewControllerDidAttemptDismiss()
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
override fun coordinatorLayoutDidEndTouch() {
|
|
580
|
+
if (!isKeyboardSettleDeferred) return
|
|
581
|
+
isKeyboardSettleDeferred = false
|
|
582
|
+
setupSheetDetentsForSizeChange()
|
|
583
|
+
}
|
|
584
|
+
|
|
461
585
|
// =============================================================================
|
|
462
586
|
// MARK: - TrueSheetDimViewDelegate
|
|
463
587
|
// =============================================================================
|
|
@@ -495,24 +619,30 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
495
619
|
|
|
496
620
|
override fun bottomSheetViewDidTapGrabber() {
|
|
497
621
|
val nextIndex = (currentDetentIndex + 1) % detents.size
|
|
498
|
-
if (nextIndex == 0 && detents.size == 1
|
|
499
|
-
|
|
622
|
+
if (nextIndex == 0 && detents.size == 1) {
|
|
623
|
+
if (dismissible) {
|
|
624
|
+
dismiss(animated = true)
|
|
625
|
+
} else {
|
|
626
|
+
delegate?.viewControllerDidAttemptDismiss()
|
|
627
|
+
}
|
|
500
628
|
} else {
|
|
501
|
-
|
|
629
|
+
resize(nextIndex)
|
|
502
630
|
}
|
|
503
631
|
}
|
|
504
632
|
|
|
505
633
|
override fun bottomSheetViewDidAccessibilityIncrement() {
|
|
506
634
|
if (currentDetentIndex < detents.size - 1) {
|
|
507
|
-
|
|
635
|
+
resize(currentDetentIndex + 1)
|
|
508
636
|
}
|
|
509
637
|
}
|
|
510
638
|
|
|
511
639
|
override fun bottomSheetViewDidAccessibilityDecrement() {
|
|
512
640
|
if (currentDetentIndex > 0) {
|
|
513
|
-
|
|
641
|
+
resize(currentDetentIndex - 1)
|
|
514
642
|
} else if (dismissible) {
|
|
515
643
|
dismiss(animated = true)
|
|
644
|
+
} else {
|
|
645
|
+
delegate?.viewControllerDidAttemptDismiss()
|
|
516
646
|
}
|
|
517
647
|
}
|
|
518
648
|
|
|
@@ -526,7 +656,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
526
656
|
}
|
|
527
657
|
|
|
528
658
|
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
529
|
-
handleSlide(sheetView
|
|
659
|
+
handleSlide(sheetView)
|
|
530
660
|
}
|
|
531
661
|
}
|
|
532
662
|
|
|
@@ -540,7 +670,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
540
670
|
return
|
|
541
671
|
}
|
|
542
672
|
|
|
543
|
-
|
|
673
|
+
// The slide-out owns the sheet now — a present settle landing mid-dismiss must not finish the present
|
|
674
|
+
if (!isPresented || isBeingDismissed) return
|
|
544
675
|
|
|
545
676
|
when (newState) {
|
|
546
677
|
BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
|
|
@@ -555,27 +686,44 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
555
686
|
}
|
|
556
687
|
}
|
|
557
688
|
|
|
558
|
-
private fun handleSlide(sheetView: View
|
|
689
|
+
private fun handleSlide(sheetView: View) {
|
|
559
690
|
// Skip during dismiss animation
|
|
560
691
|
if (isBeingDismissed) return
|
|
561
692
|
|
|
562
693
|
val behavior = behavior ?: return
|
|
563
694
|
|
|
695
|
+
if (interactionState is InteractionState.Dragging || pendingDetentIndex >= 0) {
|
|
696
|
+
updateStateDimensionsForPosition(sheetView.top)
|
|
697
|
+
}
|
|
698
|
+
|
|
564
699
|
when (behavior.state) {
|
|
565
|
-
BottomSheetBehavior.STATE_DRAGGING
|
|
566
|
-
|
|
700
|
+
BottomSheetBehavior.STATE_DRAGGING -> handleDragChange(sheetView)
|
|
701
|
+
|
|
702
|
+
BottomSheetBehavior.STATE_SETTLING -> {
|
|
703
|
+
handleDragChange(sheetView)
|
|
704
|
+
dismissKeyboardForSettleTarget(sheetView)
|
|
705
|
+
}
|
|
567
706
|
|
|
568
707
|
else -> { }
|
|
569
708
|
}
|
|
570
709
|
|
|
571
710
|
emitChangePositionDelegate(sheetView.top)
|
|
572
711
|
|
|
712
|
+
// Drive the parent's stack translation from our actual frame position —
|
|
713
|
+
// an independently-timed animation lags behind (e.g. auto detent shrink)
|
|
714
|
+
// and reveals the parent above this sheet's top edge. Skipped while
|
|
715
|
+
// dragging so the parent stays put during drag-to-dismiss, and while the
|
|
716
|
+
// keyboard is active so growing for it doesn't move the parent (like iOS).
|
|
717
|
+
if (!isPresentAnimating && interactionState !is InteractionState.Dragging && !isKeyboardActive) {
|
|
718
|
+
syncParentTranslation(sheetView.top)
|
|
719
|
+
}
|
|
720
|
+
|
|
573
721
|
// Position on every slide frame — during keyboard transitions the IME
|
|
574
722
|
// animation callbacks are sparser than the sheet's settle frames, and the
|
|
575
723
|
// footer rides the sheet between them, jittering against the keyboard.
|
|
576
724
|
// positionFooter derives from current state, so double-driving it with
|
|
577
725
|
// keyboardDidChangeHeight is harmless.
|
|
578
|
-
positionFooter(
|
|
726
|
+
positionFooter()
|
|
579
727
|
|
|
580
728
|
if (!isKeyboardTransitioning) {
|
|
581
729
|
updateDimAmount(sheetView.top)
|
|
@@ -583,13 +731,27 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
583
731
|
}
|
|
584
732
|
|
|
585
733
|
private fun handleStateSettled(sheetView: View, newState: Int) {
|
|
734
|
+
// The settle's last slide frame has positioned the footer
|
|
735
|
+
containerView?.footerView?.syncTranslationState()
|
|
736
|
+
|
|
586
737
|
if (interactionState is InteractionState.Reconfiguring) return
|
|
587
738
|
|
|
739
|
+
// A release already at a stop settles without a slide frame
|
|
740
|
+
keyboardSettleStartTop = -1
|
|
741
|
+
|
|
588
742
|
// Sheet has reached a stable state — any in-flight resize target is now stale,
|
|
589
743
|
// whether settled at the target, interrupted by drag, or superseded.
|
|
590
744
|
pendingDetentIndex = -1
|
|
591
745
|
|
|
592
|
-
val index = detentCalculator.getDetentIndexForState(newState) ?:
|
|
746
|
+
val index = detentCalculator.getDetentIndexForState(newState) ?: run {
|
|
747
|
+
// Unmapped settle — end any drag anyway so deferred size changes aren't
|
|
748
|
+
// dropped forever (a leaked Dragging state skips every reconfigure)
|
|
749
|
+
if (interactionState is InteractionState.Dragging) {
|
|
750
|
+
interactionState = InteractionState.Idle
|
|
751
|
+
}
|
|
752
|
+
flushPendingSizeChange()
|
|
753
|
+
return
|
|
754
|
+
}
|
|
593
755
|
val position = getPositionDpForView(sheetView)
|
|
594
756
|
val detentInfo = DetentInfo(index, position)
|
|
595
757
|
|
|
@@ -597,21 +759,30 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
597
759
|
if (isPresentAnimating) {
|
|
598
760
|
isPresentAnimating = false
|
|
599
761
|
finishPresent()
|
|
762
|
+
flushPendingSizeChange()
|
|
600
763
|
return
|
|
601
764
|
}
|
|
602
765
|
|
|
766
|
+
val wasDragging = interactionState is InteractionState.Dragging
|
|
767
|
+
|
|
603
768
|
when (interactionState) {
|
|
604
769
|
is InteractionState.Dragging -> {
|
|
605
770
|
val detent = detentCalculator.getDetentValueForIndex(detentInfo.index)
|
|
606
771
|
delegate?.viewControllerDidDragEnd(detentInfo.index, detentInfo.position, detent)
|
|
607
772
|
|
|
608
|
-
// Skip detent change if keyboard inset is still active —
|
|
609
|
-
//
|
|
610
|
-
if (
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
773
|
+
// Skip detent change if keyboard inset is still active — the keyboard floor
|
|
774
|
+
// landing below commits the detent instead.
|
|
775
|
+
if (keyboardInset == 0) {
|
|
776
|
+
val changed = detentInfo.index != knownDetentIndex
|
|
777
|
+
detentIndexBeforeKeyboard = -1
|
|
778
|
+
if (detentInfo.index != currentDetentIndex) {
|
|
779
|
+
currentDetentIndex = detentInfo.index
|
|
780
|
+
setupDimmedBackground()
|
|
781
|
+
}
|
|
782
|
+
if (changed) {
|
|
783
|
+
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
784
|
+
this@TrueSheetViewController.sheetView?.updateGrabberAccessibilityValue(detentInfo.index, detents.size)
|
|
785
|
+
}
|
|
615
786
|
}
|
|
616
787
|
|
|
617
788
|
interactionState = InteractionState.Idle
|
|
@@ -629,14 +800,28 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
629
800
|
}
|
|
630
801
|
}
|
|
631
802
|
|
|
632
|
-
|
|
633
|
-
|
|
803
|
+
if (hasKeyboardFloor && wasDragging && newState != BottomSheetBehavior.STATE_EXPANDED) {
|
|
804
|
+
// Dragged down onto a keyboard-free stop (the floor or the keyboard detent) —
|
|
805
|
+
// dismiss the keyboard and commit that detent. keyboardWillHide reconfigures
|
|
806
|
+
// the detents (and the container) to it.
|
|
807
|
+
commitDetentAndDismissKeyboard(index, sheetView)
|
|
808
|
+
} else {
|
|
809
|
+
updateStateDimensions()
|
|
810
|
+
}
|
|
634
811
|
|
|
635
812
|
if (!isKeyboardTransitioning) {
|
|
636
813
|
updateDimAmount(animated = true)
|
|
637
814
|
}
|
|
638
815
|
|
|
639
816
|
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
817
|
+
|
|
818
|
+
flushPendingSizeChange()
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private fun flushPendingSizeChange() {
|
|
822
|
+
if (!hasPendingSizeChange) return
|
|
823
|
+
hasPendingSizeChange = false
|
|
824
|
+
setupSheetDetentsForSizeChange()
|
|
640
825
|
}
|
|
641
826
|
|
|
642
827
|
// =============================================================================
|
|
@@ -698,6 +883,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
698
883
|
val sheet = this.sheetView ?: return
|
|
699
884
|
if (isPresented) return
|
|
700
885
|
|
|
886
|
+
invalidateScreenMetrics()
|
|
701
887
|
shouldAnimatePresent = animated
|
|
702
888
|
currentDetentIndex = detentIndex
|
|
703
889
|
interactionState = InteractionState.Idle
|
|
@@ -710,7 +896,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
710
896
|
setupSheetDetents()
|
|
711
897
|
setupDimmedBackground()
|
|
712
898
|
setupKeyboardObserver()
|
|
899
|
+
// Before the slide, so an input focused in onWillPresent insets the scrollable
|
|
900
|
+
containerView?.setupKeyboardHandler()
|
|
713
901
|
|
|
902
|
+
displayedBackgroundIndex = detentIndex
|
|
903
|
+
setupBackground(animated = false)
|
|
714
904
|
sheet.setupBackground()
|
|
715
905
|
sheet.setupElevation()
|
|
716
906
|
sheet.setupGrabber()
|
|
@@ -735,13 +925,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
735
925
|
fun resize(detentIndex: Int) {
|
|
736
926
|
if (!isPresented) return
|
|
737
927
|
|
|
738
|
-
// Commit the new index so keyboardWillHide restores to it instead of the stale
|
|
928
|
+
// Commit the new index so keyboardWillHide restores to it instead of the stale
|
|
929
|
+
// one; the sheet keeps avoiding the keyboard meanwhile
|
|
739
930
|
if (detentIndexBeforeKeyboard >= 0) {
|
|
740
931
|
detentIndexBeforeKeyboard = detentIndex
|
|
932
|
+
if (hasKeyboardFloor) {
|
|
933
|
+
resizePromise?.invoke()
|
|
934
|
+
resizePromise = null
|
|
935
|
+
return
|
|
936
|
+
}
|
|
741
937
|
}
|
|
742
938
|
|
|
743
939
|
pendingDetentIndex = detentIndex
|
|
744
|
-
updateStateDimensions(deferShrink = true)
|
|
745
940
|
setupDimmedBackground()
|
|
746
941
|
setStateForDetentIndex(detentIndex)
|
|
747
942
|
resizePromise?.invoke()
|
|
@@ -763,6 +958,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
763
958
|
behavior.isHideable = true
|
|
764
959
|
behavior.isDraggable = draggable
|
|
765
960
|
behavior.scrollingExpandsSheet = scrollableOptions?.scrollingExpandsSheet ?: true
|
|
961
|
+
behavior.dismissThreshold = dismissThreshold
|
|
962
|
+
behavior.lowestSheetTop = { detentCalculator.getLowestSheetTop() }
|
|
766
963
|
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
767
964
|
behavior.addBottomSheetCallback(sheetCallback)
|
|
768
965
|
|
|
@@ -807,6 +1004,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
807
1004
|
return
|
|
808
1005
|
}
|
|
809
1006
|
|
|
1007
|
+
// Bring the parent back alongside the slide-out (like iOS) instead of after it
|
|
1008
|
+
parentSheetView?.resetTranslation()
|
|
1009
|
+
|
|
810
1010
|
sheet.animate()
|
|
811
1011
|
.y(realScreenHeight.toFloat())
|
|
812
1012
|
.setDuration(DISMISS_DURATION)
|
|
@@ -817,14 +1017,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
817
1017
|
}
|
|
818
1018
|
|
|
819
1019
|
private fun finishPresent() {
|
|
1020
|
+
coordinatorLayout?.blocksTouches = false
|
|
1021
|
+
|
|
820
1022
|
// Restore isHideable to actual value after present animation
|
|
821
1023
|
behavior?.isHideable = dismissible
|
|
822
1024
|
|
|
823
|
-
containerView?.setupKeyboardHandler()
|
|
824
|
-
|
|
825
1025
|
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
826
|
-
delegate?.viewControllerDidPresent(index, position, detent)
|
|
827
1026
|
parentSheetView?.viewControllerDidBlur()
|
|
1027
|
+
delegate?.viewControllerDidPresent(index, position, detent)
|
|
828
1028
|
delegate?.viewControllerDidFocus()
|
|
829
1029
|
sheetView?.updateGrabberAccessibilityValue(index, detents.size)
|
|
830
1030
|
|
|
@@ -866,11 +1066,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
866
1066
|
|
|
867
1067
|
val maxAvailableHeight = realScreenHeight - topInset
|
|
868
1068
|
|
|
869
|
-
|
|
1069
|
+
// With a keyboard floor only the last detent stays keyboard-shifted; the
|
|
1070
|
+
// collapsed and half stops are the keyboard-free first and second detents,
|
|
1071
|
+
// so a short drag down from the keyboard-expanded sheet rests at the second
|
|
1072
|
+
// and a long one at the first, like iOS
|
|
1073
|
+
val peekHeight = minOf(detentCalculator.getDetentHeight(detents[0], includeKeyboard = !hasKeyboardFloor), maxAvailableHeight)
|
|
870
1074
|
|
|
871
1075
|
val halfExpandedDetentHeight = when (detents.size) {
|
|
872
1076
|
1 -> peekHeight
|
|
873
|
-
else -> detentCalculator.getDetentHeight(detents[1])
|
|
1077
|
+
else -> detentCalculator.getDetentHeight(detents[1], includeKeyboard = !hasKeyboardFloor)
|
|
874
1078
|
}
|
|
875
1079
|
|
|
876
1080
|
val maxDetentHeight = minOf(detentCalculator.getDetentHeight(detents.last()), maxAvailableHeight)
|
|
@@ -880,8 +1084,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
880
1084
|
|
|
881
1085
|
val expandedOffset = realScreenHeight - maxDetentHeight
|
|
882
1086
|
|
|
883
|
-
// fitToContents works better with <= 2 detents when no expanded offset
|
|
884
|
-
|
|
1087
|
+
// fitToContents works better with <= 2 detents when no expanded offset, but
|
|
1088
|
+
// it drops the half stop the keyboard floor relies on
|
|
1089
|
+
val fitToContents = !hasKeyboardFloor && detents.size < 3 && expandedOffset == 0
|
|
885
1090
|
|
|
886
1091
|
configureDetents(
|
|
887
1092
|
behavior = behavior,
|
|
@@ -892,13 +1097,25 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
892
1097
|
animate = isPresented
|
|
893
1098
|
)
|
|
894
1099
|
|
|
895
|
-
updateStateDimensions()
|
|
896
|
-
|
|
897
1100
|
if (isPresented && applyState) {
|
|
898
1101
|
// Prefer the pending target while a resize animation is in flight so a
|
|
899
1102
|
// layout-driven reconfigure doesn't revert to the stale currentDetentIndex.
|
|
900
1103
|
val targetIndex = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
1104
|
+
|
|
1105
|
+
// Track the settle like resize() — the container (and the footer pinned
|
|
1106
|
+
// to its bottom) follows the sheet's visible height instead of jumping
|
|
1107
|
+
// to the target and riding the animation
|
|
1108
|
+
pendingDetentIndex = targetIndex
|
|
1109
|
+
sheetView?.let { updateStateDimensionsForPosition(it.top) }
|
|
901
1110
|
setStateForDetentIndex(targetIndex)
|
|
1111
|
+
|
|
1112
|
+
// Already at the target — no slide frames will follow
|
|
1113
|
+
if (!isSettling) {
|
|
1114
|
+
pendingDetentIndex = -1
|
|
1115
|
+
updateStateDimensions()
|
|
1116
|
+
}
|
|
1117
|
+
} else {
|
|
1118
|
+
updateStateDimensions()
|
|
902
1119
|
}
|
|
903
1120
|
|
|
904
1121
|
interactionState = InteractionState.Idle
|
|
@@ -922,10 +1139,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
922
1139
|
}
|
|
923
1140
|
|
|
924
1141
|
fun setupSheetDetentsForSizeChange() {
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
if (interactionState is InteractionState.Dragging)
|
|
1142
|
+
// Reconfiguring while the container tracks the sheet can interrupt its
|
|
1143
|
+
// motion. Apply content size changes once the drag or resize settles, or
|
|
1144
|
+
// the touch holding a deferred keyboard settle lifts.
|
|
1145
|
+
if (interactionState is InteractionState.Dragging || isSettling || isKeyboardSettleDeferred) {
|
|
1146
|
+
hasPendingSizeChange = true
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
hasPendingSizeChange = false
|
|
929
1150
|
|
|
930
1151
|
setupSheetDetents()
|
|
931
1152
|
positionFooter()
|
|
@@ -939,6 +1160,30 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
939
1160
|
// MARK: - Dimmed Background
|
|
940
1161
|
// =============================================================================
|
|
941
1162
|
|
|
1163
|
+
fun setupBackground(animated: Boolean = true) {
|
|
1164
|
+
val sheet = sheetView ?: return
|
|
1165
|
+
val color = detentBackgrounds.getOrNull(displayedBackgroundIndex)
|
|
1166
|
+
?: backgroundColor ?: sheet.getDefaultBackgroundColor()
|
|
1167
|
+
if (animated && color == targetBackgroundColor) return
|
|
1168
|
+
|
|
1169
|
+
backgroundAnimator?.cancel()
|
|
1170
|
+
targetBackgroundColor = color
|
|
1171
|
+
val startColor = sheetBackgroundColor ?: color
|
|
1172
|
+
if (!animated || !isPresented || startColor == color) {
|
|
1173
|
+
sheetBackgroundColor = color
|
|
1174
|
+
sheet.updateBackgroundColor(color)
|
|
1175
|
+
return
|
|
1176
|
+
}
|
|
1177
|
+
backgroundAnimator = ValueAnimator.ofArgb(startColor, color).apply {
|
|
1178
|
+
duration = 200L
|
|
1179
|
+
addUpdateListener {
|
|
1180
|
+
sheetBackgroundColor = it.animatedValue as Int
|
|
1181
|
+
sheet.updateBackgroundColor(it.animatedValue as Int)
|
|
1182
|
+
}
|
|
1183
|
+
start()
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
942
1187
|
fun setupDimmedBackground() {
|
|
943
1188
|
val coordinator = this.coordinatorLayout ?: run {
|
|
944
1189
|
RNLog.e(reactContext, "TrueSheet: coordinatorLayout is null in setupDimmedBackground")
|
|
@@ -951,6 +1196,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
951
1196
|
if (dimView == null) {
|
|
952
1197
|
dimView = TrueSheetDimView(reactContext).apply {
|
|
953
1198
|
delegate = this@TrueSheetViewController
|
|
1199
|
+
setDimColor(dimColor)
|
|
954
1200
|
}
|
|
955
1201
|
}
|
|
956
1202
|
if (!parentDimVisible) {
|
|
@@ -964,6 +1210,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
964
1210
|
if (parentDimView == null) {
|
|
965
1211
|
parentDimView = TrueSheetDimView(reactContext).apply {
|
|
966
1212
|
delegate = this@TrueSheetViewController
|
|
1213
|
+
setDimColor(dimColor)
|
|
967
1214
|
}
|
|
968
1215
|
}
|
|
969
1216
|
parentDimView?.attach(parentBottomSheet, parentController.sheetCornerRadius)
|
|
@@ -977,26 +1224,57 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
977
1224
|
}
|
|
978
1225
|
|
|
979
1226
|
fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
1227
|
+
if (dimmed) {
|
|
1228
|
+
// While keyboard is active or transitioning, or settling in place after a content
|
|
1229
|
+
// resize (the detent top moves before the sheet does), use the target detent position
|
|
1230
|
+
val top = if (isKeyboardActive || isSettlingInPlace) {
|
|
1231
|
+
detentCalculator.getSheetTopForDetentIndex(currentDetentIndex)
|
|
1232
|
+
} else {
|
|
1233
|
+
val keyboardOffset = if (isBeingDismissed) 0 else currentKeyboardInset
|
|
1234
|
+
(sheetTop ?: sheetView?.top ?: return) + keyboardOffset
|
|
1235
|
+
}
|
|
990
1236
|
|
|
991
|
-
|
|
992
|
-
val targetAlpha = dimView?.calculateAlpha(
|
|
1237
|
+
dimProgress = dimView?.calculateProgress(
|
|
993
1238
|
top,
|
|
994
1239
|
dimmedDetentIndex,
|
|
995
1240
|
detentCalculator::getSheetTopForDetentIndex
|
|
996
1241
|
) ?: 0f
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1242
|
+
applyDimAmount(animated)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// The sheets below blend this sheet's dim into theirs
|
|
1246
|
+
var parent = parentSheetView?.viewController
|
|
1247
|
+
while (parent != null) {
|
|
1248
|
+
parent.applyDimAmount(animated)
|
|
1249
|
+
parent = parent.parentSheetView?.viewController
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Dims by this sheet's progress, blending in the dim of the sheets presented on top by theirs,
|
|
1255
|
+
* so everything below the topmost sheet dims with its color and opacity.
|
|
1256
|
+
*/
|
|
1257
|
+
private fun applyDimAmount(animated: Boolean) {
|
|
1258
|
+
if (!dimmed) return
|
|
1259
|
+
|
|
1260
|
+
var color = dimColor
|
|
1261
|
+
var opacity = dimOpacity
|
|
1262
|
+
(delegate as? TrueSheetView)?.let { hostView ->
|
|
1263
|
+
TrueSheetStackManager.getSheetsAbove(hostView).asReversed().forEach {
|
|
1264
|
+
val child = it.viewController
|
|
1265
|
+
if (!child.dimmed) return@forEach
|
|
1266
|
+
val below = (1f - child.dimProgress) * opacity
|
|
1267
|
+
val above = child.dimProgress * child.dimOpacity
|
|
1268
|
+
if (below + above > 0f) color = ColorUtils.blendARGB(color, child.dimColor, above / (below + above))
|
|
1269
|
+
opacity = below + above
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Keep opacity in the color so a transparent dim still receives touches.
|
|
1274
|
+
color = ColorUtils.setAlphaComponent(color, (Color.alpha(color) * opacity).roundToInt())
|
|
1275
|
+
dimViews.forEach {
|
|
1276
|
+
it.setDimColor(color)
|
|
1277
|
+
if (animated) it.animate().alpha(dimProgress).setDuration(200).start() else it.alpha = dimProgress
|
|
1000
1278
|
}
|
|
1001
1279
|
}
|
|
1002
1280
|
|
|
@@ -1006,39 +1284,54 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1006
1284
|
|
|
1007
1285
|
var footerKeyboardOffset: Int = 0
|
|
1008
1286
|
|
|
1009
|
-
|
|
1287
|
+
/**
|
|
1288
|
+
* false keeps an absolute footer pinned to the sheet's bottom edge behind
|
|
1289
|
+
* the keyboard instead of rising above it.
|
|
1290
|
+
*/
|
|
1291
|
+
override var footerAvoidsKeyboard: Boolean = true
|
|
1292
|
+
|
|
1293
|
+
fun positionFooter() {
|
|
1010
1294
|
if (!isPresented) return
|
|
1011
1295
|
val footerView = containerView?.footerView ?: return
|
|
1012
1296
|
val sheet = sheetView ?: return
|
|
1013
1297
|
|
|
1014
|
-
val keyboardShift = if (currentKeyboardInset > 0) maxOf(0, currentKeyboardInset + footerKeyboardOffset) else 0
|
|
1015
|
-
|
|
1016
1298
|
// A relative footer is laid out by Yoga (pinned to the container's bottom
|
|
1017
1299
|
// edge) and stays in the layout flow behind the keyboard — only an
|
|
1018
1300
|
// absolute footer floats above it
|
|
1019
1301
|
if (!absoluteFooter) {
|
|
1020
1302
|
footerView.translationY = 0f
|
|
1303
|
+
footerView.syncTranslationState()
|
|
1021
1304
|
return
|
|
1022
1305
|
}
|
|
1023
1306
|
|
|
1307
|
+
// The footer's absorbed safe-area inset rides below the keyboard's top
|
|
1308
|
+
// edge — its content stays flush with the keyboard without dropping the
|
|
1309
|
+
// inset padding from the layout, which would jump at the transition edges
|
|
1310
|
+
val keyboardShift = if (currentKeyboardInset > 0 && footerAvoidsKeyboard) {
|
|
1311
|
+
maxOf(0, currentKeyboardInset + footerKeyboardOffset - footerView.bottomInset)
|
|
1312
|
+
} else {
|
|
1313
|
+
0
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1024
1316
|
val footerHeight = footerView.height
|
|
1025
1317
|
val sheetHeight = sheet.height
|
|
1026
|
-
val sheetTop = sheet.top
|
|
1027
1318
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
// when the keyboard detents reset on release.
|
|
1035
|
-
if (slideOffset != null && slideOffset < 0 && keyboardShift == 0) {
|
|
1036
|
-
footerY -= (footerHeight * slideOffset)
|
|
1037
|
-
}
|
|
1319
|
+
// Pinned to the screen bottom (above the keyboard) while the sheet sits at
|
|
1320
|
+
// or above its lowest stop; dragged past it the footer rides down with the
|
|
1321
|
+
// sheet instead, like iOS. A resize settle always pins — the sheet may be
|
|
1322
|
+
// animating up from below a stop that just grew
|
|
1323
|
+
val anchorTop = if (pendingDetentIndex >= 0) sheet.top else minOf(sheet.top, detentCalculator.getLowestSheetTop())
|
|
1324
|
+
val footerY = (sheetHeight - anchorTop - footerHeight - keyboardShift).toFloat()
|
|
1038
1325
|
|
|
1039
1326
|
// Clamp to prevent footer going above safe area
|
|
1040
1327
|
val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
|
|
1041
1328
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
1329
|
+
// Mid-motion slides are synced once the sheet settles or the keyboard
|
|
1330
|
+
// lands (handleStateSettled, keyboardDidShow/keyboardDidHide)
|
|
1331
|
+
if (!isSheetMoving) footerView.syncTranslationState()
|
|
1332
|
+
if (!footerAvoidsKeyboard) {
|
|
1333
|
+
containerView?.contentView?.updateContentInset()
|
|
1334
|
+
}
|
|
1042
1335
|
}
|
|
1043
1336
|
|
|
1044
1337
|
// =============================================================================
|
|
@@ -1057,6 +1350,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1057
1350
|
?: reactContext.currentActivity?.currentFocus
|
|
1058
1351
|
}
|
|
1059
1352
|
|
|
1353
|
+
fun clearFocusedView() {
|
|
1354
|
+
focusedViewBeforeBlur = null
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1060
1357
|
fun restoreFocusedView() {
|
|
1061
1358
|
val viewToFocus = focusedViewBeforeBlur ?: return
|
|
1062
1359
|
focusedViewBeforeBlur = null
|
|
@@ -1064,7 +1361,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1064
1361
|
if (!viewToFocus.isAttachedToWindow) return
|
|
1065
1362
|
if (viewToFocus.requestFocus()) {
|
|
1066
1363
|
viewToFocus.postDelayed({
|
|
1067
|
-
KeyboardUtils.show(viewToFocus)
|
|
1364
|
+
if (viewToFocus.isFocused) KeyboardUtils.show(viewToFocus)
|
|
1068
1365
|
}, 100)
|
|
1069
1366
|
}
|
|
1070
1367
|
}
|
|
@@ -1079,9 +1376,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1079
1376
|
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
1080
1377
|
override fun keyboardWillShow(height: Int) {
|
|
1081
1378
|
if (!shouldHandleKeyboard()) return
|
|
1082
|
-
// An absolute footer rises above the keyboard, so it drops the bottom
|
|
1083
|
-
// inset padding — before detents are configured against its height
|
|
1084
|
-
if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
|
|
1085
1379
|
// If a resize is in flight, restore to its target — not the stale current
|
|
1086
1380
|
detentIndexBeforeKeyboard = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
1087
1381
|
pendingDetentIndex = -1
|
|
@@ -1094,17 +1388,25 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1094
1388
|
|
|
1095
1389
|
override fun keyboardWillHide() {
|
|
1096
1390
|
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1097
|
-
|
|
1391
|
+
// Mid-drag (the drag itself dismissed the keyboard) the release decides
|
|
1392
|
+
// the detent and a reconfigure would kill the gesture — the floor stops
|
|
1393
|
+
// stay put until the settle flushes the pending reconfigure
|
|
1394
|
+
if (interactionState is InteractionState.Dragging) {
|
|
1395
|
+
hasPendingSizeChange = true
|
|
1396
|
+
return
|
|
1397
|
+
}
|
|
1098
1398
|
val restoring = !isBeingDismissed && detentIndexBeforeKeyboard >= 0
|
|
1399
|
+
if (restoring) {
|
|
1400
|
+
currentDetentIndex = detentIndexBeforeKeyboard
|
|
1401
|
+
}
|
|
1099
1402
|
|
|
1100
|
-
//
|
|
1101
|
-
//
|
|
1102
|
-
|
|
1403
|
+
// A scroll-committed keyboard detent still holds the keyboard-shifted
|
|
1404
|
+
// top too — reconfigure so the sheet settles with the keyboard, once
|
|
1405
|
+
// any touch on the sheet lifts
|
|
1406
|
+
if (!isBeingDismissed && coordinatorLayout?.isTouchingSheet == true) {
|
|
1407
|
+
isKeyboardSettleDeferred = true
|
|
1408
|
+
} else if (!isBeingDismissed || isKeyboardDismissProgrammatic) {
|
|
1103
1409
|
setupSheetDetents()
|
|
1104
|
-
if (restoring) {
|
|
1105
|
-
currentDetentIndex = detentIndexBeforeKeyboard
|
|
1106
|
-
setStateForDetentIndex(currentDetentIndex)
|
|
1107
|
-
}
|
|
1108
1410
|
}
|
|
1109
1411
|
|
|
1110
1412
|
// Same-frame reposition — the reconfigure above shrinks the container
|
|
@@ -1117,12 +1419,24 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1117
1419
|
)
|
|
1118
1420
|
}
|
|
1119
1421
|
|
|
1422
|
+
override fun keyboardDidShow(height: Int) {
|
|
1423
|
+
// The keyboard's last frame has positioned the footer
|
|
1424
|
+
containerView?.footerView?.syncTranslationState()
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1120
1427
|
override fun keyboardDidHide() {
|
|
1428
|
+
containerView?.footerView?.syncTranslationState()
|
|
1121
1429
|
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1122
|
-
containerView?.footerView?.keyboardVisible = false
|
|
1123
|
-
detentIndexBeforeKeyboard = -1
|
|
1124
1430
|
isKeyboardDismissProgrammatic = false
|
|
1125
|
-
|
|
1431
|
+
// Keep detentIndexBeforeKeyboard for the settle's detent-change check
|
|
1432
|
+
if (interactionState is InteractionState.Dragging) {
|
|
1433
|
+
hasPendingSizeChange = true
|
|
1434
|
+
return
|
|
1435
|
+
}
|
|
1436
|
+
detentIndexBeforeKeyboard = -1
|
|
1437
|
+
if (!isKeyboardSettleDeferred) {
|
|
1438
|
+
setupSheetDetents(applyState = false)
|
|
1439
|
+
}
|
|
1126
1440
|
positionFooter()
|
|
1127
1441
|
updateDimAmount(
|
|
1128
1442
|
sheetTop = detentCalculator.getSheetTopForDetentIndex(currentDetentIndex),
|
|
@@ -1141,7 +1455,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1141
1455
|
// Handle case where keyboard is already visible and focus moves into the sheet
|
|
1142
1456
|
if (!shouldHandleKeyboard()) return
|
|
1143
1457
|
if (detentIndexBeforeKeyboard < 0 && (keyboardObserver?.currentHeight ?: 0) > 0) {
|
|
1144
|
-
if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
|
|
1145
1458
|
detentIndexBeforeKeyboard = currentDetentIndex
|
|
1146
1459
|
currentDetentIndex = detents.size - 1
|
|
1147
1460
|
setupSheetDetents()
|
|
@@ -1169,8 +1482,28 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1169
1482
|
detentIndexBeforeKeyboard = -1
|
|
1170
1483
|
}
|
|
1171
1484
|
|
|
1485
|
+
private fun handleSettling(sheetView: View) {
|
|
1486
|
+
// A keyboard-floor drag was released — the first settle frame reveals the target
|
|
1487
|
+
keyboardSettleStartTop = if (hasKeyboardFloor && interactionState is InteractionState.Dragging) sheetView.top else -1
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Dismisses the keyboard as soon as a released drag is known to land on a
|
|
1492
|
+
* keyboard-free stop, rather than waiting for the landing. BottomSheetBehavior
|
|
1493
|
+
* keeps its target private, so read it off the first settle frame: heading
|
|
1494
|
+
* down means the half stop or the floor; heading up from below the half stop
|
|
1495
|
+
* means the half stop; only heading up from above it means the expanded stop.
|
|
1496
|
+
*/
|
|
1497
|
+
private fun dismissKeyboardForSettleTarget(sheetView: View) {
|
|
1498
|
+
if (keyboardSettleStartTop < 0) return
|
|
1499
|
+
val startTop = keyboardSettleStartTop
|
|
1500
|
+
keyboardSettleStartTop = -1
|
|
1501
|
+
|
|
1502
|
+
val headingToKeyboardFreeStop = sheetView.top > startTop || startTop > detentCalculator.getKeyboardHalfStopTop()
|
|
1503
|
+
if (headingToKeyboardFreeStop && shouldHandleKeyboard()) dismissKeyboard()
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1172
1506
|
private fun handleDragBegin(sheetView: View) {
|
|
1173
|
-
detentIndexBeforeKeyboard = -1
|
|
1174
1507
|
pendingDetentIndex = -1
|
|
1175
1508
|
|
|
1176
1509
|
val position = getPositionDpForView(sheetView)
|
|
@@ -1179,43 +1512,25 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1179
1512
|
interactionState = InteractionState.Dragging(startTop = sheetView.top)
|
|
1180
1513
|
}
|
|
1181
1514
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1515
|
+
// Commits a non-keyboard detent and hides the keyboard; keyboardWillHide then
|
|
1516
|
+
// reconfigures the detents so the sheet settles at the committed index.
|
|
1517
|
+
private fun commitDetentAndDismissKeyboard(index: Int, sheetView: View) {
|
|
1518
|
+
val changed = index != knownDetentIndex
|
|
1519
|
+
detentIndexBeforeKeyboard = -1
|
|
1520
|
+
currentDetentIndex = index
|
|
1521
|
+
dismissKeyboard()
|
|
1522
|
+
setupDimmedBackground()
|
|
1185
1523
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
for (i in detents.indices) {
|
|
1193
|
-
val nonKeyboardHeight = minOf(detentCalculator.getDetentHeight(detents[i], includeKeyboard = false), maxAvailableHeight)
|
|
1194
|
-
val nonKeyboardTop = realScreenHeight - nonKeyboardHeight
|
|
1195
|
-
val midpoint = keyboardTop + (nonKeyboardTop - keyboardTop) / 2
|
|
1196
|
-
|
|
1197
|
-
if (sheetView.top >= midpoint) {
|
|
1198
|
-
// Target position matches the keyboard-expanded position — keep keyboard open
|
|
1199
|
-
if (nonKeyboardTop == keyboardTop) break
|
|
1200
|
-
|
|
1201
|
-
detentIndexBeforeKeyboard = -1
|
|
1202
|
-
currentDetentIndex = i
|
|
1203
|
-
dismissKeyboard()
|
|
1204
|
-
setupDimmedBackground()
|
|
1205
|
-
|
|
1206
|
-
val position = getPositionDpForView(sheetView)
|
|
1207
|
-
val detent = detentCalculator.getDetentValueForIndex(i)
|
|
1208
|
-
delegate?.viewControllerDidChangeDetent(i, position, detent)
|
|
1209
|
-
break
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1524
|
+
if (!changed) return
|
|
1525
|
+
val position = getPositionDpForView(sheetView)
|
|
1526
|
+
val detent = detentCalculator.getDetentValueForIndex(index)
|
|
1527
|
+
delegate?.viewControllerDidChangeDetent(index, position, detent)
|
|
1528
|
+
this.sheetView?.updateGrabberAccessibilityValue(index, detents.size)
|
|
1212
1529
|
}
|
|
1213
1530
|
|
|
1214
1531
|
private fun handleDragChange(sheetView: View) {
|
|
1215
1532
|
if (interactionState !is InteractionState.Dragging) return
|
|
1216
1533
|
|
|
1217
|
-
updateStateDimensionsForDrag(sheetView.top)
|
|
1218
|
-
|
|
1219
1534
|
val position = getPositionDpForView(sheetView)
|
|
1220
1535
|
val detent = detentCalculator.getDetentValueForIndex(currentDetentIndex)
|
|
1221
1536
|
delegate?.viewControllerDidDragChange(currentDetentIndex, position, detent)
|
|
@@ -1236,6 +1551,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1236
1551
|
delegate?.viewControllerWillBlur()
|
|
1237
1552
|
delegate?.viewControllerWillDismiss()
|
|
1238
1553
|
parentSheetView?.viewControllerWillFocus()
|
|
1554
|
+
|
|
1555
|
+
// Dismissed mid-present: finishPresent() won't run. Stop blocking touches, and settle the
|
|
1556
|
+
// pending present well before didDismiss so JS clears isPresenting and unmounts the lazy native view
|
|
1557
|
+
coordinatorLayout?.blocksTouches = false
|
|
1558
|
+
presentPromise?.invoke()
|
|
1559
|
+
presentPromise = null
|
|
1239
1560
|
}
|
|
1240
1561
|
|
|
1241
1562
|
private fun emitDidDismissEvents() {
|
|
@@ -1250,14 +1571,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1250
1571
|
}
|
|
1251
1572
|
|
|
1252
1573
|
private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
|
|
1574
|
+
if (isPresented && !isPresentAnimating && !isBeingDismissed) {
|
|
1575
|
+
val index = detentCalculator.getBackgroundIndex(currentTop, displayedBackgroundIndex)
|
|
1576
|
+
if (index != displayedBackgroundIndex) {
|
|
1577
|
+
displayedBackgroundIndex = index
|
|
1578
|
+
setupBackground()
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1253
1581
|
// Dedupe emissions for same position
|
|
1254
1582
|
if (currentTop == lastEmittedPositionPx) return
|
|
1255
1583
|
|
|
1256
1584
|
lastEmittedPositionPx = currentTop
|
|
1257
1585
|
val visibleHeight = realScreenHeight - currentTop
|
|
1258
1586
|
val position = detentCalculator.getPositionDp(visibleHeight)
|
|
1259
|
-
val interpolatedIndex = detentCalculator.
|
|
1260
|
-
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
1587
|
+
val (interpolatedIndex, detent) = detentCalculator.getInterpolatedPositionInfo(currentTop)
|
|
1261
1588
|
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
1262
1589
|
}
|
|
1263
1590
|
|
|
@@ -1265,9 +1592,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1265
1592
|
* Updates position emission, footer, and dim amount together.
|
|
1266
1593
|
* This pattern is commonly used during animations and state changes.
|
|
1267
1594
|
*/
|
|
1268
|
-
private fun updateSheetVisuals(effectiveTop: Int
|
|
1595
|
+
private fun updateSheetVisuals(effectiveTop: Int) {
|
|
1269
1596
|
emitChangePositionDelegate(effectiveTop)
|
|
1270
|
-
positionFooter(
|
|
1597
|
+
positionFooter()
|
|
1271
1598
|
updateDimAmount(effectiveTop)
|
|
1272
1599
|
}
|
|
1273
1600
|
|
|
@@ -1287,13 +1614,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1287
1614
|
/**
|
|
1288
1615
|
* Updates the Fabric state with the current/target detent size so Yoga sizes
|
|
1289
1616
|
* the container to the sheet's visible height.
|
|
1290
|
-
*
|
|
1291
|
-
* Unlike iOS, resize animations run over multiple frames (ViewDragHelper), so
|
|
1292
|
-
* shrinking is deferred to settle ([deferShrink]) when a resize animation is
|
|
1293
|
-
* about to run — growing applies immediately so content is laid out before
|
|
1294
|
-
* the sheet reveals it.
|
|
1295
1617
|
*/
|
|
1296
|
-
private fun updateStateDimensions(
|
|
1618
|
+
private fun updateStateDimensions() {
|
|
1297
1619
|
if (detents.isEmpty()) return
|
|
1298
1620
|
|
|
1299
1621
|
val targetIndex = (if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex)
|
|
@@ -1302,21 +1624,21 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1302
1624
|
val newHeight = minOf(detentCalculator.getDetentHeight(detents[targetIndex]), maxAvailableHeight)
|
|
1303
1625
|
val newWidth = getStateWidth()
|
|
1304
1626
|
|
|
1305
|
-
if (deferShrink && newWidth == lastStateWidth && newHeight < lastStateHeight) return
|
|
1306
|
-
|
|
1307
1627
|
setStateDimensions(newWidth, newHeight)
|
|
1308
1628
|
}
|
|
1309
1629
|
|
|
1310
1630
|
/**
|
|
1311
|
-
* Tracks the sheet's visible height during
|
|
1312
|
-
* animation) so the container resizes with the finger, like iOS. Clamped to the
|
|
1631
|
+
* Tracks the sheet's visible height during drags and resize animations. Clamped to the
|
|
1313
1632
|
* smallest detent — dragging below it slides the sheet out without resizing.
|
|
1314
1633
|
*/
|
|
1315
|
-
private fun
|
|
1634
|
+
private fun updateStateDimensionsForPosition(sheetTop: Int) {
|
|
1316
1635
|
if (detents.isEmpty()) return
|
|
1317
1636
|
|
|
1318
1637
|
val maxAvailableHeight = realScreenHeight - topInset
|
|
1319
|
-
val minHeight = minOf(
|
|
1638
|
+
val minHeight = minOf(
|
|
1639
|
+
detentCalculator.getDetentHeight(detents.first(), includeKeyboard = !hasKeyboardFloor),
|
|
1640
|
+
maxAvailableHeight
|
|
1641
|
+
)
|
|
1320
1642
|
val newHeight = detentCalculator.getVisibleSheetHeight(sheetTop).coerceIn(minHeight, maxAvailableHeight)
|
|
1321
1643
|
|
|
1322
1644
|
setStateDimensions(getStateWidth(), newHeight)
|
|
@@ -1336,9 +1658,27 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1336
1658
|
delegate?.viewControllerDidChangeSize(width, height)
|
|
1337
1659
|
}
|
|
1338
1660
|
|
|
1339
|
-
|
|
1661
|
+
/**
|
|
1662
|
+
* Realtime (non-animated) counterpart of TrueSheetStackManager.updateParentTranslation,
|
|
1663
|
+
* driven by onSlide so the parent tracks this sheet's actual position.
|
|
1664
|
+
*/
|
|
1665
|
+
private fun syncParentTranslation(sheetTop: Int) {
|
|
1666
|
+
val parent = parentSheetView ?: return
|
|
1667
|
+
val minSheetTop = detentCalculator.getSheetTopForDetentIndex(0)
|
|
1668
|
+
parent.syncTranslationForChild(sheetTop, minSheetTop)
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
fun translateSheet(translationY: Int, animated: Boolean = true, onEnd: (() -> Unit)? = null) {
|
|
1340
1672
|
val sheet = sheetView ?: return
|
|
1341
1673
|
|
|
1674
|
+
if (!animated) {
|
|
1675
|
+
sheet.animate().cancel()
|
|
1676
|
+
sheet.translationY = translationY.toFloat()
|
|
1677
|
+
emitChangePositionDelegate(sheet.top + translationY)
|
|
1678
|
+
onEnd?.invoke()
|
|
1679
|
+
return
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1342
1682
|
sheet.animate()
|
|
1343
1683
|
.translationY(translationY.toFloat())
|
|
1344
1684
|
.setDuration(TRANSLATE_ANIMATION_DURATION)
|
|
@@ -1385,41 +1725,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1385
1725
|
// MARK: - RootView Touch Handling
|
|
1386
1726
|
// =============================================================================
|
|
1387
1727
|
|
|
1388
|
-
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1389
|
-
val footer = containerView?.footerView
|
|
1390
|
-
val action = event.actionMasked
|
|
1391
|
-
|
|
1392
|
-
if (footer != null && footer.isShown) {
|
|
1393
|
-
if (action == MotionEvent.ACTION_DOWN) {
|
|
1394
|
-
val loc = ScreenUtils.getScreenLocation(footer)
|
|
1395
|
-
val x = event.rawX.toInt()
|
|
1396
|
-
val y = event.rawY.toInt()
|
|
1397
|
-
footerOwnsTouchStream = x >= loc[0] &&
|
|
1398
|
-
x <= loc[0] + footer.width &&
|
|
1399
|
-
y >= loc[1] &&
|
|
1400
|
-
y <= loc[1] + footer.height
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
if (footerOwnsTouchStream) {
|
|
1404
|
-
val loc = ScreenUtils.getScreenLocation(footer)
|
|
1405
|
-
val localEvent = MotionEvent.obtain(event)
|
|
1406
|
-
localEvent.setLocation(event.rawX - loc[0], event.rawY - loc[1])
|
|
1407
|
-
val handled = footer.dispatchTouchEvent(localEvent)
|
|
1408
|
-
localEvent.recycle()
|
|
1409
|
-
|
|
1410
|
-
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
1411
|
-
footerOwnsTouchStream = false
|
|
1412
|
-
}
|
|
1413
|
-
if (handled) return true
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
1418
|
-
footerOwnsTouchStream = false
|
|
1419
|
-
}
|
|
1420
|
-
return super.dispatchTouchEvent(event)
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
1728
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
1424
1729
|
eventDispatcher?.let {
|
|
1425
1730
|
if (touchDeduper.shouldDispatch(event)) {
|