Flutter中的Widget实在是太多了,很容易忽略不少实用的Widget。那么我我的很喜欢Flutter官方在YouTube上的Flutter Widget of the Week 系列视频。真的是能够发现宝藏,好比今天的主角Semantics。java
Semantics(语义)
用于描述Widget的含义最终达到描述应用程序的UI。这些描述能够经过辅助工具、搜索引擎和其余语义分析软件使用。它有点像HTML5的语义元素,在Android、iOS上更可能是用于读屏,帮助一些有视力障碍的人使用咱们的软件(Android TalkBack
和 iOS VoiceOver
)。git
说真的,作了几年的Android,基本没有关注过这方面的问题。惟一能想起来的就是给ImageView
添加contentDescription
属性,来描述一下图片的含义。但这确定远远不够。。。github
虽然咱们对Semantics
感到陌生 ,可是它在Flutter中能够说是无处不在。ide
举几个例子:Image
中就有 semanticLabel
和excludeFromSemantics(默认false)
这两个属性,一个用于描述图片语义,一个表示是否去除图片语义。源码中表现为: 工具
Semantics
,同时
image
属性为true,告诉咱们这个Widget是一个图片。
再看一个例子:IconButton
的语义属性button
为true,告诉咱们这个Widget是个按钮,是否可点击经过onPressed来决定。在IconButton
中有一个tooltip
属性。添加了tooltip
最终就是嵌套一个Tooltip
组件。 学习
Tooltip
它其实就是一个万能的语义。咱们正常使用时,长按就能够看到描述Widget的信息。使用读屏时,能够直接读出对应的描述信息。
RenderObject
的
void describeSemanticsConfiguration(SemanticsConfiguration config)
这个重写方法中实现。将组件的语义添加至
SemanticsConfiguration
中。好比
Text
:
semanticsLabel
属性,那么就使用
ExcludeSemantics
去除默认生成的语义,以
semanticsLabel
为准。
默认的语义在TextSpan
中的 computeSemanticsInformation
方法实现:优化
@override
void 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
中。搜索引擎
@override
void 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;
}
}
复制代码
最后介绍几个概念:spa
当Flutter渲染控件树时,它还会维护第二个控件树,称为Semantics Tree。debug
Semantics Tree的每一个节点都是SemanticsNode
,它可能对应于一个或一组Widget。
每一个SemanticsNode
都会对应一个SemanticsConfiguration
,保存着语义属性信息。
点到为止,扯得有点多了。这部分主要为了说明Flutter已经在提供的Widget中全面支持了语义。下来讲说具体怎么去使用。
上面的源码中,咱们应该已经接触到了Semantics
和 ExcludeSemantics
。下面详细介绍一下:
语义组件包含功能不少,当前有50个属性。这里我介绍一些重要的属性:
label
: 提供Widget的文本描述。也就是基础的语义信息。
container
: 该节点是否在语义树中引入一个新的语义节点(SemanticsNode)。它能够不受上层的语义拆分、合并,也就是独立出来。
explicitChildNodes
: 默认为false,表示是否强制显示子Widget的语义信息。能够理解为拆分语义。
scopesRoute
: 若是非空,该节点是否对应于子树的根,该子树应该声明路由名。一般与explicitChildNodes
一块儿设置为true,使用在路由跳转地方,好比页面的跳转,Dialog
、BottomSheet
、PopupMenu
的弹出部分。
好比MaterialPageRoute
中以下:
namesRoute
: 若是非空,则节点是否包含路由的语义标签。好比AppBar
上的title,就表示当前路由名称。其余的属性见名知意,我就很少解释了。语义说到这里,可能你仍是以为很抽象。那么你能够在MaterialApp
中添加showSemanticsDebugger: true
来查看语义视图。
做用是排除子Widget中的语义。好比有张图片只是装饰做用并不须要解释含义,可使用ExcludeSemantics
。
它放弃了在它以前的同一个语义容器中绘制的全部Widget的语义。这个Widget不多用到,整个Flutter源码中也就只有Drawer
中用到了它,当抽屉打开时,能够去除其余语义,避免读屏器读出被抽屉覆盖的语义内容,形成使用者的困扰。
用索引表示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(),
],
)
复制代码
做用是将其子Widget的语义合并在一块儿。这个Widget我认为是颇有重要的,经过它咱们能够将信息合并,便于阅读。
我用一个简单的页面举例:
上图中都是图片及文字,咱们来看看它的语义视图。
其实优化的方法很简单。
去除Image
的语义,使用咱们上面提到的excludeFromSemantics
属性或是ExcludeSemantics
都行。我在处理Image
的语义时比较极端,将excludeFromSemantics
都改成了true。个人理解是大多数的图片都是装饰做用,屏幕上过多的语义描述也会带来没必要要的困扰。若是有点击事件的图或者须要描述的图片,单独添加Semantics
。
合并语义,将纵向排列的一组内容信息用MergeSemantics
包裹便可。
代码这里就不贴出来,能够点击这里查看。
那么最终的效果以下:
不用我多说什么了吧,效果一目了然。固然这个例子只是一个很简单的示例。若是给CustomPainter
添加语义,就相对复杂了。这部分咱们能够参考TimePicker
的处理,或者Flutter Deer中饼状图的处理:
![]() |
![]() |
![]() |
---|---|---|
饼状图页面 | 饼状图未添加语义 | 饼状图添加语义 |
我这里就不展开说了,有兴趣的能够去了解一下。
语义信息的完整。好比日历上显示的都是数字,咱们须要将完整的日期信息补足。
语义信息的整合。这个就是上面的例子,将同类信息合并,便于阅读。
去除多余的语义信息。尽可能保证语义的简洁,好比图片这类的语义咱们大多数均可以忽略。
其实Flutter已经帮咱们作了不少语义化的工做,甚至考虑的很全面(因此学习它的方法就在源码中)。咱们真正须要处理的内容并很少。
我本身在添加语义的过程当中,也尝试体验了TalkBack,尽管是看着手机操做的,可是仍是很不方便。不可思议一个没有语义适配的页面是多么糟糕。
其实关于Semantics
的资料是不多的,甚至在发展成熟的Android上也不多有人说起(iOS的状况不清楚)。感受咱们忽略了一群人,尽管他们可能不会用到咱们的App。写这篇博客的初衷也是这样,补充一下这方面的资料,帮助有须要的人。
我也是根据本身的理解去实现语义化的,并不知道在实际的使用中是否是很合适。可是大方向必定没错。
最后我在个人开源项目Flutter Deer中也添加了语义的支持,有兴趣的能够查看,欢迎交流这方面的内容!
最后最后,还不点个赞?给做者我一点鼓励!立刻过年了,看能不能在掘金冲个v3。月更伤不起啊!