The state that can be used to control VerticalPager and HorizontalPager

Summary

Public constructors

PagerState(
    currentPage: Int,
    currentPageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float
)
Cmn

Public functions

suspend Unit
animateScrollToPage(
    page: Int,
    pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float,
    animationSpec: AnimationSpec<Float>
)

Scroll animate to a given page.

Cmn
open Float

Dispatch scroll delta in pixels avoiding all scroll related mechanisms.

Cmn
Float

An utility function to help to calculate a given page's offset.

Cmn
open suspend Unit
scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit)

Call this function to take control of scrolling and gain the ability to send scroll events via ScrollScope.scrollBy.

Cmn
suspend Unit
scrollToPage(
    page: Int,
    pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float
)

Scroll (jump immediately) to a given page.

Cmn
Unit
@ExperimentalFoundationApi
ScrollScope.updateCurrentPage(
    page: Int,
    pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float
)

Jump immediately to a given page with a given pageOffsetFraction inside a ScrollScope.

Cmn
Unit

Used to update targetPage during a programmatic scroll operation.

Cmn

Public properties

final Boolean

Whether this ScrollableState can scroll backward (consume a negative delta).

Cmn
final Boolean

Whether this ScrollableState can scroll forward (consume a positive delta).

Cmn
Int

The page that sits closest to the snapped position.

Cmn
Float

Indicates how far the current page is to the snapped position, this will vary from -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end of the layout).

Cmn
InteractionSource

InteractionSource that will be used to dispatch drag events when this list is being dragged.

Cmn
open Boolean

Whether this ScrollableState is currently scrolling by gesture, fling or programmatically or not.

Cmn
PagerLayoutInfo

A PagerLayoutInfo that contains useful information about the Pager's last layout pass.

Cmn
abstract Int

The total amount of pages present in this pager.

Cmn
Int

The page that is currently "settled".

Cmn
Int

The page this Pager intends to settle to.

Cmn

Public constructors

PagerState

PagerState(
    currentPage: Int = 0,
    currentPageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float = 0.0f
)
Parameters
currentPage: Int = 0

The initial page to be displayed

currentPageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float = 0.0f

The offset of the initial page with respect to the start of the layout.

Public functions

animateScrollToPage

suspend fun animateScrollToPage(
    page: Int,
    pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float = 0.0f,
    animationSpec: AnimationSpec<Float> = spring()
): Unit

Scroll animate to a given page. If the page is too far away from currentPage we will not compose all pages in the way. We will pre-jump to a nearer page, compose and animate the rest of the pages until page.

Please refer to the sample to learn how to use this API.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.rememberCoroutineScope

val state = rememberPagerState { 10 }
val animationScope = rememberCoroutineScope()
Column {
    HorizontalPager(
        modifier = Modifier.weight(0.7f),
        state = state
    ) { page ->
        Box(
            modifier = Modifier
                .padding(10.dp)
                .background(Color.Blue)
                .fillMaxWidth()
                .aspectRatio(1f),
            contentAlignment = Alignment.Center
        ) {
            Text(text = page.toString(), fontSize = 32.sp)
        }
    }

    Box(
        modifier = Modifier
            .weight(0.3f)
            .fillMaxWidth(), contentAlignment = Alignment.Center
    ) {
        Button(onClick = {
            animationScope.launch {
                state.animateScrollToPage(state.currentPage + 1)
            }
        }) {
            Text(text = "Next Page")
        }
    }
}
Parameters
page: Int

The destination page to scroll to

pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float = 0.0f

A fraction of the page size that indicates the offset the destination page will be offset from its snapped position.

animationSpec: AnimationSpec<Float> = spring()

An AnimationSpec to move between pages. We'll use a spring as the default animation.

dispatchRawDelta

open fun dispatchRawDelta(delta: Float): Float

Dispatch scroll delta in pixels avoiding all scroll related mechanisms.

NOTE: unlike scroll, dispatching any delta with this method won't trigger nested scroll, won't stop ongoing scroll/drag animation and will bypass scrolling of any priority. This method will also ignore reverseDirection and other parameters set in scrollable.

This method is used internally for nested scrolling dispatch and other low level operations, allowing implementers of ScrollableState influence the consumption as suits them. Manually dispatching delta via this method will likely result in a bad user experience, you must prefer scroll method over this one.

Parameters
delta: Float

amount of scroll dispatched in the nested scroll process

Returns
Float

the amount of delta consumed

getOffsetFractionForPage

fun getOffsetFractionForPage(page: Int): Float

An utility function to help to calculate a given page's offset. Since this is based off currentPageOffsetFraction the same concept applies: a fraction offset that represents how far page is from the settled position (represented by currentPage offset). The difference here is that currentPageOffsetFraction is a value between -0.5 and 0.5 and the value calculate by this function can be larger than these numbers if page is different than currentPage.

