androidandroid-jetpack-composeviewpagerindicator

Pager indicator for compose


I am using Accompanist's paging indicator. It is working fine but it is not customizable. I want to set exact amount of dots should be visible in screen and those dots should be size configurable on scrolls. There is another question similar to this thread but owner already accepted AndroidView approach which I want it Composable way.


Solution

  • I made a sample looks similar, logic for scaling is raw but it looks similar. Need to convert from

    enter image description here

    @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,
        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
            listState.animateScrollToItem(
                currentItem,
                (widthInPx / 2 - viewportSize.width / 2).toInt()
            )
        }
    
    
        LazyRow(
            modifier = modifier.width(totalWidth),
            state = listState,
            contentPadding = PaddingValues(vertical = space),
            horizontalArrangement = Arrangement.spacedBy(space),
            userScrollEnabled = false
        ) {
    
            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
                        )
                )
            }
        }
    }
    

    Usage

    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 = 20.dp,
            indicatorCount = 7,
            activeColor = Color(0xff2196F3),
            inActiveColor = Color(0xffBBDEFB),
            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
                )
            }
        }
    }
    

    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