How to add fading edge effect to Android Jetpack Compose Column or Row?

Viewed 3544

I need to implement LazyColumn with top fading edge effect. On Android I use fade gradient for ListView or RecyclerView, but couldn't find any solution for Jetpack Compose!

enter image description here

I tried to modify canvas:

@Composable
fun Screen() {
    Box(
        Modifier
            .fillMaxWidth()
            .background(color = Color.Yellow)
    ) {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .drawWithContent {
                    val colors = listOf(Color.Transparent, Color.Black)
                    drawContent()
                    drawRect(
                        brush = Brush.verticalGradient(colors),
                        blendMode = BlendMode.DstIn
                    )
                }
        ) {
            itemsIndexed((1..1000).toList()) { item, index ->
                Text(
                    text = "Item $item: $index value",
                    modifier = Modifier.padding(12.dp),
                    color = Color.Red,
                    fontSize = 24.sp
                )
            }
        }
    }
}

But have wrong result:

enter image description here

4 Answers

Quick hack which fixes the issue: add .graphicsLayer { alpha = 0.99f } to your modifer

By default Jetpack Compose disables alpha compositing for performance reasons (as explained here; see the "Custom Modifier" section). Without alpha compositing, blend modes which affect transparency (e.g. DstIn) don't have the desired effect. Currently the best workaround is to add .graphicsLayer { alpha = 0.99F } to the modifier on the LazyColumn; this forces Jetpack Compose to enable alpha compositing by making the LazyColumn imperceptibly transparent.

With this change, your code looks like this:

@Composable
fun Screen() {
    Box(
        Modifier
            .fillMaxWidth()
            .background(color = Color.Yellow)
    ) {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                // Workaround to enable alpha compositing
                .graphicsLayer { alpha = 0.99F }
                .drawWithContent {
                    val colors = listOf(Color.Transparent, Color.Black)
                    drawContent()
                    drawRect(
                        brush = Brush.verticalGradient(colors),
                        blendMode = BlendMode.DstIn
                    )
                }
        ) {
            itemsIndexed((1..1000).toList()) { item, index ->
                Text(
                    text = "Item $item: $index value",
                    modifier = Modifier.padding(12.dp),
                    color = Color.Red,
                    fontSize = 24.sp
                )
            }
        }
    }
}

which produces the correct result

What you could do is place a Spacer on top of the list, and draw a gradient on that Box. Make the Box small so only a small portion of the list has the overlay. Make the color the same as the background of the screen, and it will look like the content is fading.

val screenBackgroundColor = MaterialTheme.colors.background

    Box(Modifier.fillMaxSize()) {

        LazyColumn(Modifier.fillMaxSize()) {
            //your items
        }

        //Gradient overlay
        Spacer(
            Modifier
                .fillMaxWidth()
                .height(32.dp)
                .background(
                    brush = Brush.verticalGradient(
                        colors = listOf(
                            Color.Transparent,
                            screenBackgroundColor
                        )
                    )
                )
                //.align(Alignment) to control the position of the overlay
        )
    }

Here's how it would look like:

screenshot

However, this doesn't seem like quite what you asked for since it seems like you want the actual list content to fade out. I don't know how you would apply an alpha to only a portion of a view. Perhaps try to dig into the .alpha sources to figure out.

Just a little nudge in the right direction. What this piece of code does is place a Box composable at the top of your LazyColumn with an alpha modifier for fading. You can make multiple of these Box composables in a Column again to create a smoother effect.

@Composable
fun FadingExample() {
    Box(
        Modifier
            .fillMaxWidth()
            .requiredHeight(500.dp)) {

        LazyColumn(Modifier.fillMaxSize()) {

        }

        Box(
            Modifier
                .fillMaxWidth()
                .height(10.dp)
                .alpha(0.5f)
                .background(Color.Transparent)
                .align(Alignment.TopCenter)
        ) {

        }
    }
}

