使用JetPack撰写的更易于访问的图表:添加内容说明
#a11y #kotlin #android #jetpackcompose

数据可视化依赖于数据的可视化表示。这些数据通常用某种颜色组合描绘。在移动应用程序上,它们通常还包括依赖触摸的不同交互。

但是如果您的用户看不到怎么办?或者,如果它们是色盲的,并且颜色的组合对它们来说是看不见的吗?或者如果他们使用开关设备或硬件键盘进行导航?

根据我的经验,例如,对于这些用户组,应用程序的数据可视化通常无法访问。我想试验需要进行多少更改和代码,以使线图更容易访问。令我惊讶的是,代码的数量并不多。当然,试验花了一些时间,但是解决了这些问题后,我现在有一个可以在其他项目中使用的示例项目。

,由于我喜欢分享我所学到的知识,所以我正在编写这一系列博客文章,以帮助您使用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 Point10
  • 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年被突出显示:

Year 2019 from the graph has a white rectangle around it, and the points for each graph are also rectangles instead of circles. At the right bottom of the graph values are visible: 2019: All: 27%, Eng.: 26% and ICT: 28%.

我们现在有一个荧光笔组件。当前的实现仅在使用指针输入时概述了当前的选择,因此尚未改善可访问性。接下来,让我们添加一些内容说明以使图形更易于访问。

添加内容描述

接下来,我们要为使用辅助技术(例如对讲机)的任何人添加内容描述。我们首先想将focusable-修饰符添加到每个荧光笔的子元素中。顾名思义,它使元素变得可集中,这意味着具有不同辅助技术的人可以达到这一目标。没有它,该元素就会隐藏在屏幕读取器中,而不可集中使用,例如硬件键盘或开关设备。

第二步是为每个荧光笔的子元素添加内容描述。因为我们使用Box-component,所以我们需要使用semantics-Modifier的contentDescription-Property-与某些组件(例如Images)相反。

当每个突出显示的部分显示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

包起来

在这篇博客文章中,我们研究了图形示例项目的初始代码及其构建方式。然后,我们添加了一个荧光笔组件,以帮助我们完成创建更多可访问图的任务。最后,我们为每个突出显示的部分添加了一个内容描述。

请记住,此代码已简化,将其应用于代码库时,您可能需要找出如何调整它以正常工作。

您有任何疑问,评论或反馈,还是想说其他话?请分享;我想听!

博客文章中的链接