Flutter Widgets 之 RubyText

Posted 老梁写代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter Widgets 之 RubyText相关的知识,希望对你有一定的参考价值。

​​
最近在用 Flutter 做一个日语类 App,需要用到上面这种展示效果。htmlruby 标签可以达到这个目的,可惜 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 用于 text
  • rubyStyle 用于 ruby
  • textDirection 表示文本方向

然后,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行的 dataRubyTextData
    • 第2行的 .inherit 表示是否继承父节点的 style,比如 TextSpan
    • 第5行的 boldTextOverride 用于判断父节点是否设置了加粗
  • 然后计算 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

Flutter Widgets 之 ShaderMask

Flutter Widgets 之 ShaderMask

meng5619 Flutter Widgets 之 BottomNavigationBar 和 BottomNavigationBarItem

Flutter之Cupertino (iOS风格) Widgets