Flutter Widgets 之 RubyText
Posted feelang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter Widgets 之 RubyText相关的知识,希望对你有一定的参考价值。
最近在用 Flutter 做一个日语类 App,需要用到上面这种展示效果。html 的 ruby
标签可以达到这个目的,可惜 Flutter 不行,只能自己动手实现。
我一开始想得比较简单,不就是一个 Row
或者 Wrap
里面嵌入许多 Column
吗,直到读完 RubyText 的源码,我才发现是我肤浅了。
如上图所示,日语中的汉字和它的读音跟中文的汉字不一样,不是以字为单位,也就是说一个汉字可能对应多个假名,一个假名也可能对应多个汉字(比较少见),自然就会带来一个问题:汉字和假名,上下两行,一个长一个短,单纯用 Column 布局不好看。
那怎么办呢?一起来看下 RubyText 的实现方式:
源码地址:https://github.com/YeungKC/RubyText
用法
RubyText(
[
RubyTextData(
'検査',
ruby: 'けんさ',
),
],
);
分成上下两行的文本,上面一行叫 ruby,下面一行叫 text。
RubyText
要做的事情就是对 ruby 和 text 进行排版,使上下两行看起来比较和谐。
对于一个带 ruby 的 text:
- 如果 ruby 长度大于 text,那么需要加大 text 的 letter space,使上下两行左右对齐
- 反之,则需要增加 ruby 的 letter space
对于不带 ruby 的 text,则无需计算。
先从 RubyTextData
开始分析:
const RubyTextData(
this.text,
this.ruby,
this.style,
this.rubyStyle,
this.textDirection = TextDirection.rtl,
);
text
必传,表示文本,比如上例中的【検査】ruby
非必需,表示振り仮名,有些字无需 ruby,所以可以为空style
用于 textrubyStyle
用于 rubytextDirection
表示文本方向
然后,RubyTextData 作为数组传给 RubyText
:
class RubyText extends StatelessWidget
const RubyText(
this.data,
Key? key,
this.spacing = 0.0,
this.style,
this.rubyStyle,
this.textAlign,
this.textDirection,
this.softWrap,
this.overflow,
this.maxLines,
) : super(key: key);
final List<RubyTextData> data;
剩余参数可以对照 Flutter 自带的 Text
widget.
重点分析一下 RubyText
拿到 List<RubyTextData>
后的处理流程:
- 首先将
RubyTextData
映射成一个WidgetSpan
- 然后用
Text.rich
构造出 Text Widget
这个流程中比较特殊的地方是 WidgetSpan
的 child 是一个 RubySpanWidget
:
class RubySpanWidget extends HookWidget
const RubySpanWidget(this.data, Key? key) : super(key: key);
final RubyTextData data;
其中一个 RubySpanWidget
对应一个 RubyTextData
:
注意,HookWidget 不是 flutter 自带的,是一个三方 package => flutter_hooks
我们来逐行分析一下 build
方法的内部实现逻辑:
final defaultTextStyle = DefaultTextStyle.of(context).style;
final boldTextOverride = MediaQuery.boldTextOverride(context);
defaultTextStyle
值来自 DefaultTextStyle
:
The text style to apply to descendant Text widgets which don’t have an explicit style.
意思是,如果没有给后代 Text widgets 指定 style,Flutter 将会默认使用 DefaultTextStyle
,这个值肯定也是通过父节点一级一级计算出来的。
继续往下分析,这里用到了 flutter hooks 的 useMemorized
:
final result = useMemoized(
()
//...
,
[defaultTextStyle, boldTextOverride, data],,
)
Caches the instance of a complex object.
useMemoized will immediately call valueBuilder on first call and store its result. Later, when the HookWidget rebuilds, the call to useMemoized will return the previously created instance without calling valueBuilder.
A subsequent call of useMemoized with different keys will call useMemoized again to create a new instance.
通过注释可以知道,useMemorized
用于缓存比较复杂的对象,如果 keys 不发生变化,所缓存的复杂对象就不会被重新计算。
T useMemoized<T>(
T Function() valueBuilder, [
List<Object?> keys = const <Object>[],
])
return use(
_MemoizedHook(
valueBuilder,
keys: keys,
),
);
通过以上源码可以看出,useMemoized
有两个参数:
- valueBuilder => 高阶函数,用于计算
- keys => 计算的输入值。
了解 FP(函数式编程)的朋友可能知道,在 FP 的世界中,function 没有 side effects,一个 input 对应一个 output。
具体到这里的useMemoized
,也是一样的道理。
它的作用是根据 keys([defaultTextStyle, boldTextOverride, data]
)计算出 result,只要 keys 不发生变化,计算过程就不会重复执行。
我们具体看一下计算过程:
-
首先是计算 textStyle:
var effectiveTextStyle = data.style; if (effectiveTextStyle == null || effectiveTextStyle.inherit) effectiveTextStyle = defaultTextStyle.merge(effectiveTextStyle); if (boldTextOverride) effectiveTextStyle = effectiveTextStyle .merge(const TextStyle(fontWeight: FontWeight.bold)); assert(effectiveTextStyle.fontSize != null, 'must be has a font size.');
- 第1行的
data
是RubyTextData
- 第2行的
.inherit
表示是否继承父节点的 style,比如TextSpan
- 第5行的
boldTextOverride
用于判断父节点是否设置了加粗
- 第1行的
-
然后计算
rubyTextStyle
:final defaultRubyTextStyle = effectiveTextStyle.merge( TextStyle(fontSize: effectiveTextStyle.fontSize! / 1.5), ); // ruby text style var effectiveRubyTextStyle = data.rubyStyle; if (effectiveRubyTextStyle == null || effectiveRubyTextStyle.inherit) effectiveRubyTextStyle = defaultRubyTextStyle.merge(effectiveRubyTextStyle); if (boldTextOverride) effectiveRubyTextStyle = effectiveRubyTextStyle .merge(const TextStyle(fontWeight: FontWeight.bold));
- rubyText 的 fontSize 是 text 的 2/3
这两个 styles 计算出来之后,为了使 ruby 和 text 上下两行左右对齐,就可以进一步计算出 letter space 了。
如果 ruby 或 text 为空或者只有一个字符,则不需要计算,因为单个字符不存在 letter space。
计算的逻辑比较简单:
- 分别计算出 ruby 和 text 的宽度
- 然后计算两个宽度的差值
- 如果 ruby 宽度小于 text,则将差值作为 letter space 平均分配到 ruby 中去,反之亦然
其中 _measurementWidth
这个函数比较关键:
double _measurementWidth(
String text,
TextStyle style,
TextDirection textDirection = TextDirection.rtl,
)
final textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: textDirection,
textAlign: TextAlign.center,
)..layout();
return textPainter.width;
它的逻辑是用样式(style)和文本(text)构造出一个 TextPainter
对象,然后调用 layout()
进行布局,最后通过 width
属性获取宽度值。当然,这一切都是在内存中进行的,并没有渲染出图形。
以上就是 RubyText
的源码分析,逻辑很简单,欢迎评论区交流,也可加 vx: feelang
以上是关于Flutter Widgets 之 RubyText的主要内容,如果未能解决你的问题,请参考以下文章
Flutter Widgets 之 ListWheelScrollView
Flutter Widgets 之 FutureBuilder
meng5619 Flutter Widgets 之 BottomNavigationBar 和 BottomNavigationBarItem