Add multi-toggle preference UI for device details page
BUG: 343317785 Test: atest DeviceSettingRepositoryTest Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: I67e7647fee39e789cc1342943f69e7ddc170d0eb
This commit is contained in:
24
res/drawable/ic_close.xml
Normal file
24
res/drawable/ic_close.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2024 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24.0dp"
|
||||||
|
android:height="24.0dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
|
||||||
|
android:fillColor="#FF000000"/>
|
||||||
|
</vector>
|
@@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.settings.bluetooth.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.boundsInParent
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.role
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.semantics.toggleableState
|
||||||
|
import androidx.compose.ui.state.ToggleableState
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.android.settings.R
|
||||||
|
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||||
|
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
||||||
|
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||||
|
import com.android.settingslib.spa.widget.dialog.getDialogWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MultiTogglePreferenceGroup(
|
||||||
|
preferenceModels: List<DeviceSettingModel.MultiTogglePreference>,
|
||||||
|
) {
|
||||||
|
var settingIdForPopUp by remember { mutableStateOf<Int?>(null) }
|
||||||
|
|
||||||
|
settingIdForPopUp?.let { id ->
|
||||||
|
preferenceModels.find { it.id == id }?.let { dialog(it) { settingIdForPopUp = null } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(SettingsDimension.itemPadding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
) {
|
||||||
|
preferenceModels.forEach { preferenceModel ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.height(64.dp),
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxSize().padding(8.dp).semantics {
|
||||||
|
role = Role.Switch
|
||||||
|
toggleableState =
|
||||||
|
if (preferenceModel.isActive) {
|
||||||
|
ToggleableState.On
|
||||||
|
} else {
|
||||||
|
ToggleableState.Off
|
||||||
|
}
|
||||||
|
contentDescription = preferenceModel.title
|
||||||
|
},
|
||||||
|
onClick = { settingIdForPopUp = preferenceModel.id },
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = getButtonColors(preferenceModel.isActive),
|
||||||
|
contentPadding = PaddingValues(0.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
preferenceModel.toggles[preferenceModel.state.selectedIndex]
|
||||||
|
.icon
|
||||||
|
.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = LocalContentColor.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row { Text(text = preferenceModel.title, fontSize = 12.sp) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getButtonColors(isActive: Boolean) =
|
||||||
|
if (isActive) {
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun dialog(
|
||||||
|
multiTogglePreference: DeviceSettingModel.MultiTogglePreference,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = { onDismiss() },
|
||||||
|
modifier = Modifier.width(getDialogWidth()),
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
content = {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth().height(192.dp),
|
||||||
|
content = {
|
||||||
|
Box {
|
||||||
|
Button(
|
||||||
|
onClick = { onDismiss() },
|
||||||
|
modifier = Modifier.padding(8.dp).align(Alignment.TopEnd).size(48.dp),
|
||||||
|
contentPadding = PaddingValues(12.dp),
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(containerColor = Color.Transparent),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(id = R.drawable.ic_close),
|
||||||
|
null,
|
||||||
|
tint = MaterialTheme.colorScheme.inverseSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) {
|
||||||
|
dialogContent(multiTogglePreference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiTogglePreference) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(24.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
Text(text = multiTogglePreference.title, fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
var selectedRect by remember { mutableStateOf<Rect?>(null) }
|
||||||
|
val offset =
|
||||||
|
selectedRect?.let { rect ->
|
||||||
|
animateFloatAsState(targetValue = rect.left, finishedListener = {}).value
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.height(64.dp)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
shape = RoundedCornerShape(28.dp)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
offset?.let { offset ->
|
||||||
|
with(LocalDensity.current) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier.offset(offset.toDp(), 0.dp)
|
||||||
|
.height(selectedRect!!.height.toDp())
|
||||||
|
.width(selectedRect!!.width.toDp())
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
shape = RoundedCornerShape(20.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
for ((idx, toggle) in multiTogglePreference.toggles.withIndex()) {
|
||||||
|
val selected = idx == multiTogglePreference.state.selectedIndex
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier.weight(1f)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.height(48.dp)
|
||||||
|
.background(
|
||||||
|
Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(28.dp)
|
||||||
|
)
|
||||||
|
.onGloballyPositioned { layoutCoordinates ->
|
||||||
|
if (selected) {
|
||||||
|
selectedRect = layoutCoordinates.boundsInParent()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
multiTogglePreference.updateState(
|
||||||
|
DeviceSettingStateModel.MultiTogglePreferenceState(idx)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
bitmap = toggle.icon.asImageBitmap(),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = LocalContentColor.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(32.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
for (toggle in multiTogglePreference.toggles) {
|
||||||
|
Text(
|
||||||
|
text = toggle.label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.weight(1f).padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user