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

實作雙指撥動縮放手勢,支援應用程式中的可縮放內容。這是改善無障礙功能的標準方法,可讓使用者直覺地調整文字和 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
        )
    }
}

字型縮放

這項策略的目標更明確,只會修改 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
        )
    }
}

共用示範 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。確認應用程式的版面配置可正確調整大小,不會中斷、重疊或隱藏內容。進一步瞭解如何建構自動調整式版面配置