支持可缩放的用户内容

实现双指张合缩放手势,以支持应用中的可缩放内容。这是提高无障碍性的标准且与平台一致的方法,可让用户直观地调整文本和界面元素的大小,以满足其需求。您的应用可以定义自定义缩放行为,通过精细控制和情境行为提供一种体验,用户通常可以比屏幕放大等系统级功能更快地发现这种体验。

选择扩缩策略

本指南中介绍的策略会使界面重排和重组,以适应屏幕宽度。这提供了显著的无障碍优势,因为用户无需进行水平平移和令人沮丧的“锯齿形”移动,否则阅读长行文本就需要这样做。

延伸阅读:研究证实,对于低视力用户,与需要二维平移的界面相比,重排内容的可读性明显更高,也更易于浏览。如需了解详情,请参阅移动设备上的平移和扫描内容与可自动调节内容的比较。

缩放所有元素或仅缩放文本元素

下表展示了每种缩放策略的视觉效果。

策略 密度缩放 字体缩放

行为

按比例缩放所有内容。内容会重排以适应其容器,因此用户无需水平平移即可查看所有内容。

仅影响文本元素。整体布局和非文本组件的大小保持不变。

哪些内容会扩缩

所有视觉元素:文本、组件(按钮、图标)、图片和布局间距(内边距、外边距)

纯文字

演示

建议

现在,您已经了解了视觉差异,下表可帮助您权衡利弊,并为您的内容选择最佳策略。

界面类型

建议的出价策略

推理

阅读密集型布局

示例:新闻报道、即时通讯应用

密度或字体缩放

最好使用密度缩放来缩放整个内容区域,包括内嵌图片。

如果只需要缩放文本,字体缩放是一种简单的替代方案。

视觉结构化布局

示例:应用商店、社交媒体信息流

密度缩放

保留轮播界面或网格中图片与文字之间的视觉关系。重排特性可避免水平平移,因为水平平移会与嵌套滚动元素发生冲突。

在 Jetpack Compose 中检测缩放手势

如需支持用户可缩放的内容,您必须先检测多点触控手势。在 Jetpack Compose 中,您可以使用 Modifier.transformable 执行此操作。

transformable 修饰符是一个高级 API,可提供自上次手势事件以来的 zoomChange 增量。这会将状态更新逻辑简化为直接累加(例如 scale *= zoomChange),因此非常适合本指南中介绍的自适应扩缩策略。

实现示例

以下示例展示了如何实现密度缩放和字体缩放策略。

密度缩放

此方法会缩放界面区域的基本 density。因此,所有基于布局的测量值(包括内边距、间距和组件大小)都会缩放,就像屏幕尺寸或分辨率发生了变化一样。由于文字大小也取决于密度,因此也会按比例缩放。如果您想统一放大特定区域内的所有元素,同时保持界面的整体视觉节奏和比例,此策略非常有效。

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()
        }
    }
}

共享演示界面

这是上述两个示例使用的共享 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
            )
        }
    }
}

提示和注意事项

为了打造更精致、更易于访问的体验,请考虑以下建议:

  • 考虑提供非手势缩放控件:有些用户可能难以使用手势。为了支持这些用户,请考虑提供一种不依赖于手势的替代方法来调整或重置缩放比例。
  • 针对所有缩放比例进行构建:针对应用内缩放以及系统级字体或显示设置测试界面。检查应用的布局是否能正确自适应,而不会破坏、重叠或隐藏内容。详细了解如何构建自适应布局。