How to Create Fading Edge Modifier in Jetpack Compose
Welcome 👋
In this article, we’ll create a highly customizable Fading Edge modifier in Jetpack Compose.
Let’s get started 🚀
Preparation
Before we step up, to the implementation, we need to define an enum class FadeSide
that represents all sides of the view, and which we will use to specify the side we want to draw the gradient.
enum class FadeSide {
LEFT, RIGHT, BOTTOM, TOP
}
Also, to draw the gradient by a specified side we need an extension function getFadeOffsets
for Size
class that returns a pair of start and end gradient offsets.
fun Size.getFadeOffsets(side: FadeSide): Pair<Offset, Offset> {
return when (side) {
FadeSide.LEFT -> Offset.Zero to Offset(width, 0f)
FadeSide.RIGHT -> Offset(width, 0f) to Offset.Zero
FadeSide.BOTTOM -> Offset(0f, height) to Offset.Zero
FadeSide.TOP -> Offset.Zero to Offset(0f, height)
}
}
Defining the Function and Parameters
Alright, let’s define an extension function fadingEdge
for the Modifier
and quickly go through its parameters.
fun Modifier.fadingEdge(
vararg sides: FadeSide,
color: Color,
width: Dp,
isVisible: Boolean,
spec: AnimationSpec<Dp>?
)
- Sides: Accepts values from the
FadeSide
enum, allowing multiple sides to be defined usingvararg
. - Color: Represents the color of the gradient.
- Width: Specifies the width of the gradient edge, measured in
Dp
. - IsVisible: Allows to control the visibility of the fade effect.
- Spec: This parameter is nullable, and gives a possibility to provide a specification for an animation. If null, no animation will be applied.
Implementation
For the final touch, let’s write the implementation.
First, we utilize a composed
modifier to be able to create a state for the animation.
fun Modifier.fadingEdge(
// Parameters
) = composed {
// Implementation...
}
Now, we use the require
function to validate the width parameter:
require(width > 0.dp) { "Invalid fade width: Width must be greater than 0" }
Let’s write the animation logic. In this scenario, we first check whether the spec
parameter isn't null. Then, based on the isVisible
condition and the provided spec
, we animate the width
parameter from 0 to a specified width.
val animatedWidth = spec?.let {
animateDpAsState(
targetValue = if (isVisible) width else 0.dp,
animationSpec = spec,
label = "Fade width"
).value
}
Great, now we’re set to draw the content and apply the fade effect on top using the drawWithContent
function.
drawWithContent {
// Draw the content
this@drawWithContent.drawContent()
// Go through all provided sides
sides.forEach { side ->
// Get start and end gradient offsets
val (start, end) = this.size.getFadeOffsets(side)
// Define the static width
val staticWidth = if (isVisible) width.toPx() else 0f
// Define the final width
val widthPx = animatedWidth?.toPx() ?: staticWidth
// Calculate fraction based on view size
val fraction = when(side) {
FadeSide.LEFT, FadeSide.RIGHT -> widthPx / this.size.width
FadeSide.BOTTOM, FadeSide.TOP -> widthPx / this.size.height
}
// Draw the gradient
drawRect(
brush = Brush.linearGradient(
0f to color,
fraction to Color.Transparent,
start = start,
end = end
),
size = this.size
)
}
}
Additionally, for the call simplicity, we can create separate functions for each side. For instance, we can implement a function for the left side like this:
fun Modifier.leftFadingEdge(
color: Color,
isVisible: Boolean = true,
width: Dp = 16.dp,
spec: AnimationSpec<Dp>? = null
) = fadingEdge(FadeSide.LEFT, color = color, width = width, isVisible = isVisible, spec = spec)
The implementation for the remaining sides will be similar. By the way, you can access these functions and all the other source code on Gist.
Example
Let’s craft a simple LazyRow
with a gradient on its left and right sides that will be visible only in case the content is overlapped.
val listState = rememberLazyListState()
LazyRow(
state = listState,
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.leftFadingEdge(
color = Color.White,
width = 32.dp,
spec = tween(500),
isVisible = listState.canScrollBackward,
)
.rightFadingEdge(
color = Color.White,
width = 32.dp,
spec = tween(500),
isVisible = listState.canScrollForward
)
) {
items(20) { item ->
Text(
text = "item $item",
modifier = Modifier
.background(Color.Gray, RoundedCornerShape(8.dp))
.padding(8.dp)
)
}
}
The output:
Thank you for reading this article! ❤️ If you found it enjoyable and valuable, show your appreciation by clapping 👏 and following for more exciting articles 😊