@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.
@@ -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 }) return
34
- view.showDialogInternal()
35
- if (view.hasDialog()) active.add(WeakReference(view))
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.forEach { target ->
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 = GradientDrawable().apply {
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(dismissOnBackdropPress)
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
- pendingDismissReason = "back"
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(this, heightPx)
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(sheetDialog: BottomSheetDialog, heightPx: Int) {
371
+ private fun configureShownDialog(
372
+ sheetDialog: BottomSheetDialog,
373
+ resolvedSheetBackgroundColor: Int,
374
+ presentationCompletion: () -> Unit,
375
+ ) {
259
376
  sheetDialog.window?.apply {
260
- setDimAmount(dimAmount.coerceIn(0.0, 1.0).toFloat())
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.layoutParams = bottomSheet.layoutParams.apply { height = heightPx }
267
- bottomSheet.setBackgroundColor(Color.TRANSPARENT)
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) = Unit
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
- internal fun dismissDialogInternal(reason: String, animated: Boolean) {
289
- val currentDialog = dialog ?: return
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)