@lodev09/react-native-true-sheet 3.5.0 → 3.5.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +150 -136
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +173 -217
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetAnimator.kt +22 -11
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogFragment.kt +320 -0
- package/android/src/main/res/values/styles.xml +2 -8
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ package com.lodev09.truesheet.core
|
|
|
3
3
|
import android.animation.Animator
|
|
4
4
|
import android.animation.AnimatorListenerAdapter
|
|
5
5
|
import android.animation.ValueAnimator
|
|
6
|
+
import android.util.Log
|
|
6
7
|
import android.view.animation.AccelerateInterpolator
|
|
7
8
|
import android.view.animation.DecelerateInterpolator
|
|
8
9
|
import android.widget.FrameLayout
|
|
@@ -29,6 +30,8 @@ class TrueSheetAnimator(private val provider: TrueSheetAnimatorProvider) {
|
|
|
29
30
|
private var presentAnimator: ValueAnimator? = null
|
|
30
31
|
private var dismissAnimator: ValueAnimator? = null
|
|
31
32
|
|
|
33
|
+
var isAnimating: Boolean = false
|
|
34
|
+
|
|
32
35
|
/**
|
|
33
36
|
* Animate the sheet presenting from bottom of screen to target position.
|
|
34
37
|
* @param toTop The target top position of the sheet
|
|
@@ -41,30 +44,38 @@ class TrueSheetAnimator(private val provider: TrueSheetAnimatorProvider) {
|
|
|
41
44
|
return
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
val
|
|
47
|
+
val fromTop = provider.realScreenHeight
|
|
48
|
+
val distance = fromTop - toTop
|
|
45
49
|
|
|
46
50
|
presentAnimator?.cancel()
|
|
47
|
-
presentAnimator = ValueAnimator.ofFloat(
|
|
51
|
+
presentAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
48
52
|
duration = PRESENT_DURATION
|
|
49
53
|
interpolator = DecelerateInterpolator()
|
|
50
54
|
|
|
51
55
|
addUpdateListener { animator ->
|
|
52
56
|
val fraction = animator.animatedValue as Float
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
// Calculate effective top based on animation progress
|
|
58
|
+
val effectiveTop = fromTop - (distance * fraction).toInt()
|
|
59
|
+
// Adjust translationY to compensate for bottomSheet.top position
|
|
60
|
+
bottomSheet.translationY = (effectiveTop - bottomSheet.top).toFloat()
|
|
56
61
|
onUpdate(effectiveTop)
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
addListener(object : AnimatorListenerAdapter() {
|
|
65
|
+
override fun onAnimationStart(animation: Animator) {
|
|
66
|
+
isAnimating = true
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
override fun onAnimationEnd(animation: Animator) {
|
|
61
70
|
bottomSheet.translationY = 0f
|
|
62
71
|
presentAnimator = null
|
|
72
|
+
isAnimating = false
|
|
63
73
|
onEnd()
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
override fun onAnimationCancel(animation: Animator) {
|
|
67
77
|
presentAnimator = null
|
|
78
|
+
isAnimating = false
|
|
68
79
|
onEnd()
|
|
69
80
|
}
|
|
70
81
|
})
|
|
@@ -101,13 +112,19 @@ class TrueSheetAnimator(private val provider: TrueSheetAnimatorProvider) {
|
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
addListener(object : AnimatorListenerAdapter() {
|
|
115
|
+
override fun onAnimationStart(animation: Animator) {
|
|
116
|
+
isAnimating = true
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
override fun onAnimationEnd(animation: Animator) {
|
|
105
120
|
dismissAnimator = null
|
|
121
|
+
isAnimating = false
|
|
106
122
|
onEnd()
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
override fun onAnimationCancel(animation: Animator) {
|
|
110
126
|
dismissAnimator = null
|
|
127
|
+
isAnimating = false
|
|
111
128
|
onEnd()
|
|
112
129
|
}
|
|
113
130
|
})
|
|
@@ -125,10 +142,4 @@ class TrueSheetAnimator(private val provider: TrueSheetAnimatorProvider) {
|
|
|
125
142
|
dismissAnimator?.cancel()
|
|
126
143
|
dismissAnimator = null
|
|
127
144
|
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Check if any animation is currently running.
|
|
131
|
-
*/
|
|
132
|
-
val isAnimating: Boolean
|
|
133
|
-
get() = presentAnimator?.isRunning == true || dismissAnimator?.isRunning == true
|
|
134
145
|
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import android.app.Dialog
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.drawable.ShapeDrawable
|
|
6
|
+
import android.graphics.drawable.shapes.RoundRectShape
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
import android.util.TypedValue
|
|
9
|
+
import android.view.LayoutInflater
|
|
10
|
+
import android.view.View
|
|
11
|
+
import android.view.ViewGroup
|
|
12
|
+
import android.view.WindowManager
|
|
13
|
+
import android.widget.FrameLayout
|
|
14
|
+
import androidx.activity.OnBackPressedCallback
|
|
15
|
+
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
16
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
17
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
18
|
+
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
19
|
+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
20
|
+
import com.lodev09.truesheet.BuildConfig
|
|
21
|
+
import com.lodev09.truesheet.R
|
|
22
|
+
import com.lodev09.truesheet.utils.ScreenUtils
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// MARK: - Delegate Protocol
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
interface TrueSheetDialogFragmentDelegate {
|
|
29
|
+
fun onDialogShow()
|
|
30
|
+
fun onDialogDismiss()
|
|
31
|
+
fun onDialogCancel()
|
|
32
|
+
fun onStateChanged(sheetView: View, newState: Int)
|
|
33
|
+
fun onSlide(sheetView: View, slideOffset: Float)
|
|
34
|
+
fun onBackPressed()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// MARK: - TrueSheetDialogFragment
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Custom BottomSheetDialogFragment for TrueSheet.
|
|
43
|
+
* Provides a Material Design bottom sheet with proper lifecycle management.
|
|
44
|
+
*
|
|
45
|
+
* This fragment handles:
|
|
46
|
+
* - Dialog creation with proper theming (edge-to-edge support)
|
|
47
|
+
* - BottomSheetBehavior configuration and callbacks
|
|
48
|
+
* - Background styling with corner radius
|
|
49
|
+
* - Grabber view management
|
|
50
|
+
* - Back press handling
|
|
51
|
+
*
|
|
52
|
+
* The parent TrueSheetViewController handles:
|
|
53
|
+
* - React Native touch dispatching
|
|
54
|
+
* - Detent calculations
|
|
55
|
+
* - Animations
|
|
56
|
+
* - Keyboard/modal observers
|
|
57
|
+
* - Stacking and dimming
|
|
58
|
+
*/
|
|
59
|
+
class TrueSheetDialogFragment : BottomSheetDialogFragment() {
|
|
60
|
+
|
|
61
|
+
companion object {
|
|
62
|
+
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
63
|
+
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
64
|
+
private const val DEFAULT_CORNER_RADIUS = 16f // dp
|
|
65
|
+
|
|
66
|
+
fun newInstance(): TrueSheetDialogFragment = TrueSheetDialogFragment()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// MARK: - Properties
|
|
71
|
+
// =============================================================================
|
|
72
|
+
|
|
73
|
+
var delegate: TrueSheetDialogFragmentDelegate? = null
|
|
74
|
+
|
|
75
|
+
// Content view provided by the controller
|
|
76
|
+
var contentView: View? = null
|
|
77
|
+
|
|
78
|
+
// React context for theme resolution and screen utils
|
|
79
|
+
var reactContext: ThemedReactContext? = null
|
|
80
|
+
|
|
81
|
+
// Configuration
|
|
82
|
+
var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
83
|
+
var sheetBackgroundColor: Int? = null
|
|
84
|
+
var edgeToEdgeFullScreen: Boolean = false
|
|
85
|
+
var grabberEnabled: Boolean = true
|
|
86
|
+
var grabberOptions: GrabberOptions? = null
|
|
87
|
+
var draggable: Boolean = true
|
|
88
|
+
|
|
89
|
+
var dismissible: Boolean = true
|
|
90
|
+
set(value) {
|
|
91
|
+
field = value
|
|
92
|
+
(dialog as? BottomSheetDialog)?.apply {
|
|
93
|
+
setCanceledOnTouchOutside(value)
|
|
94
|
+
setCancelable(value)
|
|
95
|
+
behavior.isHideable = value
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// MARK: - Computed Properties
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
val bottomSheetDialog: BottomSheetDialog?
|
|
104
|
+
get() = dialog as? BottomSheetDialog
|
|
105
|
+
|
|
106
|
+
val behavior: BottomSheetBehavior<FrameLayout>?
|
|
107
|
+
get() = bottomSheetDialog?.behavior
|
|
108
|
+
|
|
109
|
+
val bottomSheetView: FrameLayout?
|
|
110
|
+
get() = dialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
111
|
+
|
|
112
|
+
private val edgeToEdgeEnabled: Boolean
|
|
113
|
+
get() {
|
|
114
|
+
val defaultEnabled = android.os.Build.VERSION.SDK_INT >= 36
|
|
115
|
+
return BuildConfig.EDGE_TO_EDGE_ENABLED || bottomSheetDialog?.edgeToEdgeEnabled == true || defaultEnabled
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
val topInset: Int
|
|
119
|
+
get() = reactContext?.let { if (edgeToEdgeEnabled) ScreenUtils.getInsets(it).top else 0 } ?: 0
|
|
120
|
+
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// MARK: - Lifecycle
|
|
123
|
+
// =============================================================================
|
|
124
|
+
|
|
125
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
126
|
+
super.onCreate(savedInstanceState)
|
|
127
|
+
// Prevent dialog from being recreated on configuration change
|
|
128
|
+
// The controller manages state separately
|
|
129
|
+
retainInstance = true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
133
|
+
val ctx = reactContext ?: requireContext()
|
|
134
|
+
|
|
135
|
+
val style = if (edgeToEdgeEnabled) {
|
|
136
|
+
R.style.TrueSheetEdgeToEdgeEnabledDialog
|
|
137
|
+
} else {
|
|
138
|
+
R.style.TrueSheetDialog
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
val dialog = BottomSheetDialog(ctx, style)
|
|
142
|
+
|
|
143
|
+
dialog.window?.apply {
|
|
144
|
+
setWindowAnimations(0)
|
|
145
|
+
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
|
146
|
+
// Clear default dim - TrueSheet uses custom TrueSheetDimView for dimming
|
|
147
|
+
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
dialog.setOnShowListener {
|
|
151
|
+
setupBottomSheetBehavior()
|
|
152
|
+
setupBackground()
|
|
153
|
+
setupGrabber()
|
|
154
|
+
// Re-apply dismissible after show since behavior may reset it
|
|
155
|
+
dialog.behavior.isHideable = dismissible
|
|
156
|
+
delegate?.onDialogShow()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
dialog.setCanceledOnTouchOutside(dismissible)
|
|
160
|
+
dialog.setCancelable(dismissible)
|
|
161
|
+
dialog.behavior.isHideable = dismissible
|
|
162
|
+
dialog.behavior.isDraggable = draggable
|
|
163
|
+
dialog.behavior.maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
164
|
+
|
|
165
|
+
// Handle back press
|
|
166
|
+
dialog.onBackPressedDispatcher.addCallback(
|
|
167
|
+
this,
|
|
168
|
+
object : OnBackPressedCallback(true) {
|
|
169
|
+
override fun handleOnBackPressed() {
|
|
170
|
+
delegate?.onBackPressed()
|
|
171
|
+
if (dismissible) {
|
|
172
|
+
dismiss()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return dialog
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = contentView
|
|
182
|
+
|
|
183
|
+
override fun onCancel(dialog: android.content.DialogInterface) {
|
|
184
|
+
super.onCancel(dialog)
|
|
185
|
+
delegate?.onDialogCancel()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
override fun onDismiss(dialog: android.content.DialogInterface) {
|
|
189
|
+
super.onDismiss(dialog)
|
|
190
|
+
delegate?.onDialogDismiss()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override fun onDestroyView() {
|
|
194
|
+
// Detach content view to prevent it from being destroyed with the fragment
|
|
195
|
+
(contentView?.parent as? ViewGroup)?.removeView(contentView)
|
|
196
|
+
super.onDestroyView()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// =============================================================================
|
|
200
|
+
// MARK: - Setup
|
|
201
|
+
// =============================================================================
|
|
202
|
+
|
|
203
|
+
private fun setupBottomSheetBehavior() {
|
|
204
|
+
val behavior = this.behavior ?: return
|
|
205
|
+
|
|
206
|
+
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
|
207
|
+
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
208
|
+
delegate?.onSlide(sheetView, slideOffset)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
212
|
+
delegate?.onStateChanged(sheetView, newState)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fun setupBackground() {
|
|
218
|
+
val bottomSheet = bottomSheetView ?: return
|
|
219
|
+
val ctx = reactContext ?: return
|
|
220
|
+
|
|
221
|
+
val radius = if (sheetCornerRadius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else sheetCornerRadius
|
|
222
|
+
|
|
223
|
+
// Rounded corners only on top
|
|
224
|
+
val outerRadii = floatArrayOf(
|
|
225
|
+
radius,
|
|
226
|
+
radius,
|
|
227
|
+
radius,
|
|
228
|
+
radius,
|
|
229
|
+
0f,
|
|
230
|
+
0f,
|
|
231
|
+
0f,
|
|
232
|
+
0f
|
|
233
|
+
)
|
|
234
|
+
val backgroundColor = sheetBackgroundColor ?: getDefaultBackgroundColor(ctx)
|
|
235
|
+
|
|
236
|
+
bottomSheet.background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
|
|
237
|
+
paint.color = backgroundColor
|
|
238
|
+
}
|
|
239
|
+
bottomSheet.clipToOutline = true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fun setupGrabber() {
|
|
243
|
+
val bottomSheet = bottomSheetView ?: return
|
|
244
|
+
val ctx = reactContext ?: return
|
|
245
|
+
|
|
246
|
+
// Remove existing grabber
|
|
247
|
+
bottomSheet.findViewWithTag<View>(GRABBER_TAG)?.let {
|
|
248
|
+
bottomSheet.removeView(it)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!grabberEnabled || !draggable) return
|
|
252
|
+
|
|
253
|
+
val grabberView = TrueSheetGrabberView(ctx, grabberOptions).apply {
|
|
254
|
+
tag = GRABBER_TAG
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
bottomSheet.addView(grabberView)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// =============================================================================
|
|
261
|
+
// MARK: - Configuration
|
|
262
|
+
// =============================================================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Configure detent-related behavior settings.
|
|
266
|
+
* Called by the controller when detents change.
|
|
267
|
+
*/
|
|
268
|
+
fun configureDetents(
|
|
269
|
+
peekHeight: Int,
|
|
270
|
+
halfExpandedRatio: Float,
|
|
271
|
+
expandedOffset: Int,
|
|
272
|
+
fitToContents: Boolean,
|
|
273
|
+
skipCollapsed: Boolean = false,
|
|
274
|
+
animate: Boolean = false
|
|
275
|
+
) {
|
|
276
|
+
val behavior = this.behavior ?: return
|
|
277
|
+
|
|
278
|
+
behavior.apply {
|
|
279
|
+
isFitToContents = fitToContents
|
|
280
|
+
this.skipCollapsed = skipCollapsed
|
|
281
|
+
setPeekHeight(peekHeight, animate)
|
|
282
|
+
this.halfExpandedRatio = halfExpandedRatio.coerceIn(0f, 0.999f)
|
|
283
|
+
this.expandedOffset = expandedOffset
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Set the behavior state.
|
|
289
|
+
*/
|
|
290
|
+
fun setState(state: Int) {
|
|
291
|
+
behavior?.state = state
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Update draggable state.
|
|
296
|
+
*/
|
|
297
|
+
fun updateDraggable(enabled: Boolean) {
|
|
298
|
+
draggable = enabled
|
|
299
|
+
behavior?.isDraggable = enabled
|
|
300
|
+
if (isAdded) setupGrabber()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// MARK: - Helpers
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
private fun getDefaultBackgroundColor(context: ThemedReactContext): Int {
|
|
308
|
+
val typedValue = TypedValue()
|
|
309
|
+
return if (context.theme.resolveAttribute(
|
|
310
|
+
com.google.android.material.R.attr.colorSurfaceContainerLow,
|
|
311
|
+
typedValue,
|
|
312
|
+
true
|
|
313
|
+
)
|
|
314
|
+
) {
|
|
315
|
+
typedValue.data
|
|
316
|
+
} else {
|
|
317
|
+
Color.WHITE
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<resources>
|
|
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
|
-
</style>
|
|
8
|
-
|
|
9
3
|
<!-- Fast fade out animation for hiding sheet when rn-screen is presented -->
|
|
10
4
|
<style name="TrueSheetFastFadeOut" parent="Animation.AppCompat.Dialog">
|
|
11
5
|
<item name="android:windowEnterAnimation">@null</item>
|
|
@@ -14,7 +8,7 @@
|
|
|
14
8
|
|
|
15
9
|
<!-- Default BottomSheetDialog style with programmatic animations -->
|
|
16
10
|
<style name="TrueSheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
|
|
17
|
-
<item name="android:windowAnimationStyle">@
|
|
11
|
+
<item name="android:windowAnimationStyle">@null</item>
|
|
18
12
|
</style>
|
|
19
13
|
|
|
20
14
|
<!-- BottomSheetDialog style with edge-to-edge and programmatic animations -->
|
|
@@ -22,6 +16,6 @@
|
|
|
22
16
|
<item name="android:windowIsFloating">false</item>
|
|
23
17
|
<item name="enableEdgeToEdge">true</item>
|
|
24
18
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
|
25
|
-
<item name="android:windowAnimationStyle">@
|
|
19
|
+
<item name="android:windowAnimationStyle">@null</item>
|
|
26
20
|
</style>
|
|
27
21
|
</resources>
|
package/package.json
CHANGED