In this article we will find out how to write a
ViewPager composable in Jetpack Compose. Here’s what we want our composable to offer:
- The selected item will be centered on the container.
- The pager can scroll horizontally or vertically.
- Items can occupy a fraction of the container size, so that we can have neighbouring items peaking on the sides.
- It’s possible to overshoot items so that the user can scroll past the first or last item, but the pager then resets back to centering the item.
- We can specify a separation between items.
- We can indicate which item will be initially centered.
From our set of requirements above it is clear we will need to use the
Layout composable to create our
Pager, this is akin to creating a custom
ViewGroup in the view system — this will give us the flexibility to control the measuring and positioning of the children in the
The Pager signature
Now that we have a clear set of requirements, let’s define the signature of our
Pager composable function:
Let’s have a look at this in detail:
- Our composable Pager takes first a list of items, of type
T. Our pager will accept items of the same type; we will see later how we can modify this to accept heterogeneous types.
- It is a good practise for any composable to accept a
Modifierso that we can customize it at the composition site, so we do that here as well.
- Our Pager will scroll either horizontally or vertically, so we accept a parameter to indicate the scrolling orientation.
- We also allow callers to specify which item will be initially centered in the
itemFractionparameter indicates which fraction of the overall width or height of the container the items will take. This fraction defaults to 1, so items will be as wide (horizontal scroll) or as tall (vertical scroll) as the container if no value is provided.
- This parameter indicates, in
DPs, the space between items.
overshootFractionindicates, as a fraction of the parent width or height, how much the first and last items can be scrolled off position. A default value of 0.5f means the first and last items can be pushed so that they are half way off from their resting positions.
- This is a callback that will get called whenever an item becomes selected (in the central position). This will allow clients of the
Pagerto be notified of scroll events.
- The last parameter is the content factory, this will build the items for our
Pager— this factory will be called with the items in the list provided as the 1st argument and for each item we will receive a
Composablethat we will display in the
- Finally, we do some sanity check before we build our
Pager, we check that the fractions are within range and that the selected index is within bounds.
Setting up some helper methods
As we have to handle both horizontal and vertical scroll, we will need to check the orientation parameter during the composition process, and based on its value either get a width or a height. To make our code more readable we will extract this into extensions functions, so let’s define those now so that we can reference them later. These are the functions that we will use throughout the
This function returns the maximum dimension on our
Constraints based on our scroll orientation.
This function makes a copy of our
Constraints where the minimum dimension not in the scroll direction is set to 0, while on the scroll direction we set the minimum dimension to be equal to the maximum dimension. Note also that we are making use of the
itemFraction to constrain the
Pager items to a fraction of the container dimension in the scroll direction.
Our next helper function gets the size of our
Parcelables based on the scroll direction. If we are scrolling horizontally, then we want to know the maximum height of the
Pageritems; otherwise, if we are scrolling vertically, we want to calculate the maximum width.
Next we have an extension function on
VelocityTracker that will provide the calculated velocity based on our scroll axis.
Our final helper method, similar to the previous ones, calculates the pointer input change based on our scroll direction.
Creating the Pager state
Now that we have our
Pager signature defined and our helper functions it’s time to create our
Pager state. For simple composables we can keep state within the composable function itself, but when composables are a bit more complex, like this one, it is preferable to create a class to host the state and reference that class from within the composable. This state is
remembered in the composable, leveraging the
remember functions in Jetpack Compose.
Let’s see our state class:
Our state closely maps the arguments we pass to our
Pager composable. We will reference these elements during composition and animation, and as our
Pager composable needs to react to changes to them, we wrap them in
mutableStateOf. Worth of note is the
dragOffset that we will use to allow the user to drag the
Pager items and fling them; the
dragOffset will indicate, in pixels, how much the user has dragged the
When we create a state class it is also customary to create a
remember function that wraps our state into a
remember lambda, and call that from the composable. Let’s add this function and see how we are calling it from our composable:
This is fairly straightforward, I’ll just point out that, by convention,
Composable functions that return a value, like in this case, should not use Pascal case.
And finally let’s see how we call this from our composable:
So after the sanity checks we get an instance of our state and we initialize it with the arguments we receive. We are converting the item spacing from
DPs to pixels as all measurements take place in pixels; converting it here means we don’t have to do it multiple times later.
We also get our coroutine scope as we will need it for the dragging and flinging operations.
A small thing worth of note is that our state listener takes an index representing the position of the selected item, but the composable caller instead expects the item, so we map the index to an item when calling the listener.
Measuring and laying the content
Next we will see how we measure the items to display on the
Pager and how to position them, based on the parameters we receive and the current drag offset. For this we will use the
Layout composable; this composable takes 3 arguments — the content to display, a
Modifier and a lambda that does the actual measuring and laying of the content. Let’s look at our code and then we’ll break it down into these sections:
Let’s have a look at each block now, let’s start with the content:
Here we are using the content factory to generate the composables for each of the pages in our
Pager. We wrap each composable in a
Box so that we can ensure that the pager item will fill the parent, either horizontally (for horizontal scroll), or vertically (for vertical scroll). In case the content is not set to fill the parent, we use the
Alignment.Center to ensure the actual content is centered in the
Next the modifier:
There are 2 attributes here, first we clip the content to the bounds (so that if the user is dragging, the content does not draw outside the container box), and we apply an
inputModifier from our state — we have not seen that yet, but this is what will handle dragging and calculate the drag offset to display our
Pager items. For now, let’s just take it at face value that this will update the drag offset, we will see in a bit how this works.
Finally, let’s look at the measure block, this is where we measure and place the children in our
This section is a bit more complex, so let’s walk it in detail:
- We first get the size of our composable, using our helper method. Here we check the orientation and we get the max width or height from the
- Next we make a copy of our incoming
Constraintswith the min and max on the scroll direction set to the same value, and the min on the other direction set to 0.
- Now that we have our
Constraintsit’s time to measure our content, so we walk the
Placeables and measure them with the loose constraints.
- After we measure the
Placeables we check their max size (height for horizontal scroll, width for vertical scroll).
- Based on our
Constraints, we calculate how wide or tall the children will be, from the fraction we received as argument. We store this value in the state as it will be used for dragging.
- Now it’s time to lay out the items, we use the dimensions we just calculated.
- As we want to place the selected item in the center of the container, we calculate the offset an item will need to display centered.
- We create a couple of variables to access our drag offset, a float and an integer so that accessing these values is a bit more readable.
- We use the drag offset to calculate what will be the first and last visible items in the
Pager, this will allow us to optimize the rendering by only displaying those items that are visible.
- Now it’s time to place the items, we loop over the visible items, in the range
- For each item we calculate its position based on how much the user has dragged the content. There is a bit of math involved here, but basically the offset for an item is the size of the items preceding it (including the separator), minus the drag offset, plus the offset needed to center an item on the screen. It’s important to note here that whenever the
Pagerwill recompose, this is what we will use for dragging and flinging.
- Finally, once we have calculated our positioning coordinates, we place each item. If we are scrolling horizontally the horizontal (x) coordinate is our calculated offset and the vertical coordinate (y) is 0. Otherwise, if we are scrolling vertically, our horizontal coordinate is 0 and the vertical coordinate is our offset.
With this our
Pager is ready to display our content. If we were to run this (removing the state’s
inputModifier as it’s not yet defined) we would see the items displayed with the selected item centered and, if the item fraction is less than 1, the neighbouring items peeking out on the sides.
Our next steps is to add the logic to drag and fling the content. Let’s check that out.
Now that we have the measuring and laying out logic in place we need to add the necessary logic to handle dragging and flinging. We will do this in our state class:
Let’s have a detailed look at how we handle input events on the
- We define an animation spec for the fling behaviour, we use a
Springanimation that will slow down and come to a rest. We specify
LowBouncyso that there is a bit of wobble when coming to a rest, but not much, and a
StiffnessLowso that the
Pagerdoes not come to rest too soon.
- We create an extension function on
Modifierfor the input handling, the one we saw used in the
Layoutcomposable a bit earlier.
- Here we define a set of helper functions scoped to our modifier, this one calculates which item index is currently selected based on our dragoffset; whichever item is closest to the center is considered the selected item.
- The next helper function calculates the currently selected item and notifies the listener whenever this value changes.
- The final helper function calculates how much can we overshoot on drag based on the container size and the overshoot fraction. This will be used to restrict how far the first and last items in the
Pagercan be pushed off center.
- Here we start the input handling logic. This is a
suspendfunction that will suspend until a gesture is detected, at which point the lambda within is executed.
- Once a gesture is detected we install a pointer input block that awaits input events.
- Once a pointer input is detected, we initialize some variables that we will use to calculate the velocity and the overscroll range, then we await for the “pointer down” event.
- Once we are notified of a “pointer down” event, we create a drag handler that will update our drag offset based on the drag events. As we have to handle both horizontal and vertical scroll we define the handler here so that we can use it in both horizontal and vertical scenarios. The drag handler will also update our velocity tracker and recalculate the centered item in the
- Based on our orientation we listen for either horizontal or vertical drag events and process those in the handler we just discussed. As long as the pointer is down the handler will continue to be called for each drag event.
- Once the pointer event has finished the
verticalDragfunctions complete — this means the user has lifted the finger so it’s now time to calculate the velocity and fling the content. We use one of our extensions functions here which will give us the velocity on the scroll axis.
- The function to animate the fling is a
suspendfunction therefore we launch a coroutine so that we can call it.
- Here we calculate which item will be visible based on the fling direction and the velocity. As we always want the selected item to be centered, once we have the final drag offset based on the velocity and initial drag offset, we calculate which item is closest to that final position, then update the final drag offset so that it corresponds to that item being centered on the screen.
- Finally, when we know where to animate the drag offset to, we run the animation with our animation spec.
- As a last step, at each update of the animation we calculate which item is currently centered and update our listener accordingly.
With this we are almost done, there is just one last item to handle.
Selecting an initial item
One of our requirements was to be able to select which item would be initially selected in the
Pager. For that, we will add a helper method in our state to snap to that position:
In this method we calculate the drag offset for the item at the requested position, then set our drag offset to that value immediately, without animation, using the
The last step is to call this from our
Pager composable. Because this is something we only want to do once, on first composition, we will use an
Effect, specifically a
LaunchedEffect — these trigger upon initial composition, and whenever the keys change:
Because our initially selected index depends on the items and the index itself, we use those as keys for the effect.
We have now our
Pager fulfilling all the requirements we described a while back. The overall implementation is shown below:
This is how we can use it:
and this is the final result:
Using heterogeneous items
This implementation of the
Pager takes a factory that builds the composables that we will display in the pager. Let’s update our
Pager to instead accept a set of
Composables. The changes are fairly straightforward, only the
Pager itself needs to change, the state remains the same. Let’s see how we can update our
Pager to accommodate this scenario:
There are only some small changes here:
Pageris no longer typed.
- We’ve removed the
- The item selected listener now returns the index, instead of the item.
- We are no longer building the composables with a factory, simply using the provided lambda in our
With this update, we can call our pager like so:
where we alternate between a
Box composable and an
Image composable in our
There is however a lot of common code between the 2
Pager variants, so we can consolidate much of it by calling one
Pager from the other. We will refactor our original
Pager to call the new one:
So we have refactored our original
Pager by calling the new
Pager with the content being a lambda that calls our factory method. We also provide an item selected listener that translates the index into the corresponding item.
With this our work is complete, we have 2 different
Pager implementations that satisfy different needs.
The final code with the 2
Pagers is available on this gist.
Pager is not a full replacement for the
ViewPager in the view system, some of the functionality is missing, for instance item transformations and updates on scroll state (idle, settling, scrolling) to name a couple.
This implementation, while somewhat basic, handles the core pager functionality and offers some customization parameters that will hopefully satisfy a wide range of use cases.
With this article I intended to showcase how to build new components in Jetpack Compose and showcase how to handle animations and user input in those components, I hope you found that useful and I’ll see you on the next article.