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