RubyText for Flutter Widgets
I recently
used Flutter to make a Japanese app, and I need to use the above display effect. HTMLruby
Labels 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 aRow
orWrap
embedded manyColumn
Well, 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.
RubyText
All 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 withRubyTextData
Start the analysis:
const RubyTextData(
this.text, {
this.ruby,
this.style,
this.rubyStyle,
this.textDirection = TextDirection.rtl,
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
text
Required, indicating text, such as 【検CHE】in the above exampleruby
Not required, it means vibrating name, some words do not need ruby, so it can be emptystyle
for textrubyStyle
for rubytextDirection
Indicates 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 analysisRubyText
getList<RubyTextData>
Post processing flow:
- First put
RubyTextData
map to aWidgetSpan
- then use
Text.rich
Construct a Text Widget
What is special about this process is thatWidgetSpan
the 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 theRubySpanWidget
corresponds to oneRubyTextData
:
Note that HookWidget does not come with flutter, it is a three-party package => flutter_hooks
Let's analyze it line by linebuild
The internal implementation logic of the method:
final defaultTextStyle = DefaultTextStyle.of(context).style;
final boldTextOverride = MediaQuery.boldTextOverride(context);
- 1
- 2
defaultTextStyle
value 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,useMemorized
It 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,useMemoized
There 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 1
data
YesRubyTextData
- line 2
.inherit
Indicates whether to inherit the style of the parent node, such asTextSpan
- line 5
ballTextOverride
Used to determine whether the parent node is set to bold
-
then calculate
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)); }
- 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_measurementWidth
This 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 aTextPainter
object, then calllayout()
layout, and finally passwidth
property to get the width value. Of course, this is all happening in memory and not rendering out the graphics.
The above isRubyText
The source code analysis, the logic is very simple, welcome to communicate in the comment area, you can also add vx: feelang