react-native-advanced-text 0.2.6 → 0.2.7

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,8 +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
- import kotlin.math.ceil
30
28
 
31
29
  class AdvancedTextView : TextView {
32
30
 
@@ -165,25 +163,9 @@ class AdvancedTextView : TextView {
165
163
  fun setAdvancedFontFamily(family: String) {
166
164
  if (fontFamily == family) return
167
165
  fontFamily = family
168
- typeface = resolveTypeface(family, Typeface.NORMAL)
166
+ typeface = Typeface.create(family, Typeface.NORMAL)
169
167
  }
170
168
 
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
169
  fun setMenuOptions(menuOptions: List<String>) {
188
170
  if (this.menuOptions == menuOptions) return
189
171
  this.menuOptions = menuOptions
@@ -289,13 +271,13 @@ class AdvancedTextView : TextView {
289
271
  setTextSize(fontSize)
290
272
 
291
273
  typeface = when (fontWeight) {
292
- "bold" -> resolveTypeface(fontFamily, Typeface.BOLD)
293
- "italic" -> resolveTypeface(fontFamily, Typeface.ITALIC)
294
- else -> resolveTypeface(fontFamily, Typeface.NORMAL)
274
+ "bold" -> Typeface.create(fontFamily, Typeface.BOLD)
275
+ "italic" -> Typeface.create(fontFamily, Typeface.ITALIC)
276
+ else -> Typeface.create(fontFamily, Typeface.NORMAL)
295
277
  }
296
278
 
297
279
 
298
- applyLineHeight(spannableString)
280
+ setLineSpacing(0f, lineHeightMultiplier)
299
281
 
300
282
  // Android's letterSpacing is in em; the prop is in dp/points. The ratio
301
283
  // dp / fontSize is scale-invariant, so no density conversion is needed.
@@ -312,41 +294,6 @@ class AdvancedTextView : TextView {
312
294
  }
313
295
  }
314
296
 
315
- /**
316
- * Applies `lineHeightMultiplier` as a CSS-style, exact-per-line-height span
317
- * instead of `setLineSpacing(0, multiplier)`.
318
- *
319
- * The Fabric ShadowNode measures `lineHeight` by asking React Native's own
320
- * TextLayoutManager to lay out a fragment with that lineHeight -- which RN
321
- * implements with its own CSS-style line-height span (every line's box is
322
- * forced to exactly `lineHeight`, ascent/descent redistributed evenly).
323
- * That is a *different* algorithm from `setLineSpacing`, which scales the
324
- * font's natural per-line advance instead; the two do not produce the same
325
- * total height, and the gap between them is added on every wrapped line,
326
- * so it grows with text length. Using the same CSS-style algorithm here
327
- * (see CssLineHeightSpan) keeps this view's rendered height identical to
328
- * what was measured, regardless of how many lines the text wraps onto.
329
- */
330
- private fun applyLineHeight(spannableString: SpannableString) {
331
- if (lineHeightMultiplier == 1.0f || spannableString.isEmpty()) {
332
- return
333
- }
334
-
335
- val metrics = paint.fontMetricsInt
336
- val naturalLineHeight = -metrics.ascent + metrics.descent
337
- if (naturalLineHeight <= 0) {
338
- return
339
- }
340
-
341
- val targetLineHeight = ceil(lineHeightMultiplier * naturalLineHeight).toInt()
342
- spannableString.setSpan(
343
- CssLineHeightSpan(targetLineHeight),
344
- 0,
345
- spannableString.length,
346
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
347
- )
348
- }
349
-
350
297
  override fun onDraw(canvas: Canvas) {
351
298
  drawHighlightedWordBackgrounds(canvas)
352
299
  super.onDraw(canvas)
@@ -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
+ AttributedString AdvancedTextViewShadowNode::getAttributedString() const {
98
46
  const auto& props = getConcreteProps();
99
47
 
100
48
  auto textAttributes = TextAttributes::defaultTextAttributes();
@@ -120,32 +68,13 @@ AttributedString AdvancedTextViewShadowNode::getAttributedString(
120
68
  textAttributes.letterSpacing = props.letterSpacing;
121
69
  }
122
70
 
123
- // The component treats `lineHeight` as a multiple of the font's own
124
- // natural line height (see CssLineHeightSpan.kt on Android / the
125
- // 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).
131
- if (props.lineHeight > 0 && props.lineHeight != 1.0) {
132
- #if defined(ANDROID)
133
- // Android's natural single-line height depends on the resolved
134
- // font/size/weight and isn't a fixed ratio of fontSize, so a constant
135
- // approximation drifts from the real value CssLineHeightSpan.kt derives
136
- // from Paint.getFontMetricsInt() -- by an amount that compounds with
137
- // every wrapped line, which is why the error scaled with paragraph
138
- // length. Measure the real natural height for these exact attributes
139
- // instead, the same way the view derives it.
140
- auto naturalLineHeight =
141
- measureNaturalSingleLineHeight(layoutContext, textAttributes);
142
- textAttributes.lineHeight = naturalLineHeight > 0
143
- ? props.lineHeight * naturalLineHeight
144
- : props.lineHeight * textAttributes.fontSize * kLineHeightFudgeFactor;
145
- #else
71
+ if (props.lineHeight > 0) {
72
+ // The component treats `lineHeight` as a multiple of the font size
73
+ // (see AdvancedTextView.mm / AdvancedTextView.kt). `TextAttributes`
74
+ // expects an absolute value; approximate the platform default single
75
+ // line height as 1.2 * fontSize.
146
76
  textAttributes.lineHeight =
147
- props.lineHeight * textAttributes.fontSize * kLineHeightFudgeFactor;
148
- #endif
77
+ props.lineHeight * textAttributes.fontSize * 1.2;
149
78
  }
150
79
 
151
80
  auto attributedString = AttributedString{};
@@ -180,7 +109,7 @@ Size AdvancedTextViewShadowNode::measureContent(
180
109
  textLayoutContext.surfaceId = getSurfaceId();
181
110
 
182
111
  auto measurement = textLayoutManager_->measure(
183
- AttributedStringBox{getAttributedString(layoutContext)},
112
+ AttributedStringBox{getAttributedString()},
184
113
  paragraphAttributes,
185
114
  textLayoutContext,
186
115
  layoutConstraints);
@@ -86,20 +86,7 @@ class AdvancedTextViewShadowNode final : public ConcreteViewShadowNode<
86
86
  * Builds a single-fragment `AttributedString` from the component props
87
87
  * (text + font attributes), mirroring how the native views render it.
88
88
  */
89
- AttributedString getAttributedString(const LayoutContext& layoutContext) const;
90
-
91
- #if defined(ANDROID)
92
- /*
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.
98
- */
99
- Float measureNaturalSingleLineHeight(
100
- const LayoutContext& layoutContext,
101
- const TextAttributes& baseTextAttributes) const;
102
- #endif
89
+ AttributedString getAttributedString() const;
103
90
 
104
91
  // Lazily created on first measure, then reused / copied across clones.
105
92
  mutable std::shared_ptr<const TextLayoutManager> textLayoutManager_;
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.7",
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",
@@ -1,47 +0,0 @@
1
- package com.advancedtext
2
-
3
- import android.graphics.Paint
4
- import android.text.style.LineHeightSpan
5
- import kotlin.math.ceil
6
- import kotlin.math.floor
7
-
8
- /**
9
- * A [LineHeightSpan] that forces every line's box to exactly [lineHeightPx],
10
- * following CSS `line-height` semantics: the difference between the target
11
- * height and the font's natural ascent+descent ("leading") is split evenly
12
- * above and below, and the very first line's top / very last line's bottom
13
- * are pinned back to the font's own ascent/descent so the half-leading at
14
- * the block's own edges doesn't inflate its total height.
15
- *
16
- * This exists because `AdvancedTextView`'s Fabric ShadowNode measures the
17
- * `lineHeight` prop through React Native's `TextLayoutManager`, which applies
18
- * `lineHeight` using this exact CSS-style algorithm internally -- a different
19
- * one from Android's own `TextView.setLineSpacing(0, multiplier)`. Rendering
20
- * with `setLineSpacing` instead of this span made the real height diverge
21
- * from the measured one by a per-line amount, which is why the discrepancy
22
- * grew with the number of wrapped lines.
23
- *
24
- * See: https://www.w3.org/TR/css-inline-3/#inline-height
25
- */
26
- internal class CssLineHeightSpan(private val lineHeightPx: Int) : LineHeightSpan {
27
-
28
- override fun chooseHeight(
29
- text: CharSequence,
30
- start: Int,
31
- end: Int,
32
- spanstartv: Int,
33
- v: Int,
34
- fm: Paint.FontMetricsInt
35
- ) {
36
- val leading = lineHeightPx - ((-fm.ascent) + fm.descent)
37
- fm.ascent -= ceil(leading / 2.0f).toInt()
38
- fm.descent += floor(leading / 2.0f).toInt()
39
-
40
- if (start == 0) {
41
- fm.top = fm.ascent
42
- }
43
- if (end == text.length) {
44
- fm.bottom = fm.descent
45
- }
46
- }
47
- }