实现双指张合缩放手势,以支持应用中的可缩放内容。这是提高无障碍功能的标准方法,可让用户直观地调整文字和界面元素的大小,以满足自己的需求。您的应用可以定义具有精细控制和情境行为的自定义缩放行为,从而提供比屏幕放大等系统级功能更易于用户发现的体验。
选择扩缩策略
本指南中介绍的策略会导致界面重新流动和重组,以适应屏幕宽度。这样一来,用户无需进行水平平移,也无需像以前那样以令人沮丧的“之字形”方式来阅读长文本行,从而显著提升了无障碍体验。
延伸阅读:研究证实,对于低视力用户而言,与需要二维平移的界面相比,重排内容的可读性更高,也更易于浏览。如需了解详情,请参阅移动设备上的平移扫描内容与重排内容对比。
缩放所有元素或仅缩放文本元素
下表展示了每种缩放策略的视觉效果。
| 策略 | 密度缩放 | 字体缩放 |
|---|---|---|
行为 |
按比例缩放所有内容。内容会重新排布以适应容器,因此用户无需水平平移即可查看所有内容。 |
仅影响文本元素。整体布局和非文字组件保持相同的大小。 |
什么是体重秤 |
所有视觉元素:文字、组件(按钮、图标)、图片和布局间距(内边距、外边距) |
纯文字 |
演示 |
建议
现在,您已经了解了视觉上的差异,下表将帮助您权衡利弊,并为您的内容选择最佳策略。
界面类型 |
推荐的策略 |
推理 |
以阅读为主的布局 示例:新闻文章、即时通讯应用 |
密度或字体缩放比例 |
建议使用密度缩放来缩放整个内容区域,包括内嵌图片。 如果只需要缩放文字,字体缩放是一种简单明了的替代方案。 |
直观的结构化布局 示例:应用商店、社交媒体信息流 |
密度缩放 |
保留轮播界面或网格中图片与文字之间的视觉关系。重排特性可避免水平平移,而水平平移会与嵌套的滚动元素发生冲突。 |
在 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 ) } } }
提示和注意事项
为了打造更完善且更易于访问的体验,请考虑以下建议:
- 考虑提供非手势缩放控件:部分用户可能难以使用手势。为了支持这些用户,请考虑提供一种不依赖手势的替代方式来调整或重置缩放比例。
- 针对所有缩放比例进行构建:针对应用内缩放和系统级字体或显示设置测试界面。检查应用布局是否能正确适应,而不会出现内容中断、重叠或隐藏的情况。详细了解如何构建自适应布局。