支持可缩放的用户内容

实现双指张合缩放手势,以支持应用中的可缩放内容。这是提高无障碍功能的标准方法,可让用户直观地调整文字和界面元素的大小,以满足自己的需求。您的应用可以定义具有精细控制和情境行为的自定义缩放行为,从而提供比屏幕放大等系统级功能更易于用户发现的体验。

选择扩缩策略

本指南中介绍的策略会导致界面重新流动和重组,以适应屏幕宽度。这样一来,用户无需进行水平平移,也无需像以前那样以令人沮丧的“之字形”方式来阅读长文本行,从而显著提升了无障碍体验。

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

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

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

策略 密度缩放 字体缩放

行为

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

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

什么是体重秤

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

纯文字

演示

建议

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

界面类型

推荐的策略

推理

以阅读为主的布局

示例:新闻文章、即时通讯应用

密度或字体缩放比例

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

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

直观的结构化布局

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

密度缩放

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

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

字体缩放

此策略更具针对性,仅修改 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
        )
    }
}

共享演示界面

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

提示和注意事项

为了打造更完善且更易于访问的体验,请考虑以下建议:

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