blob: 50602d5e28d923a6397cdd7c9305d286f00d5170 [file] [log] [blame]
Vineet Kumarefa63502022-08-30 10:00:54 +00001/*
Vineet Kumar507211d2023-01-04 19:16:38 +05302 * Copyright 2023 The Android Open Source Project
Vineet Kumarefa63502022-08-30 10:00:54 +00003 *
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
Vineet Kumar507211d2023-01-04 19:16:38 +053017package androidx.tv.material3
Vineet Kumarefa63502022-08-30 10:00:54 +000018
Aditya Arora84c138c2023-01-05 11:16:55 +053019import android.view.KeyEvent.KEYCODE_BACK
Vighnesh Raut799489f2023-01-11 19:38:16 +053020import android.view.KeyEvent.KEYCODE_DPAD_LEFT
21import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
Vineet Kumar5e594582022-08-19 15:52:28 +053022import androidx.compose.animation.AnimatedContent
Aditya Arora7938e90c2022-11-17 13:32:20 +053023import androidx.compose.animation.AnimatedVisibilityScope
Vighnesh Raut431494a2023-01-19 20:03:22 +053024import androidx.compose.animation.ContentTransform
Vineet Kumar5e594582022-08-19 15:52:28 +053025import androidx.compose.animation.ExperimentalAnimationApi
Vineet Kumarefa63502022-08-30 10:00:54 +000026import androidx.compose.animation.core.tween
27import androidx.compose.animation.fadeIn
28import androidx.compose.animation.fadeOut
Doris Liu4b3a8312023-03-28 18:11:25 -070029import androidx.compose.animation.togetherWith
Vineet Kumarefa63502022-08-30 10:00:54 +000030import androidx.compose.foundation.background
31import androidx.compose.foundation.focusable
32import androidx.compose.foundation.layout.Arrangement
33import androidx.compose.foundation.layout.Box
34import androidx.compose.foundation.layout.BoxScope
35import androidx.compose.foundation.layout.Row
36import androidx.compose.foundation.layout.padding
37import androidx.compose.foundation.layout.size
38import androidx.compose.foundation.shape.CircleShape
39import androidx.compose.runtime.Composable
40import androidx.compose.runtime.LaunchedEffect
41import androidx.compose.runtime.Stable
42import androidx.compose.runtime.getValue
43import androidx.compose.runtime.mutableStateOf
44import androidx.compose.runtime.remember
vinekumarb2612472023-01-27 22:31:37 +053045import androidx.compose.runtime.rememberUpdatedState
Vineet Kumarefa63502022-08-30 10:00:54 +000046import androidx.compose.runtime.setValue
47import androidx.compose.runtime.snapshotFlow
48import androidx.compose.ui.Alignment
49import androidx.compose.ui.ExperimentalComposeUiApi
50import androidx.compose.ui.Modifier
51import androidx.compose.ui.focus.FocusDirection
Vighnesh Raut799489f2023-01-11 19:38:16 +053052import androidx.compose.ui.focus.FocusManager
Vighnesh Raut2a623462022-09-14 10:35:36 +053053import androidx.compose.ui.focus.FocusRequester
Vineet Kumarefa63502022-08-30 10:00:54 +000054import androidx.compose.ui.focus.FocusState
Vighnesh Raut2a623462022-09-14 10:35:36 +053055import androidx.compose.ui.focus.focusRequester
Vineet Kumarefa63502022-08-30 10:00:54 +000056import androidx.compose.ui.focus.onFocusChanged
57import androidx.compose.ui.graphics.Color
Vighnesh Raut799489f2023-01-11 19:38:16 +053058import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
Aditya Arora84c138c2023-01-05 11:16:55 +053059import androidx.compose.ui.input.key.key
60import androidx.compose.ui.input.key.nativeKeyCode
61import androidx.compose.ui.input.key.onKeyEvent
62import androidx.compose.ui.input.key.type
Vineet Kumarefa63502022-08-30 10:00:54 +000063import androidx.compose.ui.platform.LocalFocusManager
Vighnesh Raut2a623462022-09-14 10:35:36 +053064import androidx.compose.ui.platform.LocalLayoutDirection
Vighnesh Raut268af2a2022-12-13 16:52:41 +053065import androidx.compose.ui.unit.Dp
Vighnesh Raut2a623462022-09-14 10:35:36 +053066import androidx.compose.ui.unit.LayoutDirection
Vineet Kumarefa63502022-08-30 10:00:54 +000067import androidx.compose.ui.unit.dp
Vineet Kumarefa63502022-08-30 10:00:54 +000068import java.lang.Math.floorMod
69import kotlinx.coroutines.delay
70import kotlinx.coroutines.flow.first
71import kotlinx.coroutines.yield
72
73/**
74 * Composes a hero card rotator to highlight a piece of content.
75 *
Vighnesh Raut268af2a2022-12-13 16:52:41 +053076 * Examples:
77 * @sample androidx.tv.samples.SimpleCarousel
78 * @sample androidx.tv.samples.CarouselIndicatorWithRectangleShape
79 *
Vineet Kumar507211d2023-01-04 19:16:38 +053080 * @param modifier Modifier applied to the Carousel.
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +053081 * @param itemCount total number of items present in the carousel.
Vineet Kumarefa63502022-08-30 10:00:54 +000082 * @param carouselState state associated with this carousel.
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +053083 * @param autoScrollDurationMillis duration for which item should be visible before moving to
84 * the next item.
85 * @param contentTransformStartToEnd animation transform applied when we are moving from start to
86 * end in the carousel while scrolling to the next item
87 * @param contentTransformEndToStart animation transform applied when we are moving from end to
88 * start in the carousel while scrolling to the next item
89 * @param carouselIndicator indicator showing the position of the current item among all items.
90 * @param content defines the items for a given index.
Vineet Kumarefa63502022-08-30 10:00:54 +000091 */
Vineet Kumarefa63502022-08-30 10:00:54 +000092@Suppress("IllegalExperimentalApiUsage")
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +053093@OptIn(ExperimentalComposeUiApi::class)
Vineet Kumar507211d2023-01-04 19:16:38 +053094@ExperimentalTvMaterial3Api
Vineet Kumarefa63502022-08-30 10:00:54 +000095@Composable
96fun Carousel(
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +053097 itemCount: Int,
Vineet Kumarefa63502022-08-30 10:00:54 +000098 modifier: Modifier = Modifier,
99 carouselState: CarouselState = remember { CarouselState() },
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530100 autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplayItemMillis,
101 contentTransformStartToEnd: ContentTransform = CarouselDefaults.contentTransform,
102 contentTransformEndToStart: ContentTransform = CarouselDefaults.contentTransform,
Vineet Kumarefa63502022-08-30 10:00:54 +0000103 carouselIndicator:
104 @Composable BoxScope.() -> Unit = {
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530105 CarouselDefaults.IndicatorRow(
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530106 itemCount = itemCount,
107 activeItemIndex = carouselState.activeItemIndex,
Vineet Kumar5e594582022-08-19 15:52:28 +0530108 modifier = Modifier
109 .align(Alignment.BottomEnd)
110 .padding(16.dp),
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530111 )
Vineet Kumarefa63502022-08-30 10:00:54 +0000112 },
Vighnesh Raut431494a2023-01-19 20:03:22 +0530113 content: @Composable CarouselScope.(index: Int) -> Unit
Vineet Kumarefa63502022-08-30 10:00:54 +0000114) {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530115 CarouselStateUpdater(carouselState, itemCount)
Vineet Kumarefa63502022-08-30 10:00:54 +0000116 var focusState: FocusState? by remember { mutableStateOf(null) }
117 val focusManager = LocalFocusManager.current
Vighnesh Raut2a623462022-09-14 10:35:36 +0530118 val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
Aditya Arora7938e90c2022-11-17 13:32:20 +0530119 val carouselOuterBoxFocusRequester = remember { FocusRequester() }
Vighnesh Raut2a623462022-09-14 10:35:36 +0530120 var isAutoScrollActive by remember { mutableStateOf(false) }
Vineet Kumarefa63502022-08-30 10:00:54 +0000121
122 AutoScrollSideEffect(
Vighnesh Raut431494a2023-01-19 20:03:22 +0530123 autoScrollDurationMillis = autoScrollDurationMillis,
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530124 itemCount = itemCount,
Vineet Kumar01abb022023-01-14 14:59:09 +0530125 carouselState = carouselState,
126 doAutoScroll = shouldPerformAutoScroll(focusState),
Vighnesh Raut2a623462022-09-14 10:35:36 +0530127 onAutoScrollChange = { isAutoScrollActive = it })
Vineet Kumar11d7e402022-12-04 13:18:52 +0530128
Vineet Kumarefa63502022-08-30 10:00:54 +0000129 Box(modifier = modifier
Vineet Kumar11d7e402022-12-04 13:18:52 +0530130 .bringIntoViewIfChildrenAreFocused()
Aditya Arora7938e90c2022-11-17 13:32:20 +0530131 .focusRequester(carouselOuterBoxFocusRequester)
Vineet Kumarefa63502022-08-30 10:00:54 +0000132 .onFocusChanged {
133 focusState = it
Vighnesh Raut799489f2023-01-11 19:38:16 +0530134
135 // When the carousel gains focus for the first time
Vighnesh Raut2a623462022-09-14 10:35:36 +0530136 if (it.isFocused && isAutoScrollActive) {
Vineet Kumarefa63502022-08-30 10:00:54 +0000137 focusManager.moveFocus(FocusDirection.Enter)
138 }
139 }
Vighnesh Raut799489f2023-01-11 19:38:16 +0530140 .handleKeyEvents(
141 carouselState = carouselState,
142 outerBoxFocusRequester = carouselOuterBoxFocusRequester,
143 focusManager = focusManager,
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530144 itemCount = itemCount,
Vighnesh Raut799489f2023-01-11 19:38:16 +0530145 isLtr = isLtr,
146 )
147 .focusable()
148 ) {
Vineet Kumar5e594582022-08-19 15:52:28 +0530149 AnimatedContent(
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530150 targetState = carouselState.activeItemIndex,
Vighnesh Raut431494a2023-01-19 20:03:22 +0530151 transitionSpec = {
152 if (carouselState.isMovingBackward) {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530153 contentTransformEndToStart
Vighnesh Raut431494a2023-01-19 20:03:22 +0530154 } else {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530155 contentTransformStartToEnd
Vighnesh Raut431494a2023-01-19 20:03:22 +0530156 }
157 }
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530158 ) { activeItemIndex ->
Aditya Arora7938e90c2022-11-17 13:32:20 +0530159 LaunchedEffect(Unit) {
160 this@AnimatedContent.onAnimationCompletion {
Vighnesh Raut799489f2023-01-11 19:38:16 +0530161 // Outer box is focused
162 if (!isAutoScrollActive && focusState?.isFocused == true) {
Aditya Arora7938e90c2022-11-17 13:32:20 +0530163 carouselOuterBoxFocusRequester.requestFocus()
164 focusManager.moveFocus(FocusDirection.Enter)
Vighnesh Raut2a623462022-09-14 10:35:36 +0530165 }
Aditya Arora7938e90c2022-11-17 13:32:20 +0530166 }
Vighnesh Raut2a623462022-09-14 10:35:36 +0530167 }
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530168 // it is possible for the itemCount to have changed during the transition.
169 // This can cause the itemIndex to be greater than or equal to itemCount and cause
170 // IndexOutOfBoundsException. Guarding against this by checking against itemCount
vinekumarb2612472023-01-27 22:31:37 +0530171 // before invoking.
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530172 if (itemCount > 0) {
Vighnesh Raut431494a2023-01-19 20:03:22 +0530173 CarouselScope(carouselState = carouselState)
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530174 .content(if (activeItemIndex < itemCount) activeItemIndex else 0)
vinekumarb2612472023-01-27 22:31:37 +0530175 }
Vighnesh Raut2a623462022-09-14 10:35:36 +0530176 }
Vineet Kumarefa63502022-08-30 10:00:54 +0000177 this.carouselIndicator()
178 }
179}
180
Vineet Kumar01abb022023-01-14 14:59:09 +0530181@Composable
182private fun shouldPerformAutoScroll(focusState: FocusState?): Boolean {
183 val carouselIsFocused = focusState?.isFocused ?: false
184 val carouselHasFocus = focusState?.hasFocus ?: false
Vighnesh Raut799489f2023-01-11 19:38:16 +0530185 return !(carouselIsFocused || carouselHasFocus)
Vineet Kumar01abb022023-01-14 14:59:09 +0530186}
187
Aditya Arora7938e90c2022-11-17 13:32:20 +0530188@Suppress("IllegalExperimentalApiUsage")
189@OptIn(ExperimentalAnimationApi::class)
190private suspend fun AnimatedVisibilityScope.onAnimationCompletion(action: suspend () -> Unit) {
191 snapshotFlow { transition.currentState == transition.targetState }.first { it }
192 action.invoke()
193}
194
Vineet Kumar507211d2023-01-04 19:16:38 +0530195@OptIn(ExperimentalTvMaterial3Api::class)
Vineet Kumarefa63502022-08-30 10:00:54 +0000196@Composable
197private fun AutoScrollSideEffect(
Vighnesh Raut431494a2023-01-19 20:03:22 +0530198 autoScrollDurationMillis: Long,
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530199 itemCount: Int,
Vineet Kumarefa63502022-08-30 10:00:54 +0000200 carouselState: CarouselState,
Vineet Kumar01abb022023-01-14 14:59:09 +0530201 doAutoScroll: Boolean,
Vighnesh Raut2a623462022-09-14 10:35:36 +0530202 onAutoScrollChange: (isAutoScrollActive: Boolean) -> Unit = {},
Vineet Kumarefa63502022-08-30 10:00:54 +0000203) {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530204 // Needed to ensure that the code within LaunchedEffect receives updates to the itemCount.
205 val updatedItemCount by rememberUpdatedState(newValue = itemCount)
Vighnesh Raut2a623462022-09-14 10:35:36 +0530206 if (doAutoScroll) {
Vineet Kumarefa63502022-08-30 10:00:54 +0000207 LaunchedEffect(carouselState) {
208 while (true) {
209 yield()
Vighnesh Raut431494a2023-01-19 20:03:22 +0530210 delay(autoScrollDurationMillis)
Vineet Kumarefa63502022-08-30 10:00:54 +0000211 if (carouselState.activePauseHandlesCount > 0) {
212 snapshotFlow { carouselState.activePauseHandlesCount }
213 .first { pauseHandleCount -> pauseHandleCount == 0 }
214 }
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530215 carouselState.moveToNextItem(updatedItemCount)
Vineet Kumarefa63502022-08-30 10:00:54 +0000216 }
217 }
218 }
Vighnesh Raut2a623462022-09-14 10:35:36 +0530219 onAutoScrollChange(doAutoScroll)
Vineet Kumarefa63502022-08-30 10:00:54 +0000220}
221
Aditya Arora7938e90c2022-11-17 13:32:20 +0530222@Suppress("IllegalExperimentalApiUsage")
Vineet Kumar507211d2023-01-04 19:16:38 +0530223@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
Vighnesh Raut799489f2023-01-11 19:38:16 +0530224private fun Modifier.handleKeyEvents(
Aditya Arora7938e90c2022-11-17 13:32:20 +0530225 carouselState: CarouselState,
Vighnesh Raut799489f2023-01-11 19:38:16 +0530226 outerBoxFocusRequester: FocusRequester,
227 focusManager: FocusManager,
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530228 itemCount: Int,
Aditya Arora7938e90c2022-11-17 13:32:20 +0530229 isLtr: Boolean
Vighnesh Raut799489f2023-01-11 19:38:16 +0530230): Modifier = onKeyEvent {
231 // Ignore KeyUp action type
232 if (it.type == KeyUp) {
233 return@onKeyEvent KeyEventPropagation.ContinuePropagation
234 }
Aditya Arora7938e90c2022-11-17 13:32:20 +0530235
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530236 val showPreviousItemAndGetKeyEventPropagation = {
237 if (carouselState.isFirstItem()) {
Vighnesh Raut799489f2023-01-11 19:38:16 +0530238 KeyEventPropagation.ContinuePropagation
239 } else {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530240 carouselState.moveToPreviousItem(itemCount)
Vighnesh Raut799489f2023-01-11 19:38:16 +0530241 outerBoxFocusRequester.requestFocus()
242 KeyEventPropagation.StopPropagation
Aditya Arora7938e90c2022-11-17 13:32:20 +0530243 }
244 }
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530245 val showNextItemAndGetKeyEventPropagation = {
246 if (carouselState.isLastItem(itemCount)) {
Vighnesh Raut799489f2023-01-11 19:38:16 +0530247 KeyEventPropagation.ContinuePropagation
248 } else {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530249 carouselState.moveToNextItem(itemCount)
Vighnesh Raut799489f2023-01-11 19:38:16 +0530250 outerBoxFocusRequester.requestFocus()
251 KeyEventPropagation.StopPropagation
252 }
253 }
254
255 when (it.key.nativeKeyCode) {
256 KEYCODE_BACK -> {
257 focusManager.moveFocus(FocusDirection.Exit)
258 KeyEventPropagation.ContinuePropagation
259 }
260
Aditya Arorab2cf37e2023-01-17 15:49:42 +0530261 KEYCODE_DPAD_LEFT -> {
262 // Ignore long press key event for manual scrolling
263 if (it.nativeKeyEvent.repeatCount > 0) {
264 return@onKeyEvent KeyEventPropagation.StopPropagation
265 }
266
267 if (isLtr) {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530268 showPreviousItemAndGetKeyEventPropagation()
Aditya Arorab2cf37e2023-01-17 15:49:42 +0530269 } else {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530270 showNextItemAndGetKeyEventPropagation()
Aditya Arorab2cf37e2023-01-17 15:49:42 +0530271 }
Vighnesh Raut799489f2023-01-11 19:38:16 +0530272 }
273
Aditya Arorab2cf37e2023-01-17 15:49:42 +0530274 KEYCODE_DPAD_RIGHT -> {
275 // Ignore long press key event for manual scrolling
276 if (it.nativeKeyEvent.repeatCount > 0) {
277 return@onKeyEvent KeyEventPropagation.StopPropagation
278 }
279
280 if (isLtr) {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530281 showNextItemAndGetKeyEventPropagation()
Aditya Arorab2cf37e2023-01-17 15:49:42 +0530282 } else {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530283 showPreviousItemAndGetKeyEventPropagation()
Aditya Arorab2cf37e2023-01-17 15:49:42 +0530284 }
Vighnesh Raut799489f2023-01-11 19:38:16 +0530285 }
286
287 else -> KeyEventPropagation.ContinuePropagation
288 }
289}
Aditya Arora7938e90c2022-11-17 13:32:20 +0530290
Vineet Kumar507211d2023-01-04 19:16:38 +0530291@OptIn(ExperimentalTvMaterial3Api::class)
Vineet Kumarefa63502022-08-30 10:00:54 +0000292@Composable
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530293private fun CarouselStateUpdater(carouselState: CarouselState, itemCount: Int) {
294 LaunchedEffect(carouselState, itemCount) {
295 if (itemCount != 0) {
296 carouselState.activeItemIndex = floorMod(carouselState.activeItemIndex, itemCount)
Vineet Kumarefa63502022-08-30 10:00:54 +0000297 }
298 }
299}
300
301/**
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530302 * State of the Carousel which allows the user to specify the first item that is shown when the
Vineet Kumarefa63502022-08-30 10:00:54 +0000303 * Carousel is instantiated in the constructor.
304 *
305 * It also provides the user with support to pause and resume the auto-scroll behaviour of the
306 * Carousel.
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530307 * @param initialActiveItemIndex the index of the first active item
Vineet Kumarefa63502022-08-30 10:00:54 +0000308 */
309@Stable
Vineet Kumar507211d2023-01-04 19:16:38 +0530310@ExperimentalTvMaterial3Api
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530311class CarouselState(initialActiveItemIndex: Int = 0) {
Vineet Kumarefa63502022-08-30 10:00:54 +0000312 internal var activePauseHandlesCount by mutableStateOf(0)
313
314 /**
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530315 * The index of the item that is currently displayed by the carousel
Vineet Kumarefa63502022-08-30 10:00:54 +0000316 */
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530317 var activeItemIndex by mutableStateOf(initialActiveItemIndex)
Vineet Kumarefa63502022-08-30 10:00:54 +0000318 internal set
319
320 /**
Vighnesh Raut431494a2023-01-19 20:03:22 +0530321 * Tracks whether we are scrolling backward in the Carousel. By default, we are moving forward
322 * because of auto-scroll
323 */
324 internal var isMovingBackward = false
325 private set
326
327 /**
Vineet Kumarefa63502022-08-30 10:00:54 +0000328 * Pauses the auto-scrolling behaviour of Carousel.
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530329 * The pause request is ignored if [itemIndex] is not the current item that is visible.
Vineet Kumarefa63502022-08-30 10:00:54 +0000330 * Returns a [ScrollPauseHandle] that can be used to resume
331 */
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530332 fun pauseAutoScroll(itemIndex: Int): ScrollPauseHandle {
333 if (this.activeItemIndex != itemIndex) {
Vineet Kumarefa63502022-08-30 10:00:54 +0000334 return NoOpScrollPauseHandle
335 }
336 return ScrollPauseHandleImpl(this)
337 }
338
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530339 internal fun isFirstItem() = activeItemIndex == 0
Vighnesh Raut2a623462022-09-14 10:35:36 +0530340
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530341 internal fun isLastItem(itemCount: Int) = activeItemIndex == itemCount - 1
Vighnesh Raut2a623462022-09-14 10:35:36 +0530342
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530343 internal fun moveToPreviousItem(itemCount: Int) {
344 // No items available for carousel
345 if (itemCount == 0) return
Vighnesh Raut2a623462022-09-14 10:35:36 +0530346
Vighnesh Raut431494a2023-01-19 20:03:22 +0530347 isMovingBackward = true
348
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530349 // Go to previous item
350 activeItemIndex = floorMod(activeItemIndex - 1, itemCount)
Vighnesh Raut2a623462022-09-14 10:35:36 +0530351 }
352
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530353 internal fun moveToNextItem(itemCount: Int) {
354 // No items available for carousel
355 if (itemCount == 0) return
Vighnesh Raut2a623462022-09-14 10:35:36 +0530356
Vighnesh Raut431494a2023-01-19 20:03:22 +0530357 isMovingBackward = false
358
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530359 // Go to next item
360 activeItemIndex = floorMod(activeItemIndex + 1, itemCount)
Vineet Kumarefa63502022-08-30 10:00:54 +0000361 }
362}
363
Vineet Kumar507211d2023-01-04 19:16:38 +0530364@ExperimentalTvMaterial3Api
Vineet Kumarefa63502022-08-30 10:00:54 +0000365/**
366 * Handle returned by [CarouselState.pauseAutoScroll] that can be used to resume auto-scroll.
367 */
368sealed interface ScrollPauseHandle {
369 /**
370 * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
371 */
372 fun resumeAutoScroll()
373}
374
Vineet Kumar507211d2023-01-04 19:16:38 +0530375@OptIn(ExperimentalTvMaterial3Api::class)
Vineet Kumarefa63502022-08-30 10:00:54 +0000376internal object NoOpScrollPauseHandle : ScrollPauseHandle {
377 /**
378 * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
379 */
380 override fun resumeAutoScroll() {}
381}
382
Vineet Kumar507211d2023-01-04 19:16:38 +0530383@OptIn(ExperimentalTvMaterial3Api::class)
Vineet Kumarefa63502022-08-30 10:00:54 +0000384internal class ScrollPauseHandleImpl(private val carouselState: CarouselState) : ScrollPauseHandle {
385 private var active by mutableStateOf(true)
Vighnesh Raut799489f2023-01-11 19:38:16 +0530386
Vineet Kumarefa63502022-08-30 10:00:54 +0000387 init {
388 carouselState.activePauseHandlesCount += 1
389 }
Vighnesh Raut799489f2023-01-11 19:38:16 +0530390
Vineet Kumarefa63502022-08-30 10:00:54 +0000391 /**
392 * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
393 */
394 override fun resumeAutoScroll() {
395 if (active) {
396 active = false
397 carouselState.activePauseHandlesCount -= 1
398 }
399 }
400}
401
Vineet Kumar507211d2023-01-04 19:16:38 +0530402@ExperimentalTvMaterial3Api
Vineet Kumarefa63502022-08-30 10:00:54 +0000403object CarouselDefaults {
404 /**
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530405 * Default time for which the item is visible to the user.
Vineet Kumarefa63502022-08-30 10:00:54 +0000406 */
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530407 const val TimeToDisplayItemMillis: Long = 5000
Vineet Kumarefa63502022-08-30 10:00:54 +0000408
409 /**
Vighnesh Raut431494a2023-01-19 20:03:22 +0530410 * Transition applied when bringing it into view and removing it from the view
Vineet Kumarefa63502022-08-30 10:00:54 +0000411 */
Vighnesh Raut431494a2023-01-19 20:03:22 +0530412 val contentTransform: ContentTransform
413 @Composable get() =
414 fadeIn(animationSpec = tween(100))
Doris Liu4b3a8312023-03-28 18:11:25 -0700415 .togetherWith(fadeOut(animationSpec = tween(100)))
Vineet Kumarefa63502022-08-30 10:00:54 +0000416
417 /**
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530418 * An indicator showing the position of the current active item among the items of the
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530419 * carousel.
Vineet Kumarefa63502022-08-30 10:00:54 +0000420 *
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530421 * @param itemCount total number of items in the carousel
422 * @param activeItemIndex the current active item index
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530423 * @param modifier Modifier applied to the indicators' container
424 * @param spacing spacing between the indicator dots
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530425 * @param indicator indicator dot representing each item in the carousel
Vineet Kumarefa63502022-08-30 10:00:54 +0000426 */
Vineet Kumar507211d2023-01-04 19:16:38 +0530427 @ExperimentalTvMaterial3Api
Vineet Kumarefa63502022-08-30 10:00:54 +0000428 @Composable
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530429 fun IndicatorRow(
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530430 itemCount: Int,
431 activeItemIndex: Int,
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530432 modifier: Modifier = Modifier,
433 spacing: Dp = 8.dp,
434 indicator: @Composable (isActive: Boolean) -> Unit = { isActive ->
435 val activeColor = Color.White
Vighnesh Raut431494a2023-01-19 20:03:22 +0530436 val inactiveColor = activeColor.copy(alpha = 0.3f)
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530437 Box(
438 modifier = Modifier
439 .size(8.dp)
440 .background(
441 color = if (isActive) activeColor else inactiveColor,
442 shape = CircleShape,
443 ),
444 )
445 }
Vineet Kumarefa63502022-08-30 10:00:54 +0000446 ) {
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530447 Row(
448 horizontalArrangement = Arrangement.spacedBy(spacing),
449 verticalAlignment = Alignment.CenterVertically,
450 modifier = modifier,
451 ) {
Vighnesh Raut2f9f7a02023-03-17 18:34:20 +0530452 repeat(itemCount) {
453 val isActive = it == activeItemIndex
Vighnesh Raut268af2a2022-12-13 16:52:41 +0530454 indicator(isActive = isActive)
Vineet Kumarefa63502022-08-30 10:00:54 +0000455 }
456 }
457 }
458}