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