支援可供使用者縮放的內容

實作雙指撥動縮放手勢,支援應用程式中的可縮放內容。這是改善無障礙功能的標準方法,與平台一致,可讓使用者直覺地調整文字和 UI 元素的大小,以符合需求。應用程式可以定義自訂縮放行為,提供精細控制和情境行為,讓使用者能更快找到所需體驗,比螢幕放大等系統層級功能更有效率。

選擇資源調度策略

本指南介紹的策略會導致 UI 重新排列及重組,以符合螢幕寬度。這項功能可大幅提升無障礙體驗,使用者不必水平平移,也不必在閱讀長行文字時,做出令人沮喪的「鋸齒狀」動作。

延伸閱讀:研究證實,對於低視能使用者來說,重排內容的可讀性明顯高於需要雙向平移的介面,且更容易瀏覽。詳情請參閱「行動裝置上的平移掃描和流動式排版內容比較」。

縮放所有元素或僅限文字元素

下表說明每種縮放策略的視覺效果。

策略 密度縮放 字型縮放

行為

按比例縮放所有內容。內容會重新排列,以符合容器大小,因此使用者不必水平平移,就能查看所有內容。

只會影響文字元素。整體版面配置和非文字元件的大小則維持不變。

體重計

所有視覺元素:文字、元件 (按鈕、圖示)、圖片和版面配置間距 (邊框、邊界)

只有文字

示範

建議

瞭解視覺效果的差異後,請參考下表,權衡利弊並選擇最適合內容的策略。

UI 類型

建議策略

推論

閱讀密集型版面配置

範例:新聞報導、訊息應用程式

密度或字型縮放

建議使用密度縮放功能,縮放整個內容區域,包括內嵌圖片。

如果只需要縮放文字,字型縮放是簡單的替代方案。

視覺結構化版面配置

示例:應用程式商店、社群媒體動態消息

密度縮放

保留輪轉介面或格狀檢視中圖片和文字之間的視覺關係。重排功能可避免水平平移,以免與巢狀捲動元素發生衝突。

在 Jetpack Compose 中偵測縮放手勢

如要支援使用者可縮放的內容,您必須先偵測多點觸控手勢。在 Jetpack Compose 中,您可以使用 Modifier.transformable 執行這項操作。

transformable 修飾符是高階 API,可提供自上次手勢事件以來的 zoomChange 差異。這會將狀態更新邏輯簡化為直接累積 (例如 scale *= zoomChange),因此非常適合本指南介紹的自動調整策略。

實作範例

以下範例說明如何實作密度縮放和字型縮放策略。

密度縮放

這種方法會縮放 UI 區域的基本 density。因此,所有以版面配置為基礎的測量值 (包括邊框間距、間距和元件大小) 都會縮放,就像螢幕大小或解析度有所變更一樣。由於文字大小也取決於密度,因此也會按比例縮放。如要統一放大特定區域內的所有元素,同時維持 UI 的整體視覺節奏和比例,這個策略就很有效。

private class DensityScalingState(
    // Note: For accessibility, typical min/max values are ~0.75x and ~3.5x.
    private val minScale: Float = 0.75f,
    private val maxScale: Float = 3.5f,
    private val currentDensity: Density
) {
    val transformableState = TransformableState { zoomChange, _, _ ->
        scaleFactor.floatValue =
            (scaleFactor.floatValue * zoomChange).coerceIn(minScale, maxScale)
    }
    val scaleFactor = mutableFloatStateOf(1f)
    fun scaledDensity(): Density {
        return Density(
            currentDensity.density * scaleFactor.floatValue,
            currentDensity.fontScale
        )
    }
}

@Composable
fun DensityScalingSample() {
    val currentDensity = LocalDensity.current
    val scaleState =
        remember(currentDensity) { DensityScalingState(currentDensity = currentDensity) }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .transformable(state = scaleState.transformableState),
        contentAlignment = Alignment.TopCenter
    ) {
        CompositionLocalProvider(
            LocalDensity provides scaleState.scaledDensity()
        ) {
            DemoCard()
        }
    }
}

字型縮放

這個策略更具針對性,只會修改 fontScale 係數。結果是只有文字元素會放大或縮小,而所有其他版面配置元件 (例如容器、邊框間距和圖示) 則維持固定大小。這項策略非常適合用於提升閱讀密集型應用程式的文字可讀性。

class FontScaleState(
    // Note: For accessibility, typical min/max values are ~0.75x and ~3.5x.
    private val minScale: Float = 0.75f,
    private val maxScale: Float = 3.5f,
    private val currentDensity: Density
) {
    val transformableState = TransformableState { zoomChange, _, _ ->
        scaleFactor.floatValue =
            (scaleFactor.floatValue * zoomChange).coerceIn(minScale, maxScale)
    }
    val scaleFactor = mutableFloatStateOf(1f)
    fun scaledFont(): Density {
        return Density(
            currentDensity.density,
            currentDensity.fontScale * scaleFactor.floatValue
        )
    }
}

@Composable
fun FontScalingSample() {
    val currentDensity = LocalDensity.current
    val scaleState = remember { FontScaleState(currentDensity = currentDensity) }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .transformable(state = scaleState.transformableState),
        contentAlignment = Alignment.TopCenter
    ) {
        CompositionLocalProvider(
            LocalDensity provides scaleState.scaledFont()
        ) {
            DemoCard()
        }
    }
}

共用示範 UI

這是上述兩個範例共用的 DemoCard 可組合函式,用於突顯不同的縮放行為。

@Composable
private fun DemoCard() {
    Card(
        modifier = Modifier
            .width(360.dp)
            .padding(16.dp),
        shape = RoundedCornerShape(12.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text("Demo Card", style = MaterialTheme.typography.headlineMedium)
            var isChecked by remember { mutableStateOf(true) }
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text("Demo Switch", Modifier.weight(1f), style = MaterialTheme.typography.bodyLarge)
                Switch(checked = isChecked, onCheckedChange = { isChecked = it })
            }
            Row(verticalAlignment = Alignment.CenterVertically) {
                Icon(Icons.Filled.Person, "Icon", Modifier.size(32.dp))
                Spacer(Modifier.width(8.dp))
                Text("Demo Icon", style = MaterialTheme.typography.bodyLarge)
            }
            Row(
                Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Box(
                    Modifier
                        .width(100.dp)
                        .weight(1f)
                        .height(80.dp)
                        .background(Color.Blue)
                )
                Box(
                    Modifier
                        .width(100.dp)
                        .weight(1f)
                        .height(80.dp)
                        .background(Color.Red)
                )
            }
            Text(
                "Demo Text: Lorem ipsum dolor sit amet, consectetur adipiscing elit," +
                    " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
                style = MaterialTheme.typography.bodyMedium,
                textAlign = TextAlign.Justify
            )
        }
    }
}

提示和注意事項

如要打造更精緻、更易於使用的體驗,請參考下列建議:

  • 考慮提供非手勢縮放控制項:部分使用者可能難以使用手勢。為支援這些使用者,請考慮提供不依賴手勢的其他方式來調整或重設縮放比例。
  • 針對所有縮放比例建構:測試 UI 是否能因應應用程式內縮放,以及系統層級的字型或顯示設定。確認應用程式的版面配置能正確調整,不會破壞、重疊或隱藏內容。進一步瞭解如何建構自動調整式版面配置。