简体   繁体   中英

How to create dot indicator (with color and size transiton) in Jetpack Compose

I want to have a horizontal dot indicator that has color transition between two dots that is scrolling and also dot's size transition while scrolling

I need to show only limited dots for a huge amount of items.

在此处输入图像描述

In view system, we used this library https://github.com/Tinkoff/ScrollingPagerIndicator , which is very smooth and has a very nice color and size transition effects.

I tried to implement it with scroll state rememberLazyListState() , but it is more complex than I thought.

Do you know any solution in Jetpack Compose?

Is it possible to use the current library with AndroidView? Because it needs XML view, recycler view and viewpager, I am wondering how is it possible to use it with AndroidView?

I could integrate this library https://github.com/Tinkoff/ScrollingPagerIndicator easily with compose.

@Composable
fun DotIndicator(scrollState: LazyListState, modifier: Modifier = Modifier) {
    AndroidViewBinding(
        modifier = modifier,
        factory = DotIndicatorBinding::inflate,
    ) {
        dotIndicator.setDotCount(scrollState.layoutInfo.totalItemsCount)
        dotIndicator.setCurrentPosition(scrollState.firstVisibleItemIndex)
        scrollState.layoutInfo.visibleItemsInfo.firstOrNull()?.size?.let { firstItemSize ->
            val firstItemOffset = scrollState.firstVisibleItemScrollOffset
            val offset = (firstItemOffset.toFloat() / firstItemSize.toFloat()).coerceIn(0f, 1f)
            dotIndicator.onPageScrolled(scrollState.firstVisibleItemIndex, offset)
        }
    }
}

For the integration I had to add XML file as well ->

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data />

    <ru.tinkoff.scrollingpagerindicator.ScrollingPagerIndicator
        android:id="@+id/dot_indicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:spi_dotColor="@color/ds_primary_25"
        app:spi_dotSelectedColor="?attr/colorPrimary"
        app:spi_dotSelectedSize="8dp"
        app:spi_dotSize="8dp"
        app:spi_dotSpacing="4dp" />

</layout>

And also adding this dependency to your Gradle file ->

api "androidx.compose.ui:ui-viewbinding:1.1.1"

I made a sample looks similar, logic for scaling is raw but it looks similar.

在此处输入图像描述

@OptIn(ExperimentalPagerApi::class)
@Composable
fun PagerIndicator(
    modifier: Modifier = Modifier,
    pagerState: PagerState,
    indicatorCount: Int = 5,
    indicatorSize: Dp = 16.dp,
    indicatorShape: Shape = CircleShape,
    space: Dp = 8.dp,
    activeColor: Color = Color(0xffEC407A),
    inActiveColor: Color = Color.LightGray,
    orientation: IndicatorOrientation = IndicatorOrientation.Horizontal,
    onClick: ((Int) -> Unit)? = null
) {

    val listState = rememberLazyListState()

    val totalWidth: Dp = indicatorSize * indicatorCount + space * (indicatorCount - 1)
    val widthInPx = LocalDensity.current.run { indicatorSize.toPx() }

    val currentItem by remember {
        derivedStateOf {
            pagerState.currentPage
        }
    }

    val itemCount = pagerState.pageCount

    LaunchedEffect(key1 = currentItem) {
        val viewportSize = listState.layoutInfo.viewportSize
        if (orientation == IndicatorOrientation.Horizontal) {
            listState.animateScrollToItem(
                currentItem,
                (widthInPx / 2 - viewportSize.width / 2).toInt()
            )
        } else {
            listState.animateScrollToItem(
                currentItem,
                (widthInPx / 2 - viewportSize.height / 2).toInt()
            )
        }

    }

    if (orientation == IndicatorOrientation.Horizontal) {
        LazyRow(
            modifier = modifier.width(totalWidth),
            state = listState,
            contentPadding = PaddingValues(vertical = space),
            horizontalArrangement = Arrangement.spacedBy(space),
            userScrollEnabled = false
        ) {
            indicatorItems(
                itemCount,
                currentItem,
                indicatorCount,
                indicatorShape,
                activeColor,
                inActiveColor,
                indicatorSize,
                onClick
            )
        }
    } else {
        LazyColumn(
            modifier = modifier.height(totalWidth),
            state = listState,
            contentPadding = PaddingValues(horizontal = space),
            verticalArrangement = Arrangement.spacedBy(space),
            userScrollEnabled = false
        ) {
            indicatorItems(
                itemCount,
                currentItem,
                indicatorCount,
                indicatorShape,
                activeColor,
                inActiveColor,
                indicatorSize,
                onClick
            )
        }
    }

}

