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