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 * 
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 * 
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 * 
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