1 /*
<lambda>null2  * Copyright 2019 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.compose.material
18 
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.VectorConverter
21 import androidx.compose.foundation.BorderStroke
22 import androidx.compose.foundation.interaction.FocusInteraction
23 import androidx.compose.foundation.interaction.HoverInteraction
24 import androidx.compose.foundation.interaction.Interaction
25 import androidx.compose.foundation.interaction.InteractionSource
26 import androidx.compose.foundation.interaction.MutableInteractionSource
27 import androidx.compose.foundation.interaction.PressInteraction
28 import androidx.compose.foundation.layout.Arrangement
29 import androidx.compose.foundation.layout.PaddingValues
30 import androidx.compose.foundation.layout.Row
31 import androidx.compose.foundation.layout.RowScope
32 import androidx.compose.foundation.layout.defaultMinSize
33 import androidx.compose.foundation.layout.padding
34 import androidx.compose.runtime.Composable
35 import androidx.compose.runtime.CompositionLocalProvider
36 import androidx.compose.runtime.Immutable
37 import androidx.compose.runtime.LaunchedEffect
38 import androidx.compose.runtime.NonRestartableComposable
39 import androidx.compose.runtime.Stable
40 import androidx.compose.runtime.State
41 import androidx.compose.runtime.getValue
42 import androidx.compose.runtime.mutableStateListOf
43 import androidx.compose.runtime.remember
44 import androidx.compose.runtime.rememberUpdatedState
45 import androidx.compose.ui.Alignment
46 import androidx.compose.ui.Modifier
47 import androidx.compose.ui.geometry.Offset
48 import androidx.compose.ui.graphics.Color
49 import androidx.compose.ui.graphics.Shape
50 import androidx.compose.ui.graphics.compositeOver
51 import androidx.compose.ui.semantics.Role
52 import androidx.compose.ui.semantics.role
53 import androidx.compose.ui.semantics.semantics
54 import androidx.compose.ui.unit.Dp
55 import androidx.compose.ui.unit.dp
56 
57 /**
58  * [Material Design contained button](https://material.io/components/buttons#contained-button)
59  *
60  * Contained buttons are high-emphasis, distinguished by their use of elevation and fill. They
61  * contain actions that are primary to your app.
62  *
63  * ![Contained button
64  * image](https://developer.android.com/images/reference/androidx/compose/material/contained-button.png)
65  *
66  * The default text style for internal [Text] components will be set to [Typography.button].
67  *
68  * @sample androidx.compose.material.samples.ButtonSample
69  *
70  * If you need to add an icon just put it inside the [content] slot together with a spacing and a
71  * text:
72  *
73  * @sample androidx.compose.material.samples.ButtonWithIconSample
74  * @param onClick Will be called when the user clicks the button
75  * @param modifier Modifier to be applied to the button
76  * @param enabled Controls the enabled state of the button. When `false`, this button will not be
77  *   clickable
78  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
79  *   emitting [Interaction]s for this button. You can use this to change the button's appearance or
80  *   preview the button in different states. Note that if `null` is provided, interactions will
81  *   still happen internally.
82  * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
83  *   states. This controls the size of the shadow below the button. Pass `null` here to disable
84  *   elevation for this button. See [ButtonDefaults.elevation].
85  * @param shape Defines the button's shape as well as its shadow
86  * @param border Border to draw around the button
87  * @param colors [ButtonColors] that will be used to resolve the background and content color for
88  *   this button in different states. See [ButtonDefaults.buttonColors].
89  * @param contentPadding The spacing values to apply internally between the container and the
90  *   content
91  * @param content The content displayed on the button, expected to be text, icon or image.
92  */
93 @OptIn(ExperimentalMaterialApi::class)
94 @Composable
95 fun Button(
96     onClick: () -> Unit,
97     modifier: Modifier = Modifier,
98     enabled: Boolean = true,
99     interactionSource: MutableInteractionSource? = null,
100     elevation: ButtonElevation? = ButtonDefaults.elevation(),
101     shape: Shape = MaterialTheme.shapes.small,
102     border: BorderStroke? = null,
103     colors: ButtonColors = ButtonDefaults.buttonColors(),
104     contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
105     content: @Composable RowScope.() -> Unit
106 ) {
107     @Suppress("NAME_SHADOWING")
108     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
109     val contentColor by colors.contentColor(enabled)
110     Surface(
111         onClick = onClick,
112         modifier = modifier.semantics { role = Role.Button },
113         enabled = enabled,
114         shape = shape,
115         color = colors.backgroundColor(enabled).value,
116         contentColor = contentColor.copy(alpha = 1f),
117         border = border,
118         elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
119         interactionSource = interactionSource
120     ) {
121         CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
122             ProvideTextStyle(value = MaterialTheme.typography.button) {
123                 Row(
124                     Modifier.defaultMinSize(
125                             minWidth = ButtonDefaults.MinWidth,
126                             minHeight = ButtonDefaults.MinHeight
127                         )
128                         .padding(contentPadding),
129                     horizontalArrangement = Arrangement.Center,
130                     verticalAlignment = Alignment.CenterVertically,
131                     content = content
132                 )
133             }
134         }
135     }
136 }
137 
138 /**
139  * [Material Design outlined button](https://material.io/components/buttons#outlined-button)
140  *
141  * Outlined buttons are medium-emphasis buttons. They contain actions that are important, but aren't
142  * the primary action in an app.
143  *
144  * ![Outlined button
145  * image](https://developer.android.com/images/reference/androidx/compose/material/outlined-button.png)
146  *
147  * The default text style for internal [Text] components will be set to [Typography.button].
148  *
149  * @sample androidx.compose.material.samples.OutlinedButtonSample
150  * @param onClick Will be called when the user clicks the button
151  * @param modifier Modifier to be applied to the button
152  * @param enabled Controls the enabled state of the button. When `false`, this button will not be
153  *   clickable
154  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
155  *   emitting [Interaction]s for this button. You can use this to change the button's appearance or
156  *   preview the button in different states. Note that if `null` is provided, interactions will
157  *   still happen internally.
158  * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
159  *   states. An OutlinedButton typically has no elevation, see [Button] for a button with elevation.
160  * @param shape Defines the button's shape as well as its shadow
161  * @param border Border to draw around the button
162  * @param colors [ButtonColors] that will be used to resolve the background and content color for
163  *   this button in different states. See [ButtonDefaults.outlinedButtonColors].
164  * @param contentPadding The spacing values to apply internally between the container and the
165  *   content
166  * @param content The content displayed on the button, expected to be text, icon or image.
167  */
168 @Composable
169 @NonRestartableComposable
OutlinedButtonnull170 fun OutlinedButton(
171     onClick: () -> Unit,
172     modifier: Modifier = Modifier,
173     enabled: Boolean = true,
174     interactionSource: MutableInteractionSource? = null,
175     elevation: ButtonElevation? = null,
176     shape: Shape = MaterialTheme.shapes.small,
177     border: BorderStroke? = ButtonDefaults.outlinedBorder,
178     colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
179     contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
180     content: @Composable RowScope.() -> Unit
181 ) =
182     Button(
183         onClick = onClick,
184         modifier = modifier,
185         enabled = enabled,
186         interactionSource = interactionSource,
187         elevation = elevation,
188         shape = shape,
189         border = border,
190         colors = colors,
191         contentPadding = contentPadding,
192         content = content
193     )
194 
195 /**
196  * [Material Design text button](https://material.io/components/buttons#text-button)
197  *
198  * Text buttons are typically used for less-pronounced actions, including those located in dialogs
199  * and cards. In cards, text buttons help maintain an emphasis on card content.
200  *
201  * ![Text button
202  * image](https://developer.android.com/images/reference/androidx/compose/material/text-button.png)
203  *
204  * The default text style for internal [Text] components will be set to [Typography.button].
205  *
206  * @sample androidx.compose.material.samples.TextButtonSample
207  * @param onClick Will be called when the user clicks the button
208  * @param modifier Modifier to be applied to the button
209  * @param enabled Controls the enabled state of the button. When `false`, this button will not be
210  *   clickable
211  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
212  *   emitting [Interaction]s for this button. You can use this to change the button's appearance or
213  *   preview the button in different states. Note that if `null` is provided, interactions will
214  *   still happen internally.
215  * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
216  *   states. A TextButton typically has no elevation, see [Button] for a button with elevation.
217  * @param shape Defines the button's shape as well as its shadow
218  * @param border Border to draw around the button
219  * @param colors [ButtonColors] that will be used to resolve the background and content color for
220  *   this button in different states. See [ButtonDefaults.textButtonColors].
221  * @param contentPadding The spacing values to apply internally between the container and the
222  *   content
223  * @param content The content displayed on the button, expected to be text.
224  */
225 @Composable
226 @NonRestartableComposable
227 fun TextButton(
228     onClick: () -> Unit,
229     modifier: Modifier = Modifier,
230     enabled: Boolean = true,
231     interactionSource: MutableInteractionSource? = null,
232     elevation: ButtonElevation? = null,
233     shape: Shape = MaterialTheme.shapes.small,
234     border: BorderStroke? = null,
235     colors: ButtonColors = ButtonDefaults.textButtonColors(),
236     contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
237     content: @Composable RowScope.() -> Unit
238 ) =
239     Button(
240         onClick = onClick,
241         modifier = modifier,
242         enabled = enabled,
243         interactionSource = interactionSource,
244         elevation = elevation,
245         shape = shape,
246         border = border,
247         colors = colors,
248         contentPadding = contentPadding,
249         content = content
250     )
251 
252 /**
253  * Represents the elevation for a button in different states.
254  *
255  * See [ButtonDefaults.elevation] for the default elevation used in a [Button].
256  */
257 @Stable
258 interface ButtonElevation {
259     /**
260      * Represents the elevation used in a button, depending on [enabled] and [interactionSource].
261      *
262      * @param enabled whether the button is enabled
263      * @param interactionSource the [InteractionSource] for this button
264      */
265     @Composable fun elevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp>
266 }
267 
268 /**
269  * Represents the background and content colors used in a button in different states.
270  *
271  * See [ButtonDefaults.buttonColors] for the default colors used in a [Button]. See
272  * [ButtonDefaults.outlinedButtonColors] for the default colors used in a [OutlinedButton]. See
273  * [ButtonDefaults.textButtonColors] for the default colors used in a [TextButton].
274  */
275 @Stable
276 interface ButtonColors {
277     /**
278      * Represents the background color for this button, depending on [enabled].
279      *
280      * @param enabled whether the button is enabled
281      */
backgroundColornull282     @Composable fun backgroundColor(enabled: Boolean): State<Color>
283 
284     /**
285      * Represents the content color for this button, depending on [enabled].
286      *
287      * @param enabled whether the button is enabled
288      */
289     @Composable fun contentColor(enabled: Boolean): State<Color>
290 }
291 
292 /** Contains the default values used by [Button] */
293 object ButtonDefaults {
294     private val ButtonHorizontalPadding = 16.dp
295     private val ButtonVerticalPadding = 8.dp
296 
297     /** The default content padding used by [Button] */
298     val ContentPadding =
299         PaddingValues(
300             start = ButtonHorizontalPadding,
301             top = ButtonVerticalPadding,
302             end = ButtonHorizontalPadding,
303             bottom = ButtonVerticalPadding
304         )
305 
306     /**
307      * The default min width applied for the [Button]. Note that you can override it by applying
308      * Modifier.widthIn directly on [Button].
309      */
310     val MinWidth = 64.dp
311 
312     /**
313      * The default min height applied for the [Button]. Note that you can override it by applying
314      * Modifier.heightIn directly on [Button].
315      */
316     val MinHeight = 36.dp
317 
318     /**
319      * The default size of the icon when used inside a [Button].
320      *
321      * @sample androidx.compose.material.samples.ButtonWithIconSample
322      */
323     val IconSize = 18.dp
324 
325     /**
326      * The default size of the spacing between an icon and a text when they used inside a [Button].
327      *
328      * @sample androidx.compose.material.samples.ButtonWithIconSample
329      */
330     val IconSpacing = 8.dp
331 
332     /**
333      * Creates a [ButtonElevation] that will animate between the provided values according to the
334      * Material specification for a [Button].
335      *
336      * @param defaultElevation the elevation to use when the [Button] is enabled, and has no other
337      *   [Interaction]s.
338      * @param pressedElevation the elevation to use when the [Button] is enabled and is pressed.
339      * @param disabledElevation the elevation to use when the [Button] is not enabled.
340      */
341     @Deprecated("Use another overload of elevation", level = DeprecationLevel.HIDDEN)
342     @Composable
343     fun elevation(
344         defaultElevation: Dp = 2.dp,
345         pressedElevation: Dp = 8.dp,
346         disabledElevation: Dp = 0.dp
347     ): ButtonElevation =
348         elevation(
349             defaultElevation,
350             pressedElevation,
351             disabledElevation,
352             hoveredElevation = 4.dp,
353             focusedElevation = 4.dp,
354         )
355 
356     /**
357      * Creates a [ButtonElevation] that will animate between the provided values according to the
358      * Material specification for a [Button].
359      *
360      * @param defaultElevation the elevation to use when the [Button] is enabled, and has no other
361      *   [Interaction]s.
362      * @param pressedElevation the elevation to use when the [Button] is enabled and is pressed.
363      * @param disabledElevation the elevation to use when the [Button] is not enabled.
364      * @param hoveredElevation the elevation to use when the [Button] is enabled and is hovered.
365      * @param focusedElevation the elevation to use when the [Button] is enabled and is focused.
366      */
367     @Suppress("UNUSED_PARAMETER")
368     @Composable
369     fun elevation(
370         defaultElevation: Dp = 2.dp,
371         pressedElevation: Dp = 8.dp,
372         disabledElevation: Dp = 0.dp,
373         hoveredElevation: Dp = 4.dp,
374         focusedElevation: Dp = 4.dp,
375     ): ButtonElevation {
376         return remember(
377             defaultElevation,
378             pressedElevation,
379             disabledElevation,
380             hoveredElevation,
381             focusedElevation
382         ) {
383             DefaultButtonElevation(
384                 defaultElevation = defaultElevation,
385                 pressedElevation = pressedElevation,
386                 disabledElevation = disabledElevation,
387                 hoveredElevation = hoveredElevation,
388                 focusedElevation = focusedElevation
389             )
390         }
391     }
392 
393     /**
394      * Creates a [ButtonColors] that represents the default background and content colors used in a
395      * [Button].
396      *
397      * @param backgroundColor the background color of this [Button] when enabled
398      * @param contentColor the content color of this [Button] when enabled
399      * @param disabledBackgroundColor the background color of this [Button] when not enabled
400      * @param disabledContentColor the content color of this [Button] when not enabled
401      */
402     @Composable
403     fun buttonColors(
404         backgroundColor: Color = MaterialTheme.colors.primary,
405         contentColor: Color = contentColorFor(backgroundColor),
406         disabledBackgroundColor: Color =
407             MaterialTheme.colors.onSurface
408                 .copy(alpha = 0.12f)
409                 .compositeOver(MaterialTheme.colors.surface),
410         disabledContentColor: Color =
411             MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
412     ): ButtonColors =
413         DefaultButtonColors(
414             backgroundColor = backgroundColor,
415             contentColor = contentColor,
416             disabledBackgroundColor = disabledBackgroundColor,
417             disabledContentColor = disabledContentColor
418         )
419 
420     /**
421      * Creates a [ButtonColors] that represents the default background and content colors used in an
422      * [OutlinedButton].
423      *
424      * @param backgroundColor the background color of this [OutlinedButton]
425      * @param contentColor the content color of this [OutlinedButton] when enabled
426      * @param disabledContentColor the content color of this [OutlinedButton] when not enabled
427      */
428     @Composable
429     fun outlinedButtonColors(
430         backgroundColor: Color = MaterialTheme.colors.surface,
431         contentColor: Color = MaterialTheme.colors.primary,
432         disabledContentColor: Color =
433             MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
434     ): ButtonColors =
435         DefaultButtonColors(
436             backgroundColor = backgroundColor,
437             contentColor = contentColor,
438             disabledBackgroundColor = backgroundColor,
439             disabledContentColor = disabledContentColor
440         )
441 
442     /**
443      * Creates a [ButtonColors] that represents the default background and content colors used in a
444      * [TextButton].
445      *
446      * @param backgroundColor the background color of this [TextButton]
447      * @param contentColor the content color of this [TextButton] when enabled
448      * @param disabledContentColor the content color of this [TextButton] when not enabled
449      */
450     @Composable
451     fun textButtonColors(
452         backgroundColor: Color = Color.Transparent,
453         contentColor: Color = MaterialTheme.colors.primary,
454         disabledContentColor: Color =
455             MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
456     ): ButtonColors =
457         DefaultButtonColors(
458             backgroundColor = backgroundColor,
459             contentColor = contentColor,
460             disabledBackgroundColor = backgroundColor,
461             disabledContentColor = disabledContentColor
462         )
463 
464     /** The default color opacity used for an [OutlinedButton]'s border color */
465     const val OutlinedBorderOpacity = 0.12f
466 
467     /** The default [OutlinedButton]'s border size */
468     val OutlinedBorderSize = 1.dp
469 
470     /** The default disabled content color used by all types of [Button]s */
471     val outlinedBorder: BorderStroke
472         @Composable
473         get() =
474             BorderStroke(
475                 OutlinedBorderSize,
476                 MaterialTheme.colors.onSurface.copy(alpha = OutlinedBorderOpacity)
477             )
478 
479     private val TextButtonHorizontalPadding = 8.dp
480 
481     /** The default content padding used by [TextButton] */
482     val TextButtonContentPadding =
483         PaddingValues(
484             start = TextButtonHorizontalPadding,
485             top = ContentPadding.calculateTopPadding(),
486             end = TextButtonHorizontalPadding,
487             bottom = ContentPadding.calculateBottomPadding()
488         )
489 }
490 
491 /** Default [ButtonElevation] implementation. */
492 @Stable
493 private class DefaultButtonElevation(
494     private val defaultElevation: Dp,
495     private val pressedElevation: Dp,
496     private val disabledElevation: Dp,
497     private val hoveredElevation: Dp,
498     private val focusedElevation: Dp,
499 ) : ButtonElevation {
500     @Composable
elevationnull501     override fun elevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
502         val interactions = remember { mutableStateListOf<Interaction>() }
503         LaunchedEffect(interactionSource) {
504             interactionSource.interactions.collect { interaction ->
505                 when (interaction) {
506                     is HoverInteraction.Enter -> {
507                         interactions.add(interaction)
508                     }
509                     is HoverInteraction.Exit -> {
510                         interactions.remove(interaction.enter)
511                     }
512                     is FocusInteraction.Focus -> {
513                         interactions.add(interaction)
514                     }
515                     is FocusInteraction.Unfocus -> {
516                         interactions.remove(interaction.focus)
517                     }
518                     is PressInteraction.Press -> {
519                         interactions.add(interaction)
520                     }
521                     is PressInteraction.Release -> {
522                         interactions.remove(interaction.press)
523                     }
524                     is PressInteraction.Cancel -> {
525                         interactions.remove(interaction.press)
526                     }
527                 }
528             }
529         }
530 
531         val interaction = interactions.lastOrNull()
532 
533         val target =
534             if (!enabled) {
535                 disabledElevation
536             } else {
537                 when (interaction) {
538                     is PressInteraction.Press -> pressedElevation
539                     is HoverInteraction.Enter -> hoveredElevation
540                     is FocusInteraction.Focus -> focusedElevation
541                     else -> defaultElevation
542                 }
543             }
544 
545         val animatable = remember { Animatable(target, Dp.VectorConverter) }
546 
547         LaunchedEffect(target) {
548             if (animatable.targetValue != target) {
549                 if (!enabled) {
550                     // No transition when moving to a disabled state
551                     animatable.snapTo(target)
552                 } else {
553                     val lastInteraction =
554                         when (animatable.targetValue) {
555                             pressedElevation -> PressInteraction.Press(Offset.Zero)
556                             hoveredElevation -> HoverInteraction.Enter()
557                             focusedElevation -> FocusInteraction.Focus()
558                             else -> null
559                         }
560                     animatable.animateElevation(
561                         from = lastInteraction,
562                         to = interaction,
563                         target = target
564                     )
565                 }
566             }
567         }
568 
569         return animatable.asState()
570     }
571 }
572 
573 /** Default [ButtonColors] implementation. */
574 @Immutable
575 private class DefaultButtonColors(
576     private val backgroundColor: Color,
577     private val contentColor: Color,
578     private val disabledBackgroundColor: Color,
579     private val disabledContentColor: Color
580 ) : ButtonColors {
581     @Composable
backgroundColornull582     override fun backgroundColor(enabled: Boolean): State<Color> {
583         return rememberUpdatedState(if (enabled) backgroundColor else disabledBackgroundColor)
584     }
585 
586     @Composable
contentColornull587     override fun contentColor(enabled: Boolean): State<Color> {
588         return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
589     }
590 
equalsnull591     override fun equals(other: Any?): Boolean {
592         if (this === other) return true
593         if (other == null || this::class != other::class) return false
594 
595         other as DefaultButtonColors
596 
597         if (backgroundColor != other.backgroundColor) return false
598         if (contentColor != other.contentColor) return false
599         if (disabledBackgroundColor != other.disabledBackgroundColor) return false
600         if (disabledContentColor != other.disabledContentColor) return false
601 
602         return true
603     }
604 
hashCodenull605     override fun hashCode(): Int {
606         var result = backgroundColor.hashCode()
607         result = 31 * result + contentColor.hashCode()
608         result = 31 * result + disabledBackgroundColor.hashCode()
609         result = 31 * result + disabledContentColor.hashCode()
610         return result
611     }
612 }
613