@capgo/capacitor-pretty-toast 8.1.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.
Files changed (85) hide show
  1. package/CapgoCapacitorPrettyToast.podspec +17 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +28 -0
  4. package/README.md +341 -0
  5. package/android/build.gradle +71 -0
  6. package/android/src/main/AndroidManifest.xml +6 -0
  7. package/android/src/main/java/com/toast/PrettyToastPlugin.kt +197 -0
  8. package/android/src/main/java/com/toast/ToastOverlay.kt +495 -0
  9. package/android/src/main/java/com/toast/anim/CutoutMorphAnimator.kt +235 -0
  10. package/android/src/main/java/com/toast/anim/SlideAnimator.kt +64 -0
  11. package/android/src/main/java/com/toast/anim/ToastAnimator.kt +23 -0
  12. package/android/src/main/java/com/toast/backdrop/BackdropSampler.kt +142 -0
  13. package/android/src/main/java/com/toast/backdrop/OutlineController.kt +100 -0
  14. package/android/src/main/java/com/toast/cutout/CutoutDetector.kt +88 -0
  15. package/android/src/main/java/com/toast/cutout/CutoutInfo.kt +28 -0
  16. package/android/src/main/java/com/toast/gesture/ToastGestureHandler.kt +68 -0
  17. package/android/src/main/java/com/toast/ui/IconMapper.kt +26 -0
  18. package/android/src/main/java/com/toast/ui/PassThroughFrameLayout.kt +53 -0
  19. package/android/src/main/java/com/toast/ui/ToastViewFactory.kt +224 -0
  20. package/android/src/main/java/com/toast/util/Density.kt +17 -0
  21. package/android/src/main/java/com/toast/util/StatusBarController.kt +24 -0
  22. package/android/src/main/java/com/toast/util/ToastConstants.kt +36 -0
  23. package/android/src/main/res/.gitkeep +0 -0
  24. package/android/src/main/res/drawable/ic_arrow_downward.xml +9 -0
  25. package/android/src/main/res/drawable/ic_arrow_upward.xml +9 -0
  26. package/android/src/main/res/drawable/ic_cancel.xml +9 -0
  27. package/android/src/main/res/drawable/ic_check_circle.xml +9 -0
  28. package/android/src/main/res/drawable/ic_favorite.xml +9 -0
  29. package/android/src/main/res/drawable/ic_info.xml +9 -0
  30. package/android/src/main/res/drawable/ic_mail.xml +9 -0
  31. package/android/src/main/res/drawable/ic_notifications.xml +9 -0
  32. package/android/src/main/res/drawable/ic_touch_app.xml +9 -0
  33. package/android/src/main/res/drawable/ic_warning.xml +9 -0
  34. package/android/src/main/res/drawable/ic_wifi.xml +9 -0
  35. package/android/src/main/res/values/colors.xml +3 -0
  36. package/android/src/main/res/values/strings.xml +3 -0
  37. package/android/src/main/res/values/styles.xml +3 -0
  38. package/android/src/test/java/com/toast/PrettyToastPluginTest.kt +26 -0
  39. package/dist/docs.json +459 -0
  40. package/dist/esm/controller.d.ts +30 -0
  41. package/dist/esm/controller.js +271 -0
  42. package/dist/esm/controller.js.map +1 -0
  43. package/dist/esm/definitions.d.ts +144 -0
  44. package/dist/esm/definitions.js +2 -0
  45. package/dist/esm/definitions.js.map +1 -0
  46. package/dist/esm/driver.d.ts +19 -0
  47. package/dist/esm/driver.js +24 -0
  48. package/dist/esm/driver.js.map +1 -0
  49. package/dist/esm/icons.d.ts +14 -0
  50. package/dist/esm/icons.js +138 -0
  51. package/dist/esm/icons.js.map +1 -0
  52. package/dist/esm/index.d.ts +2 -0
  53. package/dist/esm/index.js +2 -0
  54. package/dist/esm/index.js.map +1 -0
  55. package/dist/esm/internal-plugin.d.ts +2 -0
  56. package/dist/esm/internal-plugin.js +5 -0
  57. package/dist/esm/internal-plugin.js.map +1 -0
  58. package/dist/esm/internal-types.d.ts +31 -0
  59. package/dist/esm/internal-types.js +2 -0
  60. package/dist/esm/internal-types.js.map +1 -0
  61. package/dist/esm/toast.d.ts +1 -0
  62. package/dist/esm/toast.js +5 -0
  63. package/dist/esm/toast.js.map +1 -0
  64. package/dist/esm/web-renderer.d.ts +36 -0
  65. package/dist/esm/web-renderer.js +296 -0
  66. package/dist/esm/web-renderer.js.map +1 -0
  67. package/dist/esm/web.d.ts +10 -0
  68. package/dist/esm/web.js +28 -0
  69. package/dist/esm/web.js.map +1 -0
  70. package/dist/plugin.cjs.js +770 -0
  71. package/dist/plugin.cjs.js.map +1 -0
  72. package/dist/plugin.js +773 -0
  73. package/dist/plugin.js.map +1 -0
  74. package/ios/Sources/PrettyToastPlugin/CustomHostingView.swift +13 -0
  75. package/ios/Sources/PrettyToastPlugin/PassThroughWindow.swift +143 -0
  76. package/ios/Sources/PrettyToastPlugin/PrettyToastColorParser.swift +94 -0
  77. package/ios/Sources/PrettyToastPlugin/PrettyToastPlugin.swift +138 -0
  78. package/ios/Sources/PrettyToastPlugin/PrettyToastView.swift +267 -0
  79. package/ios/Sources/PrettyToastPlugin/Toast.swift +29 -0
  80. package/ios/Sources/PrettyToastPlugin/ToastManager.swift +392 -0
  81. package/ios/Tests/PrettyToastPluginTests/PrettyToastPluginTests.swift +21 -0
  82. package/package.json +98 -0
  83. package/scripts/check-capacitor-plugin-wiring.mjs +254 -0
  84. package/scripts/deploy-example-capgo.mjs +86 -0
  85. package/scripts/test-ios.sh +14 -0
