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