Распространенные случаи использования размытия

В следующих разделах показано, как комбинировать Modifier.blur, BlurRadiusSpec, BlurStop и BlurScope для распространенных шаблонов интерфейса. Подробнее о размытии в Compose…

Размытие краев прокручиваемого или перетаскиваемого контента

При отображении прокручиваемого списка, карусели или средства выбора вы можете использовать многоточечное прогрессивное размытие, чтобы смягчить элементы вблизи краев контейнера, сохраняя при этом активную центральную область в резком фокусе.

Затемнение верхнего и нижнего краев прокручиваемого списка

Чтобы смягчить появление и исчезновение элементов при прокрутке вертикального списка, примените BlurRadiusSpec.verticalGradient с записями BlurStop к LazyColumn. В следующем примере, где текст песни прокручивается построчно, контрольные точки 0.dp между 0.42f и 0.58f сохраняют резкость активной строки в центре, а строки ближе к верхнему и нижнему краям постепенно размываются:

@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,
    ) {
        // ...
    }
}

Рисунок 1. Вертикальный список строк текста песни, в котором верхняя и нижняя строки размыты, а центральная – четкая.

Затемнить левый и правый края горизонтального элемента управления

Аналогично, чтобы привлечь внимание к центру горизонтально прокручиваемого или перетаскиваемого компонента, например колеса выбора, карусели или шкалы измерения, примените BlurRadiusSpec.horizontalGradient с записями BlurStop. На следующих перетаскиваемых весах левый и правый края смягчаются, а деления и цифры рядом с центральной стрелкой индикатора остаются полностью резкими:

@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`
                // ...
            }
            // ...
        }
    }
}

Рисунок 2. Виджет горизонтальной шкалы линейки с размытыми делениями по краям и четкими делениями рядом со стрелкой индикатора.

Создать диагональное размытие с помощью linearGradient

BlurRadiusSpec.linearGradient изменяет радиус размытия вдоль сегмента линии между значениями start и end DpOffset в пространстве координат слоя. Внутри лямбда-блока Modifier.blur вы можете прочитать BlurScope.size, чтобы запустить диагональное прогрессивное размытие по всему главному изображению от верхнего левого угла до нижнего правого угла, сохраняя объект в верхнем левом углу полностью резким и постепенно размывая нижнюю правую область за наложенным текстом:

@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))
                },
        )
        // ...
    }
}

Портрет собаки, резкий в левом верхнем углу и постепенно размывающийся к правому нижнему углу, с резким белым текстом заголовка и подзаголовка, наложенным на размытую область.
Рисунок 3. Портрет собаки, резкий в левом верхнем углу и постепенно размывающийся к правому нижнему углу, с резким белым текстом заголовка и подзаголовка, наложенным на размытую область.

Создать видео "Лучшие моменты" с radialGradient

BlurRadiusSpec.radialGradient создает эффект кругового прожектора, изменяя радиус размытия от фокусной точки наружу. Определите center как DpOffset, а fallOffRadius – как расстояние Dp от center, на котором достигается endRadius. Если оставить для center значение DpOffset.Unspecified, градиент будет центрирован по composable-функции, а если оставить для fallOffRadius значение Dp.Unspecified, по умолчанию будет использоваться половина меньшего размера слоя.

Например, если применить BlurRadiusSpec.radialGradient к фрагменту текста и обновлять center при перетаскивании указателя, пользователь сможет перемещать по абзацу круглый прожектор, который будет подсвечивать текст, размывая все вокруг.

@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),
    ) {
        // ...
    }
}

Рисунок 4. Абзац текста с круглой областью резкого текста вокруг указателя, который постепенно размывается к краю круга.

Как создать маску размытия с помощью шейдера

Для сложных пространственных распределений размытия, выходящих за рамки линейных или радиальных градиентов, таких как повторяющиеся узоры, произвольные формы или процедурный шум, передайте пользовательский Shader, например AGSL RuntimeShader, в BlurRadiusSpec.shader. Шейдер вычисляется в пиксельных координатах слоя, начиная с верхнего левого угла, а его альфа-канал (0..1) масштабирует maxRadius.

Создайте шейдер и конфигурацию BlurRadiusSpec.shader один раз с помощью composable-функции remember, чтобы один экземпляр использовался повторно в разных рекомпозициях. Compose вызывает блок конфигурации с размером слоя при каждом разрешении, где вы устанавливаете зависимые от размера униформы:

@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),
    )
}

Изображение собаки с диагональными полосами размытия матового стекла, управляемыми настраиваемой маской шейдера.
Рисунок 5. Изображение собаки с диагональными полосами размытия, как на матовом стекле, созданными с помощью маски шейдера.