@onekeyfe/react-native-native-sheet 3.0.129 → 3.0.131
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/NativeSheet.podspec +1 -0
- package/README.md +41 -8
- package/android/src/main/java/com/onekey/nativesheet/NativeSheetView.kt +610 -41
- package/ios/NativeSheetContainerView.swift +355 -46
- package/lib/module/NativeSheetHeight.js +43 -0
- package/lib/module/NativeSheetHeight.js.map +1 -0
- package/lib/module/NativeSheetRegistry.js +181 -0
- package/lib/module/NativeSheetRegistry.js.map +1 -0
- package/lib/module/index.js +180 -21
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeSheetHeight.d.ts +20 -0
- package/lib/typescript/src/NativeSheetHeight.d.ts.map +1 -0
- package/lib/typescript/src/NativeSheetRegistry.d.ts +22 -0
- package/lib/typescript/src/NativeSheetRegistry.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +38 -12
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeSheetHeight.ts +66 -0
- package/src/NativeSheetRegistry.ts +210 -0
- package/src/index.tsx +294 -20
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
package com.onekey.nativesheet
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.animation.Animator
|
|
5
|
+
import android.animation.AnimatorListenerAdapter
|
|
6
|
+
import android.animation.ValueAnimator
|
|
4
7
|
import android.graphics.Color
|
|
8
|
+
import android.graphics.Rect
|
|
5
9
|
import android.graphics.drawable.GradientDrawable
|
|
10
|
+
import android.os.Build
|
|
6
11
|
import android.view.Gravity
|
|
7
12
|
import android.view.KeyEvent
|
|
8
13
|
import android.view.MotionEvent
|
|
9
14
|
import android.view.View
|
|
10
15
|
import android.view.ViewGroup
|
|
16
|
+
import android.view.ViewTreeObserver
|
|
17
|
+
import android.view.Window
|
|
18
|
+
import android.view.WindowManager
|
|
19
|
+
import android.view.animation.AccelerateDecelerateInterpolator
|
|
11
20
|
import android.widget.FrameLayout
|
|
21
|
+
import androidx.core.view.ViewCompat
|
|
22
|
+
import androidx.core.view.WindowCompat
|
|
23
|
+
import androidx.core.view.WindowInsetsAnimationCompat
|
|
24
|
+
import androidx.core.view.WindowInsetsCompat
|
|
25
|
+
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
|
12
26
|
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
13
27
|
import com.facebook.react.config.ReactFeatureFlags
|
|
14
28
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
@@ -23,37 +37,111 @@ import kotlin.math.roundToInt
|
|
|
23
37
|
|
|
24
38
|
internal object NativeSheetStack {
|
|
25
39
|
private val active = mutableListOf<WeakReference<NativeSheetView>>()
|
|
40
|
+
private val pending = mutableListOf<WeakReference<NativeSheetView>>()
|
|
41
|
+
private var presenting: WeakReference<NativeSheetView>? = null
|
|
42
|
+
private val deferredDismissals =
|
|
43
|
+
mutableListOf<Triple<WeakReference<NativeSheetView>, String, Boolean>>()
|
|
44
|
+
private var transitioning = false
|
|
26
45
|
|
|
27
46
|
private fun compact() {
|
|
28
47
|
active.removeAll { it.get() == null }
|
|
48
|
+
pending.removeAll { it.get() == null }
|
|
49
|
+
deferredDismissals.removeAll { it.first.get() == null }
|
|
29
50
|
}
|
|
30
51
|
|
|
31
52
|
fun present(view: NativeSheetView) {
|
|
32
53
|
compact()
|
|
33
|
-
if (active.any { it.get() === view })
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
if (active.any { it.get() === view } || pending.any { it.get() === view }) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
pending.add(WeakReference(view))
|
|
58
|
+
processPending()
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
fun dismiss(view: NativeSheetView, reason: String, animated: Boolean) {
|
|
62
|
+
pending.removeAll { it.get() == null || it.get() === view }
|
|
63
|
+
if (transitioning) {
|
|
64
|
+
deferredDismissals.removeAll { it.first.get() == null || it.first.get() === view }
|
|
65
|
+
deferredDismissals.add(Triple(WeakReference(view), reason, animated))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
39
68
|
compact()
|
|
40
69
|
val index = active.indexOfFirst { it.get() === view }
|
|
41
70
|
if (index < 0) {
|
|
42
|
-
view.dismissDialogInternal(reason, animated)
|
|
71
|
+
view.dismissDialogInternal(reason, animated) { processDeferredDismissalOrPending() }
|
|
43
72
|
return
|
|
44
73
|
}
|
|
45
74
|
|
|
46
75
|
val targets = active.subList(index, active.size).mapNotNull { it.get() }.asReversed()
|
|
47
76
|
active.subList(index, active.size).clear()
|
|
48
|
-
targets
|
|
49
|
-
val targetReason = if (target === view) reason else if (reason == "security") reason else "system"
|
|
50
|
-
target.dismissDialogInternal(targetReason, animated)
|
|
51
|
-
}
|
|
77
|
+
dismissNext(targets, view, reason, animated)
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
fun didDismiss(view: NativeSheetView) {
|
|
55
81
|
active.removeAll { it.get() == null || it.get() === view }
|
|
56
82
|
}
|
|
83
|
+
|
|
84
|
+
fun isTop(view: NativeSheetView): Boolean {
|
|
85
|
+
compact()
|
|
86
|
+
return (presenting?.get() ?: active.lastOrNull()?.get()) === view
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun dismissNext(
|
|
90
|
+
targets: List<NativeSheetView>,
|
|
91
|
+
requestedView: NativeSheetView,
|
|
92
|
+
reason: String,
|
|
93
|
+
animated: Boolean,
|
|
94
|
+
) {
|
|
95
|
+
val target = targets.firstOrNull()
|
|
96
|
+
if (target == null) {
|
|
97
|
+
transitioning = false
|
|
98
|
+
processDeferredDismissalOrPending()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
val targetReason = when {
|
|
102
|
+
target === requestedView -> reason
|
|
103
|
+
reason == "security" -> reason
|
|
104
|
+
else -> "system"
|
|
105
|
+
}
|
|
106
|
+
transitioning = true
|
|
107
|
+
target.dismissDialogInternal(targetReason, animated) {
|
|
108
|
+
transitioning = false
|
|
109
|
+
dismissNext(targets.drop(1), requestedView, reason, animated)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private fun processPending() {
|
|
114
|
+
compact()
|
|
115
|
+
if (transitioning) return
|
|
116
|
+
val view = pending.firstOrNull()?.get() ?: return
|
|
117
|
+
pending.removeAt(0)
|
|
118
|
+
transitioning = true
|
|
119
|
+
presenting = WeakReference(view)
|
|
120
|
+
if (view.showDialogInternal {
|
|
121
|
+
presenting = null
|
|
122
|
+
transitioning = false
|
|
123
|
+
processDeferredDismissalOrPending()
|
|
124
|
+
}) {
|
|
125
|
+
active.add(WeakReference(view))
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
presenting = null
|
|
129
|
+
view.finishFailedPresentation()
|
|
130
|
+
transitioning = false
|
|
131
|
+
processDeferredDismissalOrPending()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun processDeferredDismissalOrPending() {
|
|
135
|
+
compact()
|
|
136
|
+
if (transitioning) return
|
|
137
|
+
while (deferredDismissals.isNotEmpty()) {
|
|
138
|
+
val request = deferredDismissals.removeAt(0)
|
|
139
|
+
val view = request.first.get() ?: continue
|
|
140
|
+
dismiss(view, request.second, request.third)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
processPending()
|
|
144
|
+
}
|
|
57
145
|
}
|
|
58
146
|
|
|
59
147
|
/**
|
|
@@ -149,6 +237,23 @@ class NativeSheetView(
|
|
|
149
237
|
private var dismissedForCurrentOpen = false
|
|
150
238
|
private var pendingDismissReason = "system"
|
|
151
239
|
private var dismissNotified = false
|
|
240
|
+
private var dismissalCompletion: (() -> Unit)? = null
|
|
241
|
+
private var dimAnimator: ValueAnimator? = null
|
|
242
|
+
private var heightAnimator: ValueAnimator? = null
|
|
243
|
+
private var dialogConfigured = false
|
|
244
|
+
private var presentedHeightPx = 0
|
|
245
|
+
private var navigationBarInsetPx = 0
|
|
246
|
+
private var imeInsetPx = 0
|
|
247
|
+
private var reportedImeInsetPx = 0
|
|
248
|
+
private var visibleFrameImeInsetPx = 0
|
|
249
|
+
private var imeAnimationRunning = false
|
|
250
|
+
private var extendsIntoNavigationBar = false
|
|
251
|
+
private var windowInsetHost: View? = null
|
|
252
|
+
private var imeGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
|
253
|
+
private var imeInsetFallbackRunnable: Runnable? = null
|
|
254
|
+
private var navigationBarBackgroundView: View? = null
|
|
255
|
+
private var navigationBarBackgroundColor = Color.TRANSPARENT
|
|
256
|
+
private var preImeBottomSheetBottomPx: Int? = null
|
|
152
257
|
|
|
153
258
|
init {
|
|
154
259
|
visibility = INVISIBLE
|
|
@@ -180,6 +285,7 @@ class NativeSheetView(
|
|
|
180
285
|
fun commitConfiguration() {
|
|
181
286
|
val openChanged = committedOpen != open
|
|
182
287
|
committedOpen = open
|
|
288
|
+
if (openChanged && open) dismissNotified = false
|
|
183
289
|
if (!open) dismissedForCurrentOpen = false
|
|
184
290
|
|
|
185
291
|
if (securityBlocked) {
|
|
@@ -192,34 +298,24 @@ class NativeSheetView(
|
|
|
192
298
|
NativeSheetStack.dismiss(this, "programmatic", true)
|
|
193
299
|
} else if ((openChanged || dialog == null) && !dismissedForCurrentOpen) {
|
|
194
300
|
NativeSheetStack.present(this)
|
|
301
|
+
} else if (!dismissedForCurrentOpen) {
|
|
302
|
+
updatePresentedHeight(true)
|
|
195
303
|
}
|
|
196
304
|
}
|
|
197
305
|
|
|
198
306
|
fun hasDialog(): Boolean = dialog != null
|
|
199
307
|
|
|
200
|
-
internal fun showDialogInternal() {
|
|
201
|
-
if (dialog != null || securityBlocked || !open) return
|
|
202
|
-
val activity = reactContext.currentActivity ?: return
|
|
203
|
-
if (activity.isFinishing || activity.isDestroyed) return
|
|
308
|
+
internal fun showDialogInternal(presentationCompletion: () -> Unit): Boolean {
|
|
309
|
+
if (dialog != null || securityBlocked || !open) return false
|
|
310
|
+
val activity = reactContext.currentActivity ?: return false
|
|
311
|
+
if (activity.isFinishing || activity.isDestroyed) return false
|
|
204
312
|
|
|
205
313
|
val heightPx = dpToPx(sheetHeight.coerceAtLeast(1.0))
|
|
314
|
+
val resolvedSheetBackgroundColor = sheetBackgroundColor ?: Color.WHITE
|
|
206
315
|
val content = NativeSheetDialogRootView(reactContext).apply {
|
|
207
316
|
eventDispatcher = this@NativeSheetView.eventDispatcher
|
|
208
317
|
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, heightPx)
|
|
209
|
-
background =
|
|
210
|
-
shape = GradientDrawable.RECTANGLE
|
|
211
|
-
setColor(sheetBackgroundColor ?: Color.WHITE)
|
|
212
|
-
cornerRadii = floatArrayOf(
|
|
213
|
-
dpToPx(this@NativeSheetView.cornerRadius).toFloat(),
|
|
214
|
-
dpToPx(this@NativeSheetView.cornerRadius).toFloat(),
|
|
215
|
-
dpToPx(this@NativeSheetView.cornerRadius).toFloat(),
|
|
216
|
-
dpToPx(this@NativeSheetView.cornerRadius).toFloat(),
|
|
217
|
-
0f,
|
|
218
|
-
0f,
|
|
219
|
-
0f,
|
|
220
|
-
0f,
|
|
221
|
-
)
|
|
222
|
-
}
|
|
318
|
+
background = createSheetBackground(resolvedSheetBackgroundColor)
|
|
223
319
|
clipToOutline = true
|
|
224
320
|
}
|
|
225
321
|
contentChild?.let { attachChild(content, it, 0) }
|
|
@@ -227,46 +323,150 @@ class NativeSheetView(
|
|
|
227
323
|
|
|
228
324
|
pendingDismissReason = "system"
|
|
229
325
|
dismissNotified = false
|
|
326
|
+
dialogConfigured = false
|
|
327
|
+
presentedHeightPx = heightPx
|
|
328
|
+
extendsIntoNavigationBar = Color.alpha(resolvedSheetBackgroundColor) > 0
|
|
329
|
+
navigationBarBackgroundColor = resolvedSheetBackgroundColor
|
|
230
330
|
val nextDialog = BottomSheetDialog(activity).apply {
|
|
231
331
|
setContentView(content)
|
|
232
332
|
setCancelable(dismissOnPanDown || dismissOnBackPress || dismissOnBackdropPress)
|
|
233
|
-
setCanceledOnTouchOutside(
|
|
333
|
+
setCanceledOnTouchOutside(false)
|
|
234
334
|
dismissWithAnimation = true
|
|
335
|
+
window?.apply {
|
|
336
|
+
setDimAmount(0f)
|
|
337
|
+
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
|
338
|
+
navigationBarColor = Color.TRANSPARENT
|
|
339
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
340
|
+
isNavigationBarContrastEnforced = false
|
|
341
|
+
}
|
|
342
|
+
configureEdgeToEdgeWindow(this)
|
|
343
|
+
}
|
|
235
344
|
setOnKeyListener { _, keyCode, event ->
|
|
236
345
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
|
346
|
+
if (hideImeIfVisible()) return@setOnKeyListener true
|
|
237
347
|
if (!dismissOnBackPress) return@setOnKeyListener true
|
|
238
|
-
|
|
348
|
+
NativeSheetStack.dismiss(this@NativeSheetView, "back", true)
|
|
349
|
+
return@setOnKeyListener true
|
|
239
350
|
}
|
|
240
351
|
false
|
|
241
352
|
}
|
|
242
|
-
setOnCancelListener {
|
|
243
|
-
if (pendingDismissReason == "system") pendingDismissReason = "backdrop"
|
|
244
|
-
}
|
|
245
353
|
setOnDismissListener {
|
|
246
354
|
handleDialogDismissed()
|
|
247
355
|
}
|
|
248
356
|
setOnShowListener {
|
|
249
|
-
configureShownDialog(
|
|
357
|
+
configureShownDialog(
|
|
358
|
+
this,
|
|
359
|
+
resolvedSheetBackgroundColor,
|
|
360
|
+
presentationCompletion,
|
|
361
|
+
)
|
|
250
362
|
}
|
|
251
363
|
}
|
|
252
364
|
|
|
253
365
|
dialogContent = content
|
|
254
366
|
dialog = nextDialog
|
|
255
367
|
nextDialog.show()
|
|
368
|
+
return true
|
|
256
369
|
}
|
|
257
370
|
|
|
258
|
-
private fun configureShownDialog(
|
|
371
|
+
private fun configureShownDialog(
|
|
372
|
+
sheetDialog: BottomSheetDialog,
|
|
373
|
+
resolvedSheetBackgroundColor: Int,
|
|
374
|
+
presentationCompletion: () -> Unit,
|
|
375
|
+
) {
|
|
259
376
|
sheetDialog.window?.apply {
|
|
260
|
-
|
|
377
|
+
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
|
261
378
|
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
379
|
+
configureEdgeToEdgeWindow(this)
|
|
380
|
+
}
|
|
381
|
+
animateDimAmount(
|
|
382
|
+
sheetDialog,
|
|
383
|
+
dimAmount.coerceIn(0.0, 1.0).toFloat(),
|
|
384
|
+
true,
|
|
385
|
+
) {
|
|
386
|
+
if (open && !securityBlocked) {
|
|
387
|
+
onPresented?.invoke(pxToDp(presentedHeightPx))
|
|
388
|
+
}
|
|
389
|
+
presentationCompletion()
|
|
262
390
|
}
|
|
391
|
+
sheetDialog
|
|
392
|
+
.findViewById<View>(com.google.android.material.R.id.touch_outside)
|
|
393
|
+
?.setOnClickListener {
|
|
394
|
+
if (dismissOnBackdropPress) {
|
|
395
|
+
NativeSheetStack.dismiss(this, "backdrop", true)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
263
398
|
val bottomSheet = sheetDialog.findViewById<FrameLayout>(
|
|
264
399
|
com.google.android.material.R.id.design_bottom_sheet,
|
|
265
400
|
) ?: return
|
|
266
|
-
bottomSheet.
|
|
267
|
-
bottomSheet.
|
|
401
|
+
bottomSheet.clipChildren = true
|
|
402
|
+
bottomSheet.clipToPadding = true
|
|
403
|
+
bottomSheet.setPadding(0, 0, 0, 0)
|
|
404
|
+
ViewCompat.setOnApplyWindowInsetsListener(bottomSheet) { view, insets ->
|
|
405
|
+
view.setPadding(0, 0, 0, 0)
|
|
406
|
+
insets
|
|
407
|
+
}
|
|
408
|
+
val container = sheetDialog.findViewById<FrameLayout>(
|
|
409
|
+
com.google.android.material.R.id.container,
|
|
410
|
+
)
|
|
411
|
+
val coordinator = sheetDialog.findViewById<View>(
|
|
412
|
+
com.google.android.material.R.id.coordinator,
|
|
413
|
+
)
|
|
414
|
+
container?.fitsSystemWindows = false
|
|
415
|
+
coordinator?.fitsSystemWindows = false
|
|
416
|
+
val insetHost = sheetDialog.window?.decorView ?: container ?: bottomSheet
|
|
417
|
+
clearWindowInsetsObservation()
|
|
418
|
+
windowInsetHost = insetHost
|
|
419
|
+
navigationBarInsetPx = getNavigationBarInset(insetHost)
|
|
420
|
+
updateNavigationBarBackground(sheetDialog)
|
|
421
|
+
ViewCompat.setOnApplyWindowInsetsListener(insetHost) { _, insets ->
|
|
422
|
+
applyNavigationBarInset(sheetDialog, bottomSheet, insets)
|
|
423
|
+
scheduleImeInsetFallback(insetHost, bottomSheet)
|
|
424
|
+
insets
|
|
425
|
+
}
|
|
426
|
+
ViewCompat.setWindowInsetsAnimationCallback(
|
|
427
|
+
insetHost,
|
|
428
|
+
object : WindowInsetsAnimationCompat.Callback(
|
|
429
|
+
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE,
|
|
430
|
+
) {
|
|
431
|
+
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
|
|
432
|
+
if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
|
|
433
|
+
cancelImeInsetFallback()
|
|
434
|
+
imeAnimationRunning = true
|
|
435
|
+
}
|
|
436
|
+
super.onPrepare(animation)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
override fun onProgress(
|
|
440
|
+
insets: WindowInsetsCompat,
|
|
441
|
+
runningAnimations: MutableList<WindowInsetsAnimationCompat>,
|
|
442
|
+
): WindowInsetsCompat {
|
|
443
|
+
cancelImeInsetFallback()
|
|
444
|
+
applyNavigationBarInset(sheetDialog, bottomSheet, insets)
|
|
445
|
+
applyImeInset(bottomSheet, insets)
|
|
446
|
+
return insets
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
450
|
+
super.onEnd(animation)
|
|
451
|
+
if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
|
|
452
|
+
cancelImeInsetFallback()
|
|
453
|
+
imeAnimationRunning = false
|
|
454
|
+
ViewCompat.getRootWindowInsets(insetHost)?.let { insets ->
|
|
455
|
+
applyNavigationBarInset(sheetDialog, bottomSheet, insets)
|
|
456
|
+
applyImeInset(bottomSheet, insets)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
)
|
|
462
|
+
imeGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
463
|
+
scheduleImeInsetFallback(insetHost, bottomSheet)
|
|
464
|
+
}.also { listener ->
|
|
465
|
+
insetHost.viewTreeObserver.addOnGlobalLayoutListener(listener)
|
|
466
|
+
}
|
|
467
|
+
ViewCompat.requestApplyInsets(insetHost)
|
|
468
|
+
bottomSheet.background = createSheetBackground(resolvedSheetBackgroundColor)
|
|
268
469
|
sheetDialog.behavior.apply {
|
|
269
|
-
peekHeight = heightPx
|
|
270
470
|
isFitToContents = true
|
|
271
471
|
isHideable = dismissOnPanDown
|
|
272
472
|
isDraggable = dismissOnPanDown
|
|
@@ -279,27 +479,366 @@ class NativeSheetView(
|
|
|
279
479
|
}
|
|
280
480
|
}
|
|
281
481
|
|
|
282
|
-
override fun onSlide(bottomSheet: View, slideOffset: Float)
|
|
482
|
+
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
|
483
|
+
if (slideOffset < 1f) {
|
|
484
|
+
dimAnimator?.cancel()
|
|
485
|
+
sheetDialog.window?.setDimAmount(
|
|
486
|
+
dimAmount.coerceIn(0.0, 1.0).toFloat() * slideOffset.coerceIn(0f, 1f),
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
dialogConfigured = true
|
|
493
|
+
applyPresentedHeight(
|
|
494
|
+
sheetDialog,
|
|
495
|
+
bottomSheet,
|
|
496
|
+
dpToPx(sheetHeight.coerceAtLeast(1.0)),
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private fun updatePresentedHeight(animated: Boolean) {
|
|
501
|
+
val sheetDialog = dialog ?: return
|
|
502
|
+
val targetHeightPx = dpToPx(sheetHeight.coerceAtLeast(1.0))
|
|
503
|
+
if (!dialogConfigured) {
|
|
504
|
+
presentedHeightPx = targetHeightPx
|
|
505
|
+
dialogContent?.let { content ->
|
|
506
|
+
content.layoutParams = content.layoutParams.apply {
|
|
507
|
+
height = targetHeightPx
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
val bottomSheet = sheetDialog.findViewById<FrameLayout>(
|
|
513
|
+
com.google.android.material.R.id.design_bottom_sheet,
|
|
514
|
+
) ?: return
|
|
515
|
+
val currentHeightPx = presentedHeightPx
|
|
516
|
+
if (currentHeightPx == targetHeightPx) {
|
|
517
|
+
applyPresentedHeight(sheetDialog, bottomSheet, targetHeightPx)
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
heightAnimator?.cancel()
|
|
522
|
+
if (!animated) {
|
|
523
|
+
applyPresentedHeight(sheetDialog, bottomSheet, targetHeightPx)
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
heightAnimator = ValueAnimator.ofInt(currentHeightPx, targetHeightPx).apply {
|
|
527
|
+
duration = 280
|
|
528
|
+
interpolator = FastOutSlowInInterpolator()
|
|
529
|
+
addUpdateListener { animator ->
|
|
530
|
+
applyPresentedHeight(sheetDialog, bottomSheet, animator.animatedValue as Int)
|
|
531
|
+
}
|
|
532
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
533
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
534
|
+
if (heightAnimator === animation) {
|
|
535
|
+
heightAnimator = null
|
|
536
|
+
}
|
|
537
|
+
}
|
|
283
538
|
})
|
|
539
|
+
start()
|
|
284
540
|
}
|
|
285
|
-
onPresented?.invoke(pxToDp(heightPx))
|
|
286
541
|
}
|
|
287
542
|
|
|
288
|
-
|
|
289
|
-
|
|
543
|
+
private fun applyPresentedHeight(
|
|
544
|
+
sheetDialog: BottomSheetDialog,
|
|
545
|
+
bottomSheet: FrameLayout,
|
|
546
|
+
heightPx: Int,
|
|
547
|
+
) {
|
|
548
|
+
val clampedHeightPx = heightPx.coerceAtLeast(1)
|
|
549
|
+
presentedHeightPx = clampedHeightPx
|
|
550
|
+
dialogContent?.let { content ->
|
|
551
|
+
content.layoutParams = content.layoutParams.apply {
|
|
552
|
+
height = clampedHeightPx
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
val navigationBarExtensionPx = if (extendsIntoNavigationBar) {
|
|
556
|
+
navigationBarInsetPx
|
|
557
|
+
} else {
|
|
558
|
+
0
|
|
559
|
+
}
|
|
560
|
+
bottomSheet.layoutParams = bottomSheet.layoutParams.apply {
|
|
561
|
+
height = clampedHeightPx + navigationBarExtensionPx
|
|
562
|
+
}
|
|
563
|
+
sheetDialog.behavior.peekHeight = clampedHeightPx + navigationBarExtensionPx
|
|
564
|
+
dialogContent?.requestLayout()
|
|
565
|
+
bottomSheet.requestLayout()
|
|
566
|
+
applyImeOffset(bottomSheet)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private fun getNavigationBarInset(view: View): Int =
|
|
570
|
+
ViewCompat.getRootWindowInsets(view)
|
|
571
|
+
?.let { insets ->
|
|
572
|
+
maxOf(
|
|
573
|
+
insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom,
|
|
574
|
+
insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars()).bottom,
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
?: 0
|
|
578
|
+
|
|
579
|
+
private fun applyNavigationBarInset(
|
|
580
|
+
sheetDialog: BottomSheetDialog,
|
|
581
|
+
bottomSheet: FrameLayout,
|
|
582
|
+
insets: WindowInsetsCompat,
|
|
583
|
+
) {
|
|
584
|
+
val nextNavigationBarInsetPx = maxOf(
|
|
585
|
+
insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom,
|
|
586
|
+
insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars()).bottom,
|
|
587
|
+
)
|
|
588
|
+
if (navigationBarInsetPx == nextNavigationBarInsetPx) return
|
|
589
|
+
navigationBarInsetPx = nextNavigationBarInsetPx
|
|
590
|
+
updateNavigationBarBackground(sheetDialog)
|
|
591
|
+
applyPresentedHeight(sheetDialog, bottomSheet, presentedHeightPx)
|
|
592
|
+
applyImeOffset(bottomSheet)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private fun updateNavigationBarBackground(sheetDialog: BottomSheetDialog) {
|
|
596
|
+
if (!extendsIntoNavigationBar || navigationBarInsetPx <= 0) {
|
|
597
|
+
navigationBarBackgroundView?.let { view ->
|
|
598
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
599
|
+
}
|
|
600
|
+
navigationBarBackgroundView = null
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
val decorView = sheetDialog.window?.decorView as? ViewGroup ?: return
|
|
604
|
+
val backgroundView = navigationBarBackgroundView ?: View(reactContext).apply {
|
|
605
|
+
isClickable = false
|
|
606
|
+
isFocusable = false
|
|
607
|
+
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
608
|
+
decorView.addView(this)
|
|
609
|
+
navigationBarBackgroundView = this
|
|
610
|
+
}
|
|
611
|
+
backgroundView.setBackgroundColor(navigationBarBackgroundColor)
|
|
612
|
+
backgroundView.layoutParams = FrameLayout.LayoutParams(
|
|
613
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
614
|
+
navigationBarInsetPx,
|
|
615
|
+
Gravity.BOTTOM,
|
|
616
|
+
)
|
|
617
|
+
backgroundView.bringToFront()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private fun applyImeInset(
|
|
621
|
+
bottomSheet: FrameLayout,
|
|
622
|
+
insets: WindowInsetsCompat?,
|
|
623
|
+
) {
|
|
624
|
+
val insetHost = windowInsetHost ?: return
|
|
625
|
+
reportedImeInsetPx = insets
|
|
626
|
+
?.getInsets(WindowInsetsCompat.Type.ime())
|
|
627
|
+
?.bottom
|
|
628
|
+
?: 0
|
|
629
|
+
val visibleFrame = Rect()
|
|
630
|
+
insetHost.getWindowVisibleDisplayFrame(visibleFrame)
|
|
631
|
+
val hostLocation = IntArray(2)
|
|
632
|
+
insetHost.getLocationOnScreen(hostLocation)
|
|
633
|
+
val obscuredBottomPx =
|
|
634
|
+
(hostLocation[1] + insetHost.height - visibleFrame.bottom).coerceAtLeast(0)
|
|
635
|
+
val minimumKeyboardInsetPx = dpToPx(100.0)
|
|
636
|
+
visibleFrameImeInsetPx = if (obscuredBottomPx >= minimumKeyboardInsetPx) {
|
|
637
|
+
obscuredBottomPx
|
|
638
|
+
} else {
|
|
639
|
+
0
|
|
640
|
+
}
|
|
641
|
+
val imeVisible =
|
|
642
|
+
insets?.isVisible(WindowInsetsCompat.Type.ime()) == true ||
|
|
643
|
+
reportedImeInsetPx >= minimumKeyboardInsetPx ||
|
|
644
|
+
visibleFrameImeInsetPx > 0
|
|
645
|
+
imeInsetPx = if (imeVisible) {
|
|
646
|
+
maxOf(reportedImeInsetPx, visibleFrameImeInsetPx)
|
|
647
|
+
} else {
|
|
648
|
+
0
|
|
649
|
+
}
|
|
650
|
+
applyImeOffset(bottomSheet)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private fun applyImeOffset(bottomSheet: FrameLayout) {
|
|
654
|
+
val sheetWindow = dialog?.window
|
|
655
|
+
val decorView = sheetWindow?.decorView
|
|
656
|
+
val isTop = NativeSheetStack.isTop(this)
|
|
657
|
+
val behavior = BottomSheetBehavior.from(bottomSheet)
|
|
658
|
+
if (decorView == null || !isTop || imeInsetPx <= 0) {
|
|
659
|
+
bottomSheet.translationY = 0f
|
|
660
|
+
val originalBottomPx = preImeBottomSheetBottomPx
|
|
661
|
+
if (originalBottomPx != null) {
|
|
662
|
+
val layoutHeight = bottomSheet.layoutParams.height.takeIf { it > 0 }
|
|
663
|
+
?: bottomSheet.height
|
|
664
|
+
val restoredTop = originalBottomPx - layoutHeight
|
|
665
|
+
if (isTop && imeAnimationRunning) {
|
|
666
|
+
behavior.isFitToContents = false
|
|
667
|
+
behavior.expandedOffset = restoredTop.coerceAtLeast(0)
|
|
668
|
+
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
669
|
+
ViewCompat.offsetTopAndBottom(bottomSheet, restoredTop - bottomSheet.top)
|
|
670
|
+
bottomSheet.invalidate()
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
ViewCompat.offsetTopAndBottom(bottomSheet, restoredTop - bottomSheet.top)
|
|
674
|
+
preImeBottomSheetBottomPx = null
|
|
675
|
+
}
|
|
676
|
+
if (isTop || originalBottomPx != null) {
|
|
677
|
+
behavior.isFitToContents = true
|
|
678
|
+
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
679
|
+
bottomSheet.requestLayout()
|
|
680
|
+
}
|
|
681
|
+
return
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
val layoutHeight = bottomSheet.layoutParams.height.takeIf { it > 0 }
|
|
685
|
+
?: bottomSheet.height
|
|
686
|
+
val originalBottomPx = preImeBottomSheetBottomPx
|
|
687
|
+
?: (bottomSheet.top + layoutHeight).also {
|
|
688
|
+
preImeBottomSheetBottomPx = it
|
|
689
|
+
}
|
|
690
|
+
val restingTop = (originalBottomPx - layoutHeight).coerceAtLeast(0)
|
|
691
|
+
val keyboardOcclusionPx =
|
|
692
|
+
(imeInsetPx - navigationBarInsetPx).coerceAtLeast(0)
|
|
693
|
+
val desiredTop = (restingTop - keyboardOcclusionPx).coerceAtLeast(0)
|
|
694
|
+
behavior.isFitToContents = false
|
|
695
|
+
behavior.expandedOffset = desiredTop
|
|
696
|
+
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
697
|
+
bottomSheet.translationY = 0f
|
|
698
|
+
ViewCompat.offsetTopAndBottom(bottomSheet, desiredTop - bottomSheet.top)
|
|
699
|
+
bottomSheet.invalidate()
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private fun clearWindowInsetsObservation() {
|
|
703
|
+
cancelImeInsetFallback()
|
|
704
|
+
val insetHost = windowInsetHost
|
|
705
|
+
val globalLayoutListener = imeGlobalLayoutListener
|
|
706
|
+
if (insetHost != null) {
|
|
707
|
+
ViewCompat.setOnApplyWindowInsetsListener(insetHost, null)
|
|
708
|
+
ViewCompat.setWindowInsetsAnimationCallback(insetHost, null)
|
|
709
|
+
if (globalLayoutListener != null && insetHost.viewTreeObserver.isAlive) {
|
|
710
|
+
insetHost.viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
windowInsetHost = null
|
|
714
|
+
imeGlobalLayoutListener = null
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private fun scheduleImeInsetFallback(
|
|
718
|
+
insetHost: View,
|
|
719
|
+
bottomSheet: FrameLayout,
|
|
720
|
+
) {
|
|
721
|
+
cancelImeInsetFallback()
|
|
722
|
+
val fallback = Runnable {
|
|
723
|
+
if (windowInsetHost !== insetHost || imeAnimationRunning) return@Runnable
|
|
724
|
+
imeInsetFallbackRunnable = null
|
|
725
|
+
applyImeInset(
|
|
726
|
+
bottomSheet,
|
|
727
|
+
ViewCompat.getRootWindowInsets(insetHost),
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
imeInsetFallbackRunnable = fallback
|
|
731
|
+
insetHost.postDelayed(fallback, 48)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private fun cancelImeInsetFallback() {
|
|
735
|
+
val fallback = imeInsetFallbackRunnable ?: return
|
|
736
|
+
windowInsetHost?.removeCallbacks(fallback)
|
|
737
|
+
imeInsetFallbackRunnable = null
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private fun configureEdgeToEdgeWindow(window: Window) {
|
|
741
|
+
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
742
|
+
window.addFlags(
|
|
743
|
+
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
|
|
744
|
+
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR,
|
|
745
|
+
)
|
|
746
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
747
|
+
window.attributes = window.attributes.apply {
|
|
748
|
+
setFitInsetsTypes(0)
|
|
749
|
+
setFitInsetsSides(0)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private fun hideImeIfVisible(): Boolean {
|
|
755
|
+
val insetHost = dialog?.window?.decorView ?: return false
|
|
756
|
+
val insets = ViewCompat.getRootWindowInsets(insetHost) ?: return false
|
|
757
|
+
if (!insets.isVisible(WindowInsetsCompat.Type.ime())) return false
|
|
758
|
+
val window = dialog?.window ?: return false
|
|
759
|
+
WindowCompat.getInsetsController(window, insetHost)
|
|
760
|
+
.hide(WindowInsetsCompat.Type.ime())
|
|
761
|
+
return true
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
internal fun dismissDialogInternal(
|
|
765
|
+
reason: String,
|
|
766
|
+
animated: Boolean,
|
|
767
|
+
completion: () -> Unit,
|
|
768
|
+
) {
|
|
769
|
+
val currentDialog = dialog
|
|
770
|
+
if (currentDialog == null) {
|
|
771
|
+
completion()
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
dismissalCompletion = completion
|
|
290
775
|
pendingDismissReason = reason
|
|
776
|
+
heightAnimator?.cancel()
|
|
777
|
+
heightAnimator = null
|
|
291
778
|
if (animated) {
|
|
292
779
|
currentDialog.dismissWithAnimation = true
|
|
293
780
|
} else {
|
|
294
781
|
currentDialog.dismissWithAnimation = false
|
|
295
782
|
}
|
|
783
|
+
animateDimAmount(currentDialog, 0f, animated)
|
|
296
784
|
currentDialog.dismiss()
|
|
297
785
|
}
|
|
298
786
|
|
|
787
|
+
private fun animateDimAmount(
|
|
788
|
+
sheetDialog: BottomSheetDialog,
|
|
789
|
+
target: Float,
|
|
790
|
+
animated: Boolean,
|
|
791
|
+
completion: () -> Unit = {},
|
|
792
|
+
) {
|
|
793
|
+
val window = sheetDialog.window
|
|
794
|
+
if (window == null) {
|
|
795
|
+
completion()
|
|
796
|
+
return
|
|
797
|
+
}
|
|
798
|
+
dimAnimator?.cancel()
|
|
799
|
+
if (!animated) {
|
|
800
|
+
window.setDimAmount(target)
|
|
801
|
+
completion()
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
dimAnimator = ValueAnimator.ofFloat(window.attributes.dimAmount, target).apply {
|
|
805
|
+
duration = 250
|
|
806
|
+
interpolator = AccelerateDecelerateInterpolator()
|
|
807
|
+
addUpdateListener { animator ->
|
|
808
|
+
window.setDimAmount(animator.animatedValue as Float)
|
|
809
|
+
}
|
|
810
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
811
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
812
|
+
completion()
|
|
813
|
+
}
|
|
814
|
+
})
|
|
815
|
+
start()
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
299
819
|
private fun handleDialogDismissed() {
|
|
300
820
|
val child = contentChild
|
|
821
|
+
clearWindowInsetsObservation()
|
|
822
|
+
navigationBarBackgroundView?.let { view ->
|
|
823
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
824
|
+
}
|
|
825
|
+
navigationBarBackgroundView = null
|
|
301
826
|
dialogContent = null
|
|
302
827
|
dialog = null
|
|
828
|
+
dialogConfigured = false
|
|
829
|
+
presentedHeightPx = 0
|
|
830
|
+
navigationBarInsetPx = 0
|
|
831
|
+
imeInsetPx = 0
|
|
832
|
+
reportedImeInsetPx = 0
|
|
833
|
+
visibleFrameImeInsetPx = 0
|
|
834
|
+
imeAnimationRunning = false
|
|
835
|
+
extendsIntoNavigationBar = false
|
|
836
|
+
navigationBarBackgroundColor = Color.TRANSPARENT
|
|
837
|
+
preImeBottomSheetBottomPx = null
|
|
838
|
+
dimAnimator?.cancel()
|
|
839
|
+
dimAnimator = null
|
|
840
|
+
heightAnimator?.cancel()
|
|
841
|
+
heightAnimator = null
|
|
303
842
|
child?.let { attachChild(this, it, 0) }
|
|
304
843
|
NativeSheetStack.didDismiss(this)
|
|
305
844
|
if (!dismissNotified) {
|
|
@@ -307,6 +846,21 @@ class NativeSheetView(
|
|
|
307
846
|
dismissedForCurrentOpen = committedOpen
|
|
308
847
|
onDismiss?.invoke(pendingDismissReason)
|
|
309
848
|
}
|
|
849
|
+
val completion = dismissalCompletion
|
|
850
|
+
dismissalCompletion = null
|
|
851
|
+
completion?.invoke()
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
internal fun finishFailedPresentation() {
|
|
855
|
+
if (dismissNotified) return
|
|
856
|
+
dismissNotified = true
|
|
857
|
+
dismissedForCurrentOpen = committedOpen
|
|
858
|
+
val reason = when {
|
|
859
|
+
securityBlocked -> "security"
|
|
860
|
+
!open -> "programmatic"
|
|
861
|
+
else -> "system"
|
|
862
|
+
}
|
|
863
|
+
onDismiss?.invoke(reason)
|
|
310
864
|
}
|
|
311
865
|
|
|
312
866
|
fun onDropViewInstance() {
|
|
@@ -325,6 +879,21 @@ class NativeSheetView(
|
|
|
325
879
|
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
326
880
|
}
|
|
327
881
|
|
|
882
|
+
private fun createSheetBackground(color: Int) = GradientDrawable().apply {
|
|
883
|
+
shape = GradientDrawable.RECTANGLE
|
|
884
|
+
setColor(color)
|
|
885
|
+
cornerRadii = floatArrayOf(
|
|
886
|
+
dpToPx(this@NativeSheetView.cornerRadius).toFloat(),
|
|
887
|
+
dpToPx(this@NativeSheetView.cornerRadius).toFloat(),
|
|
888
|
+
dpToPx(this@NativeSheetView.cornerRadius).toFloat(),
|
|
889
|
+
dpToPx(this@NativeSheetView.cornerRadius).toFloat(),
|
|
890
|
+
0f,
|
|
891
|
+
0f,
|
|
892
|
+
0f,
|
|
893
|
+
0f,
|
|
894
|
+
)
|
|
895
|
+
}
|
|
896
|
+
|
|
328
897
|
private fun createHandleLayoutParams() = LayoutParams(dpToPx(36.0), dpToPx(5.0)).apply {
|
|
329
898
|
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
|
330
899
|
topMargin = dpToPx(8.0)
|