1 /*
2  * Copyright 2024 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 @file:JvmName("ExposedDropdownMenu_android")
18 
19 package androidx.compose.material
20 
21 import android.graphics.Rect as ViewRect
22 import android.view.View
23 import android.view.ViewTreeObserver
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.DisposableEffect
26 import androidx.compose.runtime.remember
27 import androidx.compose.ui.graphics.toComposeIntRect
28 import androidx.compose.ui.platform.LocalView
29 import androidx.compose.ui.unit.IntRect
30 
31 internal actual class WindowBoundsCalculator(private val view: View) {
getVisibleWindowBoundsnull32     actual fun getVisibleWindowBounds(): IntRect = view.getWindowBounds()
33 }
34 
35 @Composable
36 internal actual fun platformWindowBoundsCalculator(): WindowBoundsCalculator {
37     val view = LocalView.current
38     return remember(view) { WindowBoundsCalculator(view) }
39 }
40 
41 @Composable
OnPlatformWindowBoundsChangenull42 internal actual fun OnPlatformWindowBoundsChange(block: () -> Unit) {
43     val view = LocalView.current
44     DisposableEffect(view) {
45         val listener = OnGlobalLayoutListener(view, block)
46         onDispose { listener.dispose() }
47     }
48 }
49 
50 /**
51  * Subscribes to onGlobalLayout and correctly removes the callback when the View is detached. Logic
52  * copied from AndroidPopup.android.kt.
53  */
54 private class OnGlobalLayoutListener(
55     private val view: View,
56     private val onGlobalLayoutCallback: () -> Unit
57 ) : View.OnAttachStateChangeListener, ViewTreeObserver.OnGlobalLayoutListener {
58     private var isListeningToGlobalLayout = false
59 
60     init {
61         view.addOnAttachStateChangeListener(this)
62         registerOnGlobalLayoutListener()
63     }
64 
onViewAttachedToWindownull65     override fun onViewAttachedToWindow(p0: View) = registerOnGlobalLayoutListener()
66 
67     override fun onViewDetachedFromWindow(p0: View) = unregisterOnGlobalLayoutListener()
68 
69     override fun onGlobalLayout() = onGlobalLayoutCallback()
70 
71     private fun registerOnGlobalLayoutListener() {
72         if (isListeningToGlobalLayout || !view.isAttachedToWindow) return
73         view.viewTreeObserver.addOnGlobalLayoutListener(this)
74         isListeningToGlobalLayout = true
75     }
76 
unregisterOnGlobalLayoutListenernull77     private fun unregisterOnGlobalLayoutListener() {
78         if (!isListeningToGlobalLayout) return
79         view.viewTreeObserver.removeOnGlobalLayoutListener(this)
80         isListeningToGlobalLayout = false
81     }
82 
disposenull83     fun dispose() {
84         unregisterOnGlobalLayoutListener()
85         view.removeOnAttachStateChangeListener(this)
86     }
87 }
88 
getWindowBoundsnull89 private fun View.getWindowBounds(): IntRect =
90     ViewRect().let {
91         this.getWindowVisibleDisplayFrame(it)
92         it.toComposeIntRect()
93     }
94