實作雙指撥動縮放手勢,支援應用程式中的可縮放內容。這是改善無障礙功能的標準方法,與平台一致,可讓使用者直覺地調整文字和 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 是否能因應應用程式內縮放,以及系統層級的字型或顯示設定。確認應用程式的版面配置能正確調整,不會破壞、重疊或隱藏內容。進一步瞭解如何建構自動調整式版面配置。