1 /*
<lambda>null2 * Copyright 2024 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.material3
18
19 import androidx.compose.animation.core.FiniteAnimationSpec
20 import androidx.compose.foundation.BorderStroke
21 import androidx.compose.foundation.interaction.Interaction
22 import androidx.compose.foundation.interaction.MutableInteractionSource
23 import androidx.compose.foundation.interaction.collectIsPressedAsState
24 import androidx.compose.foundation.layout.Arrangement
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.layout.PaddingValues
27 import androidx.compose.foundation.layout.Row
28 import androidx.compose.foundation.layout.RowScope
29 import androidx.compose.foundation.layout.calculateEndPadding
30 import androidx.compose.foundation.layout.calculateStartPadding
31 import androidx.compose.foundation.layout.defaultMinSize
32 import androidx.compose.foundation.layout.padding
33 import androidx.compose.foundation.shape.CircleShape
34 import androidx.compose.foundation.shape.CornerBasedShape
35 import androidx.compose.foundation.shape.CornerSize
36 import androidx.compose.foundation.shape.RoundedCornerShape
37 import androidx.compose.material3.internal.ProvideContentColorTextStyle
38 import androidx.compose.material3.internal.rememberAnimatedShape
39 import androidx.compose.material3.tokens.BaselineButtonTokens
40 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
41 import androidx.compose.material3.tokens.SplitButtonSmallTokens
42 import androidx.compose.material3.tokens.StateTokens
43 import androidx.compose.runtime.Composable
44 import androidx.compose.runtime.CompositionLocalProvider
45 import androidx.compose.runtime.getValue
46 import androidx.compose.runtime.remember
47 import androidx.compose.ui.Alignment
48 import androidx.compose.ui.Modifier
49 import androidx.compose.ui.draw.drawWithContent
50 import androidx.compose.ui.graphics.Shape
51 import androidx.compose.ui.graphics.drawOutline
52 import androidx.compose.ui.layout.Layout
53 import androidx.compose.ui.layout.layoutId
54 import androidx.compose.ui.platform.LocalDensity
55 import androidx.compose.ui.platform.LocalLayoutDirection
56 import androidx.compose.ui.semantics.Role
57 import androidx.compose.ui.semantics.role
58 import androidx.compose.ui.semantics.semantics
59 import androidx.compose.ui.unit.Dp
60 import androidx.compose.ui.unit.constrainHeight
61 import androidx.compose.ui.unit.constrainWidth
62 import androidx.compose.ui.unit.dp
63 import androidx.compose.ui.unit.offset
64 import androidx.compose.ui.util.fastFirst
65 import androidx.compose.ui.util.fastMaxOfOrNull
66 import androidx.compose.ui.util.fastSumBy
67
68 /**
69 * A [SplitButtonLayout] let user define a button group consisting of 2 buttons. The leading button
70 * performs a primary action, and the trailing button performs a secondary action that is
71 * contextually related to the primary action.
72 *
73 * @sample androidx.compose.material3.samples.FilledSplitButtonSample
74 * @sample androidx.compose.material3.samples.SplitButtonWithDropdownMenuSample
75 * @sample androidx.compose.material3.samples.TonalSplitButtonSample
76 * @sample androidx.compose.material3.samples.ElevatedSplitButtonSample
77 * @sample androidx.compose.material3.samples.OutlinedSplitButtonSample
78 * @sample androidx.compose.material3.samples.SplitButtonWithUnCheckableTrailingButtonSample
79 * @sample androidx.compose.material3.samples.SplitButtonWithTextSample
80 * @sample androidx.compose.material3.samples.SplitButtonWithIconSample
81 *
82 * Choose the best split button for an action based on the amount of emphasis it needs. The more
83 * important an action is, the higher emphasis its button should be.
84 *
85 * Use [SplitButtonDefaults.LeadingButton] and [SplitButtonDefaults.TrailingButton] to construct a
86 * `FilledSplitButton`. Filled split button is the high-emphasis version of split button. It should
87 * be used for emphasizing important or final actions.
88 *
89 * Use [SplitButtonDefaults.TonalLeadingButton] and [SplitButtonDefaults.TonalTrailingButton] to
90 * construct a `tonal SplitButton`. Tonal split button is the medium-emphasis version of split
91 * buttons. It's a middle ground between `filled SplitButton` and `outlined SplitButton`
92 *
93 * Use [SplitButtonDefaults.ElevatedLeadingButton] and [SplitButtonDefaults.ElevatedTrailingButton]
94 * to construct a `elevated SplitButton`. Elevated split buttons are essentially `tonal
95 * SplitButton`s with a shadow. To prevent shadow creep, only use them when absolutely necessary,
96 * such as when the button requires visual separation from patterned container.
97 *
98 * Use [SplitButtonDefaults.OutlinedLeadingButton] and [SplitButtonDefaults.OutlinedTrailingButton]
99 * to construct a `outlined SplitButton`. Outlined split buttons are medium-emphasis buttons. They
100 * contain actions that are important, but are not the primary action in an app. Outlined buttons
101 * pair well with `filled SplitButton`s to indicate an alternative, secondary action.
102 *
103 * @param leadingButton the leading button. You can specify your own composable or construct a
104 * [SplitButtonDefaults.LeadingButton]
105 * @param trailingButton the trailing button.You can specify your own composable or construct a
106 * [SplitButtonDefaults.TrailingButton]
107 * @param modifier the [Modifier] to be applied to this split button.
108 * @param spacing The spacing between the [leadingButton] and [trailingButton]
109 */
110 @ExperimentalMaterial3ExpressiveApi
111 @Composable
112 fun SplitButtonLayout(
113 leadingButton: @Composable () -> Unit,
114 trailingButton: @Composable () -> Unit,
115 modifier: Modifier = Modifier,
116 spacing: Dp = SplitButtonDefaults.Spacing,
117 ) {
118 Layout(
119 {
120 // Override min component size enforcement to avoid create extra padding internally
121 // Enforce it on the parent instead
122 CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
123 Box(
124 modifier = Modifier.layoutId(LeadingButtonLayoutId),
125 contentAlignment = Alignment.Center,
126 content = { leadingButton() }
127 )
128 Box(
129 modifier = Modifier.layoutId(TrailingButtonLayoutId),
130 contentAlignment = Alignment.Center,
131 content = { trailingButton() }
132 )
133 }
134 },
135 modifier.minimumInteractiveComponentSize(),
136 measurePolicy = { measurables, constraints ->
137 val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
138
139 val leadingButtonPlaceable =
140 measurables
141 .fastFirst { it.layoutId == LeadingButtonLayoutId }
142 .measure(looseConstraints)
143
144 val trailingButtonPlaceable =
145 measurables
146 .fastFirst { it.layoutId == TrailingButtonLayoutId }
147 .measure(
148 looseConstraints
149 .offset(
150 horizontal = -(leadingButtonPlaceable.width + spacing.roundToPx())
151 )
152 .copy(
153 minHeight = leadingButtonPlaceable.height,
154 maxHeight = leadingButtonPlaceable.height
155 )
156 )
157
158 val placeables = listOf(leadingButtonPlaceable, trailingButtonPlaceable)
159
160 val contentWidth = placeables.fastSumBy { it.width } + spacing.roundToPx()
161 val contentHeight = placeables.fastMaxOfOrNull { it.height } ?: 0
162
163 val width = constraints.constrainWidth(contentWidth)
164 val height = constraints.constrainHeight(contentHeight)
165
166 layout(width, height) {
167 leadingButtonPlaceable.placeRelative(0, 0)
168 trailingButtonPlaceable.placeRelative(
169 x = leadingButtonPlaceable.width + spacing.roundToPx(),
170 y = 0
171 )
172 }
173 }
174 )
175 }
176
177 /** Contains default values used by [SplitButtonLayout] and its style variants. */
178 @ExperimentalMaterial3ExpressiveApi
179 object SplitButtonDefaults {
180 /** Default icon size for the leading button */
181 val LeadingIconSize = BaselineButtonTokens.IconSize
182
183 /** Default icon size for the trailing button */
184 val TrailingIconSize = SplitButtonSmallTokens.TrailingIconSize
185
186 /** Default spacing between the `leading` and `trailing` button */
187 val Spacing = SplitButtonSmallTokens.BetweenSpace
188
189 /** Default size for the leading button end corners and trailing button start corners */
190 // TODO update token to dp size and use it here
191 val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
192 private val InnerCornerSizePressed = SplitButtonSmallTokens.InnerCornerPressedSize
193
194 /**
195 * Default percentage size for the leading button start corners and trailing button end corners
196 */
197 val OuterCornerSize = ShapeDefaults.CornerFull
198
199 /** Default content padding of the leading button */
200 val LeadingButtonContentPadding =
201 PaddingValues(
202 start = SplitButtonSmallTokens.LeadingButtonLeadingSpace,
203 end = SplitButtonSmallTokens.LeadingButtonTrailingSpace
204 )
205
206 /** Default content padding of the trailing button */
207 val TrailingButtonContentPadding =
208 PaddingValues(
209 start = SplitButtonSmallTokens.TrailingButtonLeadingSpace,
210 end = SplitButtonSmallTokens.TrailingButtonTrailingSpace
211 )
212
213 /**
214 * Default minimum width of the [LeadingButton], applies to all 4 variants of the split button
215 */
216 private val LeadingButtonMinWidth = 48.dp
217
218 /**
219 * Default minimum height of the split button. This applies to both [LeadingButton] and
220 * [TrailingButton]. Applies to all 4 variants of the split button
221 */
222 private val MinHeight = SplitButtonSmallTokens.ContainerHeight
223
224 /** Default minimum width of the [TrailingButton]. */
225 private val TrailingButtonMinWidth = LeadingButtonMinWidth
226
227 /** Trailing button state layer alpha when in checked state */
228 private const val TrailingButtonStateLayerAlpha = StateTokens.PressedStateLayerOpacity
229
230 /** Default shape of the leading button. */
leadingButtonShapenull231 private fun leadingButtonShape(endCornerSize: CornerSize = InnerCornerSize) =
232 RoundedCornerShape(OuterCornerSize, endCornerSize, endCornerSize, OuterCornerSize)
233
234 private val LeadingPressedShape =
235 RoundedCornerShape(
236 topStart = OuterCornerSize,
237 bottomStart = OuterCornerSize,
238 topEnd = InnerCornerSizePressed,
239 bottomEnd = InnerCornerSizePressed
240 )
241 private val TrailingPressedShape =
242 RoundedCornerShape(
243 topStart = InnerCornerSizePressed,
244 bottomStart = InnerCornerSizePressed,
245 topEnd = OuterCornerSize,
246 bottomEnd = OuterCornerSize
247 )
248 private val TrailingCheckedShape = CircleShape
249
250 /**
251 * Default shapes for the leading button. This defines the shapes the leading button should
252 * morph to when enabled, pressed etc.
253 *
254 * @param endCornerSize the size for top end corner and bottom end corner
255 */
256 fun leadingButtonShapes(endCornerSize: CornerSize = InnerCornerSize) =
257 SplitButtonShapes(
258 shape = leadingButtonShape(endCornerSize),
259 pressedShape = LeadingPressedShape,
260 checkedShape = null,
261 )
262
263 /** Default shape of the trailing button */
264 private fun trailingButtonShape(startCornerSize: CornerSize = InnerCornerSize) =
265 RoundedCornerShape(startCornerSize, OuterCornerSize, OuterCornerSize, startCornerSize)
266
267 /**
268 * Default shapes for the trailing button
269 *
270 * @param startCornerSize the size for top start corner and bottom start corner
271 */
272 fun trailingButtonShapes(startCornerSize: CornerSize = InnerCornerSize) =
273 SplitButtonShapes(
274 shape = trailingButtonShape(startCornerSize),
275 pressedShape = TrailingPressedShape,
276 checkedShape = TrailingCheckedShape
277 )
278
279 /**
280 * Create a default `leading` button that has the same visual as a Filled[Button]. To create a
281 * `tonal`, `outlined`, or `elevated` version, the default value of [Button] params can be
282 * passed in. For example, [ElevatedButton].
283 *
284 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
285 *
286 * @param onClick called when the button is clicked
287 * @param modifier the [Modifier] to be applied to this button.
288 * @param enabled controls the enabled state of the split button. When `false`, this component
289 * will not respond to user input, and it will appear visually disabled and disabled to
290 * accessibility services.
291 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
292 * on the user's interaction with the button.
293 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
294 * different states. See [ButtonDefaults.buttonColors].
295 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
296 * states. This controls the size of the shadow below the button. See
297 * [ButtonElevation.shadowElevation].
298 * @param border the border to draw around the container of this button contentPadding the
299 * spacing values to apply internally between the container and the content
300 * @param contentPadding the spacing values to apply internally between the container and the
301 * content
302 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
303 * emitting [Interaction]s for this button. You can use this to change the button's appearance
304 * or preview the button in different states. Note that if `null` is provided, interactions
305 * will still happen internally.
306 * @param content the content for the button.
307 */
308 @ExperimentalMaterial3ExpressiveApi
309 @Composable
310 fun LeadingButton(
311 onClick: () -> Unit,
312 modifier: Modifier = Modifier,
313 enabled: Boolean = true,
314 shapes: SplitButtonShapes = leadingButtonShapes(),
315 colors: ButtonColors = ButtonDefaults.buttonColors(),
316 elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
317 border: BorderStroke? = null,
318 contentPadding: PaddingValues = LeadingButtonContentPadding,
319 interactionSource: MutableInteractionSource? = null,
320 content: @Composable RowScope.() -> Unit
321 ) {
322 @Suppress("NAME_SHADOWING")
323 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
324
325 // TODO Load the motionScheme tokens from the component tokens file
326 val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
327 val pressed by interactionSource.collectIsPressedAsState()
328 val contentColor = colors.contentColor(enabled)
329 val containerColor = colors.containerColor(enabled)
330
331 Surface(
332 onClick = onClick,
333 modifier = modifier.semantics { role = Role.Button },
334 enabled = enabled,
335 shape = shapeByInteraction(shapes, pressed, checked = false, defaultAnimationSpec),
336 color = containerColor,
337 contentColor = contentColor,
338 shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
339 border = border,
340 interactionSource = interactionSource
341 ) {
342 ProvideContentColorTextStyle(
343 contentColor = contentColor,
344 textStyle = MaterialTheme.typography.labelLarge
345 ) {
346 Row(
347 Modifier.defaultMinSize(minWidth = LeadingButtonMinWidth, minHeight = MinHeight)
348 .padding(contentPadding),
349 horizontalArrangement = Arrangement.Center,
350 verticalAlignment = Alignment.CenterVertically,
351 content = content
352 )
353 }
354 }
355 }
356
357 /**
358 * Creates a `trailing` button that has the same visual as a [Button].
359 *
360 * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
361 * can be passed in. For example, [ElevatedButton].
362 *
363 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
364 *
365 * @param onClick called when the button is clicked
366 * @param modifier the [Modifier] to be applied to this button.
367 * @param enabled controls the enabled state of the split button. When `false`, this component
368 * will not respond to user input, and it will appear visually disabled and disabled to
369 * accessibility services.
370 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
371 * on the user's interaction with the button.
372 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
373 * different states. See [ButtonDefaults.buttonColors].
374 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
375 * states. This controls the size of the shadow below the button. See
376 * [ButtonElevation.shadowElevation].
377 * @param border the border to draw around the container of this button contentPadding the
378 * spacing values to apply internally between the container and the content
379 * @param contentPadding the spacing values to apply internally between the container and the
380 * content
381 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
382 * emitting [Interaction]s for this button. You can use this to change the button's appearance
383 * or preview the button in different states. Note that if `null` is provided, interactions
384 * will still happen internally.
385 * @param content the content to be placed in the button
386 */
387 @Composable
388 @ExperimentalMaterial3ExpressiveApi
TrailingButtonnull389 fun TrailingButton(
390 onClick: () -> Unit,
391 modifier: Modifier = Modifier,
392 enabled: Boolean = true,
393 shapes: SplitButtonShapes = trailingButtonShapes(),
394 colors: ButtonColors = ButtonDefaults.buttonColors(),
395 elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
396 border: BorderStroke? = null,
397 contentPadding: PaddingValues = TrailingButtonContentPadding,
398 interactionSource: MutableInteractionSource? = null,
399 content: @Composable RowScope.() -> Unit
400 ) {
401 @Suppress("NAME_SHADOWING")
402 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
403
404 // TODO Load the motionScheme tokens from the component tokens file
405 val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
406 val pressed by interactionSource.collectIsPressedAsState()
407 val layoutDirection = LocalLayoutDirection.current
408 val shape = shapeByInteraction(shapes, pressed, false, defaultAnimationSpec)
409 val contentColor = colors.contentColor(enabled)
410 val containerColor = colors.containerColor(enabled)
411
412 Surface(
413 onClick = onClick,
414 modifier = modifier.semantics { role = Role.Button },
415 enabled = enabled,
416 shape = shape,
417 color = containerColor,
418 contentColor = contentColor,
419 shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
420 border = border,
421 interactionSource = interactionSource
422 ) {
423 ProvideContentColorTextStyle(
424 contentColor = contentColor,
425 textStyle = MaterialTheme.typography.labelLarge
426 ) {
427 Row(
428 Modifier.defaultMinSize(
429 minWidth = TrailingButtonMinWidth,
430 minHeight = MinHeight
431 )
432 .then(
433 when (shape) {
434 is ShapeWithHorizontalCenterOptically -> {
435 Modifier.horizontalCenterOptically(
436 shape = shape,
437 maxStartOffset =
438 contentPadding.calculateStartPadding(layoutDirection),
439 maxEndOffset =
440 contentPadding.calculateEndPadding(layoutDirection)
441 )
442 }
443 is CornerBasedShape -> {
444 Modifier.horizontalCenterOptically(
445 shape = shape,
446 maxStartOffset =
447 contentPadding.calculateStartPadding(layoutDirection),
448 maxEndOffset =
449 contentPadding.calculateEndPadding(layoutDirection)
450 )
451 }
452 else -> {
453 Modifier
454 }
455 }
456 )
457 .padding(contentPadding),
458 horizontalArrangement = Arrangement.Center,
459 verticalAlignment = Alignment.CenterVertically,
460 content = content
461 )
462 }
463 }
464 }
465
466 /**
467 * Creates a `trailing` button that has the same visual as a [Button]. When [checked] is updated
468 * from `false` to `true`, the buttons corners will morph to `full` by default. Pressed shape
469 * and checked shape can be customized via [shapes] param.
470 *
471 * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
472 * can be passed in. For example, [ElevatedButton].
473 *
474 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
475 *
476 * @param checked indicates whether the button is checked. This will trigger the corner morphing
477 * animation to reflect the updated state.
478 * @param onCheckedChange called when the button is clicked
479 * @param modifier the [Modifier] to be applied to this button.
480 * @param enabled controls the enabled state of the split button. When `false`, this component
481 * will not respond to user input, and it will appear visually disabled and disabled to
482 * accessibility services.
483 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
484 * on the user's interaction with the button.
485 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
486 * different states. See [ButtonDefaults.buttonColors].
487 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
488 * states. This controls the size of the shadow below the button. See
489 * [ButtonElevation.shadowElevation].
490 * @param border the border to draw around the container of this button contentPadding the
491 * spacing values to apply internally between the container and the content
492 * @param contentPadding the spacing values to apply internally between the container and the
493 * content
494 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
495 * emitting [Interaction]s for this button. You can use this to change the button's appearance
496 * or preview the button in different states. Note that if `null` is provided, interactions
497 * will still happen internally.
498 * @param content the content to be placed in the button
499 */
500 @Composable
501 @ExperimentalMaterial3ExpressiveApi
TrailingButtonnull502 fun TrailingButton(
503 checked: Boolean,
504 onCheckedChange: (Boolean) -> Unit,
505 modifier: Modifier = Modifier,
506 enabled: Boolean = true,
507 shapes: SplitButtonShapes = trailingButtonShapes(),
508 colors: ButtonColors = ButtonDefaults.buttonColors(),
509 elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
510 border: BorderStroke? = null,
511 contentPadding: PaddingValues = TrailingButtonContentPadding,
512 interactionSource: MutableInteractionSource? = null,
513 content: @Composable RowScope.() -> Unit
514 ) {
515 @Suppress("NAME_SHADOWING")
516 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
517
518 // TODO Load the motionScheme tokens from the component tokens file
519 val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
520 val pressed by interactionSource.collectIsPressedAsState()
521 val layoutDirection = LocalLayoutDirection.current
522 val density = LocalDensity.current
523 val shape = shapeByInteraction(shapes, pressed, checked, defaultAnimationSpec)
524 val contentColor = colors.contentColor(enabled)
525 val containerColor = colors.containerColor(enabled)
526
527 Surface(
528 checked = checked,
529 onCheckedChange = onCheckedChange,
530 modifier =
531 modifier
532 .drawWithContent {
533 drawContent()
534 if (checked) {
535 drawOutline(
536 outline = shape.createOutline(size, layoutDirection, density),
537 color = contentColor,
538 alpha = TrailingButtonStateLayerAlpha
539 )
540 }
541 }
542 .semantics { role = Role.Button },
543 enabled = enabled,
544 shape = shape,
545 color = containerColor,
546 contentColor = contentColor,
547 shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
548 border = border,
549 interactionSource = interactionSource
550 ) {
551 ProvideContentColorTextStyle(
552 contentColor = contentColor,
553 textStyle = MaterialTheme.typography.labelLarge
554 ) {
555 Row(
556 Modifier.defaultMinSize(
557 minWidth = TrailingButtonMinWidth,
558 minHeight = MinHeight
559 )
560 .then(
561 when (shape) {
562 is ShapeWithHorizontalCenterOptically -> {
563 Modifier.horizontalCenterOptically(
564 shape = shape,
565 maxStartOffset =
566 contentPadding.calculateStartPadding(layoutDirection),
567 maxEndOffset =
568 contentPadding.calculateEndPadding(layoutDirection)
569 )
570 }
571 is CornerBasedShape -> {
572 Modifier.horizontalCenterOptically(
573 shape = shape,
574 maxStartOffset =
575 contentPadding.calculateStartPadding(layoutDirection),
576 maxEndOffset =
577 contentPadding.calculateEndPadding(layoutDirection)
578 )
579 }
580 else -> {
581 Modifier
582 }
583 }
584 )
585 .padding(contentPadding),
586 horizontalArrangement = Arrangement.Center,
587 verticalAlignment = Alignment.CenterVertically,
588 content = content
589 )
590 }
591 }
592 }
593
594 /**
595 * Create a tonal `leading` button that has the same visual as a Tonal[Button]. To create a
596 * `filled`, `outlined`, or `elevated` version, the default value of [Button] params can be
597 * passed in. For example, [ElevatedButton].
598 *
599 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
600 *
601 * @param onClick called when the button is clicked
602 * @param modifier the [Modifier] to be applied to this button.
603 * @param enabled controls the enabled state of the split button. When `false`, this component
604 * will not respond to user input, and it will appear visually disabled and disabled to
605 * accessibility services.
606 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
607 * on the user's interaction with the button.
608 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
609 * different states. See [ButtonDefaults.buttonColors].
610 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
611 * states. This controls the size of the shadow below the button. See
612 * [ButtonElevation.shadowElevation].
613 * @param border the border to draw around the container of this button contentPadding the
614 * spacing values to apply internally between the container and the content
615 * @param contentPadding the spacing values to apply internally between the container and the
616 * content
617 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
618 * emitting [Interaction]s for this button. You can use this to change the button's appearance
619 * or preview the button in different states. Note that if `null` is provided, interactions
620 * will still happen internally.
621 * @param content the content for the button.
622 */
623 @ExperimentalMaterial3ExpressiveApi
624 @Composable
TonalLeadingButtonnull625 fun TonalLeadingButton(
626 onClick: () -> Unit,
627 modifier: Modifier = Modifier,
628 enabled: Boolean = true,
629 shapes: SplitButtonShapes = leadingButtonShapes(),
630 colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
631 elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(),
632 border: BorderStroke? = null,
633 contentPadding: PaddingValues = LeadingButtonContentPadding,
634 interactionSource: MutableInteractionSource? = null,
635 content: @Composable RowScope.() -> Unit
636 ) {
637 LeadingButton(
638 modifier = modifier,
639 onClick = onClick,
640 enabled = enabled,
641 shapes = shapes,
642 colors = colors,
643 elevation = elevation,
644 border = border,
645 contentPadding = contentPadding,
646 interactionSource = interactionSource,
647 content = content,
648 )
649 }
650
651 /**
652 * Creates a tonal `trailing` button that has the same visual as a [FilledTonalButton]. When
653 * [checked] is updated from `false` to `true`, the buttons corners will morph to `full` by
654 * default. Pressed shape and checked shape can be customized via [shapes] param.
655 *
656 * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
657 * can be passed in. For example, [ElevatedButton].
658 *
659 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
660 *
661 * @param checked indicates whether the button is checked. This will trigger the corner morphing
662 * animation to reflect the updated state.
663 * @param onCheckedChange called when the button is clicked
664 * @param modifier the [Modifier] to be applied to this button.
665 * @param enabled controls the enabled state of the split button. When `false`, this component
666 * will not respond to user input, and it will appear visually disabled and disabled to
667 * accessibility services.
668 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
669 * on the user's interaction with the button.
670 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
671 * different states. See [ButtonDefaults.buttonColors].
672 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
673 * states. This controls the size of the shadow below the button. See
674 * [ButtonElevation.shadowElevation].
675 * @param border the border to draw around the container of this button contentPadding the
676 * spacing values to apply internally between the container and the content
677 * @param contentPadding the spacing values to apply internally between the container and the
678 * content
679 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
680 * emitting [Interaction]s for this button. You can use this to change the button's appearance
681 * or preview the button in different states. Note that if `null` is provided, interactions
682 * will still happen internally.
683 * @param content the content to be placed in the button
684 */
685 @ExperimentalMaterial3ExpressiveApi
686 @Composable
TonalTrailingButtonnull687 fun TonalTrailingButton(
688 checked: Boolean,
689 onCheckedChange: (Boolean) -> Unit,
690 modifier: Modifier = Modifier,
691 enabled: Boolean = true,
692 shapes: SplitButtonShapes = trailingButtonShapes(),
693 colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
694 elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(),
695 border: BorderStroke? = null,
696 contentPadding: PaddingValues = TrailingButtonContentPadding,
697 interactionSource: MutableInteractionSource? = null,
698 content: @Composable RowScope.() -> Unit
699 ) {
700 TrailingButton(
701 checked = checked,
702 onCheckedChange = onCheckedChange,
703 modifier = modifier,
704 enabled = enabled,
705 shapes = shapes,
706 colors = colors,
707 elevation = elevation,
708 border = border,
709 contentPadding = contentPadding,
710 interactionSource = interactionSource,
711 content = content
712 )
713 }
714
715 /**
716 * Create a elevated `leading` button that has the same visual as a [ElevatedButton]. To create
717 * a `filled`, `outlined`, or `elevated` version, the default value of [Button] params can be
718 * passed in. For example, [ElevatedButton].
719 *
720 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
721 *
722 * @param onClick called when the button is clicked
723 * @param modifier the [Modifier] to be applied to this button.
724 * @param enabled controls the enabled state of the split button. When `false`, this component
725 * will not respond to user input, and it will appear visually disabled and disabled to
726 * accessibility services.
727 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
728 * on the user's interaction with the button.
729 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
730 * different states. See [ButtonDefaults.buttonColors].
731 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
732 * states. This controls the size of the shadow below the button. See
733 * [ButtonElevation.shadowElevation].
734 * @param border the border to draw around the container of this button contentPadding the
735 * spacing values to apply internally between the container and the content
736 * @param contentPadding the spacing values to apply internally between the container and the
737 * content
738 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
739 * emitting [Interaction]s for this button. You can use this to change the button's appearance
740 * or preview the button in different states. Note that if `null` is provided, interactions
741 * will still happen internally.
742 * @param content the content for the button.
743 */
744 @ExperimentalMaterial3ExpressiveApi
745 @Composable
OutlinedLeadingButtonnull746 fun OutlinedLeadingButton(
747 onClick: () -> Unit,
748 modifier: Modifier = Modifier,
749 enabled: Boolean = true,
750 shapes: SplitButtonShapes = leadingButtonShapes(),
751 colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
752 elevation: ButtonElevation? = null,
753 border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled),
754 contentPadding: PaddingValues = LeadingButtonContentPadding,
755 interactionSource: MutableInteractionSource? = null,
756 content: @Composable RowScope.() -> Unit
757 ) {
758 LeadingButton(
759 modifier = modifier,
760 onClick = onClick,
761 enabled = enabled,
762 shapes = shapes,
763 colors = colors,
764 elevation = elevation,
765 border = border,
766 contentPadding = contentPadding,
767 interactionSource = interactionSource,
768 content = content,
769 )
770 }
771
772 /**
773 * Creates a outlined `trailing` button that has the same visual as a [OutlinedButton]. When
774 * [checked] is updated from `false` to `true`, the buttons corners will morph to `full` by
775 * default. Pressed shape and checked shape can be customized via [shapes] param.
776 *
777 * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
778 * can be passed in. For example, [ElevatedButton].
779 *
780 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
781 *
782 * @param checked indicates whether the button is checked. This will trigger the corner morphing
783 * animation to reflect the updated state.
784 * @param onCheckedChange called when the button is clicked
785 * @param modifier the [Modifier] to be applied to this button.
786 * @param enabled controls the enabled state of the split button. When `false`, this component
787 * will not respond to user input, and it will appear visually disabled and disabled to
788 * accessibility services.
789 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
790 * on the user's interaction with the button.
791 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
792 * different states. See [ButtonDefaults.buttonColors].
793 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
794 * states. This controls the size of the shadow below the button. See
795 * [ButtonElevation.shadowElevation].
796 * @param border the border to draw around the container of this button contentPadding the
797 * spacing values to apply internally between the container and the content
798 * @param contentPadding the spacing values to apply internally between the container and the
799 * content
800 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
801 * emitting [Interaction]s for this button. You can use this to change the button's appearance
802 * or preview the button in different states. Note that if `null` is provided, interactions
803 * will still happen internally.
804 * @param content the content to be placed in the button
805 */
806 @ExperimentalMaterial3ExpressiveApi
807 @Composable
OutlinedTrailingButtonnull808 fun OutlinedTrailingButton(
809 checked: Boolean,
810 onCheckedChange: (Boolean) -> Unit,
811 modifier: Modifier = Modifier,
812 enabled: Boolean = true,
813 shapes: SplitButtonShapes = trailingButtonShapes(),
814 colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
815 elevation: ButtonElevation? = null,
816 border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled),
817 contentPadding: PaddingValues = TrailingButtonContentPadding,
818 interactionSource: MutableInteractionSource? = null,
819 content: @Composable RowScope.() -> Unit
820 ) {
821 TrailingButton(
822 checked = checked,
823 onCheckedChange = onCheckedChange,
824 modifier = modifier,
825 enabled = enabled,
826 shapes = shapes,
827 colors = colors,
828 elevation = elevation,
829 border = border,
830 contentPadding = contentPadding,
831 interactionSource = interactionSource,
832 content = content
833 )
834 }
835
836 /**
837 * Create a elevated `leading` button that has the same visual as a [ElevatedButton]. To create
838 * a `filled`, `outlined`, or `elevated` version, the default value of [Button] params can be
839 * passed in. For example, [ElevatedButton].
840 *
841 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
842 *
843 * @param onClick called when the button is clicked
844 * @param modifier the [Modifier] to be applied to this button.
845 * @param enabled controls the enabled state of the split button. When `false`, this component
846 * will not respond to user input, and it will appear visually disabled and disabled to
847 * accessibility services.
848 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
849 * on the user's interaction with the button.
850 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
851 * different states. See [ButtonDefaults.buttonColors].
852 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
853 * states. This controls the size of the shadow below the button. See
854 * [ButtonElevation.shadowElevation].
855 * @param border the border to draw around the container of this button contentPadding the
856 * spacing values to apply internally between the container and the content
857 * @param contentPadding the spacing values to apply internally between the container and the
858 * content
859 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
860 * emitting [Interaction]s for this button. You can use this to change the button's appearance
861 * or preview the button in different states. Note that if `null` is provided, interactions
862 * will still happen internally.
863 * @param content the content for the button.
864 */
865 @ExperimentalMaterial3ExpressiveApi
866 @Composable
ElevatedLeadingButtonnull867 fun ElevatedLeadingButton(
868 onClick: () -> Unit,
869 modifier: Modifier = Modifier,
870 enabled: Boolean = true,
871 shapes: SplitButtonShapes = leadingButtonShapes(),
872 colors: ButtonColors = ButtonDefaults.elevatedButtonColors(),
873 elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
874 border: BorderStroke? = null,
875 contentPadding: PaddingValues = LeadingButtonContentPadding,
876 interactionSource: MutableInteractionSource? = null,
877 content: @Composable RowScope.() -> Unit
878 ) {
879 LeadingButton(
880 modifier = modifier,
881 onClick = onClick,
882 enabled = enabled,
883 shapes = shapes,
884 colors = colors,
885 elevation = elevation,
886 border = border,
887 contentPadding = contentPadding,
888 interactionSource = interactionSource,
889 content = content,
890 )
891 }
892
893 /**
894 * Creates a elevated `trailing` button that has the same visual as a [ElevatedButton]. When
895 * [checked] is updated from `false` to `true`, the buttons corners will morph to `full` by
896 * default. Pressed shape and checked shape can be customized via [shapes] param.
897 *
898 * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
899 * can be passed in. For example, [ElevatedButton].
900 *
901 * The default text style for internal [Text] components will be set to [Typography.labelLarge].
902 *
903 * @param checked indicates whether the button is checked. This will trigger the corner morphing
904 * animation to reflect the updated state.
905 * @param onCheckedChange called when the button is clicked
906 * @param modifier the [Modifier] to be applied to this button.
907 * @param enabled controls the enabled state of the split button. When `false`, this component
908 * will not respond to user input, and it will appear visually disabled and disabled to
909 * accessibility services.
910 * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
911 * on the user's interaction with the button.
912 * @param colors [ButtonColors] that will be used to resolve the colors for this button in
913 * different states. See [ButtonDefaults.buttonColors].
914 * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
915 * states. This controls the size of the shadow below the button. See
916 * [ButtonElevation.shadowElevation].
917 * @param border the border to draw around the container of this button contentPadding the
918 * spacing values to apply internally between the container and the content
919 * @param contentPadding the spacing values to apply internally between the container and the
920 * content
921 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
922 * emitting [Interaction]s for this button. You can use this to change the button's appearance
923 * or preview the button in different states. Note that if `null` is provided, interactions
924 * will still happen internally.
925 * @param content the content to be placed in the button
926 */
927 @ExperimentalMaterial3ExpressiveApi
928 @Composable
ElevatedTrailingButtonnull929 fun ElevatedTrailingButton(
930 checked: Boolean,
931 onCheckedChange: (Boolean) -> Unit,
932 modifier: Modifier = Modifier,
933 enabled: Boolean = true,
934 shapes: SplitButtonShapes = trailingButtonShapes(),
935 colors: ButtonColors = ButtonDefaults.elevatedButtonColors(),
936 elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
937 border: BorderStroke? = null,
938 contentPadding: PaddingValues = TrailingButtonContentPadding,
939 interactionSource: MutableInteractionSource? = null,
940 content: @Composable RowScope.() -> Unit
941 ) {
942 TrailingButton(
943 checked = checked,
944 onCheckedChange = onCheckedChange,
945 modifier = modifier,
946 enabled = enabled,
947 shapes = shapes,
948 colors = colors,
949 elevation = elevation,
950 border = border,
951 contentPadding = contentPadding,
952 interactionSource = interactionSource,
953 content = content
954 )
955 }
956 }
957
958 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
959 @Composable
shapeByInteractionnull960 private fun shapeByInteraction(
961 shapes: SplitButtonShapes,
962 pressed: Boolean,
963 checked: Boolean,
964 animationSpec: FiniteAnimationSpec<Float>
965 ): Shape {
966 val shape =
967 if (pressed) {
968 shapes.pressedShape ?: shapes.shape
969 } else if (checked) {
970 shapes.checkedShape ?: shapes.shape
971 } else shapes.shape
972
973 if (shapes.hasRoundedCornerShapes) {
974 return rememberAnimatedShape(shape as RoundedCornerShape, animationSpec)
975 }
976 return shape
977 }
978
979 /**
980 * The shapes that will be used in [SplitButtonLayout]. Split button will morph between these shapes
981 * depending on the interaction of the buttons, assuming all of the shapes are [CornerBasedShape]s.
982 *
983 * @property shape is the default shape.
984 * @property pressedShape is the pressed shape.
985 * @property checkedShape is the checked shape.
986 */
987 @ExperimentalMaterial3ExpressiveApi
988 class SplitButtonShapes(val shape: Shape, val pressedShape: Shape?, val checkedShape: Shape?) {
equalsnull989 override fun equals(other: Any?): Boolean {
990 if (this === other) return true
991 if (other == null || other !is SplitButtonShapes) return false
992
993 if (shape != other.shape) return false
994 if (pressedShape != other.pressedShape) return false
995 if (checkedShape != other.checkedShape) return false
996
997 return true
998 }
999
hashCodenull1000 override fun hashCode(): Int {
1001 var result = shape.hashCode()
1002 if (pressedShape != null) {
1003 result = 31 * result + pressedShape.hashCode()
1004 }
1005 if (checkedShape != null) {
1006 result = 31 * result + checkedShape.hashCode()
1007 }
1008
1009 return result
1010 }
1011 }
1012
1013 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
1014 private val SplitButtonShapes.hasRoundedCornerShapes: Boolean
1015 get() {
1016 // Ignore null shapes and only check default shape for RoundedCorner
1017 if (pressedShape != null && pressedShape !is RoundedCornerShape) return false
1018 if (checkedShape != null && checkedShape !is RoundedCornerShape) return false
1019 return shape is RoundedCornerShape
1020 }
1021
1022 private const val LeadingButtonLayoutId = "LeadingButton"
1023 private const val TrailingButtonLayoutId = "TrailingButton"
1024