数据可视化依赖于数据的可视化表示。这些数据通常用某种颜色组合描绘。在移动应用程序上,它们通常还包括依赖触摸的不同交互。
但是如果您的用户看不到怎么办?或者,如果它们是色盲的,并且颜色的组合对它们来说是看不见的吗?或者如果他们使用开关设备或硬件键盘进行导航?
根据我的经验,例如,对于这些用户组,应用程序的数据可视化通常无法访问。我想试验需要进行多少更改和代码,以使线图更容易访问。令我惊讶的是,代码的数量并不多。当然,试验花了一些时间,但是解决了这些问题后,我现在有一个可以在其他项目中使用的示例项目。
,由于我喜欢分享我所学到的知识,所以我正在编写这一系列博客文章,以帮助您使用Jetpack Compose构建更易于访问的图表。我们将研究三个不同的方面:
- 为非视觉访问用户添加信息(例如,对讲用户)
- 除了基于触摸的交互之外,还添加键盘交互
- 将数据与其他方式区分开,而不是颜色
这是有关为非视觉访问用户(例如屏幕读取器用户)添加信息的第一篇博客文章。以下两个博客文章将涵盖添加键盘互动,并用其他方式将数据区分开来,而不是颜色。将来可能还有有关语音访问和增加触摸目标大小的其他帖子。
我的目标不是提供最终的解决方案,而是您可以采取并改进的想法以在代码库中使用它们。当然,图形可能更复杂,这些相对简单的解决方案可能对所有事物都不起作用,但我希望它们会为您提供提高图形可访问性的指导。
让我们首先讨论我为此实验准备的示例项目。
初始项目代码
因此,在开始有关使事情更容易访问的任何探索之前,我需要构建一个小型示例应用程序。您可以在Graph Example-project repository的此博客文章中找到所有代码。 main
-Branch包含所有更改的最终版本,而koude1-branch具有我开始调整的初始代码。
这是我构建的简短视频:
该应用程序具有一条线图,其中包含来自芬兰高等教育(从2015年开始)的女性申请人的数据:信息通信技术和工程(非ICT)。该图显示了这些领域的女性申请人的百分比,并显示了这两个领域的总百分比。
当用户触摸并水平将指针拖到图表上时,选定的年度百分比将显示在右下角。这些值不可用任何其他方式可用 - 因此,如果用户无法使用指针,他们会错过此信息。
从技术上讲,此图是使用Canvas-API构建的,该图为如何添加元素的内容描述添加了一些限制。这是我们想做的事情 - 因为这是将价值传达给看不见文本的人的一种方法。接下来,让我们看一下如何将它们添加到图表中。
将内容描述添加到图表上的项目
由于该图是如何利用帆布-API的,这意味着它完全隐藏在可访问性服务中。具体而言,这意味着有人使用,例如,Talckback无法达到图内的值。另外,由于X轴和Y轴上的标签是使用drawText
-Method构建的,因此它们不可用。这是对讲如何通过屏幕读取的示例:
在视频中,我正在带有对话中浏览屏幕。视频显示光标完全跳过了图。它读取了以前的标题和图表下的标签,但没有任何标签。让我们通过添加Highlighter
component开始改善体验。
添加Highlighter
-component
在这种情况下,Highlighter
-component是覆盖图的覆盖层,突出显示了所选部分,并且在聚焦时只能看到。从技术上讲,这是一个覆盖整个图的Box
,其较小的Box
元素是突出显示区域的大小。该组件还有助于改善键盘和切换交互,就像我们在后面的博客文章中看到的那样。
Highlighter
组件看起来像这样:
@Composable
fun Highlighter(
modifier: Modifier = Modifier,
widthBetweenPoints: Float,
pixelPointsForTotal: List<Point>,
pixelPointsForTech: List<Point>,
pixelPointsForIct: List<Point>,
highlightedX: Float?
) {
Box(
modifier
.fillMaxSize(),
) {
val sectionWidth = with(LocalDensity.current) {
widthBetweenPoints.toDp()
}
pixelPointsForTotal.forEachIndexed { index, point ->
val xOffset = ((index + 1) * widthBetweenPoints - widthBetweenPoints * 0.66f).toInt()
var isHighlighted by remember { mutableStateOf(false) }
var position by remember { mutableStateOf(Pair(0f, 0f)) }
if (highlightedX == null) isHighlighted = false
highlightedX?.let {
isHighlighted = it > (position.first - widthBetweenPoints) && it < (position.second - widthBetweenPoints)
}
Box(
modifier = Modifier
.fillMaxHeight()
.width(sectionWidth)
.offset { IntOffset(xOffset, 0) }
.border(
width = Graph.Highlighter.width,
color = if (isHighlighted) MaterialTheme.colorScheme.onBackground else Color.Transparent,
shape = RoundedCornerShape(Graph.Highlighter.borderRadius),
)
.onGloballyPositioned {
position =
Pair(
it.positionInParent().x,
it.positionInParent().x + it.size.width
)
}
) {
}
}
}
}
它采用以下参数:
- 修饰符:修饰符 - 用于传递样式的修饰符。默认为
Modifier
- widthbettoppoints:float-顾名思义,点之间的宽度。它用于正确定位突出显示部分
- pixelpointsfortotal:列表 - 总计值as as
Point
10 - pixelpointsfortech:列表 - 工程值列表为
Point
(应重命名为pixelPointsForEng
) - pixelpointsforict:列表 - ICT字段的值列表为
Point
- imhinlightdx:浮动? - 当前突出显示项目的X值
注意:Point
是代码中定义的数据类,定义看起来像:
data class Point(
val x: Float,
val y: Float,
val year: Int,
val percentage: Float,
val isHighlighted: Boolean = false,
) {
val percentageString = "${percentage.toInt()} %"
}
组件内部,总计的所有像素点值(但这可能是任何列表)被映射,Lambda返回每个点的突出显示部分。每个部分的偏移均在widthBetweenPoints.
的帮助下计算
我们还需要获得荧光笔的位置。这些值是组件的开始和结束X坐标。我们将其保存到状态,在Box
-Component上,我们更改onGloballyPositioned
-Modifier的值:
var position by remember { mutableStateOf(Pair(0f, 0f)) }
...
.onGloballyPositioned {
position =
Pair(
it.positionInParent().x,
it.positionInParent().x + it.size.width
)
}
然后,我们可以使用这些值来确定当前选定的年份(highlightedX
-参数)是否在此荧光笔组件的区域内。
var isHighlighted by remember { mutableStateOf(false) }
...
if (highlightedX == null) isHighlighted = false
highlightedX?.let {
isHighlighted =
it > (position.first - widthBetweenPoints)
&& it < (position.second - widthBetweenPoints)
}
在代码段中,isHighlighted
如果指针在其区域内,则存储该值。如果highlightedX
为null(图上没有指针输入),则isHighlighted
为false。
然后,我们可以使用isHighlighted
更改border
-Modifier中区域的边框颜色:
.border(
width = Graph.Highlighter.width,
color = if (isHighlighted) MaterialTheme.colorScheme.onBackground else Color.Transparent,
shape = RoundedCornerShape(Graph.Highlighter.borderRadius),
)
此图显示了当前状态,当时2019年被突出显示:
我们现在有一个荧光笔组件。当前的实现仅在使用指针输入时概述了当前的选择,因此尚未改善可访问性。接下来,让我们添加一些内容说明以使图形更易于访问。
添加内容描述
接下来,我们要为使用辅助技术(例如对讲机)的任何人添加内容描述。我们首先想将focusable
-修饰符添加到每个荧光笔的子元素中。顾名思义,它使元素变得可集中,这意味着具有不同辅助技术的人可以达到这一目标。没有它,该元素就会隐藏在屏幕读取器中,而不可集中使用,例如硬件键盘或开关设备。
第二步是为每个荧光笔的子元素添加内容描述。因为我们使用Box
-component,所以我们需要使用semantics
-Modifier的contentDescription
-Property-与某些组件(例如Image
s)相反。
当每个突出显示的部分显示ICT,工程和总数的年度和价值时,这就是我们要添加到内容描述中的内容,因为它是该部分的相关信息。我们首先在pixelPointsForTotal.forEachIndexed {}
中形成内容描述:
val contentDesc =
"${point.year}: " +
"${stringResource(id = R.string.all)} ${point.percentageString}, " +
"${stringResource(id = R.string.eng)} ${pixelPointsForTech[index].percentageString}, " +
"${stringResource(id = R.string.ict)} ${pixelPointsForIct[index].percentageString}"
然后,我们添加focusable
修饰符,并使用我们在semantics
-Modifier中定义的内容描述:
...
.focusable()
.semantics {
contentDescription = contentDesc
},
这样,当用户专注于带有屏幕读取器的突出显示部分时,他们会听到(或插入盲文):
complete difference in code for this section is available in this PR。
包起来
在这篇博客文章中,我们研究了图形示例项目的初始代码及其构建方式。然后,我们添加了一个荧光笔组件,以帮助我们完成创建更多可访问图的任务。最后,我们为每个突出显示的部分添加了一个内容描述。
请记住,此代码已简化,将其应用于代码库时,您可能需要找出如何调整它以正常工作。
您有任何疑问,评论或反馈,还是想说其他话?请分享;我想听!