Casos de uso comuns de desfoque

As seções a seguir mostram como combinar Modifier.blur, BlurRadiusSpec, BlurStop e BlurScope para padrões comuns de interface. Para uma introdução a essas APIs, consulte Desfoque no Compose.

Desfocar as bordas do conteúdo rolável ou arrastável

Ao mostrar uma lista de rolagem, um carrossel ou um seletor, use desfoques progressivos com várias paradas para suavizar os itens perto das bordas do contêiner, mantendo a região central ativa em foco nítido.

Esmaecer as bordas de cima e de baixo de uma lista rolável

Para suavizar os itens conforme eles entram e saem da janela de visualização de uma lista vertical, aplique BlurRadiusSpec.verticalGradient com entradas BlurStop ao LazyColumn. No exemplo a seguir, que rola a letra de uma música linha por linha, as paradas de 0.dp entre 0.42f e 0.58f mantêm a linha ativa no centro nítida, enquanto as linhas nas bordas de cima e de baixo ficam progressivamente desfocadas:

@Composable
fun SongListTopBottomBlurExample(lyrics: List<String>) {
    // ...
    LazyColumn(
        state = listState,
        flingBehavior = flingBehavior,
        modifier = Modifier
            .width(330.dp)
            .height(480.dp)
            .padding(horizontal = 24.dp)
            .blur(
                blurRadiusSpec = BlurRadiusSpec.verticalGradient(
                    stops = listOf(
                        BlurStop(fraction = 0.00f, radius = 18.dp), // Heavily blurred top edge
                        BlurStop(fraction = 0.24f, radius = 8.dp), // Soft upper transition
                        BlurStop(fraction = 0.42f, radius = 0.dp), // Sharp center focus starts
                        BlurStop(fraction = 0.58f, radius = 0.dp), // Sharp center focus ends
                        BlurStop(fraction = 0.76f, radius = 8.dp), // Soft lower transition
                        BlurStop(fraction = 1.00f, radius = 18.dp), // Heavily blurred bottom edge
                    ),
                ),
                edgeTreatment = BlurredEdgeTreatment.Unbounded,
            ),
        contentPadding = PaddingValues(vertical = 36.dp),
        verticalArrangement = Arrangement.spacedBy(26.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        // ...
    }
}

Figura 1. Uma lista vertical de linhas de texto de letras de músicas em que as linhas de cima e de baixo desaparecem em desfoque, enquanto a linha central fica nítida.

Esmaecer as bordas esquerda e direita de um controle horizontal

Da mesma forma, para chamar a atenção para o centro de um componente rolável ou arrastável horizontalmente, como um seletor de roda, carrossel ou escala de medição, aplique BlurRadiusSpec.horizontalGradient com entradas BlurStop. Na balança de peso arrastável a seguir, as bordas esquerda e direita ficam mais suaves, enquanto os traços e números perto do ponteiro indicador central permanecem completamente nítidos:

@Composable
fun HorizontalScaleBlurExample() {
    var selectedValue by remember { mutableFloatStateOf(65.0f) }
    val animatedValue by animateFloatAsState(
        targetValue = selectedValue,
        animationSpec = tween(durationMillis = 900, easing = FastOutSlowInEasing),
        label = "horizontalScaleSmoothScroll",
    )
    Column(
        modifier = Modifier
            .width(340.dp)
            .background(Color(0xFF0F172A), RoundedCornerShape(24.dp))
            .padding(vertical = 24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(
            text = "${(animatedValue * 10).roundToInt() / 10.0} kg",
            color = Color.White,
            fontSize = 44.sp,
            fontWeight = FontWeight.Black,
        )
        Spacer(Modifier.height(16.dp))
        Box(
            // ...
        ) {
            // Ruler ticks blurred at the left and right edges, sharp at center
            Canvas(
                modifier = Modifier
                    .fillMaxSize()
                    .blur(
                        blurRadiusSpec = BlurRadiusSpec.horizontalGradient(
                            stops = listOf(
                                BlurStop(fraction = 0.00f, radius = 14.dp),
                                BlurStop(fraction = 0.28f, radius = 0.dp),
                                BlurStop(fraction = 0.72f, radius = 0.dp),
                                BlurStop(fraction = 1.00f, radius = 14.dp),
                            ),
                        ),
                        edgeTreatment = BlurredEdgeTreatment.Unbounded,
                    ),
            ) {
                // Draw horizontal ruler ticks offset by `animatedValue`
                // ...
            }
            // ...
        }
    }
}

Figura 2. Um widget de escala de régua horizontal que mostra marcas de escala desfocadas perto das bordas e marcas nítidas perto do ponteiro indicador.

Criar um desfoque diagonal com linearGradient

BlurRadiusSpec.linearGradient varia o raio de desfoque ao longo de um segmento de linha entre os valores start e end DpOffset no espaço de coordenadas da camada. No bloco lambda Modifier.blur, é possível ler BlurScope.size para executar um desfoque progressivo diagonal em uma imagem principal, do canto superior esquerdo ao canto inferior direito, mantendo o objeto no canto superior esquerdo completamente nítido e desfocando progressivamente a região inferior direita atrás do texto sobreposto:

@Composable
fun DiagonalLinearBlurCardExample() {
    Box(
        modifier = Modifier
            .wrapContentSize()
            .background(Color(0xFFF8FAFC))
            .padding(10.dp),
    ) {
        Image(
            painter = painterResource(id = R.drawable.dog),
            contentDescription = stringResource(id = R.string.dog_content_description),
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .size(width = 300.dp, height = 380.dp)
                .blur {
                    blurRadiusSpec = BlurRadiusSpec.linearGradient(
                        start = DpOffset(0.dp, 0.dp),
                        end = DpOffset(size.width, size.height),
                        stops = listOf(
                            BlurStop(fraction = 0.00f, radius = 0.dp),
                            BlurStop(fraction = 0.50f, radius = 0.dp),
                            BlurStop(fraction = 0.75f, radius = 14.dp),
                            BlurStop(fraction = 1.00f, radius = 28.dp),
                        ),
                    )
                    edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(24.dp))
                },
        )
        // ...
    }
}

O retrato de um cachorro que está nítido no canto superior esquerdo e fica progressivamente desfocado em direção ao canto inferior direito, com um título e um subtítulo nítidos em branco sobrepostos à região desfocada.
Figura 3. Um retrato de cachorro que é nítido no canto superior esquerdo e fica progressivamente desfocado em direção ao canto inferior direito, com um título e subtítulo brancos nítidos sobrepostos à região desfocada.

Criar um destaque de foco com radialGradient

BlurRadiusSpec.radialGradient cria um efeito de destaque circular variando o raio de desfoque para fora de um ponto focal. Defina center como um DpOffset e fallOffRadius como a distância Dp de center em que endRadius é alcançado. Se você deixar center como DpOffset.Unspecified, o gradiente será centralizado no elemento combinável. Se você deixar fallOffRadius como Dp.Unspecified, o padrão será metade da menor dimensão da camada.

Por exemplo, aplicar BlurRadiusSpec.radialGradient sobre um trecho de texto e atualizar center de eventos de arrastar o ponteiro permite que o usuário arraste um destaque de leitura circular pelo parágrafo enquanto desfoca o texto ao redor, como mostrado no exemplo a seguir:

@Composable
fun RadialSpotlightTextExample() {
    var touchOffset by remember { mutableStateOf(Offset.Unspecified) }
    Box(
        modifier = Modifier
            .size(width = 330.dp, height = 340.dp)
            .background(Color(0xFF0F172A), RoundedCornerShape(20.dp))
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragStart = { offset -> touchOffset = offset },
                    onDrag = { change, _ -> touchOffset = change.position },
                )
            }
            .blur {
                val spotlightCenter = if (touchOffset.isSpecified) {
                    DpOffset(touchOffset.x.toDp(), touchOffset.y.toDp())
                } else {
                    DpOffset(x = size.width / 2, y = size.height / 2)
                }
                blurRadiusSpec = BlurRadiusSpec.radialGradient(
                    startRadius = 0.dp,
                    endRadius = 10.dp,
                    center = spotlightCenter,
                    fallOffRadius = 215.dp,
                )
                edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(20.dp))
            }
            .padding(22.dp),
    ) {
        // ...
    }
}

