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。月更傷不起啊!