本篇是“说说”系列第二篇,另两篇链接奉上:

  • 说说Flutter中的RepaintBoundary

  • 说说Flutter中最熟悉的陌生人 —— Key

Flutter中的Widget实在是太多了,很容易忽略很多实用的Widget。那么我个人很喜欢Flutter官方在YouTube上的Flutter Widget of the Week 系列视频。真的是可以发现宝藏,比如今天的主角Semantics。

介绍

Semantics(语义) 用于描述Widget的含义最终达到描述应用程序的UI。这些描述可以通过辅助工具、搜索引擎和其他语义分析软件使用。它有点像HTML5的语义元素,在Android、iOS上更多是用于读屏,帮助一些有视力障碍的人使用我们的软件(Android TalkBackiOS VoiceOver)。

说真的,做了几年的Android,基本没有关注过这方面的问题。唯一能想起来的就是给ImageView添加contentDescription属性,来描述一下图片的含义。但这肯定远远不够。。。

虽然我们对Semantics感到陌生 ,但是它在Flutter中可以说是无处不在。

举几个例子:Image中就有 semanticLabelexcludeFromSemantics(默认false)这两个属性,一个用于描述图片语义,一个表示是否去除图片语义。源码中表现为:

那么这里就使用到了Semantics,同时image属性为true,告诉我们这个Widget是一个图片。

再看一个例子:IconButton的语义属性button为true,告诉我们这个Widget是个按钮,是否可点击通过onPressed来决定。在IconButton中有一个tooltip属性。添加了tooltip最终就是嵌套一个Tooltip组件。

Tooltip它其实就是一个万能的语义。我们正常使用时,长按就可以看到描述Widget的信息。使用读屏时,可以直接读出对应的描述信息。

而对于基础组件的语义,比如Text、Switch、TextField等都在RenderObjectvoid describeSemanticsConfiguration(SemanticsConfiguration config)这个重写方法中实现。将组件的语义添加至SemanticsConfiguration中。比如Text

如果Text添加了semanticsLabel属性,那么就使用ExcludeSemantics去除默认生成的语义,以semanticsLabel为准。

默认的语义在TextSpan 中的 computeSemanticsInformation方法实现:

