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.carousel
18 
19 import androidx.annotation.VisibleForTesting
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.DecayAnimationSpec
22 import androidx.compose.animation.core.Spring
23 import androidx.compose.animation.core.spring
24 import androidx.compose.animation.rememberSplineBasedDecay
25 import androidx.compose.foundation.gestures.Orientation
26 import androidx.compose.foundation.gestures.TargetedFlingBehavior
27 import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
28 import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
29 import androidx.compose.foundation.layout.Box
30 import androidx.compose.foundation.layout.PaddingValues
31 import androidx.compose.foundation.layout.calculateEndPadding
32 import androidx.compose.foundation.layout.calculateStartPadding
33 import androidx.compose.foundation.pager.HorizontalPager
34 import androidx.compose.foundation.pager.PageSize
35 import androidx.compose.foundation.pager.PagerDefaults
36 import androidx.compose.foundation.pager.PagerSnapDistance
37 import androidx.compose.foundation.pager.VerticalPager
38 import androidx.compose.material3.ExperimentalMaterial3Api
39 import androidx.compose.runtime.Composable
40 import androidx.compose.runtime.getValue
41 import androidx.compose.runtime.mutableStateOf
42 import androidx.compose.runtime.remember
43 import androidx.compose.runtime.setValue
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.geometry.Rect
46 import androidx.compose.ui.geometry.Size
47 import androidx.compose.ui.graphics.Outline
48 import androidx.compose.ui.graphics.Shape
49 import androidx.compose.ui.layout.layout
50 import androidx.compose.ui.platform.LocalDensity
51 import androidx.compose.ui.platform.LocalLayoutDirection
52 import androidx.compose.ui.unit.Density
53 import androidx.compose.ui.unit.Dp
54 import androidx.compose.ui.unit.LayoutDirection
55 import androidx.compose.ui.unit.dp
56 import kotlin.jvm.JvmInline
57 import kotlin.math.roundToInt
58 
59 /**
60  * [Material Design Carousel](https://m3.material.io/components/carousel/overview)
61  *
62  * A horizontal carousel meant to display many items at once for quick browsing of smaller content
63  * like album art or photo thumbnails.
64  *
65  * Note that this carousel may adjust the size of items in order to ensure a mix of large, medium,
66  * and small items fit perfectly into the available space and are arranged in a visually pleasing
67  * way. Carousel then lays out items using the large item size and clips (or masks) items depending
68  * on their scroll offset to create items which smoothly expand and collapse between the large,
69  * medium, and small sizes.
70  *
71  * For more information, see
72  * [design guidelines](https://m3.material.io/components/carousel/overview)
73  *
74  * Example of a multi-browse carousel:
75  *
76  * @sample androidx.compose.material3.samples.HorizontalMultiBrowseCarouselSample
77  * @param state The state object to be used to control the carousel's state
78  * @param preferredItemWidth The width that large, fully visible items would like to be in the
79  *   horizontal axis. This width is a target and will likely be adjusted by carousel in order to fit
80  *   a whole number of items within the container. Carousel adjusts small items first (between the
81  *   [minSmallItemWidth] and [maxSmallItemWidth]) then medium items when present, and finally large
82  *   items if necessary.
83  * @param modifier A modifier instance to be applied to this carousel container
84  * @param itemSpacing The amount of space used to separate items in the carousel
85  * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
86  * @param minSmallItemWidth The minimum allowable width of small items in dp. Depending on the
87  *   [preferredItemWidth] and the width of the carousel, the small item width will be chosen from a
88  *   range of [minSmallItemWidth] and [maxSmallItemWidth]
89  * @param maxSmallItemWidth The maximum allowable width of small items in dp. Depending on the
90  *   [preferredItemWidth] and the width of the carousel, the small item width will be chosen from a
91  *   range of [minSmallItemWidth] and [maxSmallItemWidth]
92  * @param contentPadding a padding around the whole content. This will add padding for the content
93  *   after it has been clipped. You can use it to add a padding before the first item or after the
94  *   last one. Use [itemSpacing] to add spacing between the items.
95  * @param content The carousel's content Composable
96  */
97 @ExperimentalMaterial3Api
98 @Composable
99 fun HorizontalMultiBrowseCarousel(
100     state: CarouselState,
101     preferredItemWidth: Dp,
102     modifier: Modifier = Modifier,
103     itemSpacing: Dp = 0.dp,
104     flingBehavior: TargetedFlingBehavior =
105         CarouselDefaults.singleAdvanceFlingBehavior(state = state),
106     minSmallItemWidth: Dp = CarouselDefaults.MinSmallItemSize,
107     maxSmallItemWidth: Dp = CarouselDefaults.MaxSmallItemSize,
108     contentPadding: PaddingValues = PaddingValues(0.dp),
109     content: @Composable CarouselItemScope.(itemIndex: Int) -> Unit
110 ) {
111     val density = LocalDensity.current
112     Carousel(
113         state = state,
114         orientation = Orientation.Horizontal,
115         keylineList = { availableSpace, itemSpacingPx ->
116             with(density) {
117                 multiBrowseKeylineList(
118                     density = this,
119                     carouselMainAxisSize = availableSpace,
120                     preferredItemSize = preferredItemWidth.toPx(),
121                     itemCount = state.pagerState.pageCountState.value.invoke(),
122                     itemSpacing = itemSpacingPx,
123                     minSmallItemSize = minSmallItemWidth.toPx(),
124                     maxSmallItemSize = maxSmallItemWidth.toPx(),
125                 )
126             }
127         },
128         contentPadding = contentPadding,
129         // 2 is the max number of medium and small items that can be present in a multi-browse
130         // carousel and should be the upper bounds max non focal visible items.
131         maxNonFocalVisibleItemCount = 2,
132         modifier = modifier,
133         itemSpacing = itemSpacing,
134         flingBehavior = flingBehavior,
135         content = content
136     )
137 }
138 
139 /**
140  * [Material Design Carousel](https://m3.material.io/components/carousel/overview)
141  *
142  * A horizontal carousel that displays its items with the given size except for one item at the end
143  * that is cut off.
144  *
145  * Note that the item size will be bound by the size of the carousel. Otherwise, this carousel lays
146  * out as many items as it can in the given size, and changes the size of the last cut off item such
147  * that there is a range of motion when items scroll off the edge.
148  *
149  * For more information, see
150  * [design guidelines](https://m3.material.io/components/carousel/overview)
151  *
152  * Example of an uncontained carousel:
153  *
154  * @sample androidx.compose.material3.samples.HorizontalUncontainedCarouselSample
155  * @param state The state object to be used to control the carousel's state
156  * @param itemWidth The width of items in the carousel
157  * @param modifier A modifier instance to be applied to this carousel container
158  * @param itemSpacing The amount of space used to separate items in the carousel
159  * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
160  * @param contentPadding a padding around the whole content. This will add padding for the content
161  *   after it has been clipped. You can use it to add a padding before the first item or after the
162  *   last one. Use [itemSpacing] to add spacing between the items.
163  * @param content The carousel's content Composable
164  */
165 @ExperimentalMaterial3Api
166 @Composable
HorizontalUncontainedCarouselnull167 fun HorizontalUncontainedCarousel(
168     state: CarouselState,
169     itemWidth: Dp,
170     modifier: Modifier = Modifier,
171     itemSpacing: Dp = 0.dp,
172     flingBehavior: TargetedFlingBehavior = CarouselDefaults.noSnapFlingBehavior(),
173     contentPadding: PaddingValues = PaddingValues(0.dp),
174     content: @Composable CarouselItemScope.(itemIndex: Int) -> Unit
175 ) {
176     val density = LocalDensity.current
177     Carousel(
178         state = state,
179         orientation = Orientation.Horizontal,
180         keylineList = { availableSpace, itemSpacingPx ->
181             with(density) {
182                 uncontainedKeylineList(
183                     density = this,
184                     carouselMainAxisSize = availableSpace,
185                     itemSize = itemWidth.toPx(),
186                     itemSpacing = itemSpacingPx,
187                 )
188             }
189         },
190         contentPadding = contentPadding,
191         // Since uncontained carousels only have one item that masks as it moves in/out of view,
192         // there is no need to increase the max non focal count.
193         maxNonFocalVisibleItemCount = 0,
194         modifier = modifier,
195         itemSpacing = itemSpacing,
196         flingBehavior = flingBehavior,
197         content = content
198     )
199 }
200 
201 /**
202  * [Material Design Carousel](https://m3.material.io/components/carousel/overview)
203  *
204  * Carousels contain a collection of items that changes sizes according to their placement and the
205  * chosen strategy.
206  *
207  * @param state The state object to be used to control the carousel's state.
208  * @param orientation The layout orientation of the carousel
209  * @param keylineList The list of keylines that are fixed positions along the scrolling axis which
210  *   define the state an item should be in when its center is co-located with the keyline's
211  *   position.
212  * @param contentPadding a padding around the whole content. This will add padding for the
213  * @param maxNonFocalVisibleItemCount the maximum number of items that are visible but not fully
214  *   unmasked (focal) at one time. This number helps determine how many items should be composed to
215  *   fill the entire viewport.
216  * @param modifier A modifier instance to be applied to this carousel outer layout content after it
217  *   has been clipped. You can use it to add a padding before the first item or after the last one.
218  *   Use [itemSpacing] to add spacing between the items.
219  * @param itemSpacing The amount of space used to separate items in the carousel
220  * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
221  * @param content The carousel's content Composable where each call is passed the index, from the
222  *   total item count, of the item being composed
223  */
224 @OptIn(ExperimentalMaterial3Api::class)
225 @Composable
Carouselnull226 internal fun Carousel(
227     state: CarouselState,
228     orientation: Orientation,
229     keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList,
230     contentPadding: PaddingValues,
231     maxNonFocalVisibleItemCount: Int,
232     modifier: Modifier = Modifier,
233     itemSpacing: Dp = 0.dp,
234     flingBehavior: TargetedFlingBehavior =
235         CarouselDefaults.singleAdvanceFlingBehavior(state = state),
236     content: @Composable CarouselItemScope.(itemIndex: Int) -> Unit
237 ) {
238     val beforeContentPadding = contentPadding.calculateBeforeContentPadding(orientation)
239     val afterContentPadding = contentPadding.calculateAfterContentPadding(orientation)
240     val pageSize =
241         remember(keylineList) {
242             CarouselPageSize(keylineList, beforeContentPadding, afterContentPadding)
243         }
244 
245     val snapPosition = KeylineSnapPosition(pageSize)
246 
247     if (orientation == Orientation.Horizontal) {
248         HorizontalPager(
249             state = state.pagerState,
250             // Only pass cross axis padding as main axis padding will be handled by the strategy
251             contentPadding =
252                 PaddingValues(
253                     top = contentPadding.calculateTopPadding(),
254                     bottom = contentPadding.calculateBottomPadding()
255                 ),
256             pageSize = pageSize,
257             pageSpacing = itemSpacing,
258             beyondViewportPageCount = maxNonFocalVisibleItemCount,
259             snapPosition = snapPosition,
260             flingBehavior = flingBehavior,
261             modifier = modifier
262         ) { page ->
263             val carouselItemInfo = remember { CarouselItemDrawInfoImpl() }
264             val scope = remember { CarouselItemScopeImpl(itemInfo = carouselItemInfo) }
265             val clipShape = remember {
266                 object : Shape {
267                     override fun createOutline(
268                         size: Size,
269                         layoutDirection: LayoutDirection,
270                         density: Density
271                     ): Outline {
272                         return Outline.Rectangle(carouselItemInfo.maskRect)
273                     }
274                 }
275             }
276 
277             Box(
278                 modifier =
279                     Modifier.carouselItem(
280                         index = page,
281                         state = state,
282                         strategy = { pageSize.strategy },
283                         carouselItemDrawInfo = carouselItemInfo,
284                         clipShape = clipShape
285                     )
286             ) {
287                 scope.content(page)
288             }
289         }
290     } else if (orientation == Orientation.Vertical) {
291         VerticalPager(
292             state = state.pagerState,
293             // Only pass cross axis padding as main axis padding will be handled by the strategy
294             contentPadding =
295                 PaddingValues(
296                     start = contentPadding.calculateStartPadding(LocalLayoutDirection.current),
297                     end = contentPadding.calculateEndPadding(LocalLayoutDirection.current)
298                 ),
299             pageSize = pageSize,
300             pageSpacing = itemSpacing,
301             beyondViewportPageCount = maxNonFocalVisibleItemCount,
302             snapPosition = snapPosition,
303             flingBehavior = flingBehavior,
304             modifier = modifier
305         ) { page ->
306             val carouselItemInfo = remember { CarouselItemDrawInfoImpl() }
307             val scope = remember { CarouselItemScopeImpl(itemInfo = carouselItemInfo) }
308             val clipShape = remember {
309                 object : Shape {
310                     override fun createOutline(
311                         size: Size,
312                         layoutDirection: LayoutDirection,
313                         density: Density
314                     ): Outline {
315                         return Outline.Rectangle(carouselItemInfo.maskRect)
316                     }
317                 }
318             }
319 
320             Box(
321                 modifier =
322                     Modifier.carouselItem(
323                         index = page,
324                         state = state,
325                         strategy = { pageSize.strategy },
326                         carouselItemDrawInfo = carouselItemInfo,
327                         clipShape = clipShape
328                     )
329             ) {
330                 scope.content(page)
331             }
332         }
333     }
334 }
335 
336 @Composable
PaddingValuesnull337 private fun PaddingValues.calculateBeforeContentPadding(orientation: Orientation): Float {
338     val dpValue =
339         if (orientation == Orientation.Vertical) {
340             calculateTopPadding()
341         } else {
342             calculateStartPadding(LocalLayoutDirection.current)
343         }
344 
345     return with(LocalDensity.current) { dpValue.toPx() }
346 }
347 
348 @Composable
PaddingValuesnull349 private fun PaddingValues.calculateAfterContentPadding(orientation: Orientation): Float {
350     val dpValue =
351         if (orientation == Orientation.Vertical) {
352             calculateBottomPadding()
353         } else {
354             calculateEndPadding(LocalLayoutDirection.current)
355         }
356 
357     return with(LocalDensity.current) { dpValue.toPx() }
358 }
359 
360 /**
361  * A [PageSize] implementation that maintains a strategy that is kept up-to-date with the latest
362  * available space of the container.
363  *
364  * @param keylineList The list of keylines that are fixed positions along the scrolling axis which
365  *   define the state an item should be in when its center is co-located with the keyline's
366  *   position.
367  */
368 internal class CarouselPageSize(
369     private val keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList,
370     private val beforeContentPadding: Float,
371     private val afterContentPadding: Float
372 ) : PageSize {
373 
374     private var strategyState by mutableStateOf(Strategy.Empty)
375     val strategy: Strategy
376         get() = strategyState
377 
calculateMainAxisPageSizenull378     override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
379         val keylines = keylineList.invoke(availableSpace.toFloat(), pageSpacing.toFloat())
380         strategyState =
381             Strategy(
382                 keylines,
383                 availableSpace.toFloat(),
384                 pageSpacing.toFloat(),
385                 beforeContentPadding,
386                 afterContentPadding
387             )
388 
389         // If a valid strategy is available, use the strategy's item size. Otherwise, default to
390         // a full size item as Pager does by default.
391         return if (strategy.isValid) {
392             strategy.itemMainAxisSize.roundToInt()
393         } else {
394             availableSpace
395         }
396     }
397 }
398 
399 /** This class defines ways items can be aligned along a carousel's main axis. */
400 @JvmInline
401 internal value class CarouselAlignment private constructor(internal val value: Int) {
402     companion object {
403         /** Start aligned carousels place focal items at the start/top of the container */
404         val Start = CarouselAlignment(-1)
405 
406         /** Center aligned carousels place focal items in the middle of the container */
407         val Center = CarouselAlignment(0)
408 
409         /** End aligned carousels place focal items at the end/bottom of the container */
410         val End = CarouselAlignment(1)
411     }
412 }
413 
414 /**
415  * A modifier that handles clipping and translating an item as it moves along the scrolling axis of
416  * a Carousel.
417  *
418  * @param index the index of the item in the carousel
419  * @param state the carousel state
420  * @param strategy the strategy used to mask and translate items in the carousel
421  * @param carouselItemDrawInfo the item info that should be updated with the changes in this
422  *   modifier
423  * @param clipShape the shape the item will clip itself to. This should be a rectangle with a bounds
424  *   that match the carousel item info's mask rect. Corner radii and other shape customizations can
425  *   be done by the client using [CarouselItemScope.maskClip] and [CarouselItemScope.maskBorder].
426  */
427 @OptIn(ExperimentalMaterial3Api::class)
carouselItemnull428 internal fun Modifier.carouselItem(
429     index: Int,
430     state: CarouselState,
431     strategy: () -> Strategy,
432     carouselItemDrawInfo: CarouselItemDrawInfoImpl,
433     clipShape: Shape,
434 ): Modifier {
435     return layout { measurable, constraints ->
436         val strategyResult = strategy.invoke()
437         if (!strategyResult.isValid) {
438             // If there is no strategy, avoid displaying content
439             return@layout layout(0, 0) {}
440         }
441 
442         val isVertical = state.pagerState.layoutInfo.orientation == Orientation.Vertical
443         val isRtl = layoutDirection == LayoutDirection.Rtl
444 
445         // Force the item to use the strategy's itemMainAxisSize along its main axis
446         val mainAxisSize = strategyResult.itemMainAxisSize
447         val itemConstraints =
448             if (isVertical) {
449                 constraints.copy(
450                     minWidth = constraints.minWidth,
451                     maxWidth = constraints.maxWidth,
452                     minHeight = mainAxisSize.roundToInt(),
453                     maxHeight = mainAxisSize.roundToInt()
454                 )
455             } else {
456                 constraints.copy(
457                     minWidth = mainAxisSize.roundToInt(),
458                     maxWidth = mainAxisSize.roundToInt(),
459                     minHeight = constraints.minHeight,
460                     maxHeight = constraints.maxHeight
461                 )
462             }
463 
464         val placeable = measurable.measure(itemConstraints)
465         // We always want to make the current item be the one at the front
466         val itemZIndex =
467             if (index == state.pagerState.currentPage) {
468                 1f
469             } else {
470                 if (index == 0) {
471                     0f
472                 } else {
473                     // Other items should go in reverse placement order, that is, the ones with the
474                     // higher indices should behind the ones with lower indices.
475                     1f / index.toFloat()
476                 }
477             }
478 
479         layout(placeable.width, placeable.height) {
480             placeable.placeWithLayer(
481                 0,
482                 0,
483                 zIndex = itemZIndex,
484                 layerBlock = {
485                     val scrollOffset = calculateCurrentScrollOffset(state, strategyResult)
486                     val maxScrollOffset = calculateMaxScrollOffset(state, strategyResult)
487                     // TODO: Reduce the number of times keylins are calculated
488                     val keylines =
489                         strategyResult.getKeylineListForScrollOffset(scrollOffset, maxScrollOffset)
490                     val roundedKeylines =
491                         strategyResult.getKeylineListForScrollOffset(
492                             scrollOffset = scrollOffset,
493                             maxScrollOffset = maxScrollOffset,
494                             roundToNearestStep = true
495                         )
496 
497                     // Find center of the item at this index
498                     val itemSizeWithSpacing =
499                         strategyResult.itemMainAxisSize + strategyResult.itemSpacing
500                     val unadjustedCenter =
501                         (index * itemSizeWithSpacing) + (strategyResult.itemMainAxisSize / 2f) -
502                             scrollOffset
503 
504                     // Find the keyline before and after this item's center and create an
505                     // interpolated
506                     // keyline that the item should use for its clip shape and offset
507                     val keylineBefore = keylines.getKeylineBefore(unadjustedCenter)
508                     val keylineAfter = keylines.getKeylineAfter(unadjustedCenter)
509                     val progress = getProgress(keylineBefore, keylineAfter, unadjustedCenter)
510                     val interpolatedKeyline = lerp(keylineBefore, keylineAfter, progress)
511                     val isOutOfKeylineBounds = keylineBefore == keylineAfter
512 
513                     val centerX =
514                         if (isVertical) size.height / 2f else strategyResult.itemMainAxisSize / 2f
515                     val centerY =
516                         if (isVertical) strategyResult.itemMainAxisSize / 2f else size.height / 2f
517                     val halfMaskWidth =
518                         if (isVertical) size.width / 2f else interpolatedKeyline.size / 2f
519                     val halfMaskHeight =
520                         if (isVertical) interpolatedKeyline.size / 2f else size.height / 2f
521                     val maskRect =
522                         Rect(
523                             left = centerX - halfMaskWidth,
524                             top = centerY - halfMaskHeight,
525                             right = centerX + halfMaskWidth,
526                             bottom = centerY + halfMaskHeight
527                         )
528 
529                     // Update carousel item info
530                     carouselItemDrawInfo.sizeState = interpolatedKeyline.size
531                     carouselItemDrawInfo.minSizeState = roundedKeylines.minBy { it.size }.size
532                     carouselItemDrawInfo.maxSizeState = roundedKeylines.firstFocal.size
533                     carouselItemDrawInfo.maskRectState = maskRect
534 
535                     // Clip the item
536                     clip = maskRect != Rect(0f, 0f, size.width, size.height)
537                     shape = clipShape
538 
539                     // After clipping, the items will have white space between them. Translate the
540                     // items to pin their edges together
541                     var translation = interpolatedKeyline.offset - unadjustedCenter
542                     if (isOutOfKeylineBounds) {
543                         // If this item is beyond the first or last keyline, continue to offset the
544                         // item by cutting its unadjustedOffset according to its masked size.
545                         val outOfBoundsOffset =
546                             (unadjustedCenter - interpolatedKeyline.unadjustedOffset) /
547                                 interpolatedKeyline.size
548                         translation += outOfBoundsOffset
549                     }
550                     if (isVertical) {
551                         translationY = translation
552                     } else {
553                         translationX = if (isRtl) -translation else translation
554                     }
555                 }
556             )
557         }
558     }
559 }
560 
561 /** Calculates the current scroll offset given item count, sizing, spacing, and snap position. */
562 @OptIn(ExperimentalMaterial3Api::class)
calculateCurrentScrollOffsetnull563 internal fun calculateCurrentScrollOffset(
564     state: CarouselState,
565     strategy: Strategy,
566 ): Float {
567     val itemSizeWithSpacing = strategy.itemMainAxisSize + strategy.itemSpacing
568     val currentItemScrollOffset =
569         (state.pagerState.currentPage * itemSizeWithSpacing) +
570             (state.pagerState.currentPageOffsetFraction * itemSizeWithSpacing)
571     return currentItemScrollOffset -
572         getSnapPositionOffset(strategy, state.pagerState.currentPage, state.pagerState.pageCount)
573 }
574 
575 /** Returns the max scroll offset given the item count, sizing, and spacing. */
576 @OptIn(ExperimentalMaterial3Api::class)
577 @VisibleForTesting
calculateMaxScrollOffsetnull578 internal fun calculateMaxScrollOffset(state: CarouselState, strategy: Strategy): Float {
579     val itemCount = state.pagerState.pageCount.toFloat()
580     val maxScrollPossible =
581         (strategy.itemMainAxisSize * itemCount) + (strategy.itemSpacing * (itemCount - 1))
582 
583     return (maxScrollPossible - strategy.availableSpace).coerceAtLeast(0f)
584 }
585 
586 /**
587  * Returns a float between 0 and 1 that represents how far [unadjustedOffset] is between [before]
588  * and [after].
589  *
590  * @param before the first keyline whose unadjustedOffset is less than [unadjustedOffset]
591  * @param after the first keyline whose unadjustedOffset is greater than [unadjustedOffset]
592  * @param unadjustedOffset the unadjustedOffset between [before] and [after]'s unadjustedOffset that
593  *   a progress value will be returned for
594  */
getProgressnull595 private fun getProgress(before: Keyline, after: Keyline, unadjustedOffset: Float): Float {
596     if (before == after) {
597         return 1f
598     }
599 
600     val total = after.unadjustedOffset - before.unadjustedOffset
601     return (unadjustedOffset - before.unadjustedOffset) / total
602 }
603 
604 /** Contains the default values used by [Carousel]. */
605 @ExperimentalMaterial3Api
606 object CarouselDefaults {
607 
608     /**
609      * A [TargetedFlingBehavior] that limits a fling to one item at a time. [snapAnimationSpec] can
610      * be used to control the snap animation.
611      *
612      * @param state The [CarouselState] that controls which Carousel this TargetedFlingBehavior will
613      *   be applied to.
614      * @param snapAnimationSpec The animation spec used to finally snap to the position.
615      * @return An instance of [TargetedFlingBehavior] that performs snapping to the next item. The
616      *   animation will be governed by the post scroll velocity and the Carousel will use
617      *   [snapAnimationSpec] to approach the snapped position
618      */
619     @Composable
singleAdvanceFlingBehaviornull620     fun singleAdvanceFlingBehavior(
621         state: CarouselState,
622         snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
623     ): TargetedFlingBehavior {
624         return PagerDefaults.flingBehavior(
625             state = state.pagerState,
626             pagerSnapDistance = PagerSnapDistance.atMost(1),
627             snapAnimationSpec = snapAnimationSpec,
628         )
629     }
630 
631     /**
632      * A [TargetedFlingBehavior] that flings and snaps according to the gesture velocity.
633      * [snapAnimationSpec] and [decayAnimationSpec] can be used to control the animation specs.
634      *
635      * The Carousel may use [decayAnimationSpec] or [snapAnimationSpec] to approach the target item
636      * post-scroll, depending on the gesture velocity. If the gesture has a high enough velocity to
637      * approach the target item, the Carousel will use [decayAnimationSpec] followed by
638      * [snapAnimationSpec] for the final step of the animation. If the gesture doesn't have enough
639      * velocity, it will use [snapAnimationSpec] + [snapAnimationSpec] in a similar fashion.
640      *
641      * @param state The [CarouselState] that controls which Carousel this TargetedFlingBehavior will
642      *   be applied to.
643      * @param decayAnimationSpec The animation spec used to approach the target offset when the the
644      *   fling velocity is large enough to naturally decay.
645      * @param snapAnimationSpec The animation spec used to finally snap to the position.
646      * @return An instance of [TargetedFlingBehavior] that performs flinging based on the gesture
647      *   velocity and then snapping to the closest item post-fling. The animation will be governed
648      *   by the post scroll velocity and the Carousel will use [snapAnimationSpec] to approach the
649      *   snapped position
650      */
651     @Composable
multiBrowseFlingBehaviornull652     fun multiBrowseFlingBehavior(
653         state: CarouselState,
654         decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
655         snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
656     ): TargetedFlingBehavior {
657         val pagerSnapDistance =
658             object : PagerSnapDistance {
659                 override fun calculateTargetPage(
660                     startPage: Int,
661                     suggestedTargetPage: Int,
662                     velocity: Float,
663                     pageSize: Int,
664                     pageSpacing: Int
665                 ): Int {
666                     return suggestedTargetPage
667                 }
668             }
669         return PagerDefaults.flingBehavior(
670             state = state.pagerState,
671             pagerSnapDistance = pagerSnapDistance,
672             decayAnimationSpec = decayAnimationSpec,
673             snapAnimationSpec = snapAnimationSpec,
674         )
675     }
676 
677     /**
678      * A [TargetedFlingBehavior] that flings according to the gesture velocity and does not snap
679      * post-fling.
680      *
681      * @return An instance of [TargetedFlingBehavior] that performs flinging based on the gesture
682      *   velocity and does not snap to anything post-fling.
683      */
684     @Composable
noSnapFlingBehaviornull685     fun noSnapFlingBehavior(): TargetedFlingBehavior {
686         val decayLayoutInfoProvider = remember {
687             object : SnapLayoutInfoProvider {
688                 override fun calculateSnapOffset(velocity: Float): Float = 0f
689             }
690         }
691 
692         return rememberSnapFlingBehavior(snapLayoutInfoProvider = decayLayoutInfoProvider)
693     }
694 
695     /** The minimum size that a carousel strategy can choose its small items to be. * */
696     val MinSmallItemSize = 40.dp
697 
698     /** The maximum size that a carousel strategy can choose its small items to be. * */
699     val MaxSmallItemSize = 56.dp
700 
701     internal val AnchorSize = 10.dp
702     internal const val MediumLargeItemDiffThreshold = 0.85f
703 }
704