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