@overridevoid computeSemanticsInformation(List<InlineSpanSemanticsInformation> collector) {assert(debugAssertIsValid());if (text != null || semanticsLabel != null) {collector.add(InlineSpanSemanticsInformation( ///<- 添加text,semanticsLabel: semanticsLabel,recognizer: recognizer,));}if (children != null) {for (InlineSpan child in children) {child.computeSemanticsInformation(collector);}}}List<InlineSpanSemanticsInformation> getSemanticsInformation() {final List<InlineSpanSemanticsInformation> collector = <InlineSpanSemanticsInformation>[];computeSemanticsInformation(collector);return collector;}

最终在describeSemanticsConfiguration方法调用getSemanticsInformation获取语义描述,添加至SemanticsConfiguration中。

@overridevoid describeSemanticsConfiguration(SemanticsConfiguration config) {super.describeSemanticsConfiguration(config);_semanticsInfo = text.getSemanticsInformation(); // <- 获取if (_semanticsInfo.any((InlineSpanSemanticsInformation info) => info.recognizer != null)) {config.explicitChildNodes = true;config.isSemanticBoundary = true;} else {final StringBuffer buffer = StringBuffer();for (InlineSpanSemanticsInformation info in _semanticsInfo) {buffer.write(info.semanticsLabel ?? info.text);}config.label = buffer.toString();config.textDirection = textDirection;}}

最后介绍几个概念:

  • 当Flutter渲染控件树时,它还会维护第二个控件树,称为Semantics Tree。

  • Semantics Tree的每个节点都是SemanticsNode,它可能对应于一个或一组Widget。

  • 每个SemanticsNode都会对应一个SemanticsConfiguration,保存着语义属性信息。

点到为止,扯得有点多了。这部分主要为了说明Flutter已经在提供的Widget中全面支持了语义。下来说说具体怎么去使用。

使用

上面的源码中,我们应该已经接触到了SemanticsExcludeSemantics。下面详细介绍一下:

Semantics

语义组件包含功能很多,当前有50个属性。这里我介绍一些重要的属性:

  • label: 提供Widget的文本描述。也就是基础的语义信息。

  • container: 该节点是否在语义树中引入一个新的语义节点(SemanticsNode)。它可以不受上层的语义拆分、合并,也就是独立出来。

  • explicitChildNodes: 默认为false,表示是否强制显示子Widget的语义信息。可以理解为拆分语义。

  • scopesRoute: 如果非空,该节点是否对应于子树的根,该子树应该声明路由名。通常与explicitChildNodes一起设置为true,使用在路由跳转地方,比如页面的跳转,DialogBottomSheetPopupMenu 的弹出部分。

比如MaterialPageRoute中如下:

这样做的目的也是为了将各个Widget的语义信息显示出来,避免有些语义信息读取不出。

  • namesRoute: 如果非空,则节点是否包含路由的语义标签。比如AppBar上的title,就表示当前路由名称。

其他的属性见名知意,我就不多解释了。语义说到这里,可能你还是觉得很抽象。那么你可以在MaterialApp中添加showSemanticsDebugger: true来查看语义视图。

ExcludeSemantics

作用是排除子Widget中的语义。比如有张图片只是装饰作用并不需要解释含义,可以使用ExcludeSemantics

BlockSemantics

它放弃了在它之前的同一个语义容器中绘制的所有Widget的语义。这个Widget很少用到,整个Flutter源码中也就只有Drawer中用到了它,当抽屉打开时,可以去除其他语义,避免读屏器读出被抽屉覆盖的语义内容,造成使用者的困扰。

IndexedSemantics

用索引表示Widget的语义。索引被TalkBack/Voiceover用来通知当前滚动状态。比如ListView默认实现了它。并且ListView也会将item中的语义合并,便于阅读。

当然你也可以自定义索引语义。下面的例子处理了一个语义无关的Spacer分隔符。默认的索引语义会给Spacer提供一个语义索引,会导致滚动通知错误地告诉用户有四个可见item。

ListView(addSemanticIndexes: false, /// <-- 去除默认的索引语义semanticChildCount: 2,  /// <-- 指定真实的语义数量children: const <Widget>[IndexedSemantics(index: 0, child: Text('First')), /// <---添加对应的索引Spacer(),IndexedSemantics(index: 1, child: Text('Second')),Spacer(),],)

MergeSemantics

作用是将其子Widget的语义合并在一起。这个Widget我认为是很有重要的,通过它我们可以将信息合并,便于阅读。

使用案例

我用一个简单的页面举例:

上图中都是图片及文字,我们来看看它的语义视图。


可以看到图片的部分因为没有添加语义,导致里面没有描述内容。有文字的部分,Widget的触摸范围很小,不便于操作,并且信息也不集中。你试想一下,一个视力障碍的人,怎么知道哪两段文字是一体?你是横向排列还是纵向排列?这个页面在他们那里就是失败的,即使你做的UI效果再漂亮。

其实优化的方法很简单。

  • 去除Image的语义,使用我们上面提到的excludeFromSemantics属性或是ExcludeSemantics都行。我在处理Image的语义时比较极端,将excludeFromSemantics都改为了true。我的理解是大多数的图片都是装饰作用,屏幕上过多的语义描述也会带来不必要的困扰。如果有点击事件的图或者需要描述的图片,单独添加Semantics

  • 合并语义,将纵向排列的一组内容信息用MergeSemantics包裹即可。

代码这里就不贴出来,可以点击这里查看。

那么最终的效果如下:

不用我多说什么了吧,效果一目了然。当然这个例子只是一个很简单的示例。如果给CustomPainter 添加语义,就相对复杂了。这部分我们可以参考TimePicker的处理,或者Flutter Deer中饼状图的处理:

饼状图页面 饼状图未添加语义 饼状图添加语义

我这里就不展开说了,有兴趣的可以去了解一下。

语义添加的原则

  • 语义信息的完整。比如日历上显示的都是数字,我们需要将完整的日期信息补足。

  • 语义信息的整合。这个就是上面的例子,将同类信息合并,便于阅读。

  • 去除多余的语义信息。尽量保证语义的简洁,比如图片这类的语义我们大多数都可以忽略。

有话想说

其实Flutter已经帮我们做了很多语义化的工作,甚至考虑的很全面(所以学习它的方法就在源码中)。我们真正需要处理的内容并不多。

我自己在添加语义的过程中,也尝试体验了TalkBack,尽管是看着手机操作的,但是还是很不方便。难以想象一个没有语义适配的页面是多么糟糕。

其实关于Semantics的资料是很少的,甚至在发展成熟的Android上也很少有人提及(iOS的情况不清楚)。感觉我们忽略了一群人,尽管他们可能不会用到我们的App。写这篇博客的初衷也是这样,补充一下这方面的资料,帮助有需要的人。

我也是根据自己的理解去实现语义化的,并不知道在实际的使用中是不是很合适。但是大方向一定没错。

最后我在我的开源项目Flutter Deer中也添加了语义的支持,有兴趣的可以查看,欢迎交流这方面的内容!

最后最后,还不点个赞?给作者我一点鼓励!

参考

  • Flutter: Semantics控件

说说Flutter中的Semantics相关推荐

  1. element中有多个合计_深入理解 Flutter 中的 Widget, Element, RenderObject

    这篇文章基于 Flutter stable v1.7 总结下 Flutter 当前的 UI 系统以及相关的概念, 在最后会通过自己组合一个 Gradient Button 按钮的方式来熟悉 Flutt ...

  2. 详解Flutter中各种Binding

    前言 Flutter中所有的运转都是在各种Binding中调度的,也正是这些绑定器的存在彻底解耦了Widget . Element .RenderObject 对 Platform端的依赖,阅读此文需 ...

  3. java hasfocus_说说Flutter中的无名英雄 —— Focus

    Focus系列的Widget及功能类在Flutter中可以说是无名英雄的存在,默默的付出但却不太为人所知.在日常开发使用中也不太会用到它,这是为什么呢?带着这个问题我们开始今天的内容. 1.Focus ...

  4. 初识Flutter中的Layer

    初识Flutter中的Layer 开篇 接触Flutter开发一段时间后发现自己对Flutter渲染流程重要的一环Layer的认知比较少,虽然Flutter对Widget的封装非常全面了开发者基本上只 ...

  5. flutter中的路由跳转

    在前面的基本路由和命名路由中,都演示了如何进行路由跳转,并且在路由跳转以后,可以借用系统自带的按钮就行返回上一级,当然了,也可以自定义按钮返回上一级. 返回上一级 在前面的例子中,当从Home.dar ...

  6. flutter中的生命周期

    前言 和其他的视图框架比如android的Activity一样,flutter中的视图Widget也存在生命周期,生命周期的回调函数提现在了State上面.理解flutter的生命周期,对我们写出一个 ...

  7. 一招教会你处理Flutter中的数据

    目录传送门:<Flutter快速上手指南>先导篇 在一个 App 中,数据类是必不可少. 我们需要从接口请求数据(通常为 JSON 格式),然后解析成对象,再使用它. 看看在 Flutte ...

  8. flutter中的生命周期函数

    前言:生命周期是一个组件加载到卸载的整个周期,熟悉生命周期可以让我们在合适的时机做该做的事情, flutter中的State生命周期和android以及React Native的生命周期类似. 先看一 ...

  9. 在Flutter中嵌入Native组件的正确姿势

    引言 在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一个很好的选择.本文希望向大家介绍AndroidView的使用方式 ...

最新文章

  1. Windows 系统下使用grep 命令
  2. java 取得日期_java-如何从某个日期获取日期列表?
  3. Storm累计求和Demo并且在集群上运行
  4. leetcode-14-最长公共前缀
  5. 2019牛客暑期多校训练营(第八场)G Gemstones(模拟)
  6. 流媒体测试笔记记录之————阿里云监控、OBS、FFmpeg拉流和推流变化比较记录...
  7. 市值缩水超千亿,汇顶科技站上悬崖边
  8. Ado.Net SQL语句参数化(SqlParameter用法)(多条件模糊查询的实现)
  9. 视频翻译字幕的软件哪个好?看完你就知道了
  10. 你真的搞懂Class,class了么?
  11. win的反义词_小学英语常见的英语单词反义词大汇总,一定要让孩子掌握!
  12. 计算机电子表格编辑栏,怎么在Excel中添加开发工具到工具栏
  13. maven报错The forked VM terminated without saying properly goodbye. VM crash or System.exit called
  14. java窗体jmeun刷新,java – JME 3 Swing,多幅画布
  15. leaflet图标样式
  16. torch.atan2函数详细解答
  17. samba xp linux共享文件
  18. 宇信易诚 为何成长如此之快
  19. 《老炮儿》的江湖道义就是互联网创业的规矩?
  20. 抖音取图表情包小程序源码+创作者入驻+流量主

热门文章

  1. LVM 存储系统里 命令行 lv vg pv 的关系
  2. 53KF 客服系统管理功能速成
  3. 计算机毕业设计 最新题目 选题 推荐 毕业设计 - 毕设指导 开题报告
  4. 北上资金 python_你都用 Python 来做什么?
  5. Echarts 区域缩放以及设置Y轴显示
  6. cmd如何打开某一盘符下的文件
  7. 表格计算机考试基础知识及重点试题,人社厅计算机考试题库-计算机等级考试一级的资料今年大一期末考试会考相关的试题请问大家有 爱问知识人...
  8. POI处理PPT的表格table,XSLFTable样式设置
  9. 【技术贴】BSV如何实现真正的纳米支付
  10. 2小时倒计时——JS代码