RubyText for Flutter Widgets

​​I recently
used Flutter to make a Japanese app, and I need to use the above display effect. HTMLrubyLabels can achieve this purpose, but unfortunately Flutter can't, you can only do it yourself.

I thought it was relatively simple at first, not just aRoworWrapembedded manyColumnWell, I didn't realize that I was superficial until I read the source code of RubyText.

As shown in the figure above, Chinese characters in Japanese and their pronunciation are different from Chinese characters, not in units of words, that is to say, a Chinese character may correspond to multiple kana, and a kana may also correspond to multiple Chinese characters (relatively rare), Naturally it will bring a problem: Chinese characters and kana, two lines up and down, one long and one short, simply using Column layout is not good.

then what should we do? Let's take a look at the implementation of RubyText:

Source address: https://github.com/YeungKC/RubyText

usage

RubyText ( 
    [ 
      RubyTextData ( 
        'Check' , 
        ruby ​​:  ' Check' , 
      ) , 
    ] , 
  ) ;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

The text is divided into upper and lower lines, the upper line is called ruby , and the lower line is called text.

RubyTextAll I have to do is to type the ruby ​​and text so that the upper and lower lines look harmonious.

For a text with ruby:

  • If the length of ruby ​​is greater than text, then you need to increase the letter space of text to align the upper and lower lines to the left and right
  • On the contrary, you need to increase the letter space of ruby

For text without ruby, no calculation is required.


start withRubyTextDataStart the analysis:

const RubyTextData(
  this.text, {
  this.ruby,
  this.style,
  this.rubyStyle,
  this.textDirection = TextDirection.rtl,
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • textRequired, indicating text, such as 【検CHE】in the above example
  • rubyNot required, it means vibrating name, some words do not need ruby, so it can be empty
  • stylefor text
  • rubyStylefor ruby
  • textDirectionIndicates text direction

Then, RubyTextData is passed as an array toRubyText:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

The remaining parameters can be compared with the ones that come with FlutterText widget.

Focus on analysisRubyTextgetList<RubyTextData>Post processing flow:

  • First putRubyTextDatamap to aWidgetSpan
  • then useText.richConstruct a Text Widget

What is special about this process is thatWidgetSpanthe child is aRubySpanWidget:

class RubySpanWidget extends HookWidget {
    const RubySpanWidget(this.data, {Key? key}) : super(key: key);
    final RubyTextData data;
}
  • 1
  • 2
  • 3
  • 4

One of theRubySpanWidgetcorresponds to oneRubyTextData:

Note that HookWidget does not come with flutter, it is a three-party package => flutter_hooks

Let's analyze it line by linebuildThe internal implementation logic of the method:

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

defaultTextStylevalue fromDefaultTextStyle:

The text style to apply to descendant Text widgets which don’t have an explicit style.

This means that if you don't specify a style for descendant Text widgets, Flutter will use it by defaultDefaultTextStyle, this value must also be calculated by the parent node level by level.

Continue to analyze, here is the use of flutter hooksuseMemorized:

final result = useMemoized(
    () { 
        //...  
    },
    [defaultTextStyle, boldTextOverride, data],,
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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.

It can be known from the annotation,useMemorizedIt is used to cache more complex objects. If the keys do not change, the cached complex objects will not be recalculated.

T useMemoized<T>(
  T Function() valueBuilder, [
  List<Object?> keys = const <Object>[],
]) {
  return use(
    _MemoizedHook(
      valueBuilder,
      keys: keys,
    ),
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

As can be seen from the above source code,useMemoizedThere are two parameters:

  • valueBuilder => higher-order function, used to calculate
  • keys => Calculated input values.

Friends who know FP (functional programming) may know that in the world of FP, functions have no side effects, and one input corresponds to one output.

specific to hereuseMemoized, the same is true.

Its role is based on the keys ([defaultTextStyle, boldTextOverride, data]) to calculate the result, as long as the keys do not change, the calculation process will not be repeated.

Let's look at the calculation process in detail:

  • The first is to calculate the 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • line 1dataYesRubyTextData
    • line 2.inheritIndicates whether to inherit the style of the parent node, such asTextSpan
    • line 5ballTextOverrideUsed to determine whether the parent node is set to bold
  • then calculaterubyTextStyle:

    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));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • rubyText's fontSize is 2/3 of text

After these two styles are calculated, in order to align the upper and lower lines of ruby ​​and text, the letter space can be further calculated.

If ruby ​​or text is empty or has only one character, no calculation is needed because there is no letter space for a single character.

The logic of the calculation is relatively simple:

  • Calculate the width of ruby ​​and text respectively
  • Then calculate the difference between the two widths
  • If the width of ruby ​​is smaller than text, the difference is evenly distributed to ruby ​​as letter space, and vice versa

in_measurementWidthThis function is more critical:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Its logic is to construct aTextPainterobject, then calllayout()layout, and finally passwidthproperty to get the width value. Of course, this is all happening in memory and not rendering out the graphics.

The above isRubyTextThe source code analysis, the logic is very simple, welcome to communicate in the comment area, you can also add vx: feelang

Tags: RubyText for Flutter Widgets

Flutter flutter ruby android ios

Related: RubyText for Flutter Widgets