diff --git a/Core2/Onboarding/src/main/java/com/infomaniak/library/onboarding/HorizontalPagerIndicator.kt b/Core2/Onboarding/src/main/java/com/infomaniak/library/onboarding/HorizontalPagerIndicator.kt index 282e5dc0b..3dc54f4d0 100644 --- a/Core2/Onboarding/src/main/java/com/infomaniak/library/onboarding/HorizontalPagerIndicator.kt +++ b/Core2/Onboarding/src/main/java/com/infomaniak/library/onboarding/HorizontalPagerIndicator.kt @@ -26,34 +26,110 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp +import kotlin.math.abs -// TODO: Simple placeholder code for now. Will be styled correctly in the next PR @Composable -fun HorizontalPagerIndicator(modifier: Modifier = Modifier, pagerState: PagerState) { - Row( - modifier - .wrapContentHeight() - .fillMaxWidth() - .padding(bottom = 8.dp), - horizontalArrangement = Arrangement.Center - ) { - repeat(pagerState.pageCount) { iteration -> - val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray +fun HorizontalPagerIndicator( + modifier: Modifier = Modifier, + pagerState: PagerState, + indicatorStyle: IndicatorStyle, +) { + Row(modifier, horizontalArrangement = Arrangement.Center) { + repeat(pagerState.pageCount) { index -> + val (indicatorWidth, indicatorColor: Color) = computeIndicatorProperties(index, pagerState, indicatorStyle) + Box( modifier = Modifier - .padding(2.dp) .clip(CircleShape) - .background(color) - .size(16.dp) + .background(indicatorColor) + .size(height = indicatorStyle.inactiveSize, width = indicatorWidth) ) + + if (index < pagerState.pageCount - 1) Spacer(modifier = Modifier.width(indicatorStyle.indicatorSpacing)) + } + } +} + +private fun computeIndicatorProperties( + index: Int, + pagerState: PagerState, + indicatorStyle: IndicatorStyle, +): Pair = with(indicatorStyle) { + val (extendedCurrentPageOffsetFraction, pageVisibilityProgress) = computePageProgresses(index, pagerState) + + val indicatorWidth = lerp(inactiveSize, activeWidth, pageVisibilityProgress) + + val isTransitioningToSelected = extendedCurrentPageOffsetFraction < 0 + val indicatorColor: Color = when { + index == pagerState.currentPage && isTransitioningToSelected -> { + lerp(inactiveColor, activeColor, pageVisibilityProgress) + } + index == pagerState.currentPage + 1 && isTransitioningToSelected -> { + lerp(inactiveColor, activeColor, pageVisibilityProgress) } + index <= pagerState.currentPage -> activeColor + else -> inactiveColor } + + return indicatorWidth to indicatorColor } +private fun computePageProgresses(index: Int, pagerState: PagerState): Pair { + // Extended offset fraction of the current page relative to the screen. It's as if pagerState.currentPageOffsetFraction went + // beyond -0.5 up to -1 and beyond 0.5 up to 1 + // + // Range: [-1, 1] + // 0: Page is centered on the screen + // -1: Page is completely off-screen to the left + // 1: Page is completely off-screen to the right + val extendedCurrentPageOffsetFraction = when { + // Page is one step behind the current page and partially visible on the left + index == pagerState.currentPage - 1 && pagerState.currentPageOffsetFraction < 0 -> { + 1 + pagerState.currentPageOffsetFraction + } + // Current page itself (centered or partially moved) + index == pagerState.currentPage -> pagerState.currentPageOffsetFraction + // Page is one step ahead of the current page and partially visible on the right + index == pagerState.currentPage + 1 && pagerState.currentPageOffsetFraction >= 0 -> { + -1 + pagerState.currentPageOffsetFraction + } + // Completely off-screen + else -> 1f + } + + // Progress of the page visibility i.e. what fraction of the page is currently visible + // Range: [0, 1] + // 0: Page is completely off-screen + // 1: Page is fully centered on the screen and therefore fully visible + val pageVisibilityProgress = 1 - abs(extendedCurrentPageOffsetFraction) + + return extendedCurrentPageOffsetFraction to pageVisibilityProgress +} + +data class IndicatorStyle( + val inactiveColor: Color, + val activeColor: Color, + val inactiveSize: Dp, + val activeWidth: Dp, + val indicatorSpacing: Dp, +) + @Preview @Composable private fun Preview() { - HorizontalPagerIndicator(pagerState = rememberPagerState { 3 }) + HorizontalPagerIndicator( + pagerState = rememberPagerState(pageCount = { 3 }, initialPage = 1), + indicatorStyle = IndicatorStyle( + inactiveColor = Color.LightGray, + activeColor = Color.DarkGray, + inactiveSize = 8.dp, + activeWidth = 16.dp, + indicatorSpacing = 8.dp, + ), + ) } diff --git a/Core2/Onboarding/src/main/java/com/infomaniak/library/onboarding/OnboardingScaffold.kt b/Core2/Onboarding/src/main/java/com/infomaniak/library/onboarding/OnboardingScaffold.kt index 33ca57cd0..d14b0e5a8 100644 --- a/Core2/Onboarding/src/main/java/com/infomaniak/library/onboarding/OnboardingScaffold.kt +++ b/Core2/Onboarding/src/main/java/com/infomaniak/library/onboarding/OnboardingScaffold.kt @@ -43,6 +43,7 @@ fun OnboardingScaffold( pagerState: PagerState, onboardingPages: List, bottomContent: @Composable (PaddingValues) -> Unit, + indicatorStyle: IndicatorStyle, ) { Scaffold { paddingValues -> Column { @@ -61,8 +62,12 @@ fun OnboardingScaffold( } HorizontalPagerIndicator( - modifier = Modifier.padding(PaddingValues(start = startPadding, end = endPadding)), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .padding(PaddingValues(start = startPadding, end = endPadding)), pagerState = pagerState, + indicatorStyle = indicatorStyle, ) bottomContent( @@ -147,6 +152,13 @@ private fun Preview() { ) { Text("Bottom content") } - } + }, + indicatorStyle = IndicatorStyle( + inactiveColor = Color.LightGray, + activeColor = Color.DarkGray, + inactiveSize = 8.dp, + activeWidth = 16.dp, + indicatorSpacing = 8.dp, + ) ) } diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/onboarding/OnboardingScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/onboarding/OnboardingScreen.kt index b926cd102..b4325f0c2 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/onboarding/OnboardingScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.infomaniak.library.onboarding.IndicatorStyle import com.infomaniak.library.onboarding.OnboardingPage import com.infomaniak.library.onboarding.OnboardingScaffold import com.infomaniak.swisstransfer.R @@ -83,6 +84,13 @@ fun OnboardingScreen(goToMainActivity: () -> Unit) { goToNextPage = { coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } }, ) }, + indicatorStyle = IndicatorStyle( + inactiveColor = SwissTransferTheme.materialColors.outlineVariant, + activeColor = SwissTransferTheme.materialColors.primary, + inactiveSize = 8.dp, + activeWidth = 16.dp, + indicatorSpacing = Margin.Mini, + ) ) }