Parameters
page: Int

The page to calculate the offset from. This should be between 0 and pageCount.

Returns
Float

The offset of page with respect to currentPage.

scroll

open suspend fun scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit): Unit

Call this function to take control of scrolling and gain the ability to send scroll events via ScrollScope.scrollBy. All actions that change the logical scroll position must be performed within a scroll block (even if they don't call any other methods on this object) in order to guarantee that mutual exclusion is enforced.

If scroll is called from elsewhere with the scrollPriority higher or equal to ongoing scroll, ongoing scroll will be canceled.

scrollToPage

suspend fun scrollToPage(
    page: Int,
    pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float = 0.0f
): Unit

Scroll (jump immediately) to a given page.

Please refer to the sample to learn how to use this API.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.rememberCoroutineScope

val state = rememberPagerState { 10 }
val scrollScope = rememberCoroutineScope()
Column {
    HorizontalPager(
        modifier = Modifier.height(400.dp),
        state = state
    ) { page ->
        Box(
            modifier = Modifier
                .padding(10.dp)
                .background(Color.Blue)
                .fillMaxWidth()
                .aspectRatio(1f),
            contentAlignment = Alignment.Center
        ) {
            Text(text = page.toString(), fontSize = 32.sp)
        }
    }

    Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
        androidx.compose.material.Button(onClick = {
            scrollScope.launch {
                state.scrollToPage(state.currentPage + 1)
            }
        }) {
            Text(text = "Next Page")
        }
    }
}
Parameters
page: Int

The destination page to scroll to

pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float = 0.0f

A fraction of the page size that indicates the offset the destination page will be offset from its snapped position.

updateCurrentPage

@ExperimentalFoundationApi
fun ScrollScope.updateCurrentPage(
    page: Int,
    pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float = 0.0f
): Unit

Jump immediately to a given page with a given pageOffsetFraction inside a ScrollScope. Use this method to create custom animated scrolling experiences. This will update the value of currentPage and currentPageOffsetFraction immediately, but can only be used inside a ScrollScope, use scroll to gain access to a ScrollScope.

Please refer to the sample to learn how to use this API.

import androidx.compose.animation.core.animate
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.rememberCoroutineScope

suspend fun PagerState.customAnimateScrollToPage(page: Int) {
    val preJumpPosition = if (page > currentPage) {
        (page - 1).coerceAtLeast(0)
    } else {
        (page + 1).coerceAtMost(pageCount)
    }
    scroll {
        // Update the target page
        updateTargetPage(page)

        // pre-jump to 1 page before our target page
        updateCurrentPage(preJumpPosition, 0.0f)
        val targetPageDiff = page - currentPage
        val distance = targetPageDiff * layoutInfo.pageSize.toFloat()
        var previousValue = 0.0f
        animate(
            0f,
            distance,
        ) { currentValue, _ ->
            previousValue += scrollBy(currentValue - previousValue)
        }
    }
}

val state = rememberPagerState(initialPage = 5) { 10 }
val scope = rememberCoroutineScope()

Column {
    HorizontalPager(
        modifier = Modifier
            .fillMaxSize()
            .weight(0.9f),
        state = state
    ) { page ->
        Box(
            modifier = Modifier
                .padding(10.dp)
                .background(Color.Blue)
                .fillMaxWidth()
                .aspectRatio(1f),
            contentAlignment = Alignment.Center
        ) {
            Text(text = page.toString(), fontSize = 32.sp)
        }
    }

    Button(onClick = {
        scope.launch {
            state.customAnimateScrollToPage(1)
        }
    }) {
        Text(text = "Jump to Page 1")
    }
}
Parameters
page: Int

The destination page to scroll to

pageOffsetFraction: @FloatRange(from = -0.5, to = 0.5) Float = 0.0f

A fraction of the page size that indicates the offset the destination page will be offset from its snapped position.

updateTargetPage

@ExperimentalFoundationApi
fun ScrollScope.updateTargetPage(targetPage: Int): Unit

Used to update targetPage during a programmatic scroll operation. This can only be called inside a ScrollScope and should be called anytime a custom scroll (through scroll) is executed in order to correctly update targetPage. This will not move the pages and it's still the responsibility of the caller to call ScrollScope.scrollBy in order to actually get to targetPage. By the end of the scroll block, when the Pager is no longer scrolling targetPage will assume the value of currentPage.

Please refer to the sample to learn how to use this API.

import androidx.compose.animation.core.animate
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.rememberCoroutineScope

