1 /*
2 * Copyright 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.layout
18
19 import android.os.Build
20 import android.view.View
21 import android.view.View.OnAttachStateChangeListener
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.DisposableEffect
24 import androidx.compose.runtime.NonRestartableComposable
25 import androidx.compose.runtime.Stable
26 import androidx.compose.runtime.getValue
27 import androidx.compose.runtime.mutableStateOf
28 import androidx.compose.runtime.setValue
29 import androidx.compose.runtime.snapshots.Snapshot
30 import androidx.compose.ui.R
31 import androidx.compose.ui.platform.AbstractComposeView
32 import androidx.compose.ui.platform.ComposeView
33 import androidx.compose.ui.platform.LocalView
34 import androidx.compose.ui.unit.Density
35 import androidx.compose.ui.unit.LayoutDirection
36 import androidx.core.graphics.Insets as AndroidXInsets
37 import androidx.core.view.OnApplyWindowInsetsListener
38 import androidx.core.view.ViewCompat
39 import androidx.core.view.WindowInsetsAnimationCompat
40 import androidx.core.view.WindowInsetsCompat
41 import java.util.WeakHashMap
42 import org.jetbrains.annotations.TestOnly
43
toInsetsValuesnull44 internal fun AndroidXInsets.toInsetsValues(): InsetsValues = InsetsValues(left, top, right, bottom)
45
46 internal fun ValueInsets(insets: AndroidXInsets, name: String): ValueInsets =
47 ValueInsets(insets.toInsetsValues(), name)
48
49 /**
50 * [WindowInsets] provided by the Android framework. These can be used in
51 * [rememberWindowInsetsConnection] to control the insets.
52 */
53 @Stable
54 internal class AndroidWindowInsets(internal val type: Int, private val name: String) :
55 WindowInsets {
56 internal var insets by mutableStateOf(AndroidXInsets.NONE)
57
58 /**
59 * Returns whether the insets are visible, irrespective of whether or not they intersect with
60 * the Window.
61 */
62 var isVisible by mutableStateOf(true)
63 private set
64
65 override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int {
66 return insets.left
67 }
68
69 override fun getTop(density: Density): Int {
70 return insets.top
71 }
72
73 override fun getRight(density: Density, layoutDirection: LayoutDirection): Int {
74 return insets.right
75 }
76
77 override fun getBottom(density: Density): Int {
78 return insets.bottom
79 }
80
81 @OptIn(ExperimentalLayoutApi::class)
82 internal fun update(windowInsetsCompat: WindowInsetsCompat, typeMask: Int) {
83 if (typeMask == 0 || typeMask and type != 0) {
84 insets = windowInsetsCompat.getInsets(type)
85 isVisible = windowInsetsCompat.isVisible(type)
86 }
87 }
88
89 override fun equals(other: Any?): Boolean {
90 if (this === other) return true
91 if (other !is AndroidWindowInsets) return false
92
93 return type == other.type
94 }
95
96 override fun hashCode(): Int {
97 return type
98 }
99
100 override fun toString(): String {
101 return "$name(${insets.left}, ${insets.top}, ${insets.right}, ${insets.bottom})"
102 }
103 }
104
105 /**
106 * Indicates whether access to [WindowInsets] within the [content][ComposeView.setContent] should
107 * consume the Android [android.view.WindowInsets]. The default value is `true`, meaning that access
108 * to [WindowInsets.Companion] will consume the Android WindowInsets.
109 *
110 * This property should be set prior to first composition.
111 */
112 var AbstractComposeView.consumeWindowInsets: Boolean
113 get() = getTag(R.id.consume_window_insets_tag) as? Boolean ?: true
114 set(value) {
115 setTag(R.id.consume_window_insets_tag, value)
116 }
117
118 /**
119 * Indicates whether access to [WindowInsets] within the [content][ComposeView.setContent] should
120 * consume the Android [android.view.WindowInsets]. The default value is `true`, meaning that access
121 * to [WindowInsets.Companion] will consume the Android WindowInsets.
122 *
123 * This property should be set prior to first composition.
124 */
125 @Deprecated(
126 level = DeprecationLevel.HIDDEN,
127 message = "Please use AbstractComposeView.consumeWindowInsets"
128 )
129 var ComposeView.consumeWindowInsets: Boolean
130 get() = getTag(R.id.consume_window_insets_tag) as? Boolean ?: true
131 set(value) {
132 setTag(R.id.consume_window_insets_tag, value)
133 }
134
135 /** For the [WindowInsetsCompat.Type.captionBar]. */
136 actual val WindowInsets.Companion.captionBar: WindowInsets
137 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().captionBar
138
139 /**
140 * For the [WindowInsetsCompat.Type.displayCutout]. This insets represents the area that the display
141 * cutout (e.g. for camera) is and important content should be excluded from.
142 */
143 actual val WindowInsets.Companion.displayCutout: WindowInsets
144 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().displayCutout
145
146 /**
147 * For the [WindowInsetsCompat.Type.ime]. On API level 23 (M) and above, the soft keyboard can be
148 * detected and [ime] will update when it shows. On API 30 (R) and above, the [ime] insets will
149 * animate synchronously with the actual IME animation.
150 *
151 * Developers should set `android:windowSoftInputMode="adjustResize"` in their `AndroidManifest.xml`
152 * file and call `WindowCompat.setDecorFitsSystemWindows(window, false)` in their
153 * [android.app.Activity.onCreate].
154 */
155 actual val WindowInsets.Companion.ime: WindowInsets
156 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().ime
157
158 /**
159 * For the [WindowInsetsCompat.Type.mandatorySystemGestures]. These insets represents the space
160 * where system gestures have priority over application gestures.
161 */
162 actual val WindowInsets.Companion.mandatorySystemGestures: WindowInsets
163 @Composable
164 @NonRestartableComposable
165 get() = WindowInsetsHolder.current().mandatorySystemGestures
166
167 /**
168 * For the [WindowInsetsCompat.Type.navigationBars]. These insets represent where system UI places
169 * navigation bars. Interactive UI should avoid the navigation bars area.
170 */
171 actual val WindowInsets.Companion.navigationBars: WindowInsets
172 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().navigationBars
173
174 /** For the [WindowInsetsCompat.Type.statusBars]. */
175 actual val WindowInsets.Companion.statusBars: WindowInsets
176 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().statusBars
177
178 /** For the [WindowInsetsCompat.Type.systemBars]. */
179 actual val WindowInsets.Companion.systemBars: WindowInsets
180 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().systemBars
181
182 /** For the [WindowInsetsCompat.Type.systemGestures]. */
183 actual val WindowInsets.Companion.systemGestures: WindowInsets
184 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().systemGestures
185
186 /** For the [WindowInsetsCompat.Type.tappableElement]. */
187 actual val WindowInsets.Companion.tappableElement: WindowInsets
188 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().tappableElement
189
190 /** The insets for the curved areas in a waterfall display. */
191 actual val WindowInsets.Companion.waterfall: WindowInsets
192 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().waterfall
193
194 /**
195 * The insets that include areas where content may be covered by other drawn content. This includes
196 * all [system bars][systemBars], [display cutout][displayCutout], and [soft keyboard][ime].
197 */
198 actual val WindowInsets.Companion.safeDrawing: WindowInsets
199 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().safeDrawing
200
201 /**
202 * The insets that include areas where gestures may be confused with other input, including
203 * [system gestures][systemGestures], [mandatory system gestures][mandatorySystemGestures],
204 * [rounded display areas][waterfall], and [tappable areas][tappableElement].
205 */
206 actual val WindowInsets.Companion.safeGestures: WindowInsets
207 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().safeGestures
208
209 /**
210 * The insets that include all areas that may be drawn over or have gesture confusion, including
211 * everything in [safeDrawing] and [safeGestures].
212 */
213 actual val WindowInsets.Companion.safeContent: WindowInsets
214 @Composable @NonRestartableComposable get() = WindowInsetsHolder.current().safeContent
215
216 /**
217 * The insets that the [WindowInsetsCompat.Type.captionBar] will consume if shown. If it cannot be
218 * shown then this will be empty.
219 */
220 @ExperimentalLayoutApi
221 val WindowInsets.Companion.captionBarIgnoringVisibility: WindowInsets
222 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
223 @ExperimentalLayoutApi
224 @Composable
225 @NonRestartableComposable
226 get() = WindowInsetsHolder.current().captionBarIgnoringVisibility
227
228 /**
229 * The insets that [WindowInsetsCompat.Type.navigationBars] will consume if shown. These insets
230 * represent where system UI places navigation bars. Interactive UI should avoid the navigation bars
231 * area. If navigation bars cannot be shown, then this will be empty.
232 */
233 @ExperimentalLayoutApi
234 val WindowInsets.Companion.navigationBarsIgnoringVisibility: WindowInsets
235 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
236 @ExperimentalLayoutApi
237 @Composable
238 @NonRestartableComposable
239 get() = WindowInsetsHolder.current().navigationBarsIgnoringVisibility
240
241 /**
242 * The insets that [WindowInsetsCompat.Type.statusBars] will consume if shown. If the status bar can
243 * never be shown, then this will be empty.
244 */
245 @ExperimentalLayoutApi
246 val WindowInsets.Companion.statusBarsIgnoringVisibility: WindowInsets
247 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
248 @ExperimentalLayoutApi
249 @Composable
250 @NonRestartableComposable
251 get() = WindowInsetsHolder.current().statusBarsIgnoringVisibility
252
253 /**
254 * The insets that [WindowInsetsCompat.Type.systemBars] will consume if shown.
255 *
256 * If system bars can never be shown, then this will be empty.
257 */
258 @ExperimentalLayoutApi
259 val WindowInsets.Companion.systemBarsIgnoringVisibility: WindowInsets
260 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
261 @ExperimentalLayoutApi
262 @Composable
263 @NonRestartableComposable
264 get() = WindowInsetsHolder.current().systemBarsIgnoringVisibility
265
266 /**
267 * The insets that [WindowInsetsCompat.Type.tappableElement] will consume if active.
268 *
269 * If there are never tappable elements then this is empty.
270 */
271 @ExperimentalLayoutApi
272 val WindowInsets.Companion.tappableElementIgnoringVisibility: WindowInsets
273 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
274 @ExperimentalLayoutApi
275 @Composable
276 @NonRestartableComposable
277 get() = WindowInsetsHolder.current().tappableElementIgnoringVisibility
278
279 /**
280 * `true` when the [caption bar][captionBar] is being displayed, irrespective of whether it
281 * intersects with the Window.
282 */
283 @ExperimentalLayoutApi
284 val WindowInsets.Companion.isCaptionBarVisible: Boolean
285 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
286 @ExperimentalLayoutApi
287 @Composable
288 @NonRestartableComposable
289 get() = WindowInsetsHolder.current().captionBar.isVisible
290
291 /**
292 * `true` when the [soft keyboard][ime] is being displayed, irrespective of whether it intersects
293 * with the Window.
294 */
295 @ExperimentalLayoutApi
296 val WindowInsets.Companion.isImeVisible: Boolean
297 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
298 @ExperimentalLayoutApi
299 @Composable
300 @NonRestartableComposable
301 get() = WindowInsetsHolder.current().ime.isVisible
302
303 /**
304 * `true` when the [statusBars] are being displayed, irrespective of whether they intersects with
305 * the Window.
306 */
307 @ExperimentalLayoutApi
308 val WindowInsets.Companion.areStatusBarsVisible: Boolean
309 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
310 @ExperimentalLayoutApi
311 @Composable
312 @NonRestartableComposable
313 get() = WindowInsetsHolder.current().statusBars.isVisible
314
315 /**
316 * `true` when the [navigationBars] are being displayed, irrespective of whether they intersects
317 * with the Window.
318 */
319 @ExperimentalLayoutApi
320 val WindowInsets.Companion.areNavigationBarsVisible: Boolean
321 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
322 @ExperimentalLayoutApi
323 @Composable
324 @NonRestartableComposable
325 get() = WindowInsetsHolder.current().navigationBars.isVisible
326
327 /**
328 * `true` when the [systemBars] are being displayed, irrespective of whether they intersects with
329 * the Window.
330 */
331 @ExperimentalLayoutApi
332 val WindowInsets.Companion.areSystemBarsVisible: Boolean
333 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
334 @ExperimentalLayoutApi
335 @Composable
336 @NonRestartableComposable
337 get() = WindowInsetsHolder.current().systemBars.isVisible
338 /**
339 * `true` when the [tappableElement] is being displayed, irrespective of whether they intersects
340 * with the Window.
341 */
342 @ExperimentalLayoutApi
343 val WindowInsets.Companion.isTappableElementVisible: Boolean
344 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
345 @ExperimentalLayoutApi
346 @Composable
347 @NonRestartableComposable
348 get() = WindowInsetsHolder.current().tappableElement.isVisible
349
350 /**
351 * The [WindowInsets] for the IME before the IME started animating in. The current animated value is
352 * [WindowInsets.Companion.ime].
353 *
354 * This will be the same as [imeAnimationTarget] when there is no IME animation in progress.
355 */
356 @ExperimentalLayoutApi
357 val WindowInsets.Companion.imeAnimationSource: WindowInsets
358 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
359 @ExperimentalLayoutApi
360 @Composable
361 @NonRestartableComposable
362 get() = WindowInsetsHolder.current().imeAnimationSource
363
364 /**
365 * The [WindowInsets] for the IME when the animation completes, if it is allowed to complete
366 * successfully. The current animated value is [WindowInsets.Companion.ime].
367 *
368 * This will be the same as [imeAnimationSource] when there is no IME animation in progress.
369 */
370 @ExperimentalLayoutApi
371 val WindowInsets.Companion.imeAnimationTarget: WindowInsets
372 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
373 @ExperimentalLayoutApi
374 @Composable
375 @NonRestartableComposable
376 get() = WindowInsetsHolder.current().imeAnimationTarget
377
378 /** The insets for various values in the current window. */
379 internal class WindowInsetsHolder private constructor(insets: WindowInsetsCompat?, view: View) {
380 val captionBar = systemInsets(insets, WindowInsetsCompat.Type.captionBar(), "captionBar")
381 val displayCutout =
382 systemInsets(insets, WindowInsetsCompat.Type.displayCutout(), "displayCutout")
383 val ime = systemInsets(insets, WindowInsetsCompat.Type.ime(), "ime")
384 val mandatorySystemGestures =
385 systemInsets(
386 insets,
387 WindowInsetsCompat.Type.mandatorySystemGestures(),
388 "mandatorySystemGestures"
389 )
390 val navigationBars =
391 systemInsets(insets, WindowInsetsCompat.Type.navigationBars(), "navigationBars")
392 val statusBars = systemInsets(insets, WindowInsetsCompat.Type.statusBars(), "statusBars")
393 val systemBars = systemInsets(insets, WindowInsetsCompat.Type.systemBars(), "systemBars")
394 val systemGestures =
395 systemInsets(insets, WindowInsetsCompat.Type.systemGestures(), "systemGestures")
396 val tappableElement =
397 systemInsets(insets, WindowInsetsCompat.Type.tappableElement(), "tappableElement")
398 val waterfall =
399 ValueInsets(insets?.displayCutout?.waterfallInsets ?: AndroidXInsets.NONE, "waterfall")
400 val safeDrawing = systemBars.union(ime).union(displayCutout)
401 val safeGestures: WindowInsets =
402 tappableElement.union(mandatorySystemGestures).union(systemGestures).union(waterfall)
403 val safeContent: WindowInsets = safeDrawing.union(safeGestures)
404
405 val captionBarIgnoringVisibility =
406 valueInsetsIgnoringVisibility(
407 insets,
408 WindowInsetsCompat.Type.captionBar(),
409 "captionBarIgnoringVisibility"
410 )
411 val navigationBarsIgnoringVisibility =
412 valueInsetsIgnoringVisibility(
413 insets,
414 WindowInsetsCompat.Type.navigationBars(),
415 "navigationBarsIgnoringVisibility"
416 )
417 val statusBarsIgnoringVisibility =
418 valueInsetsIgnoringVisibility(
419 insets,
420 WindowInsetsCompat.Type.statusBars(),
421 "statusBarsIgnoringVisibility"
422 )
423 val systemBarsIgnoringVisibility =
424 valueInsetsIgnoringVisibility(
425 insets,
426 WindowInsetsCompat.Type.systemBars(),
427 "systemBarsIgnoringVisibility"
428 )
429 val tappableElementIgnoringVisibility =
430 valueInsetsIgnoringVisibility(
431 insets,
432 WindowInsetsCompat.Type.tappableElement(),
433 "tappableElementIgnoringVisibility"
434 )
435 val imeAnimationTarget =
436 valueInsetsIgnoringVisibility(insets, WindowInsetsCompat.Type.ime(), "imeAnimationTarget")
437 val imeAnimationSource =
438 valueInsetsIgnoringVisibility(insets, WindowInsetsCompat.Type.ime(), "imeAnimationSource")
439
440 /**
441 * `true` unless the `AbstractComposeView` [AbstractComposeView.consumeWindowInsets] is set to
442 * `false`.
443 */
444 val consumes =
445 (view.parent as? View)?.getTag(R.id.consume_window_insets_tag) as? Boolean ?: true
446
447 /**
448 * The number of accesses to [WindowInsetsHolder]. When this reaches zero, the listeners are
449 * removed. When it increases to 1, the listeners are added.
450 */
451 private var accessCount = 0
452
453 private val insetsListener = InsetsListener(this)
454
455 /**
456 * A usage of [WindowInsetsHolder.current] was added. We must track so that when the first one
457 * is added, listeners are set and when the last is removed, the listeners are removed.
458 */
incrementAccessorsnull459 fun incrementAccessors(view: View) {
460 if (accessCount == 0) {
461 // add listeners
462 ViewCompat.setOnApplyWindowInsetsListener(view, insetsListener)
463
464 if (view.isAttachedToWindow) {
465 view.requestApplyInsets()
466 }
467 view.addOnAttachStateChangeListener(insetsListener)
468
469 ViewCompat.setWindowInsetsAnimationCallback(view, insetsListener)
470 }
471 accessCount++
472 }
473
474 /**
475 * A usage of [WindowInsetsHolder.current] was removed. We must track so that when the first one
476 * is added, listeners are set and when the last is removed, the listeners are removed.
477 */
decrementAccessorsnull478 fun decrementAccessors(view: View) {
479 accessCount--
480 if (accessCount == 0) {
481 // remove listeners
482 ViewCompat.setOnApplyWindowInsetsListener(view, null)
483 ViewCompat.setWindowInsetsAnimationCallback(view, null)
484 view.removeOnAttachStateChangeListener(insetsListener)
485 }
486 }
487
488 /** Updates the WindowInsets values and notifies changes. */
updatenull489 fun update(windowInsets: WindowInsetsCompat, types: Int = 0) {
490 val insets =
491 if (testInsets) {
492 // WindowInsetsCompat erases insets that aren't part of the device.
493 // For example, if there is no navigation bar because of hardware keys,
494 // the bottom navigation bar will be removed. By using the constructor
495 // that doesn't accept a View, it doesn't remove the insets that aren't
496 // possible. This is important for testing on arbitrary hardware.
497 WindowInsetsCompat.toWindowInsetsCompat(windowInsets.toWindowInsets()!!)
498 } else {
499 windowInsets
500 }
501 captionBar.update(insets, types)
502 ime.update(insets, types)
503 displayCutout.update(insets, types)
504 navigationBars.update(insets, types)
505 statusBars.update(insets, types)
506 systemBars.update(insets, types)
507 systemGestures.update(insets, types)
508 tappableElement.update(insets, types)
509 mandatorySystemGestures.update(insets, types)
510
511 if (types == 0) {
512 captionBarIgnoringVisibility.value =
513 insets
514 .getInsetsIgnoringVisibility(WindowInsetsCompat.Type.captionBar())
515 .toInsetsValues()
516 navigationBarsIgnoringVisibility.value =
517 insets
518 .getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars())
519 .toInsetsValues()
520 statusBarsIgnoringVisibility.value =
521 insets
522 .getInsetsIgnoringVisibility(WindowInsetsCompat.Type.statusBars())
523 .toInsetsValues()
524 systemBarsIgnoringVisibility.value =
525 insets
526 .getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars())
527 .toInsetsValues()
528 tappableElementIgnoringVisibility.value =
529 insets
530 .getInsetsIgnoringVisibility(WindowInsetsCompat.Type.tappableElement())
531 .toInsetsValues()
532
533 val cutout = insets.displayCutout
534 if (cutout != null) {
535 val waterfallInsets = cutout.waterfallInsets
536 waterfall.value = waterfallInsets.toInsetsValues()
537 }
538 }
539 Snapshot.sendApplyNotifications()
540 }
541
542 /**
543 * Updates [WindowInsets.Companion.imeAnimationSource]. It should be called prior to [update].
544 */
updateImeAnimationSourcenull545 fun updateImeAnimationSource(windowInsets: WindowInsetsCompat) {
546 imeAnimationSource.value =
547 windowInsets.getInsets(WindowInsetsCompat.Type.ime()).toInsetsValues()
548 }
549
550 /**
551 * Updates [WindowInsets.Companion.imeAnimationTarget]. It should be called prior to [update].
552 */
updateImeAnimationTargetnull553 fun updateImeAnimationTarget(windowInsets: WindowInsetsCompat) {
554 imeAnimationTarget.value =
555 windowInsets.getInsets(WindowInsetsCompat.Type.ime()).toInsetsValues()
556 }
557
558 companion object {
559 /**
560 * A mapping of AndroidComposeView to ComposeWindowInsets. Normally a tag is a great way to
561 * do this mapping, but off-UI thread and multithreaded composition don't allow using the
562 * tag.
563 */
564 private val viewMap = WeakHashMap<View, WindowInsetsHolder>()
565
566 private var testInsets = false
567
568 /**
569 * Testing Window Insets is difficult, so we have this to help eliminate device-specifics
570 * from the WindowInsets. This is indirect because `@TestOnly` cannot be applied to a
571 * property with a backing field.
572 */
573 @TestOnly
setUseTestInsetsnull574 fun setUseTestInsets(testInsets: Boolean) {
575 this.testInsets = testInsets
576 }
577
578 @Composable
currentnull579 fun current(): WindowInsetsHolder {
580 val view = LocalView.current
581 val insets = getOrCreateFor(view)
582
583 DisposableEffect(insets) {
584 insets.incrementAccessors(view)
585 onDispose { insets.decrementAccessors(view) }
586 }
587 return insets
588 }
589
590 /**
591 * Returns the [WindowInsetsHolder] associated with [view] or creates one and associates it.
592 */
getOrCreateFornull593 private fun getOrCreateFor(view: View): WindowInsetsHolder {
594 return synchronized(viewMap) {
595 viewMap.getOrPut(view) {
596 val insets = null
597 WindowInsetsHolder(insets, view)
598 }
599 }
600 }
601
602 /** Creates a [ValueInsets] using the value from [windowInsets] if it isn't `null` */
systemInsetsnull603 private fun systemInsets(windowInsets: WindowInsetsCompat?, type: Int, name: String) =
604 AndroidWindowInsets(type, name).apply { windowInsets?.let { update(it, type) } }
605
606 /**
607 * Creates a [ValueInsets] using the "ignoring visibility" value from [windowInsets] if it
608 * isn't `null`
609 */
valueInsetsIgnoringVisibilitynull610 private fun valueInsetsIgnoringVisibility(
611 windowInsets: WindowInsetsCompat?,
612 type: Int,
613 name: String
614 ): ValueInsets {
615 val initial = windowInsets?.getInsetsIgnoringVisibility(type) ?: AndroidXInsets.NONE
616 return ValueInsets(initial, name)
617 }
618 }
619 }
620
621 private class InsetsListener(
622 val composeInsets: WindowInsetsHolder,
623 ) :
624 WindowInsetsAnimationCompat.Callback(
625 if (composeInsets.consumes) DISPATCH_MODE_STOP else DISPATCH_MODE_CONTINUE_ON_SUBTREE
626 ),
627 Runnable,
628 OnApplyWindowInsetsListener,
629 OnAttachStateChangeListener {
630 /**
631 * When [android.view.WindowInsetsController.controlWindowInsetsAnimation] is called, the
632 * [onApplyWindowInsets] is called after [onPrepare] with the target size. We don't want to
633 * report the target size, we want to always report the current size, so we must ignore those
634 * calls. However, the animation may be canceled before it progresses. On R, it won't make any
635 * callbacks, so we have to figure out whether the [onApplyWindowInsets] is from a canceled
636 * animation or if it is from the controlled animation. When [prepared] is `true` on R, we post
637 * a callback to set the [onApplyWindowInsets] insets value.
638 */
639 var prepared = false
640
641 /** `true` if there is an animation in progress. */
642 var runningAnimation = false
643
644 var savedInsets: WindowInsetsCompat? = null
645
onPreparenull646 override fun onPrepare(animation: WindowInsetsAnimationCompat) {
647 prepared = true
648 runningAnimation = true
649 super.onPrepare(animation)
650 }
651
onStartnull652 override fun onStart(
653 animation: WindowInsetsAnimationCompat,
654 bounds: WindowInsetsAnimationCompat.BoundsCompat
655 ): WindowInsetsAnimationCompat.BoundsCompat {
656 prepared = false
657 return super.onStart(animation, bounds)
658 }
659
onProgressnull660 override fun onProgress(
661 insets: WindowInsetsCompat,
662 runningAnimations: MutableList<WindowInsetsAnimationCompat>
663 ): WindowInsetsCompat {
664 composeInsets.update(insets)
665 return if (composeInsets.consumes) WindowInsetsCompat.CONSUMED else insets
666 }
667
onEndnull668 override fun onEnd(animation: WindowInsetsAnimationCompat) {
669 prepared = false
670 runningAnimation = false
671 val insets = savedInsets
672 if (animation.durationMillis != 0L && insets != null) {
673 composeInsets.updateImeAnimationSource(insets)
674 composeInsets.updateImeAnimationTarget(insets)
675 composeInsets.update(insets)
676 }
677 savedInsets = null
678 super.onEnd(animation)
679 }
680
onApplyWindowInsetsnull681 override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
682 // Keep track of the most recent insets we've seen, to ensure onEnd will always use the
683 // most recently acquired insets
684 savedInsets = insets
685 composeInsets.updateImeAnimationTarget(insets)
686 if (prepared) {
687 // There may be no callback on R if the animation is canceled after onPrepare(),
688 // so we won't know if the onPrepare() was canceled or if this is an
689 // onApplyWindowInsets() after the cancelation. We'll just post the value
690 // and if it is still preparing then we just use the value.
691 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
692 view.post(this)
693 }
694 } else if (!runningAnimation) {
695 // If an animation is running, rely on onProgress() to update the insets
696 // On APIs less than 30 where the IME animation is backported, this avoids reporting
697 // the final insets for a frame while the animation is running.
698 composeInsets.updateImeAnimationSource(insets)
699 composeInsets.update(insets)
700 }
701 return if (composeInsets.consumes) WindowInsetsCompat.CONSUMED else insets
702 }
703
704 /**
705 * On [R], we don't receive the [onEnd] call when an animation is canceled, so we post the value
706 * received in [onApplyWindowInsets] immediately after [onPrepare]. If [onProgress] or [onEnd]
707 * is received before the runnable executes then the value won't be used. Otherwise, the
708 * [onApplyWindowInsets] value will be used. It may have a janky frame, but it is the best we
709 * can do.
710 */
runnull711 override fun run() {
712 if (prepared) {
713 prepared = false
714 runningAnimation = false
715 savedInsets?.let {
716 composeInsets.updateImeAnimationSource(it)
717 composeInsets.update(it)
718 savedInsets = null
719 }
720 }
721 }
722
onViewAttachedToWindownull723 override fun onViewAttachedToWindow(view: View) {
724 view.requestApplyInsets()
725 }
726
onViewDetachedFromWindownull727 override fun onViewDetachedFromWindow(v: View) {}
728 }
729