This is a very simple implementation of FadingEdgeLazyColumn using AndroidView. Place AndroidView with gradient background applied to the top and bottom of LazyColumn.

@Stable
object GradientDefaults {
    @Stable
    val Color = androidx.compose.ui.graphics.Color.Black

    @Stable
    val Height = 30.dp
}

@Stable
sealed class Gradient {
    @Immutable
    data class Top(
        val color: Color = GradientDefaults.Color,
        val height: Dp = GradientDefaults.Height,
    ) : Gradient()

    @Immutable
    data class Bottom(
        val color: Color = GradientDefaults.Color,
        val height: Dp = GradientDefaults.Height,
    ) : Gradient()
}

@Composable
fun FadingEdgeLazyColumn(
    modifier: Modifier = Modifier,
    gradients: Set<Gradient> = setOf(Gradient.Top(), Gradient.Bottom()),
    contentGap: Dp = 0.dp,
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    verticalArrangement: Arrangement.Vertical =
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
    userScrollEnabled: Boolean = true,
    content: LazyListScope.() -> Unit,
) {
    val topGradient =
        remember(gradients) { gradients.find { it is Gradient.Top } as? Gradient.Top }
    val bottomGradient =
        remember(gradients) { gradients.find { it is Gradient.Bottom } as? Gradient.Bottom }

    ConstraintLayout(modifier = modifier) {
        val (topGradientRef, lazyColumnRef, bottomGradientRef) = createRefs()

        GradientView(
            modifier = Modifier
                .constrainAs(topGradientRef) {
                    top.linkTo(parent.top)
                    width = Dimension.matchParent
                    height = Dimension.value(topGradient?.height ?: GradientDefaults.Height)
                }
                .zIndex(2f),
            colors = intArrayOf(
                (topGradient?.color ?: GradientDefaults.Color).toArgb(),
                Color.Transparent.toArgb()
            ),
            visible = topGradient != null
        )
        LazyColumn(
            modifier = Modifier
                .constrainAs(lazyColumnRef) {
                    top.linkTo(
                        anchor = topGradientRef.top,
                        margin = when (topGradient != null) {
                            true -> contentGap
                            else -> 0.dp
                        }
                    )
                    bottom.linkTo(
                        anchor = bottomGradientRef.bottom,
                        margin = when (bottomGradient != null) {
                            true -> contentGap
                            else -> 0.dp
                        }
                    )
                    width = Dimension.matchParent
                    height = Dimension.fillToConstraints
                }
                .zIndex(1f),
            state = state,
            contentPadding = contentPadding,
            reverseLayout = reverseLayout,
            verticalArrangement = verticalArrangement,
            horizontalAlignment = horizontalAlignment,
            flingBehavior = flingBehavior,
            userScrollEnabled = userScrollEnabled,
            content = content
        )
        GradientView(
            modifier = Modifier
                .constrainAs(bottomGradientRef) {
                    bottom.linkTo(parent.bottom)
                    width = Dimension.matchParent
                    height = Dimension.value(bottomGradient?.height ?: GradientDefaults.Height)
                }
                .zIndex(2f),
            colors = intArrayOf(
                Color.Transparent.toArgb(),
                (bottomGradient?.color ?: GradientDefaults.Color).toArgb(),
            ),
            visible = bottomGradient != null
        )
    }
}

@Composable
private fun GradientView(
    modifier: Modifier = Modifier,
    @Size(value = 2) colors: IntArray,
    visible: Boolean = true,
) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            val gradientBackground = GradientDrawable(
                GradientDrawable.Orientation.TOP_BOTTOM,
                colors
            ).apply {
                cornerRadius = 0f
            }
            View(context).apply {
                layoutParams = LayoutParams(
                    LayoutParams.MATCH_PARENT,
                    LayoutParams.MATCH_PARENT
                )
                background = gradientBackground
                visibility = when (visible) {
                    true -> View.VISIBLE
                    else -> View.INVISIBLE
                }
            }
        }
    )
}
Related