1 /*
2 * 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.FloatRange
20 import androidx.compose.foundation.MutatePriority
21 import androidx.compose.foundation.gestures.ScrollScope
22 import androidx.compose.foundation.gestures.ScrollableState
23 import androidx.compose.foundation.pager.PagerState
24 import androidx.compose.material3.ExperimentalMaterial3Api
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.getValue
27 import androidx.compose.runtime.mutableFloatStateOf
28 import androidx.compose.runtime.mutableStateOf
29 import androidx.compose.runtime.saveable.Saver
30 import androidx.compose.runtime.saveable.listSaver
31 import androidx.compose.runtime.saveable.rememberSaveable
32 import androidx.compose.runtime.setValue
33 import androidx.compose.ui.geometry.Rect
34
35 /**
36 * The state that can be used to control all types of carousels.
37 *
38 * @param currentItem the current item to be scrolled to.
39 * @param currentItemOffsetFraction the offset of the current item as a fraction of the item's size.
40 * This should vary between -0.5 and 0.5 and indicates how to offset the current item from the
41 * snapped position.
42 * @param itemCount the number of items this Carousel will have.
43 */
44 @ExperimentalMaterial3Api
45 class CarouselState(
46 currentItem: Int = 0,
47 @FloatRange(from = -0.5, to = 0.5) currentItemOffsetFraction: Float = 0f,
48 itemCount: () -> Int,
49 ) : ScrollableState {
50 internal var pagerState: CarouselPagerState =
51 CarouselPagerState(currentItem, currentItemOffsetFraction, itemCount)
52
53 override val isScrollInProgress: Boolean
54 get() = pagerState.isScrollInProgress
55
dispatchRawDeltanull56 override fun dispatchRawDelta(delta: Float): Float {
57 return pagerState.dispatchRawDelta(delta)
58 }
59
scrollnull60 override suspend fun scroll(
61 scrollPriority: MutatePriority,
62 block: suspend ScrollScope.() -> Unit
63 ) {
64 pagerState.scroll(scrollPriority, block)
65 }
66
67 @ExperimentalMaterial3Api
68 companion object {
69 /** To keep current item and item offset saved */
70 val Saver: Saver<CarouselState, *> =
71 listSaver(
<lambda>null72 save = {
73 listOf(
74 it.pagerState.currentPage,
75 it.pagerState.currentPageOffsetFraction,
76 it.pagerState.pageCount,
77 )
78 },
<lambda>null79 restore = {
80 CarouselState(
81 currentItem = it[0] as Int,
82 currentItemOffsetFraction = it[1] as Float,
83 itemCount = { it[2] as Int },
84 )
85 }
86 )
87 }
88 }
89
90 /**
91 * Creates a [CarouselState] that is remembered across compositions.
92 *
93 * @param initialItem The initial item that should be scrolled to.
94 * @param itemCount The number of items this Carousel will have.
95 */
96 @ExperimentalMaterial3Api
97 @Composable
rememberCarouselStatenull98 fun rememberCarouselState(
99 initialItem: Int = 0,
100 itemCount: () -> Int,
101 ): CarouselState {
102 return rememberSaveable(saver = CarouselState.Saver) {
103 CarouselState(
104 currentItem = initialItem,
105 currentItemOffsetFraction = 0F,
106 itemCount = itemCount
107 )
108 }
109 .apply { pagerState.pageCountState.value = itemCount }
110 }
111
112 internal const val MinPageOffset = -0.5f
113 internal const val MaxPageOffset = 0.5f
114
115 internal class CarouselPagerState(
116 currentPage: Int,
117 currentPageOffsetFraction: Float,
118 updatedPageCount: () -> Int,
119 ) : PagerState(currentPage, currentPageOffsetFraction) {
120 var pageCountState = mutableStateOf(updatedPageCount)
121
122 // Observe changes to the lambda within the MutableState
123 override val pageCount: Int
124 get() = pageCountState.value.invoke()
125
126 companion object {
127 /** To keep current page and current page offset saved */
128 val Saver: Saver<CarouselPagerState, *> =
129 listSaver(
<lambda>null130 save = {
131 listOf(
132 it.currentPage,
133 (it.currentPageOffsetFraction).coerceIn(MinPageOffset, MaxPageOffset),
134 it.pageCountState.value
135 )
136 },
<lambda>null137 restore = {
138 CarouselPagerState(
139 currentPage = it[0] as Int,
140 currentPageOffsetFraction = it[1] as Float,
141 updatedPageCount = { it[2] as Int },
142 )
143 }
144 )
145 }
146 }
147
148 /**
149 * Interface to hold information about a Carousel item and its size.
150 *
151 * Example of CarouselItemDrawInfo usage:
152 *
153 * @sample androidx.compose.material3.samples.FadingHorizontalMultiBrowseCarouselSample
154 */
155 @ExperimentalMaterial3Api
156 sealed interface CarouselItemDrawInfo {
157
158 /** The size of the carousel item in the main axis in pixels */
159 val size: Float
160
161 /**
162 * The minimum size of the carousel item in the main axis in pixels, eg. the size of the item as
163 * it scrolls off the sides of the carousel
164 */
165 val minSize: Float
166
167 /**
168 * The maximum size of the carousel item in the main axis in pixels, eg. the size of the item
169 * when it is at a focal position
170 */
171 val maxSize: Float
172
173 /** The [Rect] by which the carousel item is being clipped. */
174 val maskRect: Rect
175 }
176
177 @OptIn(ExperimentalMaterial3Api::class)
178 internal class CarouselItemDrawInfoImpl : CarouselItemDrawInfo {
179
180 var sizeState by mutableFloatStateOf(0f)
181 var minSizeState by mutableFloatStateOf(0f)
182 var maxSizeState by mutableFloatStateOf(0f)
183 var maskRectState by mutableStateOf(Rect.Zero)
184
185 override val size: Float
186 get() = sizeState
187
188 override val minSize: Float
189 get() = minSizeState
190
191 override val maxSize: Float
192 get() = maxSizeState
193
194 override val maskRect: Rect
195 get() = maskRectState
196 }
197