react-native-advanced-text 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,7 +25,6 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
25
25
  import android.text.Selection
26
26
  import android.graphics.Typeface
27
27
  import androidx.core.text.getSpans
28
- import com.facebook.react.common.assets.ReactFontManager
29
28
  import kotlin.math.ceil
30
29
 
31
30
  class AdvancedTextView : TextView {
@@ -165,25 +164,9 @@ class AdvancedTextView : TextView {
165
164
  fun setAdvancedFontFamily(family: String) {
166
165
  if (fontFamily == family) return
167
166
  fontFamily = family
168
- typeface = resolveTypeface(family, Typeface.NORMAL)
167
+ typeface = Typeface.create(family, Typeface.NORMAL)
169
168
  }
170
169
 
171
- /**
172
- * `Typeface.create(family, style)` only resolves OS-registered families; it
173
- * silently substitutes a system font for anything bundled under
174
- * `assets/fonts/` (the RN/Expo custom-font pipeline) instead of failing.
175
- * The Fabric ShadowNode measures this component's text via
176
- * `TextLayoutManager`, which resolves fonts through `ReactFontManager` -
177
- * so a custom font here rendered with the substituted typeface has
178
- * different glyph widths than what was measured, wrapping onto a
179
- * different number of lines and producing extra height for some strings
180
- * but not others. Going through `ReactFontManager` keeps the rendered
181
- * typeface identical to the measured one (and is a no-op for system font
182
- * names, which it falls back to `Typeface.create` for anyway).
183
- */
184
- private fun resolveTypeface(family: String, style: Int): Typeface =
185
- ReactFontManager.getInstance().getTypeface(family, style, context.assets)
186
-
187
170
  fun setMenuOptions(menuOptions: List<String>) {
188
171
  if (this.menuOptions == menuOptions) return
189
172
  this.menuOptions = menuOptions
@@ -289,9 +272,9 @@ class AdvancedTextView : TextView {
289
272
  setTextSize(fontSize)
290
273
 
291
274
  typeface = when (fontWeight) {
292
- "bold" -> resolveTypeface(fontFamily, Typeface.BOLD)
293
- "italic" -> resolveTypeface(fontFamily, Typeface.ITALIC)
294
- else -> resolveTypeface(fontFamily, Typeface.NORMAL)
275
+ "bold" -> Typeface.create(fontFamily, Typeface.BOLD)
276
+ "italic" -> Typeface.create(fontFamily, Typeface.ITALIC)
277
+ else -> Typeface.create(fontFamily, Typeface.NORMAL)
295
278
  }
296
279
 
297
280
 
@@ -21,6 +21,13 @@ import kotlin.math.floor
21
21
  * from the measured one by a per-line amount, which is why the discrepancy
22
22
  * grew with the number of wrapped lines.
23
23
  *
24
+ * `start`/`end` here are the offsets of the *current line*, not the span's
25
+ * own range, so the first/last checks below correctly fire only once each
26
+ * per paragraph. Mirrors React Native's own
27
+ * `com.facebook.react.views.text.internal.span.CustomLineHeightSpan`
28
+ * (used internally for `<Text lineHeight={}>`) verbatim, so it inherits that
29
+ * implementation's on-device track record instead of a new one.
30
+ *
24
31
  * See: https://www.w3.org/TR/css-inline-3/#inline-height
25
32
  */
26
33
  internal class CssLineHeightSpan(private val lineHeightPx: Int) : LineHeightSpan {
@@ -11,7 +11,6 @@
11
11
 
12
12
  #include <algorithm>
13
13
  #include <cctype>
14
- #include <limits>
15
14
  #include <string>
16
15
 
17
16
  #include <react/renderer/attributedstring/AttributedStringBox.h>
@@ -33,10 +32,6 @@ std::string toLowerAscii(std::string value) {
33
32
  return value;
34
33
  }
35
34
 
36
- // Fallback only: an approximate ratio of a single line's natural height to
37
- // its font size, used if a real measurement (Android) isn't available/valid.
38
- constexpr Float kLineHeightFudgeFactor = 1.2;
39
-
40
35
  } // namespace
41
36
 
42
37
  AdvancedTextViewShadowNode::AdvancedTextViewShadowNode(
@@ -47,54 +42,7 @@ AdvancedTextViewShadowNode::AdvancedTextViewShadowNode(
47
42
  static_cast<const AdvancedTextViewShadowNode&>(sourceShadowNode)
48
43
  .textLayoutManager_) {}
49
44
 
50
- #if defined(ANDROID)
51
- Float AdvancedTextViewShadowNode::measureNaturalSingleLineHeight(
52
- const LayoutContext& layoutContext,
53
- const TextAttributes& baseTextAttributes) const {
54
- // Same font/size/weight/style as the real fragment, but with `lineHeight`
55
- // cleared so this reports the font's own, unscaled single-line height.
56
- auto probeAttributes = baseTextAttributes;
57
- probeAttributes.lineHeight = std::numeric_limits<Float>::quiet_NaN();
58
-
59
- auto probe = AttributedString{};
60
- probe.appendFragment(AttributedString::Fragment{
61
- .string = "M",
62
- .textAttributes = probeAttributes,
63
- .parentShadowView = ShadowView(*this),
64
- });
65
-
66
- auto paragraphAttributes = ParagraphAttributes{};
67
- // Measure the tight ascent+descent box (no extra top/bottom accent
68
- // padding). This is the same "natural line height" basis
69
- // CssLineHeightSpan.kt derives from `Paint.getFontMetricsInt()` on the
70
- // view side (which is unaffected by `includeFontPadding`, a Layout-level
71
- // setting) -- measuring with padding included here would double count it
72
- // once per rendered line once the resulting value is multiplied and
73
- // applied to the whole paragraph.
74
- paragraphAttributes.includeFontPadding = false;
75
-
76
- auto textLayoutContext = TextLayoutContext{};
77
- textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
78
- textLayoutContext.surfaceId = getSurfaceId();
79
-
80
- // Unconstrained so a single glyph can never be forced to wrap.
81
- auto unconstrainedLayoutConstraints = LayoutConstraints{
82
- {0, 0},
83
- {std::numeric_limits<Float>::infinity(),
84
- std::numeric_limits<Float>::infinity()}};
85
-
86
- auto measurement = textLayoutManager_->measure(
87
- AttributedStringBox{probe},
88
- paragraphAttributes,
89
- textLayoutContext,
90
- unconstrainedLayoutConstraints);
91
-
92
- return measurement.size.height;
93
- }
94
- #endif
95
-
96
- AttributedString AdvancedTextViewShadowNode::getAttributedString(
97
- const LayoutContext& layoutContext) const {
45
+ TextAttributes AdvancedTextViewShadowNode::baseTextAttributes() const {
98
46
  const auto& props = getConcreteProps();
99
47
 
100
48
  auto textAttributes = TextAttributes::defaultTextAttributes();
@@ -120,14 +68,59 @@ AttributedString AdvancedTextViewShadowNode::getAttributedString(
120
68
  textAttributes.letterSpacing = props.letterSpacing;
121
69
  }
122
70
 
71
+ return textAttributes;
72
+ }
73
+
74
+ #if defined(ANDROID)
75
+ Float AdvancedTextViewShadowNode::measureNaturalLineHeight(
76
+ const TextAttributes& baseTextAttributes,
77
+ const TextLayoutContext& textLayoutContext) const {
78
+ // `baseTextAttributes` deliberately has no `lineHeight` set, so this
79
+ // resolves to the font's own natural single-line height -- exactly what
80
+ // CssLineHeightSpan.kt multiplies on the view side.
81
+ auto probeString = AttributedString{};
82
+ probeString.appendFragment(AttributedString::Fragment{
83
+ .string = "M",
84
+ .textAttributes = baseTextAttributes,
85
+ .parentShadowView = ShadowView(*this),
86
+ });
87
+
88
+ auto probeParagraphAttributes = ParagraphAttributes{};
89
+ probeParagraphAttributes.maximumNumberOfLines = 1;
90
+ // Measure the tight ascent+descent box (no extra top/bottom accent
91
+ // padding). This is the same "natural line height" basis
92
+ // CssLineHeightSpan.kt derives from `Paint.getFontMetricsInt()` on the
93
+ // view side (which is unaffected by `includeFontPadding`, a Layout-level
94
+ // setting) -- measuring with padding included here would double count it
95
+ // once per rendered line once the resulting value is multiplied and
96
+ // applied to the whole paragraph.
97
+ probeParagraphAttributes.includeFontPadding = false;
98
+
99
+ auto measurement = textLayoutManager_->measure(
100
+ AttributedStringBox{probeString},
101
+ probeParagraphAttributes,
102
+ textLayoutContext,
103
+ LayoutConstraints{});
104
+
105
+ return measurement.size.height;
106
+ }
107
+ #endif
108
+
109
+ AttributedString AdvancedTextViewShadowNode::getAttributedString(
110
+ const TextAttributes& baseTextAttributes,
111
+ const TextLayoutContext& textLayoutContext) const {
112
+ const auto& props = getConcreteProps();
113
+
114
+ auto textAttributes = baseTextAttributes;
115
+
123
116
  // The component treats `lineHeight` as a multiple of the font's own
124
117
  // natural line height (see CssLineHeightSpan.kt on Android / the
125
118
  // NSParagraphStyle-based spacing in AdvancedTextView.mm on iOS) -- NOT the
126
- // absolute per-line value `TextAttributes.lineHeight` expects. A multiplier
127
- // of 1 *is* the natural line height, so there is nothing to override in
128
- // that case (overriding it with an approximation, as this used to do
129
- // unconditionally, made even the "no lineHeight override" case measure
130
- // taller or shorter than the real render).
119
+ // absolute per-line value `TextAttributes::lineHeight` expects. A
120
+ // multiplier of 1 *is* the natural line height, so there is nothing to
121
+ // override in that case (overriding it with an approximation, as this
122
+ // used to do unconditionally, made even the "no lineHeight override" case
123
+ // measure taller or shorter than the real render).
131
124
  if (props.lineHeight > 0 && props.lineHeight != 1.0) {
132
125
  #if defined(ANDROID)
133
126
  // Android's natural single-line height depends on the resolved
@@ -138,13 +131,13 @@ AttributedString AdvancedTextViewShadowNode::getAttributedString(
138
131
  // length. Measure the real natural height for these exact attributes
139
132
  // instead, the same way the view derives it.
140
133
  auto naturalLineHeight =
141
- measureNaturalSingleLineHeight(layoutContext, textAttributes);
134
+ measureNaturalLineHeight(textAttributes, textLayoutContext);
142
135
  textAttributes.lineHeight = naturalLineHeight > 0
143
136
  ? props.lineHeight * naturalLineHeight
144
- : props.lineHeight * textAttributes.fontSize * kLineHeightFudgeFactor;
137
+ : props.lineHeight * textAttributes.fontSize * 1.2;
145
138
  #else
146
139
  textAttributes.lineHeight =
147
- props.lineHeight * textAttributes.fontSize * kLineHeightFudgeFactor;
140
+ props.lineHeight * textAttributes.fontSize * 1.2;
148
141
  #endif
149
142
  }
150
143
 
@@ -171,16 +164,17 @@ Size AdvancedTextViewShadowNode::measureContent(
171
164
  std::make_shared<const TextLayoutManager>(getContextContainer());
172
165
  }
173
166
 
174
- auto paragraphAttributes = ParagraphAttributes{};
175
- paragraphAttributes.maximumNumberOfLines = 0; // unlimited -> natural wrapping
176
- paragraphAttributes.adjustsFontSizeToFit = false;
177
-
178
167
  auto textLayoutContext = TextLayoutContext{};
179
168
  textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
180
169
  textLayoutContext.surfaceId = getSurfaceId();
181
170
 
171
+ auto paragraphAttributes = ParagraphAttributes{};
172
+ paragraphAttributes.maximumNumberOfLines = 0; // unlimited -> natural wrapping
173
+ paragraphAttributes.adjustsFontSizeToFit = false;
174
+
182
175
  auto measurement = textLayoutManager_->measure(
183
- AttributedStringBox{getAttributedString(layoutContext)},
176
+ AttributedStringBox{
177
+ getAttributedString(baseTextAttributes(), textLayoutContext)},
184
178
  paragraphAttributes,
185
179
  textLayoutContext,
186
180
  layoutConstraints);
@@ -34,6 +34,7 @@
34
34
 
35
35
  #include <jsi/jsi.h>
36
36
  #include <react/renderer/attributedstring/AttributedString.h>
37
+ #include <react/renderer/attributedstring/TextAttributes.h>
37
38
  #include <react/renderer/components/AdvancedTextViewSpec/EventEmitters.h>
38
39
  #include <react/renderer/components/AdvancedTextViewSpec/Props.h>
39
40
  #include <react/renderer/components/AdvancedTextViewSpec/States.h>
@@ -42,6 +43,7 @@
42
43
  #include <react/renderer/core/LayoutContext.h>
43
44
  #include <react/renderer/core/ShadowNode.h>
44
45
  #include <react/renderer/core/ShadowNodeFragment.h>
46
+ #include <react/renderer/textlayoutmanager/TextLayoutContext.h>
45
47
 
46
48
  namespace facebook::react {
47
49
 
@@ -83,24 +85,42 @@ class AdvancedTextViewShadowNode final : public ConcreteViewShadowNode<
83
85
 
84
86
  private:
85
87
  /*
86
- * Builds a single-fragment `AttributedString` from the component props
87
- * (text + font attributes), mirroring how the native views render it.
88
+ * Builds the font-related `TextAttributes` from the component props
89
+ * (family/weight/style/size/letterSpacing), *excluding* `lineHeight`.
90
+ * Shared between the natural-line-height probe and the real measurement so
91
+ * both resolve to the exact same font.
88
92
  */
89
- AttributedString getAttributedString(const LayoutContext& layoutContext) const;
93
+ TextAttributes baseTextAttributes() const;
90
94
 
91
- #if defined(ANDROID)
92
95
  /*
93
- * Measures the natural (multiplier == 1) height of a single line set in
94
- * `baseTextAttributes`, by measuring a single glyph with `lineHeight`
95
- * cleared. Used to turn the component's `lineHeight` multiplier prop into
96
- * the absolute value `TextAttributes.lineHeight` expects, using this
97
- * font's *real* metrics instead of an approximation.
96
+ * `lineHeight` is exposed to JS as a multiple of the font's own natural
97
+ * line height (see CssLineHeightSpan.kt on Android), NOT the absolute
98
+ * per-line value `TextAttributes::lineHeight` expects. A hardcoded ratio
99
+ * (e.g. `fontSize * 1.2`) is only an approximation of a font's real
100
+ * natural single-line height, and native font tables (especially OEM
101
+ * ones) can deviate from it enough to make Yoga's measured block height
102
+ * visibly disagree with what the native view actually draws. To keep them
103
+ * in sync, measure a single line of the same resolved font (no
104
+ * `lineHeight` override) and use its height as the real multiplier base --
105
+ * with `includeFontPadding = false`, matching the tight ascent+descent
106
+ * basis `Paint.getFontMetricsInt()` gives `CssLineHeightSpan.kt` on the
107
+ * view side (Android only; iOS's own NSParagraphStyle-based spacing in
108
+ * AdvancedTextView.mm doesn't need this).
98
109
  */
99
- Float measureNaturalSingleLineHeight(
100
- const LayoutContext& layoutContext,
101
- const TextAttributes& baseTextAttributes) const;
110
+ #if defined(ANDROID)
111
+ Float measureNaturalLineHeight(
112
+ const TextAttributes& baseTextAttributes,
113
+ const TextLayoutContext& textLayoutContext) const;
102
114
  #endif
103
115
 
116
+ /*
117
+ * Builds a single-fragment `AttributedString` from the component props
118
+ * (text + font attributes), mirroring how the native views render it.
119
+ */
120
+ AttributedString getAttributedString(
121
+ const TextAttributes& baseTextAttributes,
122
+ const TextLayoutContext& textLayoutContext) const;
123
+
104
124
  // Lazily created on first measure, then reused / copied across clones.
105
125
  mutable std::shared_ptr<const TextLayoutManager> textLayoutManager_;
106
126
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": " Advanced text component for React Native with custom select options.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",