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.Animatable
20 import androidx.compose.animation.core.AnimationVector1D
21 import androidx.compose.animation.core.FiniteAnimationSpec
22 import androidx.compose.animation.core.SpringSpec
23 import androidx.compose.animation.core.VectorConverter
24 import androidx.compose.animation.core.animateFloatAsState
25 import androidx.compose.animation.core.spring
26 import androidx.compose.foundation.layout.Arrangement
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.Row
29 import androidx.compose.foundation.layout.padding
30 import androidx.compose.foundation.layout.size
31 import androidx.compose.foundation.layout.sizeIn
32 import androidx.compose.foundation.rememberScrollState
33 import androidx.compose.foundation.selection.toggleable
34 import androidx.compose.foundation.shape.GenericShape
35 import androidx.compose.foundation.verticalScroll
36 import androidx.compose.material3.tokens.FabBaselineTokens
37 import androidx.compose.material3.tokens.FabLargeTokens
38 import androidx.compose.material3.tokens.FabMediumTokens
39 import androidx.compose.material3.tokens.FabMenuBaselineTokens
40 import androidx.compose.material3.tokens.FabPrimaryContainerTokens
41 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
42 import androidx.compose.runtime.Composable
43 import androidx.compose.runtime.CompositionLocalProvider
44 import androidx.compose.runtime.Stable
45 import androidx.compose.runtime.getValue
46 import androidx.compose.runtime.mutableIntStateOf
47 import androidx.compose.runtime.mutableStateOf
48 import androidx.compose.runtime.remember
49 import androidx.compose.runtime.rememberCoroutineScope
50 import androidx.compose.runtime.setValue
51 import androidx.compose.ui.Alignment
52 import androidx.compose.ui.Modifier
53 import androidx.compose.ui.draw.clipToBounds
54 import androidx.compose.ui.draw.drawBehind
55 import androidx.compose.ui.draw.drawWithCache
56 import androidx.compose.ui.geometry.CornerRadius
57 import androidx.compose.ui.geometry.RoundRect
58 import androidx.compose.ui.geometry.toRect
59 import androidx.compose.ui.graphics.Color
60 import androidx.compose.ui.graphics.ColorFilter
61 import androidx.compose.ui.graphics.graphicsLayer
62 import androidx.compose.ui.graphics.layer.drawLayer
63 import androidx.compose.ui.graphics.lerp
64 import androidx.compose.ui.layout.HorizontalRuler
65 import androidx.compose.ui.layout.Layout
66 import androidx.compose.ui.layout.Placeable
67 import androidx.compose.ui.layout.layout
68 import androidx.compose.ui.node.ModifierNodeElement
69 import androidx.compose.ui.node.ParentDataModifierNode
70 import androidx.compose.ui.node.SemanticsModifierNode
71 import androidx.compose.ui.platform.InspectorInfo
72 import androidx.compose.ui.platform.LocalDensity
73 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
74 import androidx.compose.ui.semantics.isTraversalGroup
75 import androidx.compose.ui.semantics.semantics
76 import androidx.compose.ui.semantics.traversalIndex
77 import androidx.compose.ui.unit.Constraints
78 import androidx.compose.ui.unit.Density
79 import androidx.compose.ui.unit.Dp
80 import androidx.compose.ui.unit.dp
81 import androidx.compose.ui.unit.lerp
82 import androidx.compose.ui.util.fastAny
83 import androidx.compose.ui.util.fastForEachIndexed
84 import androidx.compose.ui.util.fastMap
85 import androidx.compose.ui.util.fastMaxBy
86 import androidx.compose.ui.util.fastSumBy
87 import kotlin.math.hypot
88 import kotlin.math.roundToInt
89 import kotlinx.coroutines.launch
90
91 // TODO: link to spec and image
92 /**
93 * FAB Menus should be used in conjunction with a [ToggleFloatingActionButton] to provide additional
94 * choices to the user after clicking a FAB.
95 *
96 * @sample androidx.compose.material3.samples.FloatingActionButtonMenuSample
97 * @param expanded whether the FAB Menu is expanded, which will trigger a staggered animation of the
98 * FAB Menu Items
99 * @param button a composable which triggers the showing and hiding of the FAB Menu Items via the
100 * [expanded] state, typically a [ToggleFloatingActionButton]
101 * @param modifier the [Modifier] to be applied to this FAB Menu
102 * @param horizontalAlignment the horizontal alignment of the FAB Menu Items
103 * @param content the content of this FAB Menu, typically a list of [FloatingActionButtonMenuItem]s
104 */
105 @ExperimentalMaterial3ExpressiveApi
106 @Composable
107 fun FloatingActionButtonMenu(
108 expanded: Boolean,
109 button: @Composable () -> Unit,
110 modifier: Modifier = Modifier,
111 horizontalAlignment: Alignment.Horizontal = Alignment.End,
112 content: @Composable FloatingActionButtonMenuScope.() -> Unit
113 ) {
114 var buttonHeight by remember { mutableIntStateOf(0) }
115
116 Layout(
117 modifier = modifier.padding(horizontal = FabMenuPaddingHorizontal),
118 content = {
119 FloatingActionButtonMenuItemColumn(
120 expanded,
121 horizontalAlignment,
122 { buttonHeight },
123 content
124 )
125
126 button()
127 },
128 ) { measureables, constraints ->
129 val menuItemsPlaceable = measureables[0].measure(constraints)
130
131 val buttonPaddingBottom = FabMenuButtonPaddingBottom.roundToPx()
132 var buttonPlaceable: Placeable? = null
133 val suggestedWidth: Int
134 val suggestedHeight: Int
135 if (measureables.size > 1) {
136 buttonPlaceable = measureables[1].measure(constraints)
137 buttonHeight = buttonPlaceable.height
138
139 suggestedWidth = maxOf(buttonPlaceable.width, menuItemsPlaceable.width)
140 suggestedHeight =
141 maxOf(buttonPlaceable.height + buttonPaddingBottom, menuItemsPlaceable.height)
142 } else {
143 suggestedWidth = menuItemsPlaceable.width
144 suggestedHeight = menuItemsPlaceable.height
145 }
146
147 val width = minOf(suggestedWidth, constraints.maxWidth)
148 val height = minOf(suggestedHeight, constraints.maxHeight)
149
150 layout(width, height) {
151 val menuItemsX =
152 horizontalAlignment.align(menuItemsPlaceable.width, width, layoutDirection)
153 menuItemsPlaceable.place(menuItemsX, 0)
154
155 if (buttonPlaceable != null) {
156 val buttonX =
157 horizontalAlignment.align(buttonPlaceable.width, width, layoutDirection)
158 val buttonY = height - buttonPlaceable.height - buttonPaddingBottom
159 buttonPlaceable.place(buttonX, buttonY)
160 }
161 }
162 }
163 }
164
165 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
166 @Composable
FloatingActionButtonMenuItemColumnnull167 private fun FloatingActionButtonMenuItemColumn(
168 expanded: Boolean,
169 horizontalAlignment: Alignment.Horizontal,
170 buttonHeight: () -> Int,
171 content: @Composable FloatingActionButtonMenuScope.() -> Unit
172 ) {
173 var itemCount by remember { mutableIntStateOf(0) }
174 var staggerAnim by remember { mutableStateOf<Animatable<Int, AnimationVector1D>?>(null) }
175 val coroutineScope = rememberCoroutineScope()
176 // TODO Load the motionScheme tokens from the component tokens file
177 var staggerAnimSpec: FiniteAnimationSpec<Int> = MotionSchemeKeyTokens.SlowEffects.value()
178 if (staggerAnimSpec is SpringSpec<Int>) {
179 // Apply a small visibilityThreshold to the provided SpringSpec to avoid a delay in the
180 // appearance of the last item when the list is populated.
181 staggerAnimSpec =
182 spring(
183 stiffness = staggerAnimSpec.stiffness,
184 dampingRatio = staggerAnimSpec.dampingRatio,
185 visibilityThreshold = 1
186 )
187 }
188 Layout(
189 modifier =
190 Modifier.clipToBounds()
191 .semantics {
192 isTraversalGroup = true
193 traversalIndex = -0.9f
194 }
195 .verticalScroll(state = rememberScrollState(), enabled = expanded),
196 content = {
197 val scope =
198 remember(horizontalAlignment) {
199 object : FloatingActionButtonMenuScope {
200 override val horizontalAlignment: Alignment.Horizontal
201 get() = horizontalAlignment
202 }
203 }
204 content(scope)
205 }
206 ) { measurables, constraints ->
207 itemCount = measurables.size
208
209 val targetItemCount = if (expanded) itemCount else 0
210 staggerAnim =
211 staggerAnim?.also {
212 if (it.targetValue != targetItemCount) {
213 coroutineScope.launch {
214 it.animateTo(targetValue = targetItemCount, animationSpec = staggerAnimSpec)
215 }
216 }
217 } ?: Animatable(targetItemCount, Int.VectorConverter)
218
219 val placeables = measurables.fastMap { measurable -> measurable.measure(constraints) }
220 val width = placeables.fastMaxBy { it.width }?.width ?: 0
221
222 val verticalSpacing = FabMenuItemSpacingVertical.roundToPx()
223 val verticalSpacingHeight =
224 if (placeables.isNotEmpty()) {
225 verticalSpacing * (placeables.size - 1)
226 } else {
227 0
228 }
229 val currentButtonHeight = buttonHeight()
230 val bottomPadding =
231 if (currentButtonHeight > 0)
232 currentButtonHeight +
233 FabMenuButtonPaddingBottom.roundToPx() +
234 FabMenuPaddingBottom.roundToPx()
235 else 0
236 val height = placeables.fastSumBy { it.height } + verticalSpacingHeight + bottomPadding
237 var visibleHeight = bottomPadding.toFloat()
238 placeables.fastForEachIndexed { index, placeable ->
239 val itemVisible = index >= itemCount - (staggerAnim?.value ?: 0)
240 if (itemVisible) {
241 visibleHeight += placeable.height
242 if (index < placeables.size - 1) {
243 visibleHeight += verticalSpacing
244 }
245 }
246 }
247
248 val finalHeight = if (placeables.fastAny { item -> item.isVisible }) height else 0
249 layout(width, finalHeight, rulers = { MenuItemRuler provides height - visibleHeight }) {
250 var y = 0
251 placeables.fastForEachIndexed { index, placeable ->
252 val x = horizontalAlignment.align(placeable.width, width, layoutDirection)
253 placeable.place(x, y)
254 y += placeable.height
255 if (index < placeables.size - 1) {
256 y += verticalSpacing
257 }
258 }
259 }
260 }
261 }
262
263 /** Scope for the children of [FloatingActionButtonMenu] */
264 @ExperimentalMaterial3ExpressiveApi
265 interface FloatingActionButtonMenuScope {
266 val horizontalAlignment: Alignment.Horizontal
267 }
268
269 // TODO: link to spec and image
270 /**
271 * FAB Menu Items should be used within a [FloatingActionButtonMenu] to provide additional choices
272 * to the user after clicking a FAB.
273 *
274 * @sample androidx.compose.material3.samples.FloatingActionButtonMenuSample
275 * @param onClick called when this FAB Menu Item is clicked
276 * @param text label displayed inside this FAB Menu Item
277 * @param icon optional icon for this FAB Menu Item, typically an [Icon]
278 * @param modifier the [Modifier] to be applied to this FAB Menu Item
279 * @param containerColor the color used for the background of this FAB Menu Item
280 * @param contentColor the preferred color for content inside this FAB Menu Item. Defaults to either
281 * the matching content color for [containerColor], or to the current [LocalContentColor] if
282 * [containerColor] is not a color from the theme.
283 */
284 @ExperimentalMaterial3ExpressiveApi
285 @Composable
FloatingActionButtonMenuScopenull286 fun FloatingActionButtonMenuScope.FloatingActionButtonMenuItem(
287 onClick: () -> Unit,
288 text: @Composable () -> Unit,
289 icon: @Composable () -> Unit,
290 modifier: Modifier = Modifier,
291 containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
292 contentColor: Color = contentColorFor(containerColor)
293 ) {
294 var widthAnim by remember { mutableStateOf<Animatable<Float, AnimationVector1D>?>(null) }
295 var alphaAnim by remember { mutableStateOf<Animatable<Float, AnimationVector1D>?>(null) }
296 // TODO Load the motionScheme tokens from the component tokens file
297 val widthSpring: FiniteAnimationSpec<Float> = MotionSchemeKeyTokens.FastSpatial.value()
298 val alphaSpring: FiniteAnimationSpec<Float> = MotionSchemeKeyTokens.FastEffects.value()
299 val coroutineScope = rememberCoroutineScope()
300
301 var isVisible by remember { mutableStateOf(false) }
302 // Disable min interactive component size because it interferes with the item expand
303 // animation and we know we are meeting the size requirements below.
304 CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
305 Surface(
306 modifier =
307 modifier.itemVisible({ isVisible }).layout { measurable, constraints ->
308 val placeable = measurable.measure(constraints)
309 layout(placeable.width, placeable.height) {
310 val target =
311 if (MenuItemRuler.current(Float.POSITIVE_INFINITY) <= 0) 1f else 0f
312
313 widthAnim =
314 widthAnim?.also {
315 if (it.targetValue != target) {
316 coroutineScope.launch { it.animateTo(target, widthSpring) }
317 }
318 } ?: Animatable(target, Float.VectorConverter)
319
320 val tempAlphaAnim =
321 alphaAnim?.also {
322 if (it.targetValue != target) {
323 coroutineScope.launch { it.animateTo(target, alphaSpring) }
324 }
325 } ?: Animatable(target, Float.VectorConverter)
326 alphaAnim = tempAlphaAnim
327 isVisible = tempAlphaAnim.value != 0f
328 if (isVisible) {
329 placeable.placeWithLayer(0, 0) { alpha = tempAlphaAnim.value }
330 }
331 }
332 },
333 shape = FabMenuBaselineTokens.ListItemContainerShape.value,
334 color = containerColor,
335 contentColor = contentColor,
336 onClick = onClick
337 ) {
338 Row(
339 Modifier.layout { measurable, constraints ->
340 val placeable = measurable.measure(constraints)
341 val width =
342 (placeable.width * maxOf((widthAnim?.value ?: 0f), 0f)).roundToInt()
343 layout(width, placeable.height) {
344 val x =
345 horizontalAlignment.align(placeable.width, width, layoutDirection)
346 placeable.placeWithLayer(x, 0)
347 }
348 }
349 .sizeIn(minWidth = FabMenuItemMinWidth, minHeight = FabMenuItemHeight)
350 .padding(
351 start = FabMenuItemContentPaddingStart,
352 end = FabMenuItemContentPaddingEnd
353 ),
354 verticalAlignment = Alignment.CenterVertically,
355 horizontalArrangement =
356 Arrangement.spacedBy(
357 FabMenuItemContentSpacingHorizontal,
358 Alignment.CenterHorizontally
359 )
360 ) {
361 icon()
362 CompositionLocalProvider(
363 LocalTextStyle provides MaterialTheme.typography.titleMedium,
364 content = text
365 )
366 }
367 }
368 }
369 }
370
371 private val MenuItemRuler = HorizontalRuler()
372
373 // TODO: link to spec and image
374 /**
375 * Toggleable FAB supports animating its container size, corner radius, and color when it is
376 * toggled, and should be used in conjunction with a [FloatingActionButtonMenu] to provide
377 * additional choices to the user after clicking the FAB.
378 *
379 * Use [ToggleFloatingActionButtonDefaults.animateIcon] to animate the color and size of the icon
380 * while the [ToggleFloatingActionButton] is being toggled.
381 *
382 * @sample androidx.compose.material3.samples.FloatingActionButtonMenuSample
383 * @param checked whether this Toggleable FAB is checked
384 * @param onCheckedChange callback to be invoked when this Toggleable FAB is clicked, therefore the
385 * change of the state in requested
386 * @param modifier the [Modifier] to be applied to this Toggleable FAB
387 * @param containerColor the color used for the background of this Toggleable FAB, based on the
388 * checked progress value from 0-1
389 * @param contentAlignment the alignment of this Toggleable FAB when checked
390 * @param containerSize the size of this Toggleable FAB, based on the checked progress value from
391 * 0-1
392 * @param containerCornerRadius the corner radius of this Toggleable FAB, based on the checked
393 * progress value from 0-1
394 * @param content the content of this Toggleable FAB, typically an [Icon] that switches from an Add
395 * to a Close sign at 50% checked progress
396 */
397 @ExperimentalMaterial3ExpressiveApi
398 @Composable
ToggleFloatingActionButtonnull399 fun ToggleFloatingActionButton(
400 checked: Boolean,
401 onCheckedChange: (Boolean) -> Unit,
402 modifier: Modifier = Modifier,
403 containerColor: (Float) -> Color = ToggleFloatingActionButtonDefaults.containerColor(),
404 contentAlignment: Alignment = Alignment.TopEnd,
405 containerSize: (Float) -> Dp = ToggleFloatingActionButtonDefaults.containerSize(),
406 containerCornerRadius: (Float) -> Dp =
407 ToggleFloatingActionButtonDefaults.containerCornerRadius(),
408 content: @Composable ToggleFloatingActionButtonScope.() -> Unit,
409 ) {
410 val checkedProgress =
411 animateFloatAsState(
412 targetValue = if (checked) 1f else 0f,
413 // TODO Load the motionScheme tokens from the component tokens file
414 animationSpec = MotionSchemeKeyTokens.FastSpatial.value()
415 )
416 ToggleFloatingActionButton(
417 checked,
418 onCheckedChange,
419 { checkedProgress.value },
420 modifier,
421 containerColor,
422 contentAlignment,
423 containerSize,
424 containerCornerRadius,
425 content
426 )
427 }
428
429 // TODO: link to spec and image
430 /**
431 * Toggleable FAB supports animating its container size, corner radius, and color when it is
432 * toggled, and should be used in conjunction with a [FloatingActionButtonMenu] to provide
433 * additional choices to the user after clicking the FAB.
434 *
435 * Use [ToggleFloatingActionButtonDefaults.animateIcon] to animate the color and size of the icon
436 * while the [ToggleFloatingActionButton] is being toggled.
437 *
438 * This overload of Toggleable FAB also supports a [checkedProgress] param which is used to drive
439 * the toggle animation.
440 *
441 * @sample androidx.compose.material3.samples.FloatingActionButtonMenuSample
442 * @param checked whether this Toggleable FAB is checked
443 * @param onCheckedChange callback to be invoked when this Toggleable FAB is clicked, therefore the
444 * change of the state in requested
445 * @param checkedProgress callback that provides the progress value for the checked and unchecked
446 * animations
447 * @param modifier the [Modifier] to be applied to this Toggleable FAB
448 * @param containerColor the color used for the background of this Toggleable FAB, based on the
449 * checked progress value from 0-1
450 * @param contentAlignment the alignment of this Toggleable FAB when checked
451 * @param containerSize the size of this Toggleable FAB, based on the checked progress value from
452 * 0-1
453 * @param containerCornerRadius the corner radius of this Toggleable FAB, based on the checked
454 * progress value from 0-1
455 * @param content the content of this Toggleable FAB, typically an [Icon] that switches from an Add
456 * to a Close sign at 50% checked progress
457 */
458 @ExperimentalMaterial3ExpressiveApi
459 @Composable
ToggleFloatingActionButtonnull460 private fun ToggleFloatingActionButton(
461 checked: Boolean,
462 onCheckedChange: (Boolean) -> Unit,
463 checkedProgress: () -> Float,
464 modifier: Modifier = Modifier,
465 containerColor: (Float) -> Color = ToggleFloatingActionButtonDefaults.containerColor(),
466 contentAlignment: Alignment = Alignment.TopEnd,
467 containerSize: (Float) -> Dp = ToggleFloatingActionButtonDefaults.containerSize(),
468 containerCornerRadius: (Float) -> Dp =
469 ToggleFloatingActionButtonDefaults.containerCornerRadius(),
470 content: @Composable ToggleFloatingActionButtonScope.() -> Unit,
471 ) {
472 val initialSize = remember(containerSize) { containerSize(0f) }
473 Box(Modifier.size(initialSize), contentAlignment = contentAlignment) {
474 val density = LocalDensity.current
475 val fabRippleRadius =
476 remember(initialSize) {
477 with(density) {
478 val fabSizeHalf = initialSize.toPx() / 2
479 hypot(fabSizeHalf, fabSizeHalf).toDp()
480 }
481 }
482 val shape =
483 remember(density, checkedProgress, containerCornerRadius) {
484 GenericShape { size, _ ->
485 val radius = with(density) { containerCornerRadius(checkedProgress()).toPx() }
486 addRoundRect(RoundRect(size.toRect(), CornerRadius(radius)))
487 }
488 }
489 Box(
490 modifier
491 .graphicsLayer {
492 this.shadowElevation = FabShadowElevation.toPx()
493 this.shape = shape
494 this.clip = true
495 }
496 .drawBehind {
497 val radius = with(density) { containerCornerRadius(checkedProgress()).toPx() }
498 drawRoundRect(
499 color = containerColor(checkedProgress()),
500 cornerRadius = CornerRadius(radius)
501 )
502 }
503 .toggleable(
504 value = checked,
505 onValueChange = onCheckedChange,
506 interactionSource = null,
507 indication = ripple(radius = fabRippleRadius)
508 )
509 .layout { measurable, constraints ->
510 val placeable = measurable.measure(constraints)
511 val sizePx = containerSize(checkedProgress()).roundToPx()
512 layout(sizePx, sizePx) {
513 placeable.place(
514 (sizePx - placeable.width) / 2,
515 (sizePx - placeable.height) / 2
516 )
517 }
518 }
519 ) {
520 val scope =
521 remember(checkedProgress) {
522 object : ToggleFloatingActionButtonScope {
523 override val checkedProgress: Float
524 get() = checkedProgress()
525 }
526 }
527 content(scope)
528 }
529 }
530 }
531
532 /** Contains the default values used by [ToggleFloatingActionButton] */
533 @ExperimentalMaterial3ExpressiveApi
534 object ToggleFloatingActionButtonDefaults {
535
536 @Composable
containerColornull537 fun containerColor(
538 initialColor: Color = MaterialTheme.colorScheme.primaryContainer,
539 finalColor: Color = MaterialTheme.colorScheme.primary
540 ): (Float) -> Color = { progress -> lerp(initialColor, finalColor, progress) }
541
progressnull542 fun containerSize(initialSize: Dp, finalSize: Dp = FabFinalSize): (Float) -> Dp = { progress ->
543 lerp(initialSize, finalSize, progress)
544 }
545
containerSizenull546 fun containerSize() = containerSize(FabInitialSize)
547
548 fun containerSizeMedium() = containerSize(FabMediumInitialSize)
549
550 fun containerSizeLarge() = containerSize(FabLargeInitialSize)
551
552 fun containerCornerRadius(
553 initialSize: Dp,
554 finalSize: Dp = FabFinalCornerRadius
555 ): (Float) -> Dp = { progress -> lerp(initialSize, finalSize, progress) }
556
containerCornerRadiusnull557 fun containerCornerRadius() = containerCornerRadius(FabInitialCornerRadius)
558
559 fun containerCornerRadiusMedium() = containerCornerRadius(FabMediumInitialCornerRadius)
560
561 fun containerCornerRadiusLarge() = containerCornerRadius(FabLargeInitialCornerRadius)
562
563 @Composable
564 fun iconColor(
565 initialColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
566 finalColor: Color = MaterialTheme.colorScheme.onPrimary
567 ): (Float) -> Color = { progress -> lerp(initialColor, finalColor, progress) }
568
iconSizenull569 fun iconSize(initialSize: Dp, finalSize: Dp = FabFinalIconSize): (Float) -> Dp = { progress ->
570 lerp(initialSize, finalSize, progress)
571 }
572
iconSizenull573 fun iconSize() = iconSize(FabInitialIconSize)
574
575 fun iconSizeMedium() = iconSize(FabMediumInitialIconSize)
576
577 fun iconSizeLarge() = iconSize(FabLargeInitialIconSize)
578
579 /**
580 * Modifier for animating the color and size of an icon within [ToggleFloatingActionButton]
581 * based on a progress value.
582 *
583 * @param checkedProgress callback that provides the progress value for the icon animation
584 * @param color the color of the icon, based on the checked progress value from 0-1
585 * @param size the size of the icon, based on the checked progress value from 0-1
586 */
587 @Composable
588 fun Modifier.animateIcon(
589 checkedProgress: () -> Float,
590 color: (Float) -> Color = iconColor(),
591 size: (Float) -> Dp = iconSize(),
592 ) =
593 this.layout { measurable, _ ->
594 val sizePx = size(checkedProgress()).roundToPx()
595 val placeable = measurable.measure(Constraints.fixed(sizePx, sizePx))
596 layout(sizePx, sizePx) { placeable.place(0, 0) }
597 }
<lambda>null598 .drawWithCache {
599 val layer = obtainGraphicsLayer()
600 layer.apply {
601 record { drawContent() }
602 this.colorFilter = ColorFilter.tint(color(checkedProgress()))
603 }
604
605 onDrawWithContent { drawLayer(graphicsLayer = layer) }
606 }
607 }
608
609 /** Scope for the children of [ToggleFloatingActionButton] */
610 @ExperimentalMaterial3ExpressiveApi
611 interface ToggleFloatingActionButtonScope {
612
613 val checkedProgress: Float
614 }
615
616 @Stable
itemVisiblenull617 private fun Modifier.itemVisible(isVisible: () -> Boolean) =
618 this then MenuItemVisibleElement(isVisible = isVisible)
619
620 private class MenuItemVisibleElement(private val isVisible: () -> Boolean) :
621 ModifierNodeElement<MenuItemVisibilityModifier>() {
622 override fun create() = MenuItemVisibilityModifier(isVisible)
623
624 override fun update(node: MenuItemVisibilityModifier) {
625 node.visible = isVisible
626 }
627
628 override fun InspectorInfo.inspectableProperties() {
629 name = "itemVisible"
630 value = isVisible()
631 }
632
633 override fun equals(other: Any?): Boolean {
634 if (this === other) return true
635 if (other == null || this::class != other::class) return false
636
637 other as MenuItemVisibleElement
638
639 return isVisible === other.isVisible
640 }
641
642 override fun hashCode(): Int {
643 return isVisible.hashCode()
644 }
645 }
646
647 private class MenuItemVisibilityModifier(
648 isVisible: () -> Boolean,
649 ) : ParentDataModifierNode, SemanticsModifierNode, Modifier.Node() {
650
651 var visible: () -> Boolean = isVisible
652
modifyParentDatanull653 override fun Density.modifyParentData(parentData: Any?): Any? {
654 return this@MenuItemVisibilityModifier
655 }
656
657 override val shouldClearDescendantSemantics: Boolean
658 get() = !visible()
659
applySemanticsnull660 override fun SemanticsPropertyReceiver.applySemantics() {}
661 }
662
663 private val Placeable.isVisible: Boolean
664 get() = (this.parentData as? MenuItemVisibilityModifier)?.visible?.invoke() != false
665
666 private val FabInitialSize = FabBaselineTokens.ContainerHeight
667 private val FabInitialCornerRadius = 16.dp
668 private val FabInitialIconSize = FabBaselineTokens.IconSize
669 private val FabMediumInitialSize = FabMediumTokens.ContainerHeight
670 private val FabMediumInitialCornerRadius = 20.dp
671 private val FabMediumInitialIconSize = FabMediumTokens.IconSize
672 private val FabLargeInitialSize = FabLargeTokens.ContainerHeight
673 private val FabLargeInitialCornerRadius = 28.dp
674 private val FabLargeInitialIconSize = 36.dp // TODO: FabLargeTokens.IconSize is incorrect
675 private val FabFinalSize = FabMenuBaselineTokens.CloseButtonContainerHeight
676 private val FabFinalCornerRadius = FabFinalSize.div(2)
677 private val FabFinalIconSize = FabMenuBaselineTokens.CloseButtonIconSize
678 private val FabShadowElevation = FabPrimaryContainerTokens.ContainerElevation
679 private val FabMenuPaddingHorizontal = 16.dp
680 private val FabMenuPaddingBottom = FabMenuBaselineTokens.CloseButtonBetweenSpace
681 private val FabMenuButtonPaddingBottom = 16.dp
682 private val FabMenuItemMinWidth = FabMenuBaselineTokens.ListItemContainerHeight
683 private val FabMenuItemHeight = FabMenuBaselineTokens.ListItemContainerHeight
684 private val FabMenuItemSpacingVertical = FabMenuBaselineTokens.ListItemBetweenSpace
685 private val FabMenuItemContentPaddingStart = FabMenuBaselineTokens.ListItemLeadingSpace
686 private val FabMenuItemContentPaddingEnd = FabMenuBaselineTokens.ListItemTrailingSpace
687 private val FabMenuItemContentSpacingHorizontal = FabMenuBaselineTokens.ListItemIconLabelSpace
688