Creating a custom button with Jetpack Compose, including custom animations.
- Part 1 — Integrating a new tool in a production product.
- Part 2 — Designing your first composable.
- Part 3 — Creating a custom button with Jetpack Compose.
In this part of our series on introducing Jetpack Compose into an existing project, we will create a custom button we often use around our app at Lemonade
First, let’s have a look again at the screen we are building.
We already covered how to build the “None of the above” text button on the bottom, in this article we will focus on the pink button.
The Lemonade pink button
The lemonade pink button is a UI component we use in several flows, and aside from having specific characteristics like height, corner radius, font sizes, and colors, it also has 2 specific animations to transition from enable state to disable state.
Color change animation – our button changes color from pink to grey based on its enablement state, and it animates that color change.
Translation animation – our button moves up 10 pixels when is enabled and down 10 pixels when disabled.
Let’s get started
We first start, as always with writing a composable function.
What do we have here?
Let’s start with looking at the function signature, we have:
@Composable private fun PinkButton( text: String, enabled: Boolean, onClick: () -> Unit ) { ... }
So our function takes the minimum amount of information following the declarative UI paradigm, the text
for the button, the onClick
lambda, and the enable
state, that is declared from its parent.
Next, we declare the Button
composable which takes only the state and invokes the onClick
lambda when the button is clicked.
@Composable private fun PinkButton(...) { Button( enabled: enabled, onClick: { onClick() } ) }
Inside the button, we declare the Text
composable, which takes the text from the PinkButton
and defines some font-related properties.
@Composable private fun PinkButton(...) { Button(...) { Text { text = text, fontFamily = latoFamily // We discussed it the last article fontWeight = FontWeight.Bold, fontSize = 12.sp, color = colorResource(id = R.color.my_color_resource) } } }
A word about context
One thing that is important to notice here is how we can access our resource folder easily without the need for Context
using colorResource(id = …)
, Compose is not bound to lifecycles unless explicitly declared, therefor it does not need to use the Context
interface to interact with system resources.
As a side note, if we need to access Context in a composable, we can do so using
LocalContext.current which will provide the context of the view that called
setContent.
And now we have the basic function for the button, but it still doesn’t look right doesn’t it?
Using Modifier
to modify a composable
Now that we have the basic frame for the button we want to add some modifications to it, to shape and style it to look like our desired pink button.
So in the Button
function call we will add a new parameter called Modifier
.
A Modifier
is an interface that can be implemented using the builder pattern to chain together parameters in order to assign visual and behavioral properties.
We add the modifier properties to the Button
function like this.
@Composable PinkButton(...) { Button( enabled = enabled, onclick = { onClick() } modifier = Modifier.padding(...).fillMaxWidth().height(58.dp) ) { Text(...) } }
Job Offers
Notice how we chained padding
, fillMaxWidth
, and height
to implement the Modifier
.
We can chain as many modifiers as we want and by doing so we can style our composable.
So for our composable we will add the following modifiers
@Composable PinkButton(...) { Button( enabled = enabled, onclick = { onClick() } modifier = Modifier .padding(bottom = 10.dp, start..., top..., end...) .fillMaxWidth() .height(58.dp) .clip(RoundedCornerShape(10.dp) ) { Text(...) } }
Let’s take a look at the modifiers we used here:
- padding – apply additional space along each edge of the content in
dp
: start, top, end, and bottom. - fillMaxWidth – equivilient to
match_parent
in the view system. - height – declare the preferred height of the content to be exactly.
- clip – clip the view using a specified shape, in this case we use
RoundedCornerShape
that takes adp
value of the corner radius.
Somthing that is important to remember is the modifiers are applied in the order they are written, so the padding will be implemented before the height and width in our case.
Other than visual properties we can also add behavioral properties like clickbale,
swipable and
selectable.
After we added the modifications, the button looks better but it is still missing its colors and custom animations.
Adding custom animations
Our pink button has two types of animation when its state is modified from enabled to disabled.
- Translation Y animation — the button will move 10 px on the Y axis up or down according to its enablement state.
- Color change animation — the button will animate color change from pink to grey according to its enablement state.
To achieve simple animations between values we can use a an API called animate*AsState functions.
Animate*AsState
In Jetpack Compose we have a an API that allows us to animate to a target value, the *
denotes that you can replace a data type.
We can animate values of Float
, color
, dp
etc’.
Once the function is called, it will animate to target value across recomposition until it hits its target value.
Animating the movement on the Y axis
In our function we will add one more modifier to the Button
composable.
Button( enabled = enabled, modifier = Modifier.graphicsLayer( translationY = animateFloatAsState( if (enabled) 0f else 10f).value ) )
As we can see, we are using the animateFloatAsState
API here to animte between two float values, the float value changes according to the enable
parameter from 0f
to 10f
.
So the value given to the animateFloatAsState
function will change when the enable
state changes, and will trigger an animation on the Y axis, since the value is assigned to the tranbslationY
modifier.
We can customize the animation even further with
animationSpec which allows us to define properties like easing and duration.
Animating color change
To add an animation for color change we will define two variables inside the PinkButton
function called animatedColor
and buttonColors
, we can do it like this:
@composble fun PinkButton(...) { val animatedColor val buttonColors Button(color: ButtonColors = buttonColors) { Text(...) } }
What have we here?
When be want to set colors for a button in Jetpack Compose we will assign it a ButtonColors
implemantaion that will hold values for the buttons backgroundColor
and contentColor
.
The backgroundColor
for us should be grey if the button is disabled, or pink if the button is enabled, we can assign it like this:
val buttonColors = ButtonDefaults.buttonColors( backgroundColor = if (enabled) Color.Pink else Color.Grey )
But if we do that we wont get an animation change between the two colors, to get the animation change we will use the other variable we declared — animatedColor.
val animatedColor = animateColorAsState( if (enabled) Color.Pink else Color.Grey )
We will assign animatedColor
value to the backgroundColor.
val buttonColors = ButtonDefaults.buttonColors( backgroundColor = animatedColor.value )
And now we get the animation thanks to animateColorAsState
which works exactly like animateFloatAsState
by animating its given value to the target value when the enable
state changes.
So the full code for our Pink button is now this:
And now our button looks and acts the way we wanted it to! 😍
And that’s it for now
In this article we built a custom button we two types of animations, we built it as a composable function, which means we can call it wherever we want.
We went over new terms like Modifier
, and the animate*AsState
APIs, and learned how we can leverage them to style and animate our composable views.
If you liked the article don’t forget to clap and subscribe, but only if you think I deserve it!.