10月22日 刊出
同一鍋湯,嚐起來就是淡了一點:Flutter 的字和 Skia 那張 256 格的調味表
一篇拆解 Flutter 文字在 iOS 上看起來比 SwiftUI 更淡的技術文章,被我們翻譯成廚房裡調味對照表與一鍋湯鹹淡的滋味故事。
2026 年 10 月 21 日,掘金上一位長期寫 Flutter 的開發者「戀貓de小郭」發了一篇文章,聊一件許多人看過卻說不出所以然的小事:為什麼同一套 SF Pro 字體、同樣的字號和顏色,Flutter 畫出來的字,就是比 iOS 原生的字看起來淡了一點、細了一點。
這件事聽起來離廚房很遠,其實很近。它像極了每個在家煮過湯的人都遇過的狀況:同一條排骨、同一鍋水、同一罐鹽,你照著媽媽的食譜一匙一匙加,喝起來就是比她煮的淡。材料沒變,火沒變,變的是中間那一道你自己不知道的換算。
先說開發者實際量到的數字。在 17pt、3 倍解析度的螢幕上對照,Flutter 文字的總 Ink(可以想成字跡的「墨量」)只有原生的 93.8%,邊緣像素的平均值從 0.495 掉到 0.413。就像你用同一支湯勺舀湯,明明勺子大小一樣,舀出來的份量就是少了那麼一截,舌頭說不上哪裡不對,但就是覺得薄。
問題出在哪?文章追下去,找到了字體渲染裡一個關鍵角色:Skia 在把字形放進 Glyph Atlas(一張預先快取常用字形的紋理表)之前,會先用一張 256 項的查找表,把每個邊緣像素的灰度值換算過一遍。灰度本來是 0 到 255,Skia 拿原本的數值去查表,再換成新的數值放回去。
這張表之於文字,就像貼在冰箱門上那張手寫的換算小抄之於廚房。食譜上寫「鹽少許」,你的少許和長輩的少許從來不是同一個量;湯勺有大有小,一平匙和一尖匙差得遠。那位寫文章的開發者算過,60% 的邊緣覆蓋率,經過 Flutter 那條曲線之後,只剩大約 43% 的墨量。幸好字形內部百分之百覆蓋的地方不受影響,所以一整行 17pt 的字統計下來,總墨量減少約 6.2%。就像一鍋湯裡,大塊的排骨還是那個味道,變淡的是每一口湯水的邊緣,喝的人未必說得出來,但就是感覺少了點厚度。
為什麼要特意做這張換算表?因為螢幕上的顏色數值和實際發光的亮度本來就不是線性的,就像鹽的重量和舌頭感受到的鹹度也不是一比一:湯越滾、水越多,同一克鹽的存在感就越稀薄。Skia 想用 Gamma 和 Contrast 去校正這段落差,立意良善,但在深色文字的路徑上,中間灰度被往更淺的方向推了。好心換算,換出來的字卻瘦了一圈。
更有意思的是,這件事連「誰掌廚」都解釋不了。Flutter 的 Impeller 在 iOS 上產生字形時,繞了一圈還是得透過 Skia 去拿 CoreGraphics 柵格化之後的結果,也就是說,前半段大家用的是同一批「食材像素」, SwiftUI、UIKit、CoreGraphics 畫出來的字逐像素一致,唯獨 Flutter 在進快取之前,被那張表動過手腳。這就像兩個廚師用同一攤買的菜、同一口爐子,其中一個總要在起鍋前多兌一勺水,不是火候的問題,是那道多出來的、他自己都沒意識到的習慣。
而這也是這類問題最麻煩的地方:被改淺的灰度值已經寫進 Glyph Atlas 裡了,事後你怎麼換文字顏色,那些邊緣像素也未必救得回來。湯在上桌前被兌了水,你再撒多少蔥花,也只是裝飾。
如果你也常在螢幕前琢磨這種「說不上來哪裡不對」的細節,可以回頭看看我們之前寫過的三口爐子上只有一鍋在滾,瓦斯費卻照三鍋收,同樣是渲染層的小決定,最後都落在你眼睛嚐到的那一口滋味上。
至於廚房裡的啟發就簡單多了:會做菜的人靠手感和對照,最怕的是自己不知道的那張換算表。下次覺得湯比記憶中淡,先別怪鹽,翻翻那把勺子、那張貼在冰箱上的小抄,答案常常藏在換算的那一步。