@@ -0,0 +1,495 @@
1
+ package com.toast
2
+
3
+ import android.app.Activity
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapFactory
6
+ import android.os.Handler
7
+ import android.os.Looper
8
+ import android.util.Base64
9
+ import android.util.LruCache
10
+ import android.view.View
11
+ import android.view.ViewGroup
12
+ import java.lang.ref.WeakReference
13
+ import java.net.URL
14
+ import java.util.concurrent.ExecutorService
15
+ import java.util.concurrent.Executors
16
+ import com.toast.anim.CutoutMorphAnimator
17
+ import com.toast.anim.SlideAnimator
18
+ import com.toast.anim.ToastAnimator
19
+ import com.toast.backdrop.BackdropSampler
20
+ import com.toast.backdrop.OutlineController
21
+ import com.toast.cutout.CutoutDetector
22
+ import com.toast.gesture.ToastGestureHandler
23
+ import com.toast.ui.IconMapper
24
+ import com.toast.ui.ToastViewFactory
25
+ import com.toast.util.Density
26
+ import com.toast.util.StatusBarController
27
+ import com.toast.util.ToastConstants.DISMISS_CALLBACK_BUFFER_MS
28
+
29
+ /**
30
+ * Thin orchestrator over the toast subsystems:
31
+ * - [CutoutDetector] reads device geometry into a snapshot
32
+ * - [ToastViewFactory] builds the view hierarchy from that snapshot
33
+ * - A [ToastAnimator] (cutout-morph or slide) drives show/dismiss/drag
34
+ * - [ToastGestureHandler] wires touches into the animator
35
+ *
36
+ * Owns only lifecycle/state: show flags, auto-dismiss timer, and the
37
+ * useDynamicIsland-changed recreate rule.
38
+ */
39
+ class ToastOverlay(activity: Activity) {
40
+
41
+ // Hold the hosting Activity weakly so that if it's destroyed (rotation,
42
+ // finish, process trim) while the overlay still has state lying around,
43
+ // we don't keep it alive — we just bail out of any operation that needs it.
44
+ private val activityRef = WeakReference(activity)
45
+ private val activity: Activity?
46
+ get() = activityRef.get()
47
+
48
+ private var views: ToastViewFactory.Built? = null
49
+ private var animator: ToastAnimator? = null
50
+ private var cutoutAnimator: CutoutMorphAnimator? = null
51
+ private var isCutoutMorph = false
52
+ private var backdropSampler: BackdropSampler? = null
53
+ private var outline: OutlineController? = null
54
+
55
+ private val handler = Handler(Looper.getMainLooper())
56
+ private var dismissRunnable: Runnable? = null
57
+ // Pending status-bar restore. Scheduled after the collapse animation so
58
+ // the bar doesn't flicker between queued toasts. Cancelled by the next
59
+ // show() if one arrives before it fires.
60
+ private var statusBarRestoreRunnable: Runnable? = null
61
+ private var isShowing = false
62
+ private var isDismissing = false
63
+ private var useDynamicIslandProp = true
64
+
65
+ var onDismiss: (() -> Unit)? = null
66
+ var onPress: (() -> Unit)? = null
67
+ var onActionPress: (() -> Unit)? = null
68
+
69
+ private val imageLoader: ExecutorService = Executors.newSingleThreadExecutor { r ->
70
+ Thread(r, "ToastIconLoader").apply { isDaemon = true }
71
+ }
72
+ // Bitmaps keyed by URI so a repeat show of the same custom icon paints
73
+ // synchronously — no flash of the default drawable, no re-fetch.
74
+ private val iconCache = LruCache<String, Bitmap>(8)
75
+ // Tracks the most recently requested URI. Async loads that come back
76
+ // after a newer request fires are dropped (late response, stale data).
77
+ private var currentImageUri: String = ""
78
+
79
+ fun show(
80
+ icon: String,
81
+ iconUri: String,
82
+ title: String,
83
+ message: String,
84
+ duration: Int,
85
+ autoDismiss: Boolean,
86
+ enableSwipeDismiss: Boolean,
87
+ useDynamicIsland: Boolean,
88
+ accentColor: Int?,
89
+ strokeColor: Int?,
90
+ disableBackdropSampling: Boolean,
91
+ actionLabel: String,
92
+ accessibilityAnnouncement: String,
93
+ ) {
94
+ if (useDynamicIsland != this.useDynamicIslandProp) {
95
+ this.useDynamicIslandProp = useDynamicIsland
96
+ destroy()
97
+ }
98
+ this.useDynamicIslandProp = useDynamicIsland
99
+
100
+ if (isDismissing) {
101
+ handler.postDelayed({
102
+ show(icon, iconUri, title, message, duration, autoDismiss, enableSwipeDismiss, useDynamicIsland, accentColor, strokeColor, disableBackdropSampling, actionLabel, accessibilityAnnouncement)
103
+ }, 50)
104
+ return
105
+ }
106
+
107
+ cancelAutoDismiss()
108
+ isDismissing = false
109
+
110
+ val built = ensureOverlay() ?: return
111
+ val currentAnimator = animator ?: return
112
+
113
+ val tint = updateContent(built, icon, iconUri, title, message, accentColor)
114
+ bindAction(built, actionLabel, tint)
115
+ outline?.setOverride(strokeColor)
116
+ installGestures(built, currentAnimator, enableSwipeDismiss)
117
+
118
+ if (!isShowing) {
119
+ isShowing = true
120
+ built.container.visibility = View.VISIBLE
121
+
122
+ built.pill.animate().cancel()
123
+ built.content.animate().cancel()
124
+ built.pill.translationY = 0f
125
+ built.pill.scaleX = 1f
126
+ built.pill.scaleY = 1f
127
+ built.pill.alpha = 1f
128
+ built.content.alpha = 1f
129
+
130
+ currentAnimator.show()
131
+ if (!disableBackdropSampling) {
132
+ startBackdropSampling()
133
+ } else {
134
+ stopBackdropSampling()
135
+ }
136
+
137
+ if (accessibilityAnnouncement.isNotEmpty()) {
138
+ built.pill.announceForAccessibility(accessibilityAnnouncement)
139
+ }
140
+ }
141
+
142
+ if (autoDismiss && duration > 0) {
143
+ dismissRunnable = Runnable { dismiss() }
144
+ handler.postDelayed(dismissRunnable!!, duration.toLong())
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Mutates the currently presented toast in place. Updates content on the
150
+ * live view hierarchy and restarts the auto-dismiss timer — does NOT
151
+ * re-run the expand animation.
152
+ */
153
+ fun update(
154
+ icon: String,
155
+ iconUri: String,
156
+ title: String,
157
+ message: String,
158
+ duration: Int,
159
+ autoDismiss: Boolean,
160
+ accentColor: Int?,
161
+ strokeColor: Int?,
162
+ disableBackdropSampling: Boolean,
163
+ actionLabel: String,
164
+ ) {
165
+ val built = views ?: return
166
+ if (!isShowing || isDismissing) return
167
+
168
+ val tint = updateContent(built, icon, iconUri, title, message, accentColor)
169
+ bindAction(built, actionLabel, tint)
170
+ outline?.setOverride(strokeColor)
171
+
172
+ if (disableBackdropSampling) {
173
+ stopBackdropSampling()
174
+ } else if (backdropSampler == null) {
175
+ startBackdropSampling()
176
+ }
177
+
178
+ cancelAutoDismiss()
179
+ if (autoDismiss && duration > 0) {
180
+ dismissRunnable = Runnable { dismiss() }
181
+ handler.postDelayed(dismissRunnable!!, duration.toLong())
182
+ }
183
+ }
184
+
185
+ fun dismiss() {
186
+ if (!isShowing || isDismissing) return
187
+ isDismissing = true
188
+ cancelAutoDismiss()
189
+
190
+ // Stop sampling + freeze any in-flight stroke crossfade before we
191
+ // hand off to the animator. Runs on every dismiss path — including
192
+ // the `animator == null` early return below — so we never leave a
193
+ // tick runnable or a ValueAnimator running against a view that's
194
+ // about to go away.
195
+ stopBackdropSampling()
196
+ outline?.cancel()
197
+
198
+ val currentAnimator = animator ?: run {
199
+ isShowing = false
200
+ isDismissing = false
201
+ onDismiss?.invoke()
202
+ return
203
+ }
204
+
205
+ currentAnimator.dismiss {
206
+ isShowing = false
207
+ isDismissing = false
208
+ views?.container?.visibility = View.GONE
209
+ if (isCutoutMorph) {
210
+ scheduleStatusBarRestore()
211
+ // Match iOS's 50ms buffer between morph end and onDismiss callback.
212
+ handler.postDelayed({ onDismiss?.invoke() }, DISMISS_CALLBACK_BUFFER_MS)
213
+ } else {
214
+ onDismiss?.invoke()
215
+ }
216
+ }
217
+ }
218
+
219
+ fun destroy() {
220
+ cancelAutoDismiss()
221
+ cancelStatusBarRestore()
222
+ stopBackdropSampling()
223
+ outline?.cancel()
224
+ outline = null
225
+ handler.removeCallbacksAndMessages(null)
226
+ cutoutAnimator?.cancelPendingCallbacks()
227
+
228
+ val decorView = activity?.window?.decorView as? ViewGroup
229
+ views?.container?.let { decorView?.removeView(it) }
230
+
231
+ imageLoader.shutdownNow()
232
+ iconCache.evictAll()
233
+
234
+ views = null
235
+ animator = null
236
+ cutoutAnimator = null
237
+ isShowing = false
238
+ isDismissing = false
239
+ }
240
+
241
+ private fun ensureOverlay(): ToastViewFactory.Built? {
242
+ views?.let { return it }
243
+
244
+ val activity = this.activity ?: return null
245
+ val decorView = activity.window?.decorView as? ViewGroup ?: return null
246
+ val density = Density.from(activity.resources)
247
+
248
+ val info = CutoutDetector.detect(activity, decorView, useDynamicIsland = useDynamicIslandProp)
249
+ val shouldUseMorph = useDynamicIslandProp
250
+ isCutoutMorph = shouldUseMorph
251
+
252
+ val factory = ToastViewFactory(activity, density)
253
+ val built = factory.build(info)
254
+ decorView.addView(built.container)
255
+
256
+ animator = if (shouldUseMorph) {
257
+ CutoutMorphAnimator(
258
+ pill = built.pill,
259
+ content = built.content,
260
+ info = info,
261
+ expandedCornerRadius = built.expandedCornerRadius,
262
+ density = density,
263
+ onBeforeShow = {
264
+ cancelStatusBarRestore()
265
+ this.activity?.let { StatusBarController.hide(it) }
266
+ },
267
+ ).also { cutoutAnimator = it }
268
+ } else {
269
+ SlideAnimator(built.pill, density)
270
+ }
271
+
272
+ outline = OutlineController(
273
+ pillBackground = built.pillBackground,
274
+ strokeWidthPx = built.strokeWidthPx,
275
+ )
276
+
277
+ views = built
278
+ return built
279
+ }
280
+
281
+ // MARK: - Backdrop sampling
282
+ //
283
+ // Mirrors iOS's PassThroughWindow sampler: while the toast is on-screen,
284
+ // average the luminance of the top strip of the app's content view and
285
+ // flip the outline between the toast's accent colour and a faint neutral
286
+ // white. `OutlineController` handles the 300 ms ARGB crossfade between
287
+ // the two stroke colours so the change is a soft transition, not a pop.
288
+
289
+ private fun startBackdropSampling() {
290
+ val activity = this.activity ?: return
291
+ val density = Density.from(activity.resources)
292
+ stopBackdropSampling()
293
+ val sampler = BackdropSampler(
294
+ activity = activity,
295
+ density = density,
296
+ onTintChanged = { tint -> outline?.setTint(tint, animated = true) },
297
+ )
298
+ backdropSampler = sampler
299
+ sampler.start()
300
+ // Seed the stroke with the sampler's first reading (which it computed
301
+ // synchronously in start()) so we don't flash the default grey tint.
302
+ outline?.setTint(sampler.tint, animated = false)
303
+ }
304
+
305
+ private fun stopBackdropSampling() {
306
+ backdropSampler?.stop()
307
+ backdropSampler = null
308
+ }
309
+
310
+ private fun updateContent(
311
+ built: ToastViewFactory.Built,
312
+ icon: String,
313
+ iconUri: String,
314
+ title: String,
315
+ message: String,
316
+ accentOverride: Int?,
317
+ ): Int {
318
+ val (drawableRes, defaultTint) = IconMapper.map(icon)
319
+ val tint = accentOverride ?: defaultTint
320
+ // Accent always flows to the outline stroke, regardless of which
321
+ // icon path we take.
322
+ outline?.setAccent(tint)
323
+
324
+ currentImageUri = iconUri
325
+ if (iconUri.isNotEmpty()) {
326
+ val cached = iconCache[iconUri]
327
+ if (cached != null) {
328
+ // Repeat show — apply synchronously so there's no flash of the
329
+ // default drawable while we "re-load" something we already have.
330
+ built.icon.setImageBitmap(cached)
331
+ built.icon.colorFilter = null
332
+ } else {
333
+ // Clear the view so the previous toast's icon (or any default)
334
+ // doesn't hang around while we fetch. The ImageView keeps its
335
+ // frame, so layout doesn't jump.
336
+ built.icon.setImageDrawable(null)
337
+ built.icon.colorFilter = null
338
+ loadIcon(built, iconUri)
339
+ }
340
+ } else {
341
+ built.icon.setImageResource(drawableRes)
342
+ built.icon.setColorFilter(tint)
343
+ }
344
+
345
+ built.title.text = title
346
+ if (message.isNotEmpty()) {
347
+ built.message.text = message
348
+ built.message.visibility = View.VISIBLE
349
+ } else {
350
+ built.message.visibility = View.GONE
351
+ }
352
+
353
+ return tint
354
+ }
355
+
356
+ private fun bindAction(
357
+ built: ToastViewFactory.Built,
358
+ label: String,
359
+ actionColor: Int,
360
+ ) {
361
+ if (label.isEmpty()) {
362
+ built.actionButton.visibility = View.GONE
363
+ built.actionButton.setOnClickListener(null)
364
+ return
365
+ }
366
+ built.actionButton.visibility = View.VISIBLE
367
+ built.actionButton.text = label
368
+ built.actionButton.setTextColor(actionColor)
369
+ built.actionButton.setOnClickListener { onActionPress?.invoke() }
370
+ }
371
+
372
+ /**
373
+ * Accepts http(s)://, file://, and absolute filesystem paths. Bundled
374
+ * RN asset URIs in prod (resource names) are not supported here — users
375
+ * passing `require('./icon.png')` will get the dev-mode http URL in dev
376
+ * and will need a file URI in prod. Decoded bitmaps are cached in
377
+ * [iconCache] so repeat shows apply synchronously.
378
+ */
379
+ private fun loadIcon(built: ToastViewFactory.Built, uri: String) {
380
+ val targetW = built.icon.layoutParams.width.takeIf { it > 0 } ?: DEFAULT_ICON_TARGET_PX
381
+ val targetH = built.icon.layoutParams.height.takeIf { it > 0 } ?: DEFAULT_ICON_TARGET_PX
382
+ imageLoader.execute {
383
+ val bitmap = try {
384
+ when {
385
+ uri.startsWith("http://") || uri.startsWith("https://") -> {
386
+ val bytes = URL(uri).openStream().use { it.readBytes() }
387
+ decodeSampled(bytes, targetW, targetH)
388
+ }
389
+ uri.startsWith("data:") -> decodeSampled(decodeDataUri(uri), targetW, targetH)
390
+ uri.startsWith("file://") -> decodeSampled(uri.removePrefix("file://"), targetW, targetH)
391
+ uri.startsWith("/") -> decodeSampled(uri, targetW, targetH)
392
+ else -> null
393
+ }
394
+ } catch (_: Throwable) {
395
+ null
396
+ } ?: return@execute
397
+
398
+ iconCache.put(uri, bitmap)
399
+ handler.post {
400
+ // Only apply if this URI is still the one we want to render.
401
+ // A later show() with a different URI cancels this load.
402
+ if (currentImageUri == uri) {
403
+ built.icon.setImageBitmap(bitmap)
404
+ built.icon.colorFilter = null
405
+ }
406
+ }
407
+ }
408
+ }
409
+
410
+ private fun decodeSampled(bytes: ByteArray, targetW: Int, targetH: Int): Bitmap? {
411
+ val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
412
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
413
+ val opts = BitmapFactory.Options().apply {
414
+ inSampleSize = calcInSampleSize(bounds.outWidth, bounds.outHeight, targetW, targetH)
415
+ }
416
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opts)
417
+ }
418
+
419
+ private fun decodeDataUri(uri: String): ByteArray {
420
+ val commaIndex = uri.indexOf(',')
421
+ if (commaIndex == -1) return ByteArray(0)
422
+
423
+ val metadata = uri.substring(0, commaIndex)
424
+ val payload = uri.substring(commaIndex + 1)
425
+
426
+ return if (metadata.contains(";base64")) {
427
+ Base64.decode(payload, Base64.DEFAULT)
428
+ } else {
429
+ val decodedPayload = java.net.URLDecoder.decode(payload, Charsets.UTF_8)
430
+ decodedPayload.toByteArray()
431
+ }
432
+ }
433
+
434
+ private fun decodeSampled(path: String, targetW: Int, targetH: Int): Bitmap? {
435
+ val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
436
+ BitmapFactory.decodeFile(path, bounds)
437
+ val opts = BitmapFactory.Options().apply {
438
+ inSampleSize = calcInSampleSize(bounds.outWidth, bounds.outHeight, targetW, targetH)
439
+ }
440
+ return BitmapFactory.decodeFile(path, opts)
441
+ }
442
+
443
+ private fun calcInSampleSize(srcW: Int, srcH: Int, dstW: Int, dstH: Int): Int {
444
+ if (srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0) return 1
445
+ var sample = 1
446
+ while (srcW / (sample * 2) >= dstW && srcH / (sample * 2) >= dstH) {
447
+ sample *= 2
448
+ }
449
+ return sample
450
+ }
451
+
452
+ private fun installGestures(
453
+ built: ToastViewFactory.Built,
454
+ animator: ToastAnimator,
455
+ enableSwipeDismiss: Boolean,
456
+ ) {
457
+ val resources = activity?.resources ?: return
458
+ val density = Density.from(resources)
459
+ ToastGestureHandler(
460
+ animator = animator,
461
+ density = density,
462
+ enableSwipeDismiss = enableSwipeDismiss,
463
+ onDismissRequested = { dismiss() },
464
+ onPress = { onPress?.invoke() },
465
+ ).install(built.pill)
466
+ }
467
+
468
+ private fun cancelAutoDismiss() {
469
+ dismissRunnable?.let { handler.removeCallbacks(it) }
470
+ dismissRunnable = null
471
+ }
472
+
473
+ // The animator completion already fires after the collapse animation ends;
474
+ // the extra delay here is a grace window so a queued toast's show() can
475
+ // cancel the restore before the status bar visibly flashes.
476
+ private fun scheduleStatusBarRestore() {
477
+ cancelStatusBarRestore()
478
+ val runnable = Runnable {
479
+ activity?.let { StatusBarController.show(it) }
480
+ }
481
+ statusBarRestoreRunnable = runnable
482
+ handler.postDelayed(runnable, STATUS_BAR_RESTORE_GRACE_MS)
483
+ }
484
+
485
+ private fun cancelStatusBarRestore() {
486
+ statusBarRestoreRunnable?.let { handler.removeCallbacks(it) }
487
+ statusBarRestoreRunnable = null
488
+ }
489
+
490
+ companion object {
491
+ private const val STATUS_BAR_RESTORE_GRACE_MS = 250L
492
+ // Fallback decode target if the ImageView layout params are WRAP/MATCH.
493
+ private const val DEFAULT_ICON_TARGET_PX = 128
494
+ }
495
+ }