ぼかしの一般的なユースケース

以降のセクションでは、一般的な UI パターンで Modifier.blur、BlurRadiusSpec、BlurStop、BlurScope を組み合わせる方法について説明します。これらの API の概要については、Compose でのぼかしをご覧ください。

スクロール可能なコンテンツやドラッグ可能なコンテンツの端をぼかす

スクロール リスト、カルーセル、選択ツールを表示する場合、マルチストップ プログレッシブ ブラーを使用すると、コンテナの端付近のアイテムをぼかし、アクティブな中央領域をシャープに保つことができます。

スクロール リストの上端と下端をフェードする

垂直リストのビューポートにスクロールインまたはスクロールアウトするアイテムをソフトにするには、BlurStop エントリを含む BlurRadiusSpec.verticalGradient を LazyColumn に適用します。次の例では、曲の歌詞を 1 行ずつスクロールします。0.42f と 0.58f の間の 0.dp 停止により、アクティブな行が中央にシャープに表示され、上端と下端に向かって徐々にぼやけていきます。

@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. 曲の歌詞のテキスト行が縦に並んだリスト。上下の行はぼやけてフェードアウトし、中央の行ははっきりと表示されている。

水平コントロールの左右の端をフェードする

同様に、ホイール ピッカー、カルーセル、測定スケールなど、水平方向にスクロール可能またはドラッグ可能なコンポーネントの中心にフォーカスを合わせるには、BlurStop エントリを含む BlurRadiusSpec.horizontalGradient を適用します。次のドラッグ可能な体重計では、左右の端がぼやけていますが、中央のインジケーターの針の近くの目盛りと数字は完全に鮮明です。

@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 を endRadius に到達する center からの Dp 距離として定義します。center を DpOffset.Unspecified のままにすると、グラデーションはコンポーザブルの中央に配置されます。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 をスケーリングします。

remember コンポーズ可能な関数を使用してシェーダーと BlurRadiusSpec.shader 構成を 1 回作成し、再コンポーズ全体で 1 つのインスタンスが再利用されるようにします。Compose は、解決ごとにレイヤサイズを指定して構成ブロックを呼び出します。ここで、サイズに依存する uniform を設定します。

@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. カスタム シェーダー マスクによって斜めのすりガラスのぼかし帯が適用された犬の画像。