suspend fun PagerState.customAnimateScrollToPage(page: Int) {
    val preJumpPosition = if (page > currentPage) {
        (page - 1).coerceAtLeast(0)
    } else {
        (page + 1).coerceAtMost(pageCount)
    }
    scroll {
        // Update the target page
        updateTargetPage(page)

        // pre-jump to 1 page before our target page
        updateCurrentPage(preJumpPosition, 0.0f)
        val targetPageDiff = page - currentPage
        val distance = targetPageDiff * layoutInfo.pageSize.toFloat()
        var previousValue = 0.0f
        animate(
            0f,
            distance,
        ) { currentValue, _ ->
            previousValue += scrollBy(currentValue - previousValue)
        }
    }
}

val state = rememberPagerState(initialPage = 5) { 10 }
val scope = rememberCoroutineScope()

Column {
    HorizontalPager(
        modifier = Modifier
            .fillMaxSize()
            .weight(0.9f),
        state = state
    ) { page ->
        Box(
            modifier = Modifier
                .padding(10.dp)
                .background(Color.Blue)
                .fillMaxWidth()
                .aspectRatio(1f),
            contentAlignment = Alignment.Center
        ) {
            Text(text = page.toString(), fontSize = 32.sp)
        }
    }

    Button(onClick = {
        scope.launch {
            state.customAnimateScrollToPage(1)
        }
    }) {
        Text(text = "Jump to Page 1")
    }
}

Public properties

canScrollBackward

final val canScrollBackwardBoolean

Whether this ScrollableState can scroll backward (consume a negative delta). This is typically false if the scroll position is equal to its minimum value, and true otherwise.

Note that true here does not imply that delta will be consumed - the ScrollableState may decide not to handle the incoming delta (such as if it is already being scrolled separately). Additionally, for backwards compatibility with previous versions of ScrollableState this value defaults to true.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.ui.graphics.graphicsLayer

val state = rememberLazyListState()
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
    Icon(
        Icons.Filled.KeyboardArrowUp,
        null,
        Modifier.graphicsLayer {
            // Hide the icon if we cannot scroll backward (we are the start of the list)
            // We use graphicsLayer here to control the alpha so that we only redraw when this
            // value changes, instead of recomposing
            alpha = if (state.canScrollBackward) 1f else 0f
        },
        Color.Red
    )
    val items = (1..100).toList()
    LazyColumn(
        Modifier
            .weight(1f)
            .fillMaxWidth(), state
    ) {
        items(items) {
            Text("Item is $it")
        }
    }
    Icon(
        Icons.Filled.KeyboardArrowDown,
        null,
        Modifier.graphicsLayer {
            // Hide the icon if we cannot scroll forward (we are the end of the list)
            // We use graphicsLayer here to control the alpha so that we only redraw when this
            // value changes, instead of recomposing
            alpha = if (state.canScrollForward) 1f else 0f
        },
        Color.Red
    )
}

canScrollForward

final val canScrollForwardBoolean

Whether this ScrollableState can scroll forward (consume a positive delta). This is typically false if the scroll position is equal to its maximum value, and true otherwise.

Note that true here does not imply that delta will be consumed - the ScrollableState may decide not to handle the incoming delta (such as if it is already being scrolled separately). Additionally, for backwards compatibility with previous versions of ScrollableState this value defaults to true.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.ui.graphics.graphicsLayer

val state = rememberLazyListState()
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
    Icon(
        Icons.Filled.KeyboardArrowUp,
        null,
        Modifier.graphicsLayer {
            // Hide the icon if we cannot scroll backward (we are the start of the list)
            // We use graphicsLayer here to control the alpha so that we only redraw when this
            // value changes, instead of recomposing
            alpha = if (state.canScrollBackward) 1f else 0f
        },
        Color.Red
    )
    val items = (1..100).toList()
    LazyColumn(
        Modifier
            .weight(1f)
            .fillMaxWidth(), state
    ) {
        items(items) {
            Text("Item is $it")
        }
    }
    Icon(
        Icons.Filled.KeyboardArrowDown,
        null,
        Modifier.graphicsLayer {
            // Hide the icon if we cannot scroll forward (we are the end of the list)
            // We use graphicsLayer here to control the alpha so that we only redraw when this
            // value changes, instead of recomposing
            alpha = if (state.canScrollForward) 1f else 0f
        },
        Color.Red
    )
}

currentPage

val currentPageInt

The page that sits closest to the snapped position. This is an observable value and will change as the pager scrolls either by gesture or animation.

Please refer to the sample to learn how to use this API.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Text

val pagerState = rememberPagerState { 10 }
Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        modifier = Modifier.weight(0.9f),
        state = pagerState
    ) { page ->
        Box(
            modifier = Modifier
                .padding(10.dp)
                .background(Color.Blue)
                .fillMaxWidth()
                .aspectRatio(1f),
            contentAlignment = Alignment.Center
        ) {
            Text(text = page.toString(), fontSize = 32.sp)
        }
    }
    Column(
        modifier = Modifier
            .weight(0.1f)
            .fillMaxWidth()
    ) {
        Text(text = "Current Page: ${pagerState.currentPage}")
        Text(text = "Target Page: ${pagerState.targetPage}")
        Text(text = "Settled Page Offset: ${pagerState.settledPage}")
    }
}

