0
点赞
收藏
分享

微信扫一扫

Flutter Widgets 之 RubyText

前行的跋涉者 2022-09-18 阅读 149

​​
最近在用 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 用于 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

我们来逐行分析一下 build 方法的内部实现逻辑:

final defaultTextStyle = DefaultTextStyle.of(context).style;
final boldTextOverride = MediaQuery.boldTextOverride(context);

defaultTextStyle 值来自 DefaultTextStyle

意思是,如果没有给后代 Text widgets 指定 style,Flutter 将会默认使用 DefaultTextStyle,这个值肯定也是通过父节点一级一级计算出来的。

继续往下分析,这里用到了 flutter hooks 的 useMemorized

final result = useMemoized(
    () { 
        //...  
    },
    [defaultTextStyle, boldTextOverride, data],,
)

通过注释可以知道,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

举报

相关推荐

0 条评论