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 OnPlatformWindowBoundsChangenull42internal 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 getWindowBoundsnull89private fun View.getWindowBounds(): IntRect = 90 ViewRect().let { 91 this.getWindowVisibleDisplayFrame(it) 92 it.toComposeIntRect() 93 } 94