A Playful Chips Selector in Jetpack Compose
A short article on how to create a single or multi choice selector with playful animations.
Introduction
In this short article we’ll learn how to create a Chips selector, allowing users to select one or multiple options from a set. The animation below shows what the end result will be:
Creating the chip
The first thing we need for this component is the Chip composable, which shows a label with a rounded corners background. The background color animates when the chip is selected, and the border animates clockwise, starting at the 12 o’clock position.
Basic chip
Let’s start by implementing a basic static chip that just displays the label and the background win rounded corners:
- The composable receives the label to display in the chip.
- We provide as well a boolean to indicate whether the chip is selected. The Chip composable will be stateless and will delegate (hoist) the state to its parent.
- When clicked, we forward the click event to the parent to update the state.
- As is customary, we provide a default Modifier to allow customizing the composable.
- The background color is determined based on whether the chip is selected or not.
- Likewise for the text color.
- We use a Box to draw the background, clipping to a rounded corners rectangle.
- And finally we draw the label within the box.
If we preview this composable, showing 2 chips side by side, one selected and one not, we get the following result
Adding the border
Next we want to draw the border. The border will be animated as we saw in the preview at the beginning of this article, so we can’t use the border extension function on Modifier for this and we will instead have to draw the border manually. For this, we will have to define a Path, starting at the 12 o’clock position and going around the Chip clockwise. For this exercise we will simplify things a bit and work on the assumption that the chip is wider than taller, so that the corner radius is determined by the height of the chip. The steps we need to draw the path are detalied below:
- Move to the 12 o’clock position
- Draw a line to the top right corner, offset by the corner radius
- Draw the top half of the corner using a quadratic curve
- Draw the bottom half of the right corner using a quadratic curve
- Move to the bottom left corner, offset by the corner radius
- Draw the bottom half of the left corner using a quadratic curve
- Draw the top half of the left corner using a quadratic curve
- Close the path
Now that we understand how to build the path for the border, let’s see the code:
These are the changes we’ve added:
- We define the color for the border (needs to be done here as we can’t access it outside of composition)
- We create a Path object.
- The border with will be 2 dp wide.
- We removed the clip and background Modifiers as we use
drawWithCache
. Using this modifier means that we don’t need to recompute the Path every time we draw. - Inside the
drawWithCache
block we have access to the size of the canvas, and we use that to determine the corner radius. - We build the Path following the steps outlined earlier.
- We will draw the background and border behind the content.
- First we draw the background, using the Path and a style of Fill.
- Finally, if the chip is selected, we draw the border by redrawing the path but using a style of Stroke with the color and width specified earlier.
This is the result:
Animating the border
Now that we have the border drawn, it’s time to animate it. The way we are going to do this is by using the PathMeasure class. This class allows us to measure the length of a path, and get a segment of that path. With that, we can animate the path segment from 0 to the full length, using a compose animation.
We will start by animating just the path, but because we want to animate other properties (the text and background colors, and the text alpha), we will use updateTransition to coordinate the animations.
Let’s see how we can animate the path of the chip based on its selected state:
Here are the changes to animate the border:
- We define, and remember a
pathMeasure
and apathSegment
that will measure the path and containe the segment to draw respectively. - We create a transition to coordinate the animations.
- Based on this transition, we create the animation for the path, which represents the fraction of the path that is visible, between 0f for an unselected chip and 1f for a selected chip.
- Once we’ve built the path we measure it.
- We reset the path segment as we will use it at each animation frame.
- We get a segment from the original path, based on the value of the animation.
- We have to draw the border all the time now, not only for selected chips, as we need to animate the path in and out. So here we draw the path segment using the border color.
With these changes we get this result:
Animating the background
Next we will animate the background. This is straightforward, we have 2 colors for the background based on whether the chip is selected or not, so all we need to do is use a color animation to transition from one color to the other. Color animations are native supported in Jetpack Compose using the animateColor extension on the transition animation — let’s see the necessary changes below:
- The only change is adding the background color animation as part of the transition animation.
We now get this result, with a nice smooth transition between the 2 background colors:
The text color animation follows the same principle as the background animation, so I’ll skip it and move to the alpha animation of the text, as we want to deemphasize the text for the unselected chips.
For this we will create another float animation, similar to the one we used for the border, but in this case we want to animate from 1f (for a selected chip) to 0.6f (for an unselected chip). Let’s see what changes we need for this:
- We define the alpha animation based on the transition, going from 0.6 for an unselected chip to 1f for a selected cihp.
- We apply the alpha using a
graphicsLayer
. We could have used aModifier.alpha
here as well, but usinggraphicsLayer
is more performant for animated properties as this avoids recompositions as the alpha property changes
With this our Chip is complete, the result is shown below:.
Now that we have a chip, we’ll build on it to create a chip selector.
Creating the chip selector
We want the chip selector to work in 2 modes, single selection and multi selection. As has been described in other articles, a good approach for this kind of composable is to create a state holder class that is responsible for the business logic of the composable, so we’ll do the same here. We’ll start by defining what our composable needs to be provided and what it will expose:
- the composable need to receive the list of chips to display
- it will also receive a list of pre-selected chips
- the selection mode, either single selection or multi selection
Clients of the composable will be able to observe changes to the select list by simply holding a reference to the state class. Let’s start by defining the API for the state holder class:
- This is the enum that determines if the selector works in single or multi selection mode.
- Here we declare the interface for the state holder.
- We expose the list of chips to display.
- And, as well, the list of selected chips.
- We need a means to update the list of selected chips when a chip is clicked, so we have a method to do so.
- And here we have a utility method that will let us know if a specific chip has been selected.
Now that we have the API defined, we need to create an implementation. Let’s see how we can do so:
- The implementation receives the list of chips to display.
- As well as the list of pre-selected chips.
- And the selection mode.
- We override the
selectedChips
property using amutableStateOf
, so that it’s observable. - When a chip is selected, in multi-mode we toggle it and, in single mode, we select it and deselect the previously selected chip.
- The
isSelected
method is a simple utility that lets us know if a given chip is selected. - We also provide a saver as part of this class so that state can be saved and restored on configuration change.
- As is customary, we provide a remember method that instantiates the state and, in this case, performs some sanity check to ensure the provided values are valid.
There is something that I’d like to point out at this state. This implementation works on the assumption that chips are unique — if your scenario requires providing duplicated chips, then the implementation would need to change to keep track of the indices as well as the cihps, so that for any given chip we can discern which of the duplicates the user clicked on.
Now that we have the state in place, creating the chip selector is trivial, let’s see the code:
- The composable receives the state we defined earlier.
- As is the norm, we have a default
Modifier
to offer configuration options to callers. - We let callers also configure how the chips are arranged within their container.
- We display the chips in a
FlowRow
, so that we overflow to the next line if required. - Finally we iterate over the chips and add a Chip, using the state to handle the chip selection.
And with this the implementation is complete. There are a few nieceties that we can add at this stage, like allowing callers to set colors and the border width; these improvements are available on the final gist here.