currentPageOffsetFraction

val currentPageOffsetFractionFloat

Indicates how far the current page is to the snapped position, this will vary from -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end of the layout). This is 0.0 if the currentPage is in the snapped position. The value will flip once the current page changes.

This property is observable and shouldn't be used as is in a composable function due to potential performance issues. To use it in the composition, please consider using a derived state (e.g derivedStateOf) to only have recompositions when the derived value changes.

Please refer to the sample to learn how to use this API.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Text

val pagerState = rememberPagerState { 10 }
Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        modifier = Modifier.weight(0.9f),
        state = pagerState
    ) { page ->
        Box(
            modifier = Modifier
                .padding(10.dp)
                .background(Color.Blue)
                .fillMaxWidth()
                .aspectRatio(1f),
            contentAlignment = Alignment.Center
        ) {
            Text(text = page.toString(), fontSize = 32.sp)
        }
    }
    Column(
        modifier = Modifier
            .weight(0.1f)
            .fillMaxWidth()
    ) {
        Text(text = "Current Page: ${pagerState.currentPage}")
        Text(text = "Target Page: ${pagerState.targetPage}")
        Text(text = "Settled Page Offset: ${pagerState.settledPage}")
    }
}

interactionSource

val interactionSourceInteractionSource

InteractionSource that will be used to dispatch drag events when this list is being dragged. If you want to know whether the fling (or animated scroll) is in progress, use isScrollInProgress.

isScrollInProgress

open val isScrollInProgressBoolean

Whether this ScrollableState is currently scrolling by gesture, fling or programmatically or not.

layoutInfo

val layoutInfoPagerLayoutInfo

A PagerLayoutInfo that contains useful information about the Pager's last layout pass. For instance, you can query which pages are currently visible in the layout.

This property is observable and is updated after every scroll or remeasure. If you use it in the composable function it will be recomposed on every change causing potential performance issues including infinity recomposition loop. Therefore, avoid using it in the composition.

If you want to run some side effects like sending an analytics event or updating a state based on this value consider using "snapshotFlow":

import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow

val pagerState = rememberPagerState() { 10 }
LaunchedEffect(pagerState) {
    snapshotFlow { pagerState.layoutInfo.visiblePagesInfo.firstOrNull() }
        .collect {
            // use the new first visible page info
        }
}

pageCount

abstract val pageCountInt

The total amount of pages present in this pager. The source of this data should be observable.

settledPage

val settledPageInt

The page that is currently "settled". This is an animation/gesture unaware page in the sense that it will not be updated while the pages are being scrolled, but rather when the animation/scroll settles.

Please refer to the sample to learn how to use this API.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Text

val pagerState = rememberPagerState { 10 }
Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        modifier = Modifier.weight(0.9f),
        state = pagerState
    ) { page ->
        Box(
            modifier = Modifier
                .padding(10.dp)
                .background(Color.Blue)
                .fillMaxWidth()
                .aspectRatio(1f),
            contentAlignment = Alignment.Center
        ) {
            Text(text = page.toString(), fontSize = 32.sp)
        }
    }
    Column(
        modifier = Modifier
            .weight(0.1f)
            .fillMaxWidth()
    ) {
        Text(text = "Current Page: ${pagerState.currentPage}")
        Text(text = "Target Page: ${pagerState.targetPage}")
        Text(text = "Settled Page Offset: ${pagerState.settledPage}")
    }
}

targetPage

val targetPageInt

The page this Pager intends to settle to. During fling or animated scroll (from animateScrollToPage this will represent the page this pager intends to settle to. When no scroll is ongoing, this will be equal to currentPage.

Please refer to the sample to learn how to use this API.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Text

val pagerState = rememberPagerState { 10 }
Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        modifier = Modifier.weight(0.9f),
        state = pagerState
    ) { page ->
        Box(
            modifier = Modifier
                .padding(10.dp)
                .background(Color.Blue)
                .fillMaxWidth()
                .aspectRatio(1f),
            contentAlignment = Alignment.Center
        ) {
            Text(text = page.toString(), fontSize = 32.sp)
        }
    }
    Column(
        modifier = Modifier
            .weight(0.1f)
            .fillMaxWidth()
    ) {
        Text(text = "Current Page: ${pagerState.currentPage}")
        Text(text = "Target Page: ${pagerState.targetPage}")
        Text(text = "Settled Page Offset: ${pagerState.settledPage}")
    }
}