Figura 4. Um parágrafo de texto com uma área circular de texto nítido ao redor do ponteiro que fica progressivamente desfocado em direção à borda do círculo.

Criar uma máscara de desfoque personalizada com um sombreador

Para distribuições complexas de desfoque espacial além de gradientes lineares ou radiais, como padrões repetidos, formas arbitrárias ou ruído procedural, transmita um Shader personalizado, como um AGSL RuntimeShader, para BlurRadiusSpec.shader. O sombreador é avaliado em coordenadas de pixel de camada da origem superior esquerda, e o canal Alfa de amostra (0..1) dimensiona maxRadius.

Crie o sombreador e a configuração de BlurRadiusSpec.shader uma vez usando a função combinável remember para que uma única instância seja reutilizada em várias recomposições. O Compose invoca o bloco de configuração com o tamanho da camada em cada resolução, que é onde você define uniformes dependentes de tamanho:

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
fun ShaderBlurExample() {
    // Creates 6 diagonal frosted-glass bands across the image.
    val maskShader = remember {
        RuntimeShader(
            """
            uniform float2 size;
            half4 main(float2 coord) {
                float2 uv = coord / size;
                float band = smoothstep(0.4, 0.6, fract((uv.x + uv.y) * 3.0));
                return half4(0.0, 0.0, 0.0, half(band));
            }
            """.trimIndent(),
        )
    }
    val maskBlurSpec = remember(maskShader) {
        BlurRadiusSpec.shader(maxRadius = 24.dp) { sizePx ->
            maskShader.setFloatUniform("size", sizePx.width, sizePx.height)
            maskShader
        }
    }
    Image(
        painter = painterResource(id = R.drawable.dog),
        contentDescription = stringResource(id = R.string.dog_content_description),
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .size(150.dp)
            .blur(blurRadiusSpec = maskBlurSpec),
    )
}

Imagem de um cachorro com faixas diagonais de desfoque de vidro fosco, geradas por uma máscara de sombreador personalizada.
Figura 5. Imagem de um cachorro com faixas diagonais de desfoque de vidro fosco, geradas por uma máscara de sombreador personalizada.