1 /* 2 * Copyright (C) 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 package com.android.systemui.volume.panel.component.popup.ui.composable 18 19 import android.view.Gravity 20 import androidx.annotation.GravityInt 21 import androidx.compose.foundation.layout.Arrangement 22 import androidx.compose.foundation.layout.Box 23 import androidx.compose.foundation.layout.Column 24 import androidx.compose.foundation.layout.fillMaxWidth 25 import androidx.compose.foundation.layout.padding 26 import androidx.compose.foundation.layout.size 27 import androidx.compose.foundation.layout.wrapContentHeight 28 import androidx.compose.material3.Icon 29 import androidx.compose.material3.IconButton 30 import androidx.compose.material3.IconButtonDefaults 31 import androidx.compose.material3.MaterialTheme 32 import androidx.compose.runtime.Composable 33 import androidx.compose.ui.Alignment 34 import androidx.compose.ui.Modifier 35 import androidx.compose.ui.layout.LayoutCoordinates 36 import androidx.compose.ui.layout.boundsInRoot 37 import androidx.compose.ui.res.painterResource 38 import androidx.compose.ui.res.stringResource 39 import androidx.compose.ui.semantics.paneTitle 40 import androidx.compose.ui.semantics.semantics 41 import androidx.compose.ui.unit.dp 42 import com.android.systemui.animation.DialogTransitionAnimator 43 import com.android.systemui.animation.Expandable 44 import com.android.systemui.res.R 45 import com.android.systemui.statusbar.phone.SystemUIDialog 46 import com.android.systemui.statusbar.phone.SystemUIDialogFactory 47 import com.android.systemui.statusbar.phone.create 48 import javax.inject.Inject 49 50 /** Volume panel bottom popup menu. */ 51 class VolumePanelPopup 52 @Inject 53 constructor( 54 private val dialogFactory: SystemUIDialogFactory, 55 private val dialogTransitionAnimator: DialogTransitionAnimator, 56 ) { 57 58 /** 59 * Shows a popup with the [expandable] animation. 60 * 61 * @param title is shown on the top of the popup 62 * @param content is the popup body 63 */ shownull64 fun show( 65 expandable: Expandable?, 66 @GravityInt gravity: Int, 67 title: @Composable (SystemUIDialog) -> Unit, 68 content: @Composable (SystemUIDialog) -> Unit, 69 ) { 70 val dialog = 71 dialogFactory.create( 72 theme = R.style.Theme_VolumePanel_Popup, 73 dialogGravity = gravity, 74 ) { 75 PopupComposable(it, title, content) 76 } 77 val controller = expandable?.dialogTransitionController() 78 if (controller == null) { 79 dialog.show() 80 } else { 81 dialogTransitionAnimator.show(dialog, controller) 82 } 83 } 84 85 @Composable PopupComposablenull86 private fun PopupComposable( 87 dialog: SystemUIDialog, 88 title: @Composable (SystemUIDialog) -> Unit, 89 content: @Composable (SystemUIDialog) -> Unit, 90 ) { 91 val paneTitle = stringResource(R.string.accessibility_volume_settings) 92 Box(Modifier.fillMaxWidth().semantics(properties = { this.paneTitle = paneTitle })) { 93 Column( 94 modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp), 95 verticalArrangement = Arrangement.spacedBy(20.dp), 96 ) { 97 Box( 98 modifier = 99 Modifier.padding(horizontal = 80.dp).fillMaxWidth().wrapContentHeight(), 100 contentAlignment = Alignment.Center 101 ) { 102 title(dialog) 103 } 104 105 Box( 106 modifier = 107 Modifier.padding(horizontal = 16.dp).fillMaxWidth().wrapContentHeight(), 108 contentAlignment = Alignment.Center 109 ) { 110 content(dialog) 111 } 112 } 113 114 IconButton( 115 modifier = Modifier.align(Alignment.TopEnd).size(64.dp).padding(20.dp), 116 onClick = { dialog.dismiss() }, 117 colors = 118 IconButtonDefaults.iconButtonColors( 119 contentColor = MaterialTheme.colorScheme.outline 120 ) 121 ) { 122 Icon( 123 painterResource(R.drawable.ic_close), 124 contentDescription = stringResource(R.string.accessibility_desc_close), 125 ) 126 } 127 } 128 } 129 130 companion object { 131 132 /** 133 * Returns absolute ([Gravity.LEFT], [Gravity.RIGHT] or [Gravity.CENTER_HORIZONTAL]) 134 * [GravityInt] for the popup based on the [coordinates] global position relative to the 135 * [screenWidthPx]. 136 */ 137 @GravityInt calculateGravitynull138 fun calculateGravity(coordinates: LayoutCoordinates, screenWidthPx: Float): Int { 139 val bottomCenter: Float = coordinates.boundsInRoot().bottomCenter.x 140 val rootBottomCenter: Float = screenWidthPx / 2 141 return when { 142 bottomCenter < rootBottomCenter -> Gravity.LEFT 143 bottomCenter > rootBottomCenter -> Gravity.RIGHT 144 else -> Gravity.CENTER_HORIZONTAL 145 } 146 } 147 } 148 } 149