Implementing custom pagination indicators in Jetpack Compose

Implementing custom pagination indicators in Jetpack Compose

In Jetpack Compose, we can create custom pagination indicators to enhance user experience. A paging indicator is an indicator used in an application to show the position of the current active page, usually in a sliding view or a ViewPager. Using Jetpack Compose, we can easily customize these indicators to suit the look and feel of our application. In this article, we will learn how to implement custom pagination indicators to improve user experience.

background

Pagination indicators play an important role in guiding users through multiple screens or pages in an application. While Jetpack Compose provides many built-in components, customizing pagination indicators to match your application's unique style and branding can enhance the user experience.

In this blog post, we'll explore how to create and implement custom pagination indicators in Jetpack Compose, allowing you to add a unique flair to your application's navigation.

What will we achieve in this blog?

Let's get started...
I've implemented most of the indicators using the Canvas API. Also, to show alternatives, I've also used built-in composables like Box.

Additionally, we will highlight the flexibility of pagination indicators and show how to implement them using unified logic.

Let's take a look at the calculation of common values ​​used in all indicators.

// To get scroll offset
val PagerState.pageOffset: Float
    get() = this.currentPage + this.currentPageOffsetFraction


// To get scrolled offset from snap position
fun PagerState.calculateCurrentOffsetForPage(page: Int): Float {
    
    
    return (currentPage - page) + currentPageOffsetFraction
}

This is a typical extension function for drawing indicators on the canvas. Now, let's start implementing cool indicators.

private fun DrawScope.drawIndicator(
    x: Float,
    y: Float,
    width: Float,
    height: Float,
    radius: CornerRadius
) {
    
    
    val rect = RoundRect(
        x,
        y - height / 2,
        x + width,
        y + height / 2,
        radius
    )
    val path = Path().apply {
    
     addRoundRect(rect) }
    drawPath(path = path, color = Color.White)
}

Extended Line/Point Indicator

To achieve the expand/collapse effect, we just need to animate the width of the indicator based on the page offset.

Canvas(modifier = Modifier.width(width = totalWidth)) {
    
    
        val spacing = circleSpacing.toPx()
        val dotWidth = width.toPx()
        val dotHeight = height.toPx()

        val activeDotWidth = activeLineWidth.toPx()
        var x = 0f
        val y = center.y
        
        repeat(count) {
    
     i ->
            val posOffset = pagerState.pageOffset
            val dotOffset = posOffset % 1
            val current = posOffset.toInt()

            val factor = (dotOffset * (activeDotWidth - dotWidth))

            val calculatedWidth = when {
    
    
                i == current -> activeDotWidth - factor
                i - 1 == current || (i == 0 && posOffset > count - 1) -> dotWidth + factor
                else -> dotWidth
            }

            drawIndicator(x, y, calculatedWidth, dotHeight, radius)
            x += calculatedWidth + spacing
        }
    }
  • The variable x is initialized to 0, representing the starting x coordinate for drawing the indicator.
  • The variable y is assigned the y-coordinate of the drawn indicator, calculated as the center y-coordinate of the canvas.
  • posOffset Indicates pagerState.pageOffsetthe paging offset obtained from the . Calculated using the modulo operator %1 - dotOffset, dotOffsetis posOffset the fractional part. currentThe integer part to be assigned posOffset .
    - factorDetermines the adjustment of the current position of the indicator width within the page.
  • Finally, update x to calculate the starting position of the next indicator.
    Below is the result.

sliding indicator

For this indicator we will use Boxthe combination. We just need to move the lines/dots horizontally when the page changes.

  • distanceis the width of the indicator plus the spacing

Let's see the results.

worm point indicator

For this indicator we will also use the Box combination. We just need to change the width of the current indicator to achieve the worm effect. Let's create a modifier for it.