private fun LazyListScope.indicatorItems(
    itemCount: Int,
    currentItem: Int,
    indicatorCount: Int,
    indicatorShape: Shape,
    activeColor: Color,
    inActiveColor: Color,
    indicatorSize: Dp,
    onClick: ((Int) -> Unit)?
) {
    items(itemCount) { index ->

        val isSelected = (index == currentItem)

        // Index of item in center when odd number of indicators are set
        // for 5 indicators this is 2nd indicator place
        val centerItemIndex = indicatorCount / 2

        val right1 =
            (currentItem < centerItemIndex &&
                    index >= indicatorCount - 1)

        val right2 =
            (currentItem >= centerItemIndex &&
                    index >= currentItem + centerItemIndex &&
                    index < itemCount - centerItemIndex + 1)
        val isRightEdgeItem = right1 || right2

        // Check if this item's distance to center item is smaller than half size of
        // the indicator count when current indicator at the center or
        // when we reach the end of list. End of the list only one item is on edge
        // with 10 items and 7 indicators
        // 7-3= 4th item can be the first valid left edge item and
        val isLeftEdgeItem =
            index <= currentItem - centerItemIndex &&
                    currentItem > centerItemIndex &&
                    index < itemCount - indicatorCount + 1

        Box(
            modifier = Modifier
                .graphicsLayer {
                    val scale = if (isSelected) {
                        1f
                    } else if (isLeftEdgeItem || isRightEdgeItem) {
                        .5f
                    } else {
                        .8f
                    }
                    scaleX = scale
                    scaleY = scale

                }

                .clip(indicatorShape)
                .size(indicatorSize)
                .background(
                    if (isSelected) activeColor else inActiveColor,
                    indicatorShape
                )
                .then(
                    if (onClick != null) {
                        Modifier
                            .clickable {
                                onClick.invoke(index)
                            }
                    } else Modifier
                )
        )
    }
}

enum class IndicatorOrientation {
    Horizontal, Vertical
}

Usage

@Composable
private fun PagerIndicatorSample() {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Spacer(Modifier.height(40.dp))
        val pagerState1 = rememberPagerState(initialPage = 0)
        val coroutineScope = rememberCoroutineScope()

        PagerIndicator(pagerState = pagerState1) {
            coroutineScope.launch {
                pagerState1.scrollToPage(it)
            }
        }

        HorizontalPager(
            count = 10,
            state = pagerState1,
        ) {
            Box(
                modifier = Modifier
                    .padding(10.dp)
                    .shadow(1.dp, RoundedCornerShape(8.dp))
                    .background(Color.White)
                    .fillMaxWidth()
                    .height(200.dp),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    "Text $it",
                    fontSize = 40.sp,
                    color = Color.Gray
                )
            }
        }

        val pagerState2 = rememberPagerState(initialPage = 0)

        PagerIndicator(
            pagerState = pagerState2,
            indicatorSize = 24.dp,
            indicatorCount = 7,
            activeColor = Color(0xffFFC107),
            inActiveColor = Color(0xffFFECB3),
            indicatorShape = CutCornerShape(10.dp)
        )
        HorizontalPager(
            count = 10,
            state = pagerState2,
        ) {
            Box(
                modifier = Modifier
                    .padding(10.dp)
                    .shadow(1.dp, RoundedCornerShape(8.dp))
                    .background(Color.White)
                    .fillMaxWidth()
                    .height(200.dp),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    "Text $it",
                    fontSize = 40.sp,
                    color = Color.Gray
                )
            }
        }

        Row(
            modifier = Modifier.fillMaxSize(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            val pagerState3 = rememberPagerState(initialPage = 0)

            Spacer(modifier = Modifier.width(10.dp))

            PagerIndicator(
                pagerState = pagerState3,
                orientation = IndicatorOrientation.Vertical
            )

            Spacer(modifier = Modifier.width(20.dp))
            VerticalPager(
                count = 10,
                state = pagerState3,
            ) {
                Box(
                    modifier = Modifier
                        .padding(10.dp)
                        .shadow(1.dp, RoundedCornerShape(8.dp))
                        .background(Color.White)
                        .fillMaxWidth()
                        .height(200.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        "Text $it",
                        fontSize = 40.sp,
                        color = Color.Gray
                    )
                }
            }
        }
    }
}

Need to convert from

listState.animateScrollToItem()

to

listState.animateScrollBy()

for smooth indicator change and moving with offset change from Pager.

and do some more methodical scale and color and offset calculating this one is only a temporary solution.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM