@lodev09/react-native-true-sheet 3.4.1 → 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 +28 -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,5 +1,6 @@
|
|
|
1
1
|
package com.lodev09.truesheet.core
|
|
2
2
|
|
|
3
|
+
import android.content.Context
|
|
3
4
|
import androidx.appcompat.app.AppCompatActivity
|
|
4
5
|
import androidx.fragment.app.Fragment
|
|
5
6
|
import androidx.fragment.app.FragmentManager
|
|
@@ -14,7 +15,8 @@ private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
|
|
|
14
15
|
class RNScreensFragmentObserver(
|
|
15
16
|
private val reactContext: ReactContext,
|
|
16
17
|
private val onModalPresented: () -> Unit,
|
|
17
|
-
private val
|
|
18
|
+
private val onModalWillDismiss: () -> Unit,
|
|
19
|
+
private val onModalDidDismiss: () -> Unit
|
|
18
20
|
) {
|
|
19
21
|
private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
|
|
20
22
|
private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
|
|
@@ -27,11 +29,11 @@ class RNScreensFragmentObserver(
|
|
|
27
29
|
val fragmentManager = activity.supportFragmentManager
|
|
28
30
|
|
|
29
31
|
fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
|
|
30
|
-
override fun onFragmentAttached(fm: FragmentManager,
|
|
31
|
-
super.onFragmentAttached(fm,
|
|
32
|
+
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
|
|
33
|
+
super.onFragmentAttached(fm, f, context)
|
|
32
34
|
|
|
33
|
-
if (isModalFragment(
|
|
34
|
-
activeModalFragments.add(
|
|
35
|
+
if (isModalFragment(f) && !activeModalFragments.contains(f)) {
|
|
36
|
+
activeModalFragments.add(f)
|
|
35
37
|
|
|
36
38
|
if (activeModalFragments.size == 1) {
|
|
37
39
|
onModalPresented()
|
|
@@ -39,14 +41,30 @@ class RNScreensFragmentObserver(
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
override fun onFragmentStopped(fm: FragmentManager,
|
|
43
|
-
super.onFragmentStopped(fm,
|
|
44
|
+
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
|
|
45
|
+
super.onFragmentStopped(fm, f)
|
|
44
46
|
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
+
// Ignore if app is in background (fragments stop with activity)
|
|
48
|
+
val activity = reactContext.currentActivity as? AppCompatActivity ?: return
|
|
49
|
+
if (!activity.lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (activeModalFragments.contains(f)) {
|
|
54
|
+
if (activeModalFragments.size == 1) {
|
|
55
|
+
onModalWillDismiss()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
|
|
61
|
+
super.onFragmentDestroyed(fm, f)
|
|
62
|
+
|
|
63
|
+
if (activeModalFragments.contains(f)) {
|
|
64
|
+
activeModalFragments.remove(f)
|
|
47
65
|
|
|
48
66
|
if (activeModalFragments.isEmpty()) {
|
|
49
|
-
|
|
67
|
+
onModalDidDismiss()
|
|
50
68
|
}
|
|
51
69
|
}
|
|
52
70
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import android.animation.Animator
|
|
4
|
+
import android.animation.AnimatorListenerAdapter
|
|
5
|
+
import android.animation.ValueAnimator
|
|
6
|
+
import android.view.animation.AccelerateInterpolator
|
|
7
|
+
import android.view.animation.DecelerateInterpolator
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provides the bottom sheet view and screen measurements for animations.
|
|
12
|
+
*/
|
|
13
|
+
interface TrueSheetAnimatorProvider {
|
|
14
|
+
val bottomSheetView: FrameLayout?
|
|
15
|
+
val realScreenHeight: Int
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handles present and dismiss animations for the bottom sheet.
|
|
20
|
+
* Encapsulates animation state and provides a clean callback interface.
|
|
21
|
+
*/
|
|
22
|
+
class TrueSheetAnimator(private val provider: TrueSheetAnimatorProvider) {
|
|
23
|
+
|
|
24
|
+
companion object {
|
|
25
|
+
const val PRESENT_DURATION = 300L
|
|
26
|
+
const val DISMISS_DURATION = 200L
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private var presentAnimator: ValueAnimator? = null
|
|
30
|
+
private var dismissAnimator: ValueAnimator? = null
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Animate the sheet presenting from bottom of screen to target position.
|
|
34
|
+
* @param toTop The target top position of the sheet
|
|
35
|
+
* @param onUpdate Called on each animation frame with the effective top position
|
|
36
|
+
* @param onEnd Called when animation completes or is cancelled
|
|
37
|
+
*/
|
|
38
|
+
fun animatePresent(toTop: Int, onUpdate: (effectiveTop: Int) -> Unit, onEnd: () -> Unit) {
|
|
39
|
+
val bottomSheet = provider.bottomSheetView ?: run {
|
|
40
|
+
onEnd()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
val fromY = (provider.realScreenHeight - toTop).toFloat()
|
|
45
|
+
|
|
46
|
+
presentAnimator?.cancel()
|
|
47
|
+
presentAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
|
|
48
|
+
duration = PRESENT_DURATION
|
|
49
|
+
interpolator = DecelerateInterpolator()
|
|
50
|
+
|
|
51
|
+
addUpdateListener { animator ->
|
|
52
|
+
val fraction = animator.animatedValue as Float
|
|
53
|
+
bottomSheet.translationY = fromY * fraction
|
|
54
|
+
|
|
55
|
+
val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
|
|
56
|
+
onUpdate(effectiveTop)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
60
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
61
|
+
bottomSheet.translationY = 0f
|
|
62
|
+
presentAnimator = null
|
|
63
|
+
onEnd()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun onAnimationCancel(animation: Animator) {
|
|
67
|
+
presentAnimator = null
|
|
68
|
+
onEnd()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
start()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Animate the sheet dismissing from current position to bottom of screen.
|
|
78
|
+
* @param onUpdate Called on each animation frame with the effective top position
|
|
79
|
+
* @param onEnd Called when animation completes or is cancelled
|
|
80
|
+
*/
|
|
81
|
+
fun animateDismiss(onUpdate: (effectiveTop: Int) -> Unit, onEnd: () -> Unit) {
|
|
82
|
+
val bottomSheet = provider.bottomSheetView ?: run {
|
|
83
|
+
onEnd()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val fromTop = bottomSheet.top + bottomSheet.translationY.toInt()
|
|
88
|
+
val toY = (provider.realScreenHeight - fromTop).toFloat()
|
|
89
|
+
|
|
90
|
+
dismissAnimator?.cancel()
|
|
91
|
+
dismissAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
92
|
+
duration = DISMISS_DURATION
|
|
93
|
+
interpolator = AccelerateInterpolator()
|
|
94
|
+
|
|
95
|
+
addUpdateListener { animator ->
|
|
96
|
+
val fraction = animator.animatedValue as Float
|
|
97
|
+
bottomSheet.translationY = toY * fraction
|
|
98
|
+
|
|
99
|
+
val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
|
|
100
|
+
onUpdate(effectiveTop)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
104
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
105
|
+
dismissAnimator = null
|
|
106
|
+
onEnd()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override fun onAnimationCancel(animation: Animator) {
|
|
110
|
+
dismissAnimator = null
|
|
111
|
+
onEnd()
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
start()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Cancel any running animations.
|
|
121
|
+
*/
|
|
122
|
+
fun cancel() {
|
|
123
|
+
presentAnimator?.cancel()
|
|
124
|
+
presentAnimator = null
|
|
125
|
+
dismissAnimator?.cancel()
|
|
126
|
+
dismissAnimator = null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if any animation is currently running.
|
|
131
|
+
*/
|
|
132
|
+
val isAnimating: Boolean
|
|
133
|
+
get() = presentAnimator?.isRunning == true || dismissAnimator?.isRunning == true
|
|
134
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
4
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provides screen dimensions and content measurements for detent calculations.
|
|
8
|
+
*/
|
|
9
|
+
interface TrueSheetDetentMeasurements {
|
|
10
|
+
val screenHeight: Int
|
|
11
|
+
val realScreenHeight: Int
|
|
12
|
+
val detents: MutableList<Double>
|
|
13
|
+
val contentHeight: Int
|
|
14
|
+
val headerHeight: Int
|
|
15
|
+
val contentBottomInset: Int
|
|
16
|
+
val maxSheetHeight: Int?
|
|
17
|
+
val keyboardHeight: Int
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handles all detent-related calculations for the bottom sheet.
|
|
22
|
+
* Takes a measurements provider to always read current values.
|
|
23
|
+
*/
|
|
24
|
+
class TrueSheetDetentCalculator(private val measurements: TrueSheetDetentMeasurements) {
|
|
25
|
+
|
|
26
|
+
private val screenHeight: Int get() = measurements.screenHeight
|
|
27
|
+
private val realScreenHeight: Int get() = measurements.realScreenHeight
|
|
28
|
+
private val detents: List<Double> get() = measurements.detents
|
|
29
|
+
private val contentHeight: Int get() = measurements.contentHeight
|
|
30
|
+
private val headerHeight: Int get() = measurements.headerHeight
|
|
31
|
+
private val contentBottomInset: Int get() = measurements.contentBottomInset
|
|
32
|
+
private val maxSheetHeight: Int? get() = measurements.maxSheetHeight
|
|
33
|
+
private val keyboardHeight: Int get() = measurements.keyboardHeight
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate the height in pixels for a given detent value.
|
|
37
|
+
* @param detent The detent value: -1.0 for content-fit, or 0.0-1.0 for screen fraction
|
|
38
|
+
*/
|
|
39
|
+
fun getDetentHeight(detent: Double): Int {
|
|
40
|
+
val baseHeight = if (detent == -1.0) {
|
|
41
|
+
contentHeight + headerHeight + contentBottomInset
|
|
42
|
+
} else {
|
|
43
|
+
if (detent <= 0.0 || detent > 1.0) {
|
|
44
|
+
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
45
|
+
}
|
|
46
|
+
(detent * screenHeight).toInt() + contentBottomInset
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
val height = baseHeight + keyboardHeight
|
|
50
|
+
val maxAllowedHeight = screenHeight + contentBottomInset
|
|
51
|
+
return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the expected sheet top position for a detent index.
|
|
56
|
+
*/
|
|
57
|
+
fun getSheetTopForDetentIndex(index: Int): Int {
|
|
58
|
+
if (index < 0 || index >= detents.size) return realScreenHeight
|
|
59
|
+
return realScreenHeight - getDetentHeight(detents[index])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculate visible sheet height from sheet top position.
|
|
64
|
+
*/
|
|
65
|
+
fun getVisibleSheetHeight(sheetTop: Int): Int = realScreenHeight - sheetTop
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convert visible sheet height to position in dp.
|
|
69
|
+
*/
|
|
70
|
+
fun getPositionDp(visibleSheetHeight: Int): Float = (screenHeight - visibleSheetHeight).pxToDp()
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the raw screen fraction for a detent index (without bottomInset).
|
|
74
|
+
*/
|
|
75
|
+
fun getDetentValueForIndex(index: Int): Float {
|
|
76
|
+
if (index < 0 || index >= detents.size) return 0f
|
|
77
|
+
val value = detents[index]
|
|
78
|
+
return if (value == -1.0) {
|
|
79
|
+
(contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
|
|
80
|
+
} else {
|
|
81
|
+
value.toFloat()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ====================================================================
|
|
86
|
+
// MARK: - State Mapping
|
|
87
|
+
// ====================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Maps detent index to BottomSheetBehavior state based on detent count.
|
|
91
|
+
*/
|
|
92
|
+
fun getStateForDetentIndex(index: Int): Int {
|
|
93
|
+
val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
|
|
94
|
+
return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Maps BottomSheetBehavior state to detent index.
|
|
99
|
+
* @return The detent index, or null if state is not mapped
|
|
100
|
+
*/
|
|
101
|
+
fun getDetentIndexForState(state: Int): Int? {
|
|
102
|
+
val stateMap = getDetentStateMap() ?: return null
|
|
103
|
+
return stateMap[state]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns state-to-index mapping based on detent count.
|
|
108
|
+
*/
|
|
109
|
+
private fun getDetentStateMap(): Map<Int, Int>? =
|
|
110
|
+
when (detents.size) {
|
|
111
|
+
1 -> mapOf(
|
|
112
|
+
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
113
|
+
BottomSheetBehavior.STATE_EXPANDED to 0
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
2 -> mapOf(
|
|
117
|
+
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
118
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
|
|
119
|
+
BottomSheetBehavior.STATE_EXPANDED to 1
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
3 -> mapOf(
|
|
123
|
+
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
124
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
|
|
125
|
+
BottomSheetBehavior.STATE_EXPANDED to 2
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
else -> null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ====================================================================
|
|
132
|
+
// MARK: - Interpolation
|
|
133
|
+
// ====================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Find which segment the position falls into for interpolation.
|
|
137
|
+
* @return Triple(fromIndex, toIndex, progress) where progress is 0-1, or null if no detents
|
|
138
|
+
*/
|
|
139
|
+
fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
|
|
140
|
+
val count = detents.size
|
|
141
|
+
if (count == 0) return null
|
|
142
|
+
|
|
143
|
+
val firstPos = getSheetTopForDetentIndex(0)
|
|
144
|
+
|
|
145
|
+
// Position is below first detent (sheet is being dragged down to dismiss)
|
|
146
|
+
if (positionPx > firstPos) {
|
|
147
|
+
val range = realScreenHeight - firstPos
|
|
148
|
+
val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
|
|
149
|
+
return Triple(-1, 0, progress)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (count == 1) return Triple(0, 0, 0f)
|
|
153
|
+
|
|
154
|
+
val lastPos = getSheetTopForDetentIndex(count - 1)
|
|
155
|
+
// Position is above last detent
|
|
156
|
+
if (positionPx < lastPos) {
|
|
157
|
+
return Triple(count - 1, count - 1, 0f)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Find the segment containing this position
|
|
161
|
+
for (i in 0 until count - 1) {
|
|
162
|
+
val pos = getSheetTopForDetentIndex(i)
|
|
163
|
+
val nextPos = getSheetTopForDetentIndex(i + 1)
|
|
164
|
+
|
|
165
|
+
if (positionPx in nextPos..pos) {
|
|
166
|
+
val range = pos - nextPos
|
|
167
|
+
val progress = if (range > 0) (pos - positionPx).toFloat() / range else 0f
|
|
168
|
+
return Triple(i, i + 1, maxOf(0f, minOf(1f, progress)))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return Triple(count - 1, count - 1, 0f)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1).
|
|
177
|
+
*/
|
|
178
|
+
fun getInterpolatedIndexForPosition(positionPx: Int): Float {
|
|
179
|
+
val count = detents.size
|
|
180
|
+
if (count == 0) return -1f
|
|
181
|
+
|
|
182
|
+
val segment = findSegmentForPosition(positionPx) ?: return 0f
|
|
183
|
+
val (fromIndex, _, progress) = segment
|
|
184
|
+
|
|
185
|
+
if (fromIndex == -1) return -progress
|
|
186
|
+
return fromIndex + progress
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Returns interpolated screen fraction for position.
|
|
191
|
+
*/
|
|
192
|
+
fun getInterpolatedDetentForPosition(positionPx: Int): Float {
|
|
193
|
+
val count = detents.size
|
|
194
|
+
if (count == 0) return 0f
|
|
195
|
+
|
|
196
|
+
val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
|
|
197
|
+
val (fromIndex, toIndex, progress) = segment
|
|
198
|
+
|
|
199
|
+
if (fromIndex == -1) {
|
|
200
|
+
val firstDetent = getDetentValueForIndex(0)
|
|
201
|
+
return maxOf(0f, firstDetent * (1 - progress))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
val fromDetent = getDetentValueForIndex(fromIndex)
|
|
205
|
+
val toDetent = getDetentValueForIndex(toIndex)
|
|
206
|
+
return fromDetent + progress * (toDetent - fromDetent)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -13,7 +13,7 @@ import com.lodev09.truesheet.utils.ScreenUtils
|
|
|
13
13
|
class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reactContext) {
|
|
14
14
|
|
|
15
15
|
companion object {
|
|
16
|
-
private const val MAX_ALPHA = 0.
|
|
16
|
+
private const val MAX_ALPHA = 0.5f
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
private var targetView: ViewGroup? = null
|
|
@@ -48,11 +48,6 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
|
|
|
48
48
|
targetView = null
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
fun animateAlpha(show: Boolean, duration: Long, dimmedDetentIndex: Int, currentDetentIndex: Int) {
|
|
52
|
-
val targetAlpha = if (show && currentDetentIndex >= dimmedDetentIndex) MAX_ALPHA else 0f
|
|
53
|
-
animate().alpha(targetAlpha).setDuration(duration).start()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
51
|
fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
|
|
57
52
|
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
58
53
|
val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
|
|
@@ -10,7 +10,9 @@ import androidx.core.view.WindowInsetsCompat
|
|
|
10
10
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
11
11
|
|
|
12
12
|
interface TrueSheetKeyboardObserverDelegate {
|
|
13
|
-
fun
|
|
13
|
+
fun keyboardWillShow(height: Int)
|
|
14
|
+
fun keyboardWillHide()
|
|
15
|
+
fun keyboardDidChangeHeight(height: Int)
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -44,10 +46,11 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
44
46
|
ViewCompat.setWindowInsetsAnimationCallback(targetView, null)
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
private fun updateHeight(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
private fun updateHeight(from: Int, to: Int, fraction: Float) {
|
|
50
|
+
val newHeight = (from + (to - from) * fraction).toInt()
|
|
51
|
+
if (currentHeight != newHeight) {
|
|
52
|
+
currentHeight = newHeight
|
|
53
|
+
delegate?.keyboardDidChangeHeight(newHeight)
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -69,6 +72,11 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
69
72
|
bounds: WindowInsetsAnimationCompat.BoundsCompat
|
|
70
73
|
): WindowInsetsAnimationCompat.BoundsCompat {
|
|
71
74
|
endHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
|
|
75
|
+
if (endHeight > startHeight) {
|
|
76
|
+
delegate?.keyboardWillShow(endHeight)
|
|
77
|
+
} else if (endHeight < startHeight) {
|
|
78
|
+
delegate?.keyboardWillHide()
|
|
79
|
+
}
|
|
72
80
|
return bounds
|
|
73
81
|
}
|
|
74
82
|
|
|
@@ -78,14 +86,14 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
78
86
|
} ?: return insets
|
|
79
87
|
|
|
80
88
|
val fraction = imeAnimation.interpolatedFraction
|
|
81
|
-
|
|
82
|
-
updateHeight(currentHeight)
|
|
89
|
+
updateHeight(startHeight, endHeight, fraction)
|
|
83
90
|
|
|
84
91
|
return insets
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
88
|
-
|
|
95
|
+
val finalHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
|
|
96
|
+
updateHeight(startHeight, finalHeight, 1f)
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
)
|
|
@@ -102,7 +110,17 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
102
110
|
val screenHeight = rootView.height
|
|
103
111
|
val keyboardHeight = screenHeight - rect.bottom
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
val newHeight = if (keyboardHeight > screenHeight * 0.15) keyboardHeight else 0
|
|
114
|
+
val previousHeight = currentHeight
|
|
115
|
+
|
|
116
|
+
if (previousHeight != newHeight) {
|
|
117
|
+
if (newHeight > previousHeight) {
|
|
118
|
+
delegate?.keyboardWillShow(newHeight)
|
|
119
|
+
} else if (newHeight < previousHeight) {
|
|
120
|
+
delegate?.keyboardWillHide()
|
|
121
|
+
}
|
|
122
|
+
updateHeight(previousHeight, newHeight, 1f)
|
|
123
|
+
}
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
rootView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
|
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<resources>
|
|
3
|
-
<!--
|
|
4
|
-
<style name="
|
|
5
|
-
<item name="android:windowEnterAnimation">@
|
|
6
|
-
<item name="android:windowExitAnimation">@
|
|
3
|
+
<!-- No animation style - animations handled programmatically -->
|
|
4
|
+
<style name="TrueSheetNoAnimation" parent="Animation.AppCompat.Dialog">
|
|
5
|
+
<item name="android:windowEnterAnimation">@null</item>
|
|
6
|
+
<item name="android:windowExitAnimation">@null</item>
|
|
7
7
|
</style>
|
|
8
8
|
|
|
9
|
-
<!--
|
|
10
|
-
<style name="TrueSheetFadeOutAnimation" parent="Animation.AppCompat.Dialog">
|
|
11
|
-
<item name="android:windowExitAnimation">@anim/true_sheet_fade_out</item>
|
|
12
|
-
</style>
|
|
13
|
-
|
|
14
|
-
<!-- Default BottomSheetDialog style with smooth animations -->
|
|
9
|
+
<!-- Default BottomSheetDialog style with programmatic animations -->
|
|
15
10
|
<style name="TrueSheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
|
|
16
|
-
<item name="android:windowAnimationStyle">@style/
|
|
11
|
+
<item name="android:windowAnimationStyle">@style/TrueSheetNoAnimation</item>
|
|
17
12
|
</style>
|
|
18
13
|
|
|
19
|
-
<!-- BottomSheetDialog style with edge-to-edge and
|
|
14
|
+
<!-- BottomSheetDialog style with edge-to-edge and programmatic animations -->
|
|
20
15
|
<style name="TrueSheetEdgeToEdgeEnabledDialog" parent="Theme.Design.Light.BottomSheetDialog">
|
|
21
16
|
<item name="android:windowIsFloating">false</item>
|
|
22
17
|
<item name="enableEdgeToEdge">true</item>
|
|
23
18
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
|
24
|
-
<item name="android:windowAnimationStyle">@style/
|
|
19
|
+
<item name="android:windowAnimationStyle">@style/TrueSheetNoAnimation</item>
|
|
25
20
|
</style>
|
|
26
21
|
</resources>
|
|
@@ -40,7 +40,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
40
40
|
|
|
41
41
|
@end
|
|
42
42
|
|
|
43
|
-
@
|
|
43
|
+
@protocol TrueSheetDetentMeasurements;
|
|
44
|
+
|
|
45
|
+
@interface TrueSheetViewController : UIViewController <UISheetPresentationControllerDelegate,
|
|
46
|
+
TrueSheetDetentMeasurements
|
|
44
47
|
#if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
|
|
45
48
|
,
|
|
46
49
|
RNSDismissibleModalProtocol
|