1 /*
2 * Copyright 2022 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 androidx.glance.appwidget
18
19 import androidx.annotation.RestrictTo
20 import androidx.compose.runtime.Composable
21 import androidx.compose.ui.graphics.Color
22 import androidx.glance.Emittable
23 import androidx.glance.EmittableCheckable
24 import androidx.glance.ExperimentalGlanceApi
25 import androidx.glance.GlanceModifier
26 import androidx.glance.GlanceNode
27 import androidx.glance.GlanceTheme
28 import androidx.glance.action.Action
29 import androidx.glance.action.action
30 import androidx.glance.action.clickable
31 import androidx.glance.appwidget.unit.CheckableColorProvider
32 import androidx.glance.appwidget.unit.CheckedUncheckedColorProvider.Companion.createCheckableColorProvider
33 import androidx.glance.appwidget.unit.ResourceCheckableColorProvider
34 import androidx.glance.color.DynamicThemeColorProviders
35 import androidx.glance.text.TextStyle
36 import androidx.glance.unit.ColorProvider
37 import androidx.glance.unit.FixedColorProvider
38
39 /** Set of colors to apply to a RadioButton depending on the checked state. */
40 class RadioButtonColors internal constructor(internal val radio: CheckableColorProvider)
41
42 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
43 class EmittableRadioButton(var colors: RadioButtonColors) : EmittableCheckable() {
44 override var modifier: GlanceModifier = GlanceModifier
45 var enabled: Boolean = true
46
copynull47 override fun copy(): Emittable =
48 EmittableRadioButton(colors = colors).also {
49 it.modifier = modifier
50 it.checked = checked
51 it.enabled = enabled
52 it.text = text
53 it.style = style
54 it.maxLines = maxLines
55 }
56
toStringnull57 override fun toString(): String =
58 "EmittableRadioButton(" +
59 "$text, " +
60 "modifier=$modifier, " +
61 "checked=$checked, " +
62 "enabled=$enabled, " +
63 "text=$text, " +
64 "style=$style, " +
65 "colors=$colors, " +
66 "maxLines=$maxLines, " +
67 ")"
68 }
69
70 /**
71 * Adds a radio button to the glance view.
72 *
73 * When showing a [Row] or [Column] that has [RadioButton] children, use
74 * [GlanceModifier.selectableGroup] to enable the radio group effect (unselecting the previously
75 * selected radio button when another is selected).
76 *
77 * @param checked whether the radio button is checked
78 * @param onClick the action to be run when the radio button is clicked
79 * @param modifier the modifier to apply to the radio button
80 * @param enabled if false, the radio button will not be clickable
81 * @param text the text to display to the end of the radio button
82 * @param style the style to apply to [text]
83 * @param colors the color tint to apply to the radio button
84 * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary.
85 * If the text exceeds the given number of lines, it will be truncated.
86 */
87 @Composable
88 fun RadioButton(
89 checked: Boolean,
90 onClick: Action?,
91 modifier: GlanceModifier = GlanceModifier,
92 enabled: Boolean = true,
93 text: String = "",
94 style: TextStyle? = null,
95 colors: RadioButtonColors = RadioButtonDefaults.colors(),
96 maxLines: Int = Int.MAX_VALUE,
97 ) = RadioButtonElement(checked, onClick, modifier, enabled, text, style, colors, maxLines)
98
99 /**
100 * Adds a radio button to the glance view.
101 *
102 * When showing a [Row] or [Column] that has [RadioButton] children, use
103 * [GlanceModifier.selectableGroup] to enable the radio group effect (unselecting the previously
104 * selected radio button when another is selected).
105 *
106 * @param checked whether the radio button is checked
107 * @param onClick the action to be run when the radio button is clicked
108 * @param modifier the modifier to apply to the radio button
109 * @param enabled if false, the radio button will not be clickable
110 * @param text the text to display to the end of the radio button
111 * @param style the style to apply to [text]
112 * @param colors the color tint to apply to the radio button
113 * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary.
114 * If the text exceeds the given number of lines, it will be truncated.
115 */
116 @Composable
117 fun RadioButton(
118 checked: Boolean,
119 onClick: () -> Unit,
120 modifier: GlanceModifier = GlanceModifier,
121 enabled: Boolean = true,
122 text: String = "",
123 style: TextStyle? = null,
124 colors: RadioButtonColors = RadioButtonDefaults.colors(),
125 maxLines: Int = Int.MAX_VALUE,
126 ) =
127 RadioButtonElement(
128 checked,
129 action(block = onClick),
130 modifier,
131 enabled,
132 text,
133 style,
134 colors,
135 maxLines
136 )
137
138 /**
139 * Adds a radio button to the glance view.
140 *
141 * When showing a [Row] or [Column] that has [RadioButton] children, use
142 * [GlanceModifier.selectableGroup] to enable the radio group effect (unselecting the previously
143 * selected radio button when another is selected).
144 *
145 * @param checked whether the radio button is checked
146 * @param onClick the action to be run when the radio button is clicked
147 * @param modifier the modifier to apply to the radio button
148 * @param enabled if false, the radio button will not be clickable
149 * @param text the text to display to the end of the radio button
150 * @param style the style to apply to [text]
151 * @param colors the color tint to apply to the radio button
152 * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary.
153 * If the text exceeds the given number of lines, it will be truncated.
154 * @param key A stable and unique key that identifies the action for this radio button. This ensures
155 * that the correct action is triggered, especially in cases of items that change order. If not
156 * provided we use the key that is automatically generated by the Compose runtime, which is unique
157 * for every exact code location in the composition tree.
158 */
159 @ExperimentalGlanceApi
160 @Composable
161 fun RadioButton(
162 checked: Boolean,
163 onClick: () -> Unit,
164 modifier: GlanceModifier = GlanceModifier,
165 enabled: Boolean = true,
166 text: String = "",
167 style: TextStyle? = null,
168 colors: RadioButtonColors = RadioButtonDefaults.colors(),
169 maxLines: Int = Int.MAX_VALUE,
170 key: String? = null,
171 ) =
172 RadioButtonElement(
173 checked,
174 action(key, onClick),
175 modifier,
176 enabled,
177 text,
178 style,
179 colors,
180 maxLines
181 )
182
183 /** Contains the default values used by [RadioButton]. */
184 object RadioButtonDefaults {
185 /**
186 * Creates a [RadioButtonColors] using [ColorProvider]s.
187 *
188 * @param checkedColor the tint to apply to the radio button when it is checked.
189 * @param uncheckedColor the tint to apply to the radio button when it is not checked.
190 * @return [RadioButtonColors] to tint the drawable of the [RadioButton] according to the
191 * checked state.
192 */
193 fun colors(
194 checkedColor: ColorProvider,
195 uncheckedColor: ColorProvider,
196 ): RadioButtonColors {
197 return RadioButtonColors(
198 radio =
199 createCheckableColorProvider(
200 source = "RadioButtonColors",
201 checked = checkedColor,
202 unchecked = uncheckedColor
203 )
204 )
205 }
206
207 /**
208 * Creates a [RadioButtonColors] using [FixedColorProvider]s for the given colors.
209 *
210 * @param checkedColor the [Color] to use when the RadioButton is checked
211 * @param uncheckedColor the [Color] to use when the RadioButton is not checked
212 * @return [RadioButtonColors] to tint the drawable of the [RadioButton] according to the
213 * checked state.
214 */
215 fun colors(checkedColor: Color, uncheckedColor: Color): RadioButtonColors =
216 colors(
217 checkedColor = FixedColorProvider(checkedColor),
218 uncheckedColor = FixedColorProvider(uncheckedColor)
219 )
220
221 /**
222 * Creates a default [RadioButtonColors]
223 *
224 * @return default [RadioButtonColors].
225 */
226 @Composable
227 fun colors(): RadioButtonColors {
228 val colorProvider =
229 if (GlanceTheme.colors == DynamicThemeColorProviders) {
230 // If using the m3 dynamic color theme, we need to create a color provider from xml
231 // because resource backed ColorStateLists cannot be created programmatically
232 ResourceCheckableColorProvider(R.color.glance_default_radio_button)
233 } else {
234 createCheckableColorProvider(
235 source = "CheckBoxColors",
236 checked = GlanceTheme.colors.primary,
237 unchecked = GlanceTheme.colors.onSurfaceVariant
238 )
239 }
240
241 return RadioButtonColors(colorProvider)
242 }
243 }
244
245 @Composable
RadioButtonElementnull246 private fun RadioButtonElement(
247 checked: Boolean,
248 onClick: Action?,
249 modifier: GlanceModifier = GlanceModifier,
250 enabled: Boolean = true,
251 text: String = "",
252 style: TextStyle? = null,
253 colors: RadioButtonColors = RadioButtonDefaults.colors(),
254 maxLines: Int = Int.MAX_VALUE,
255 ) {
256 val finalModifier = if (enabled && onClick != null) modifier.clickable(onClick) else modifier
257 GlanceNode(
258 factory = { EmittableRadioButton(colors) },
259 update = {
260 this.set(checked) { this.checked = it }
261 this.set(finalModifier) { this.modifier = it }
262 this.set(enabled) { this.enabled = it }
263 this.set(text) { this.text = it }
264 this.set(style) { this.style = it }
265 this.set(colors) { this.colors = it }
266 this.set(maxLines) { this.maxLines = it }
267 }
268 )
269 }
270
271 /**
272 * Use this modifier to group a list of RadioButtons together for accessibility purposes.
273 *
274 * This modifier can only be used on a [Row] or [Column]. This modifier additonally enables the
275 * radio group effect, which automatically unselects the currently selected RadioButton when another
276 * is selected. When this modifier is used, an error will be thrown if more than one RadioButton has
277 * their "checked" value set to true.
278 */
GlanceModifiernull279 fun GlanceModifier.selectableGroup(): GlanceModifier = this.then(SelectableGroupModifier)
280
281 internal object SelectableGroupModifier : GlanceModifier.Element
282
283 internal val GlanceModifier.isSelectableGroup: Boolean
284 get() = any { it is SelectableGroupModifier }
285