@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.wormTransition(
    pagerState: PagerState
) =
    drawBehind {
    
    
        val distance = size.width + 10.dp.roundToPx()
        val scrollPosition = pagerState.currentPage + pagerState.currentPageOffsetFraction
        val wormOffset = (scrollPosition % 1) * 2

        val xPos = scrollPosition.toInt() * distance
        val head = xPos + distance * 0f.coerceAtLeast(wormOffset - 1)
        val tail = xPos + size.width + 1f.coerceAtMost(wormOffset) * distance

        val worm = RoundRect(
            head, 0f, tail, size.height,
            CornerRadius(50f)
        )

        val path = Path().apply {
    
     addRoundRect(worm) }
        drawPath(path = path, color = Color.White)
    }

Here we calculate the left and right position of the indicator and drawBehinddraw the path in the modifier.

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun WormIndicator(
    count: Int,
    pagerState: PagerState,
    modifier: Modifier = Modifier,
    spacing: Dp = 10.dp,
) {
    
    

    Box(
        modifier = modifier,
        contentAlignment = Alignment.CenterStart
    ) {
    
    
        Row(
            horizontalArrangement = Arrangement.spacedBy(spacing),
            modifier = modifier
                .height(48.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
    
    
            repeat(count) {
    
    
                Box(
                    modifier = Modifier
                        .size(20.dp)
                        .background(
                            color = Color.White,
                            shape = CircleShape
                        )
                )
            }
        }

        Box(
            Modifier
                .wormTransition(pagerState)
                .size(20.dp)
        )
    }
}

The result is as follows:

jump point indicator

The implementation of this indicator is very similar to the previous indicators.

Here we'll use graphicsLayermodifiers, changing the X position and scaling of the indicator to achieve something like this...,

Let's take a look jumpingDotTransition.

@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.jumpingDotTransition(pagerState: PagerState, jumpScale: Float) =
    graphicsLayer {
    
    
        val pageOffset = pagerState.currentPageOffsetFraction
        val scrollPosition = pagerState.currentPage + pageOffset
        translationX = scrollPosition * (size.width + 8.dp.roundToPx()) // 8.dp - spacing between dots

        val scale: Float
        val targetScale = jumpScale - 1f

        scale = if (pageOffset.absoluteValue < .5) {
    
    
            1.0f + (pageOffset.absoluteValue * 2) * targetScale;
        } else {
    
    
            jumpScale + ((1 - (pageOffset.absoluteValue * 2)) * targetScale);
        }

        scaleX = scale
        scaleY = scale
    }

Here, we calculate the current page offset ( pageOffset) and add it to pagerState the current page index ( currentPage). This provides the exact scroll position of the pager.

We then multiply that value by the size of the dot indicator ( size.width) plus an extra 8 dp (the spacing between dots), and assign the result to the graphics layer's translationXproperties. This transformation causes the dot indicator pagerto move horizontally when scrolling.

Next, we use an if-else condition to pageOffsetcalculate the scaling. If pageOffset the absolute value of is less than 0.5 (indicating that the point is centered on the screen), we interpolate the scaling linearly from 1.0f to the target scaling value. On the other hand, if pageOffset the absolute value of is greater than or equal to 0.5, we inversely interpolate so that the point smoothly transitions back to its original size.

Use this modifier as before wormTransition .

Box(
   Modifier
      .jumpingDotTransition(pagerState, 0.8f)
      .size(20.dp)
      .background(
         color = activeColor,
         shape = CircleShape,
      )
)

bounce point indicator

This indicator is very similar to the previous indicators. We'll use the same logic for scaling and translating the x value. Also, we'll change the Y position for the bouncing effect. Let's see how to do it. We compute a factor by multiplying pageOffset the absolute value of by . Math.PI This factor controls the vertical movement of the point during the bouncing motion.

Based on the relationship between current and settledPage , we determine the direction of the bounce. If current is greater than or equal to settledPage, jumpOffsetmultiply to compute a negative y-value. Otherwise, we multiply the sine function by half the distance to calculate a positive y-value. This pagercreates a bounce effect in the opposite direction when scrolling back to the previous page.

You can achieve this effect by using this modifier in the same way as the previous indicator.

exchange point indicator

Here we use Canvas composition to draw the point indicator. The width of the Canvas is calculated from the total number of indicators and their combined width and spacing. Within the Canvas scope,

posOffsetIndicates the current page position and fractional offset.
dotOffsetCaptures posOffsetthe fractional portion of the , representing the offset within the current page.
current Is posOffset the integer part, indicating the current page index.
moveXDetermines the horizontal position of the point indicator. It does calculations differently depending on the relationship between iand . current If i equals current, use posOffsetas position. If i - 1equal current, use i - dotOffset as position. Otherwise, use i as the position.
Let's see the output.

Show point indicator

We will see two different effects.

Effect #1
This effect renders a dot indicator that gradually appears and scales according to the pager state.

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RevealDotIndicator1(
    count: Int,
    pagerState: PagerState,
    activeColor: Color = Color.White,
) {
    
    
    val circleSpacing = 8.dp
    val circleSize = 20.dp
    val innerCircle = 14.dp

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp), contentAlignment = Alignment.Center
    ) {
    
    
        val width = (circleSize * count) + (circleSpacing * (count - 1))

        Canvas(modifier = Modifier.width(width = width)) {
    
    
            val distance = (circleSize + circleSpacing).toPx()
            val centerY = size.height / 2
            val startX = circleSpacing.toPx()

            repeat(count) {
    
    
                val pageOffset = pagerState.calculateCurrentOffsetForPage(it)

                val scale = 0.2f.coerceAtLeast(1 - pageOffset.absoluteValue)
                val outlineStroke = Stroke(2.dp.toPx())

                val x = startX + (it * distance)
                val circleCenter = Offset(x, centerY)
                val innerRadius = innerCircle.toPx() / 2
                val radius = (circleSize.toPx() * scale) / 2

                drawCircle(
                    color = activeColor,
                    style = outlineStroke,
                    center = circleCenter,
                    radius = radius
                )

                drawCircle(
                    color = activeColor,
                    center = circleCenter,
                    radius = innerRadius
                )
            }
        }
    }
}

