1 /*
<lambda>null2 * Copyright (C) 2023 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 package com.android.permissioncontroller.wear.permission.components.material2
17
18 import androidx.compose.foundation.interaction.MutableInteractionSource
19 import androidx.compose.foundation.layout.BoxScope
20 import androidx.compose.foundation.layout.Row
21 import androidx.compose.foundation.layout.RowScope
22 import androidx.compose.foundation.layout.fillMaxWidth
23 import androidx.compose.foundation.layout.size
24 import androidx.compose.foundation.shape.CircleShape
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.remember
27 import androidx.compose.ui.Modifier
28 import androidx.compose.ui.draw.clip
29 import androidx.compose.ui.graphics.Color
30 import androidx.compose.ui.graphics.compositeOver
31 import androidx.compose.ui.res.stringResource
32 import androidx.compose.ui.semantics.Role
33 import androidx.compose.ui.semantics.role
34 import androidx.compose.ui.semantics.semantics
35 import androidx.compose.ui.semantics.stateDescription
36 import androidx.compose.ui.text.style.Hyphens
37 import androidx.compose.ui.text.style.TextAlign
38 import androidx.compose.ui.text.style.TextOverflow
39 import androidx.wear.compose.material.ChipDefaults
40 import androidx.wear.compose.material.ContentAlpha
41 import androidx.wear.compose.material.MaterialTheme
42 import androidx.wear.compose.material.Text
43 import androidx.wear.compose.material.ToggleChip
44 import androidx.wear.compose.material.ToggleChipColors
45 import androidx.wear.compose.material.ToggleChipDefaults
46 import androidx.wear.compose.material.contentColorFor
47 import com.android.permissioncontroller.wear.permission.components.R
48 import com.android.permissioncontroller.wear.permission.components.material3.WearPermissionToggleControlType
49
50 /**
51 * This component is an alternative to [ToggleChip], providing the following:
52 * - a convenient way of providing a label and a secondary label;
53 * - a convenient way of choosing the toggle control;
54 * - a convenient way of providing an icon and setting the icon to be mirrored in RTL mode;
55 */
56 @Composable
57 fun ToggleChip(
58 checked: Boolean,
59 onCheckedChanged: (Boolean) -> Unit,
60 label: String,
61 labelMaxLine: Int? = null,
62 toggleControl: WearPermissionToggleControlType,
63 modifier: Modifier = Modifier,
64 icon: Any? = null,
65 iconColor: Color = Color.Unspecified,
66 iconRtlMode: IconRtlMode = IconRtlMode.Default,
67 secondaryLabel: String? = null,
68 secondaryLabelMaxLine: Int? = null,
69 colors: ToggleChipColors = ToggleChipDefaults.toggleChipColors(),
70 enabled: Boolean = true,
71 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
72 ) {
73 val hasSecondaryLabel = secondaryLabel != null
74
<lambda>null75 val labelParam: (@Composable RowScope.() -> Unit) = {
76 Text(
77 text = label,
78 modifier = Modifier.fillMaxWidth(),
79 textAlign = TextAlign.Start,
80 overflow = TextOverflow.Ellipsis,
81 maxLines = labelMaxLine ?: if (hasSecondaryLabel) 1 else 2,
82 style = MaterialTheme.typography.button.copy(hyphens = Hyphens.Auto),
83 )
84 }
85
86 val secondaryLabelParam: (@Composable RowScope.() -> Unit)? =
<lambda>null87 secondaryLabel?.let {
88 {
89 Text(
90 text = secondaryLabel,
91 overflow = TextOverflow.Ellipsis,
92 maxLines = secondaryLabelMaxLine ?: 1,
93 style = MaterialTheme.typography.caption2.copy(hyphens = Hyphens.Auto),
94 )
95 }
96 }
97
<lambda>null98 val toggleControlParam: (@Composable () -> Unit) = {
99 Icon(
100 imageVector =
101 when (toggleControl) {
102 WearPermissionToggleControlType.Switch -> ToggleChipDefaults.switchIcon(checked)
103 WearPermissionToggleControlType.Radio -> ToggleChipDefaults.radioIcon(checked)
104 WearPermissionToggleControlType.Checkbox ->
105 ToggleChipDefaults.checkboxIcon(checked)
106 },
107 contentDescription = null,
108 // This potentially be removed once this issue is addressed:
109 // https://issuetracker.google.com/issues/287087138
110 rtlMode =
111 if (toggleControl == WearPermissionToggleControlType.Switch) {
112 IconRtlMode.Mirrored
113 } else {
114 IconRtlMode.Default
115 },
116 )
117 }
118
119 val iconParam: (@Composable BoxScope.() -> Unit)? =
<lambda>null120 icon?.let {
121 {
122 Row {
123 Icon(
124 icon = icon,
125 tint = iconColor,
126 contentDescription = null,
127 modifier = Modifier.size(ChipDefaults.IconSize).clip(CircleShape),
128 rtlMode = iconRtlMode,
129 )
130 }
131 }
132 }
133
134 ToggleChip(
135 checked = checked,
newCheckednull136 onCheckedChange = { newChecked ->
137 // Radio buttons cannot be toggled off by tapping on it again.
138 if (toggleControl != WearPermissionToggleControlType.Radio || newChecked) {
139 onCheckedChanged.invoke(newChecked)
140 }
141 },
142 label = labelParam,
143 toggleControl = toggleControlParam,
144 modifier = modifier.fillMaxWidth().toggleControlSemantics(toggleControl, checked),
145 appIcon = iconParam,
146 secondaryLabel = secondaryLabelParam,
147 colors = colors,
148 enabled = enabled,
149 interactionSource = interactionSource,
150 )
151 }
152
153 /**
154 * ToggleChipColors that disabled alpha is applied based on [ToggleChipDefaults.toggleChipColors()].
155 * It is used for a ToggleChip which would like to respond to click events, meanwhile it seems
156 * disabled.
157 */
158 @Composable
toggleChipDisabledColorsnull159 fun toggleChipDisabledColors(): ToggleChipColors {
160 val checkedStartBackgroundColor =
161 MaterialTheme.colors.surface.copy(alpha = 0f).compositeOver(MaterialTheme.colors.surface)
162 val checkedEndBackgroundColor =
163 MaterialTheme.colors.primary.copy(alpha = 0.5f).compositeOver(MaterialTheme.colors.surface)
164 val checkedContentColor = MaterialTheme.colors.onSurface
165 val checkedSecondaryContentColor = MaterialTheme.colors.onSurfaceVariant
166 val checkedToggleControlColor = MaterialTheme.colors.secondary
167 val uncheckedStartBackgroundColor = MaterialTheme.colors.surface
168 val uncheckedEndBackgroundColor = uncheckedStartBackgroundColor
169 val uncheckedContentColor = contentColorFor(checkedEndBackgroundColor)
170 val uncheckedSecondaryContentColor = uncheckedContentColor
171 val uncheckedToggleControlColor = uncheckedContentColor
172
173 return ToggleChipDefaults.toggleChipColors(
174 checkedStartBackgroundColor =
175 checkedStartBackgroundColor.copy(alpha = ContentAlpha.disabled),
176 checkedEndBackgroundColor = checkedEndBackgroundColor.copy(alpha = ContentAlpha.disabled),
177 checkedContentColor = checkedContentColor.copy(alpha = ContentAlpha.disabled),
178 checkedSecondaryContentColor =
179 checkedSecondaryContentColor.copy(alpha = ContentAlpha.disabled),
180 checkedToggleControlColor = checkedToggleControlColor.copy(alpha = ContentAlpha.disabled),
181 uncheckedStartBackgroundColor =
182 uncheckedStartBackgroundColor.copy(alpha = ContentAlpha.disabled),
183 uncheckedEndBackgroundColor =
184 uncheckedEndBackgroundColor.copy(alpha = ContentAlpha.disabled),
185 uncheckedContentColor = uncheckedContentColor.copy(alpha = ContentAlpha.disabled),
186 uncheckedSecondaryContentColor =
187 uncheckedSecondaryContentColor.copy(alpha = ContentAlpha.disabled),
188 uncheckedToggleControlColor =
189 uncheckedToggleControlColor.copy(alpha = ContentAlpha.disabled),
190 )
191 }
192
193 /**
194 * ToggleChipColors that theme background color is applied based on
195 * [ToggleChipDefaults.toggleChipColors()]. It is used for a ToggleChip having the same background
196 * color of the screen.
197 */
198 @Composable
toggleChipBackgroundColorsnull199 fun toggleChipBackgroundColors(): ToggleChipColors {
200 val checkedStartBackgroundColor =
201 MaterialTheme.colors.background
202 .copy(alpha = 0f)
203 .compositeOver(MaterialTheme.colors.background)
204 val checkedEndBackgroundColor =
205 MaterialTheme.colors.primary
206 .copy(alpha = 0.5f)
207 .compositeOver(MaterialTheme.colors.background)
208 val checkedContentColor = MaterialTheme.colors.onBackground
209 val checkedSecondaryContentColor = MaterialTheme.colors.onSurfaceVariant
210 val checkedToggleControlColor = MaterialTheme.colors.secondary
211 val uncheckedStartBackgroundColor = MaterialTheme.colors.background
212 val uncheckedEndBackgroundColor = uncheckedStartBackgroundColor
213 val uncheckedContentColor = contentColorFor(checkedEndBackgroundColor)
214 val uncheckedSecondaryContentColor = uncheckedContentColor
215 val uncheckedToggleControlColor = uncheckedContentColor
216
217 return ToggleChipDefaults.toggleChipColors(
218 checkedStartBackgroundColor = checkedStartBackgroundColor,
219 checkedEndBackgroundColor = checkedEndBackgroundColor,
220 checkedContentColor = checkedContentColor,
221 checkedSecondaryContentColor = checkedSecondaryContentColor,
222 checkedToggleControlColor = checkedToggleControlColor,
223 uncheckedStartBackgroundColor = uncheckedStartBackgroundColor,
224 uncheckedEndBackgroundColor = uncheckedEndBackgroundColor,
225 uncheckedContentColor = uncheckedContentColor,
226 uncheckedSecondaryContentColor = uncheckedSecondaryContentColor,
227 uncheckedToggleControlColor = uncheckedToggleControlColor,
228 )
229 }
230
231 @Composable
Modifiernull232 fun Modifier.toggleControlSemantics(
233 toggleControl: WearPermissionToggleControlType,
234 checked: Boolean,
235 ): Modifier {
236 val semanticsRole =
237 when (toggleControl) {
238 WearPermissionToggleControlType.Switch -> Role.Switch
239 WearPermissionToggleControlType.Radio -> Role.RadioButton
240 WearPermissionToggleControlType.Checkbox -> Role.Checkbox
241 }
242 val stateDescriptionSemantics =
243 stringResource(
244 if (checked) {
245 R.string.on
246 } else {
247 R.string.off
248 }
249 )
250
251 return semantics {
252 role = semanticsRole
253 stateDescription = stateDescriptionSemantics
254 }
255 }
256