From d8fe4991f5a7720420bfffb4ba19e856e7624339 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Dec 2025 11:13:23 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../ui/components/pager/PagerIndicator.kt | 168 ++++++++++++++---- 1 file changed, 134 insertions(+), 34 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt index 8dc43f2d53..418cc7344d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.pager +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope @@ -8,18 +9,24 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.* +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import kotlin.math.abs +import kotlin.math.min + +// six - cause the central indicator has width multiplied twice +private const val TOTAL_MAX_INDICATORS = 6 +private const val SPACER_COUNT_BETWEEN_INDICATORS = 4 /** * Horizontal pager indicator @@ -29,82 +36,163 @@ import com.tangem.core.ui.res.TangemThemePreview */ @Composable fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) { + if (pagerState.pageCount == 0) return + val listState = rememberLazyListState() val indicatorColor = TangemTheme.colors.control.key val overlayColor = TangemTheme.colors.overlay.secondary - val indicatorSize = 8.dp + + val inactiveIndicatorColor = remember(indicatorColor) { + indicatorColor.copy(alpha = 0.5f) + } + + val baseIndicatorSize = 8.dp val spacing = 4.dp - val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1) - val widthInPx = LocalDensity.current.run { indicatorSize.toPx() } - - val currentItem by remember { + val indicatorState by remember(pagerState, indicatorCount) { derivedStateOf { - pagerState.currentPage + val count = pagerState.pageCount + val current = pagerState.currentPage + + val winSize = min(indicatorCount, count) + val centerPosition = winSize / 2 + + val start = when { + count <= winSize -> 0 + current <= centerPosition -> 0 + current >= count - centerPosition - 1 -> count - winSize + else -> current - centerPosition + } + Triple(count, winSize, start) } } - val itemCount = pagerState.pageCount + val (itemCount, windowSize, windowStart) = indicatorState + val currentItem by remember { derivedStateOf { pagerState.currentPage } } - LaunchedEffect(key1 = currentItem) { - val viewportSize = listState.layoutInfo.viewportSize - listState.animateScrollToItem( - currentItem, - (widthInPx / 2 - viewportSize.width / 2).toInt(), - ) + LaunchedEffect(currentItem, windowStart) { + if (itemCount > windowSize) { + listState.animateScrollToItem(windowStart.coerceIn(0, itemCount - 1)) + } + } + + val maxContainerWidth = remember(baseIndicatorSize, spacing) { + baseIndicatorSize * TOTAL_MAX_INDICATORS + spacing * SPACER_COUNT_BETWEEN_INDICATORS } Box( modifier = modifier .height(32.dp) + .width(maxContainerWidth + 32.dp) .background( color = overlayColor, shape = CircleShape, ) - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(horizontal = 16.dp, vertical = 12.dp) + .clip(CircleShape), contentAlignment = Alignment.Center, ) { LazyRow( - modifier = Modifier - .width(totalWidth), + modifier = Modifier.wrapContentWidth(), state = listState, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(spacing), userScrollEnabled = false, ) { indicatorItems( itemCount = itemCount, currentItem = currentItem, - indicatorShape = CircleShape, activeColor = indicatorColor, - inActiveColor = indicatorColor.copy(alpha = 0.5f), - indicatorSize = indicatorSize, + inActiveColor = inactiveIndicatorColor, + baseSize = baseIndicatorSize, + windowSize = windowSize, + windowStart = windowStart, ) } } } +@Suppress("MagicNumber", "CyclomaticComplexMethod") +private fun calculateIndicatorHeight(position: Int, currentPosition: Int, baseSize: Dp, windowSize: Int): Dp { + val distance = abs(position - currentPosition) + val mediumSize = 6.dp + val smallSize = 4.dp + + if (windowSize < 5) { + return when { + distance <= 1 -> baseSize + distance == 2 -> mediumSize + else -> smallSize + } + } + + val isEdgeFocus = currentPosition == 0 || currentPosition == windowSize - 1 + val isNearEdgeFocus = currentPosition == 1 || currentPosition == windowSize - 2 + return when { + isEdgeFocus -> when { + distance <= 2 -> baseSize + distance == 3 -> mediumSize + else -> smallSize + } + isNearEdgeFocus -> when { + distance <= 1 -> baseSize + distance == 2 -> mediumSize + else -> smallSize + } + else -> when { + distance <= 1 -> baseSize + else -> mediumSize + } + } +} + @Suppress("LongParameterList") private fun LazyListScope.indicatorItems( itemCount: Int, currentItem: Int, - indicatorShape: Shape, activeColor: Color, inActiveColor: Color, - indicatorSize: Dp, + baseSize: Dp, + windowSize: Int, + windowStart: Int, ) { - items(itemCount) { index -> + val safeWindowSize = min(windowSize, itemCount) + if (safeWindowSize <= 0) return - val isSelected = index == currentItem + val windowEnd = windowStart + safeWindowSize + val currentPosInWindow = (currentItem - windowStart).coerceIn(0, safeWindowSize - 1) + + items(itemCount) { pageIndex -> + val isInWindow = pageIndex in windowStart until windowEnd + val positionInWindow = (pageIndex - windowStart).coerceIn(0, safeWindowSize - 1) + + val isSelected = pageIndex == currentItem + + val refinedHeight = if (isInWindow) { + calculateIndicatorHeight( + position = positionInWindow, + currentPosition = currentPosInWindow, + baseSize = baseSize, + windowSize = safeWindowSize, + ) + } else { + 0.dp + } + val targetWidth = if (isSelected) refinedHeight * 2 else refinedHeight + val targetShape = if (isSelected) RoundedCornerShape(16.dp) else CircleShape + val animatedWidth by animateDpAsState(targetValue = targetWidth, label = "width") + val animatedHeight by animateDpAsState(targetValue = refinedHeight, label = "height") Box( modifier = Modifier - .clip(indicatorShape) - .size(indicatorSize) + .padding(vertical = (baseSize - animatedHeight) / 2) + .clip(targetShape) + .width(animatedWidth) + .height(animatedHeight) .background( if (isSelected) activeColor else inActiveColor, - indicatorShape, + targetShape, ), ) } @@ -112,19 +200,31 @@ private fun LazyListScope.indicatorItems( @Preview(showBackground = true) @Composable -private fun PagerIndicatorPreviewFirstPage() { +private fun PagerIndicatorPreview() { TangemThemePreview { - Box( + Column( modifier = Modifier .background(TangemTheme.colors.background.primary) - .padding(), - contentAlignment = Alignment.Center, + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), ) { val pagerState = rememberPagerState( - initialPage = 0, + initialPage = 2, pageCount = { 10 }, ) PagerIndicator(pagerState = pagerState) + + val pagerState1 = rememberPagerState( + initialPage = 0, + pageCount = { 3 }, + ) + PagerIndicator(pagerState = pagerState1) + + val pagerState2 = rememberPagerState( + initialPage = 0, + pageCount = { 1 }, + ) + PagerIndicator(pagerState = pagerState2) } } } \ No newline at end of file