scale The variable determines pageOffsetthe scaling of the point indicator based on the absolute value of and radius represents the scaling radius based on circleSizeand . scaleThis creates a scaling effect where the point indicator pager grows larger and reveals itself as the state changes, as shown in the image above.

Effect #2
In the above effect, we draw a circle with a Stroke style, and simply modify the effect on the basis of #1 to get another effect.

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RevealDotIndicator2(
    count: Int,
    pagerState: PagerState,
) {
    
    
    val circleSpacing = 8.dp
    val circleSize = 20.dp
    val innerCircle = 14.dp

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp), contentAlignment = Alignment.Center
    ) {
    
    
        Canvas(modifier = Modifier) {
    
    
            val distance = (circleSize + circleSpacing).toPx()

            val centerX = size.width / 2
            val centerY = size.height / 2

            val totalWidth = distance * count
            val startX = centerX - (totalWidth / 2) + (circleSize / 2).toPx()

            repeat(count) {
    
    
                val pageOffset = pagerState.calculateCurrentOffsetForPage(it)

                val alpha = 0.8f.coerceAtLeast(1 - pageOffset.absoluteValue)
                val scale = 1f.coerceAtMost(pageOffset.absoluteValue)

                val x = startX + (it * distance)
                val circleCenter = Offset(x, centerY)
                val radius = circleSize.toPx() / 2
                val innerRadius = (innerCircle.toPx() * scale) / 2

                drawCircle(
                    color = Color.White, center = circleCenter,
                    radius = radius, alpha = alpha,
                )

                drawCircle(color = Color(0xFFE77F82), center = circleCenter, radius = innerRadius)
            }
        }
    }
}

The result is as follows:

in conclusion

Hope this article gave you valuable insight and inspiration to try out custom pager indicators in your Jetpack Compose project.

Now, it's time to take your app navigation to the next level and impress your users.

Guess you like

Origin blog.csdn.net/u011897062/article/details/131909698