Murat
Murat

Reputation: 445

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.

Upvotes: 3

Views: 5635

Answers (1)

Thracian
Thracian

Reputation: 67139

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

Upvotes: 13

Related Questions