Skip to content

Commit

Permalink
ANDROID-14497 Implement auto play in Carousel (#342)
Browse files Browse the repository at this point in the history
* ANDROID-14497 Implement auto play in Carousel

* ANDROID-14497 auto play only when visible

* ANDROID-14497 remove test code

* ANDROID-14497 fix carousel indicator jump from last to first card

* ANDROID-14497 catalog settings for xml carousel

* ANDROID-14497 catalog settings for compose carousel

* ANDROID-14497 remove unneeded visibility logic

* ANDROID-14497 small revert

* ANDROID-14497 update readme

* ANDROID-14497 Use constants

* ANDROID-14497 Use more constants
  • Loading branch information
yamal-alm authored Apr 11, 2024
1 parent 3de2342 commit ae4939d
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.google.accompanist.pager.PagerState
import com.telefonica.mistica.card.mediacard.MediaCardView
import com.telefonica.mistica.carousel.CarouselView
import com.telefonica.mistica.catalog.R.drawable.card_image_sample
import com.telefonica.mistica.catalog.databinding.CarouselFragmentCatalogBinding
import com.telefonica.mistica.compose.carousel.CarouselState
import java.util.concurrent.TimeUnit

class CarouselFragment : Fragment() {

Expand All @@ -37,6 +40,25 @@ class CarouselFragment : Fragment() {
binding.carouselPageIndicatorView
.setState(carouselState)
.setPageCount(MEDIA_CARDS_CAROUSEL_SIZE)

binding.updateCarousel.setOnClickListener {
binding.carouselContainer.removeAllViews()
val carousel = CarouselView(requireContext(), null).apply {
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)
init(carouselState)
}
binding.carouselContainer.addView(carousel)
}
}

private fun CarouselView.init(carouselState: CarouselState) {
val autoPlaySpeedSeconds = binding.autoPlaySpeed.text.toString().toLongOrNull() ?: DEFAULT_AUTO_PLAY_SPEED_SECONDS
setContent(getMediaCardsForCarousel())
.setState(carouselState)
.setItemCount(MEDIA_CARDS_CAROUSEL_SIZE)
.setAutoPlay(binding.autoPlay.isChecked())
.setAutoPlaySpeed(TimeUnit.SECONDS.toMillis(autoPlaySpeedSeconds))
.setLoop(binding.loop.isChecked())
}

private fun getMediaCardsForCarousel(): List<MediaCardView> {
Expand All @@ -60,6 +82,7 @@ class CarouselFragment : Fragment() {

private companion object {
const val MEDIA_CARDS_CAROUSEL_SIZE = 6
const val DEFAULT_AUTO_PLAY_SPEED_SECONDS = 5L
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.telefonica.mistica.compose.theme.MisticaTheme
import com.telefonica.mistica.compose.theme.brand.Brand

class ComponentComposeFragment(private val theme: Brand, private val component: @Composable () -> Unit) : Fragment() {

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
Expand All @@ -18,19 +19,60 @@ import com.telefonica.mistica.compose.card.mediacard.MediaCardImage
import com.telefonica.mistica.compose.carousel.Carousel
import com.telefonica.mistica.compose.carousel.CarouselPagerIndicator
import com.telefonica.mistica.compose.carousel.rememberCarouselState
import com.telefonica.mistica.compose.input.CheckBoxInput
import com.telefonica.mistica.compose.input.TextInput
import com.telefonica.mistica.compose.tag.Tag
import com.telefonica.mistica.tag.TagView
import java.util.concurrent.TimeUnit

private const val ITEM_COUNT = 6
private const val DEFAULT_AUTO_PLAY_SPEED_SECONDS = 5L

@Composable
fun Carousels() {
val carouselState = rememberCarouselState()
val itemCount by remember { mutableStateOf(6)}
Column(
modifier = Modifier.fillMaxSize()
modifier = Modifier
.fillMaxSize()
) {
var autoPlay by remember {
mutableStateOf(false)
}
CheckBoxInput(
modifier = Modifier.padding(16.dp),
text = "Autoplay",
checked = autoPlay,
onCheckedChange = { autoPlay = it }
)
var loop by remember {
mutableStateOf(false)
}
CheckBoxInput(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp),
text = "Loop",
checked = loop,
onCheckedChange = { loop = it }
)

var autoPlaySpeed by remember {
mutableStateOf("")
}
TextInput(
modifier = Modifier.padding(horizontal = 16.dp),
value = autoPlaySpeed,
onValueChange = { autoPlaySpeed = it },
label = "Auto play speed in seconds"
)

Carousel(
itemCount = itemCount,
modifier = Modifier.padding(top = 8.dp),
itemCount = ITEM_COUNT,
carouselState = carouselState,
autoPlay = autoPlay,
autoPlaySpeed = TimeUnit.SECONDS.toMillis(autoPlaySpeed.toLongOrNull() ?: DEFAULT_AUTO_PLAY_SPEED_SECONDS),
loop = loop,
) { page ->
CarouselItem(page)
}
Expand All @@ -39,7 +81,7 @@ fun Carousels() {
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp),
pagerCount = itemCount,
pagerCount = ITEM_COUNT,
debug = true,
)
}
Expand Down
50 changes: 47 additions & 3 deletions catalog/src/main/res/layout/carousel_fragment_catalog.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
Expand All @@ -13,11 +14,54 @@
android:paddingBottom="40dp"
>

<com.telefonica.mistica.carousel.CarouselView
android:id="@+id/carousel_view"
<com.telefonica.mistica.input.CheckBoxInput
android:id="@+id/auto_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_gravity="start"
app:inputChecked="false"
app:inputCheckText="Autoplay"/>

<com.telefonica.mistica.input.CheckBoxInput
android:id="@+id/loop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_gravity="start"
app:inputChecked="false"
app:inputCheckText="Loop"/>

<com.telefonica.mistica.input.TextInput
android:id="@+id/auto_play_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
app:inputType="text"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:inputHint="Auto play speed in seconds"/>

<com.telefonica.mistica.button.Button
android:id="@+id/update_carousel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_gravity="start"
android:text="Update carousel"/>


<FrameLayout
android:id="@+id/carousel_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.telefonica.mistica.carousel.CarouselView
android:id="@+id/carousel_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</FrameLayout>

<com.telefonica.mistica.carousel.CarouselPageIndicatorView
android:id="@+id/carousel_page_indicator_view"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.telefonica.mistica.compose.carousel.Carousel
import com.telefonica.mistica.compose.carousel.CarouselState
import com.telefonica.mistica.compose.carousel.DEFAULT_AUTO_PLAY
import com.telefonica.mistica.compose.carousel.DEFAULT_AUTO_PLAY_SPEED_MILLIS
import com.telefonica.mistica.compose.carousel.DEFAULT_LOOP
import com.telefonica.mistica.compose.composeview.AbstractMisticaComposeView

class CarouselView @JvmOverloads constructor(
Expand All @@ -24,6 +27,9 @@ class CarouselView @JvmOverloads constructor(
private lateinit var carouselState: CarouselState

private var itemCount: Int = 0
private var autoPlay: Boolean = DEFAULT_AUTO_PLAY
private var autoPlaySpeed: Long = DEFAULT_AUTO_PLAY_SPEED_MILLIS
private var loop: Boolean = DEFAULT_LOOP

/**If composable is set it will render it by default*/
private var body: (@Composable (Int) -> Unit)? = null
Expand All @@ -35,6 +41,18 @@ class CarouselView @JvmOverloads constructor(
this.carouselState = carouselState
}

fun setAutoPlay(autoPlay: Boolean) = apply {
this.autoPlay = autoPlay
}

fun setLoop(loop: Boolean) = apply {
this.loop = loop
}

fun setAutoPlaySpeed(autoPlaySpeed: Long) = apply {
this.autoPlaySpeed = autoPlaySpeed
}

fun setItemCount(itemCount: Int): CarouselView = this.apply {
this.itemCount = itemCount
}
Expand All @@ -60,6 +78,9 @@ class CarouselView @JvmOverloads constructor(
Carousel(
itemCount = itemCount,
carouselState = carouselState,
autoPlay = autoPlay,
autoPlaySpeed = autoPlaySpeed,
loop = loop,
) { position ->
body?.let { it(position) } ?: data?.get(position)?.let { CarouselItem(it) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,98 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.telefonica.mistica.compose.util.VisibilityTracker
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

internal const val DEFAULT_AUTO_PLAY_SPEED_MILLIS = 5000L
internal const val DEFAULT_AUTO_PLAY = false
internal const val DEFAULT_LOOP = false

@OptIn(ExperimentalPagerApi::class)
@Composable
fun Carousel(
modifier: Modifier = Modifier,
carouselState: CarouselState = rememberCarouselState(),
contentPadding: PaddingValues = PaddingValuesWithStartAndEndMargin(carouselState, start = 16.dp, end = 16.dp),
itemCount: Int,
autoPlay: Boolean = DEFAULT_AUTO_PLAY,
autoPlaySpeed: Long = DEFAULT_AUTO_PLAY_SPEED_MILLIS,
loop: Boolean = DEFAULT_LOOP,
content: @Composable (page: Int) -> Unit,
) {
HorizontalPager(
state = carouselState.pagerState,
contentPadding = contentPadding,
count = itemCount,
) { page ->
val (start, end) = when (page) {
0 -> 16.dp to 4.dp
itemCount-1 -> 4.dp to 16.dp
else -> 4.dp to 4.dp
var isVisible by remember {
mutableStateOf(false)
}
VisibilityTracker(
onIsFullyVisible = { isVisible = it }
) {
AutoPlay(
carouselState = carouselState,
itemCount = itemCount,
autoPlay = autoPlay,
autoPlaySpeed = autoPlaySpeed,
loop = loop,
isVisible = isVisible
)
HorizontalPager(
modifier = modifier,
state = carouselState.pagerState,
contentPadding = contentPadding,
count = itemCount,
) { page ->
val (start, end) = when (page) {
0 -> 16.dp to 4.dp
itemCount - 1 -> 4.dp to 16.dp
else -> 4.dp to 4.dp
}
Box(
modifier = modifier.padding(start = start, end = end)
) {
content(page)
}
}
Box(
modifier = modifier.padding(start = start, end = end, top = 0.dp, bottom = 0.dp)
) {
content(page)
}
}

@Composable
private fun AutoPlay(
carouselState: CarouselState,
itemCount: Int,
autoPlay: Boolean,
autoPlaySpeed: Long,
loop: Boolean,
isVisible: Boolean,
) {
val scope = rememberCoroutineScope()
LaunchedEffect(isVisible, autoPlay, autoPlaySpeed, loop, carouselState.currentPage) {
if (autoPlay && isVisible) {
delay(autoPlaySpeed)

val nextPage = carouselState.currentPage + 1
if (nextPage < itemCount) {
scope.launch {
carouselState.pagerState.animateScrollToPage(nextPage)
}
} else if (loop) {
scope.launch {
carouselState.pagerState.animateScrollToPage(0)
}
}
}
}
}

@OptIn(ExperimentalPagerApi::class)
data class PaddingValuesWithStartAndEndMargin(
private val carouselState: CarouselState,
private val start: Dp = 4.dp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ internal fun calculateWindowPosition(
if (shouldMoveTheBulletToTheEdge) {
log("Moving to the edge - $movementDirection")
when (movementDirection) {
DECREASE -> visibleWindowState.currentSelected--
DECREASE -> visibleWindowState.currentSelected = currentSelected
INCREASE -> visibleWindowState.currentSelected++
NO_MOVEMENT -> {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ modifier: Modifier = Modifier,
carouselState: CarouselState = rememberCarouselState(),
contentPadding: PaddingValues = PaddingValuesWithStartAndEndMargin(carouselState, start = 16.dp, end = 16.dp),
itemCount: Int,
autoPlay: Boolean = false,
autoPlaySpeed: Long = 5000L,
loop: Boolean = false,
content: @Composable (page: Int) -> Unit,
```

Expand All @@ -42,6 +45,9 @@ content: @Composable (page: Int) -> Unit,

`content` is the composable shown inside the Carousel.

Set `autoPlay` to `true` (`false` by default) to make the carousel automatically swipe to the next card after `autoPlaySpeed` milliseconds (5000 by default).
When the last card is reached and `loop` is set to `true` (`false` by default), the carousel will return to the first card automatically. Note: Cards will only be automatically swiped when carousel is fully visible inside the current viewport.

## CarouselPagerIndicator composable
It has the next parameters:
```kotlin
Expand Down
Loading

0 comments on commit ae4939d

Please sign in to comment.