react-native-advanced-text 0.2.8 → 0.2.11

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.
@@ -33,6 +33,7 @@ android {
33
33
  defaultConfig {
34
34
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
35
35
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
36
+ consumerProguardFiles "consumer-rules.pro"
36
37
  }
37
38
 
38
39
  buildFeatures {
@@ -0,0 +1,3 @@
1
+ # NativeTextMeasurement is only called from C++ (ShadowNodes.cpp) via JNI,
2
+ # so R8 can't see it's used and would strip or rename it.
3
+ -keep class com.advancedtext.NativeTextMeasurement { *; }
@@ -8,6 +8,9 @@ import java.util.ArrayList
8
8
 
9
9
  class AdvancedTextViewPackage : ReactPackage {
10
10
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
11
+ // Fabric measures text before any view exists, so hand the app's assets
12
+ // to the measurer as early as possible.
13
+ NativeTextMeasurement.assetManager = reactContext.assets
11
14
  val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
12
15
  viewManagers.add(AdvancedTextViewManager())
13
16
  return viewManagers
@@ -5,6 +5,7 @@ import android.graphics.Canvas
5
5
  import android.graphics.Color
6
6
  import android.graphics.Point
7
7
  import android.graphics.RectF
8
+ import android.os.Build
8
9
  import android.text.SpannableString
9
10
  import android.text.Spannable
10
11
  import android.text.Spanned
@@ -25,7 +26,6 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
25
26
  import android.text.Selection
26
27
  import android.graphics.Typeface
27
28
  import androidx.core.text.getSpans
28
- import kotlin.math.ceil
29
29
 
30
30
  class AdvancedTextView : TextView {
31
31
 
@@ -64,6 +64,15 @@ class AdvancedTextView : TextView {
64
64
  // React Native `style` instead, exactly like <Text>.
65
65
  setPadding(0, 0, 0, 0)
66
66
  setTextIsSelectable(true)
67
+ // Fabric measures this component's text via RN's own TextLayoutManager,
68
+ // which explicitly builds its StaticLayout with
69
+ // `setUseLineSpacingFromFallbacks(true)`. TextView's own equivalent
70
+ // (`setFallbackLineSpacing`) defaults differently depending on
71
+ // targetSdkVersion, so without this the two engines can compute
72
+ // slightly different natural line heights for the same font/text.
73
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
74
+ setFallbackLineSpacing(true)
75
+ }
67
76
 
68
77
  movementMethod = SmartMovementMethod
69
78
 
@@ -164,7 +173,7 @@ class AdvancedTextView : TextView {
164
173
  fun setAdvancedFontFamily(family: String) {
165
174
  if (fontFamily == family) return
166
175
  fontFamily = family
167
- typeface = Typeface.create(family, Typeface.NORMAL)
176
+ typeface = NativeTextMeasurement.resolveTypeface(family, Typeface.NORMAL)
168
177
  }
169
178
 
170
179
  fun setMenuOptions(menuOptions: List<String>) {
@@ -272,13 +281,13 @@ class AdvancedTextView : TextView {
272
281
  setTextSize(fontSize)
273
282
 
274
283
  typeface = when (fontWeight) {
275
- "bold" -> Typeface.create(fontFamily, Typeface.BOLD)
276
- "italic" -> Typeface.create(fontFamily, Typeface.ITALIC)
277
- else -> Typeface.create(fontFamily, Typeface.NORMAL)
284
+ "bold" -> NativeTextMeasurement.resolveTypeface(fontFamily, Typeface.BOLD)
285
+ "italic" -> NativeTextMeasurement.resolveTypeface(fontFamily, Typeface.ITALIC)
286
+ else -> NativeTextMeasurement.resolveTypeface(fontFamily, Typeface.NORMAL)
278
287
  }
279
288
 
280
289
 
281
- applyLineHeight(spannableString)
290
+ setLineSpacing(0f, lineHeightMultiplier)
282
291
 
283
292
  // Android's letterSpacing is in em; the prop is in dp/points. The ratio
284
293
  // dp / fontSize is scale-invariant, so no density conversion is needed.
@@ -289,45 +298,16 @@ class AdvancedTextView : TextView {
289
298
  }
290
299
 
291
300
 
292
- post {
293
- setText(spannableString, BufferType.SPANNABLE)
294
- Log.d(TAG, "Text updated with ${wordPositions.size} spans")
295
- }
296
- }
297
-
298
- /**
299
- * Applies `lineHeightMultiplier` as a CSS-style, exact-per-line-height span
300
- * instead of `setLineSpacing(0, multiplier)`.
301
- *
302
- * The Fabric ShadowNode measures `lineHeight` by asking React Native's own
303
- * TextLayoutManager to lay out a fragment with that lineHeight -- which RN
304
- * implements with its own CSS-style line-height span (every line's box is
305
- * forced to exactly `lineHeight`, ascent/descent redistributed evenly).
306
- * That is a *different* algorithm from `setLineSpacing`, which scales the
307
- * font's natural per-line advance instead; the two do not produce the same
308
- * total height, and the gap between them is added on every wrapped line,
309
- * so it grows with text length. Using the same CSS-style algorithm here
310
- * (see CssLineHeightSpan) keeps this view's rendered height identical to
311
- * what was measured, regardless of how many lines the text wraps onto.
312
- */
313
- private fun applyLineHeight(spannableString: SpannableString) {
314
- if (lineHeightMultiplier == 1.0f || spannableString.isEmpty()) {
315
- return
316
- }
317
-
318
- val metrics = paint.fontMetricsInt
319
- val naturalLineHeight = -metrics.ascent + metrics.descent
320
- if (naturalLineHeight <= 0) {
321
- return
322
- }
323
-
324
- val targetLineHeight = ceil(lineHeightMultiplier * naturalLineHeight).toInt()
325
- spannableString.setSpan(
326
- CssLineHeightSpan(targetLineHeight),
327
- 0,
328
- spannableString.length,
329
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
330
- )
301
+ // Set synchronously, not via `post {}`: in a virtualized list (e.g.
302
+ // FlashList) this view gets recycled for a new row on every scroll
303
+ // tick. A posted `setText` races the *next* recycle -- if this view
304
+ // is reused again before the queued runnable fires, whichever row's
305
+ // runnable happens to run last wins, and a row can end up rendering
306
+ // blank (with its correctly-measured height still reserved by
307
+ // Fabric) while its own `setText` was dropped. `setText` doesn't
308
+ // need to wait for anything here, so there's nothing to defer for.
309
+ setText(spannableString, BufferType.SPANNABLE)
310
+ Log.d(TAG, "Text updated with ${wordPositions.size} spans")
331
311
  }
332
312
 
333
313
  override fun onDraw(canvas: Canvas) {
@@ -0,0 +1,85 @@
1
+ package com.advancedtext
2
+
3
+ import android.content.res.AssetManager
4
+ import android.content.res.Resources
5
+ import android.graphics.Typeface
6
+ import android.text.StaticLayout
7
+ import android.text.TextPaint
8
+ import android.util.TypedValue
9
+ import com.facebook.react.common.assets.ReactFontManager
10
+
11
+ /**
12
+ * JNI bridge so the Fabric ShadowNode (C++ `measureContent()`) can ask
13
+ * Android's own `StaticLayout` -- the exact class `AdvancedTextView`'s real
14
+ * rendering is built on -- for this text's real height, instead of going
15
+ * through React Native's own text-measurement engine (`TextLayoutManager`).
16
+ *
17
+ * RN's own engine and Android's native `TextView` rendering are two
18
+ * independently-implemented paths that can drift apart by a few px per line
19
+ * (verified: RN's `TextLayoutManager` takes a different code path --
20
+ * `BoringLayout` vs `StaticLayout` -- for short/simple text than for real,
21
+ * multi-line paragraphs, and even StaticLayout-to-StaticLayout the two
22
+ * engines' resolved `Paint`/`Typeface` don't perfectly agree). Since this
23
+ * component doesn't reuse RN's own prepared text layout for drawing anyway
24
+ * (it re-renders through its own `TextView`), measuring with the same
25
+ * `StaticLayout` call the view itself is built on removes the drift
26
+ * entirely, rather than approximating it from the C++ side.
27
+ *
28
+ * Every parameter below is chosen to mirror exactly what `AdvancedTextView`
29
+ * itself configures on its own `TextView`/`Paint` in `updateTextWithHighlights()`
30
+ * -- font resolution, text size (SP, not raw px), `setLineSpacing`, and
31
+ * letter spacing. Spans (highlight/click color) are omitted: they don't
32
+ * affect font metrics or line breaking, only drawing.
33
+ */
34
+ object NativeTextMeasurement {
35
+
36
+ /** App assets, so fonts bundled under `assets/fonts/` resolve here too. */
37
+ @Volatile
38
+ var assetManager: AssetManager? = null
39
+
40
+ /**
41
+ * Resolves a font family the way React Native's own <Text> does: fonts
42
+ * registered at runtime (expo-font), then `assets/fonts/<family>.ttf|otf`,
43
+ * then system families. `AdvancedTextView` uses this too, so measuring and
44
+ * rendering always pick the same typeface.
45
+ */
46
+ @JvmStatic
47
+ fun resolveTypeface(fontFamily: String, style: Int): Typeface =
48
+ ReactFontManager.getInstance().getTypeface(fontFamily, style, assetManager)
49
+
50
+ @JvmStatic
51
+ fun measureHeight(
52
+ text: String,
53
+ fontFamily: String,
54
+ typefaceStyle: Int,
55
+ fontSizeSp: Float,
56
+ lineHeightMultiplier: Float,
57
+ letterSpacingDp: Float,
58
+ widthPx: Float
59
+ ): Float {
60
+ if (text.isEmpty() || widthPx <= 0f) {
61
+ return 0f
62
+ }
63
+
64
+ val displayMetrics = Resources.getSystem().displayMetrics
65
+ val fontSizePx =
66
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, fontSizeSp, displayMetrics)
67
+
68
+ val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG)
69
+ paint.typeface = resolveTypeface(fontFamily, typefaceStyle)
70
+ paint.textSize = fontSizePx
71
+ // Mirrors AdvancedTextView's own letterSpacing conversion: dp/points
72
+ // prop -> em, scale-invariant so no density conversion is needed.
73
+ if (letterSpacingDp != 0f && fontSizeSp > 0f) {
74
+ paint.letterSpacing = letterSpacingDp / fontSizeSp
75
+ }
76
+
77
+ val width = widthPx.toInt().coerceAtLeast(1)
78
+ val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
79
+ // `setLineSpacing(0, multiplier)` is exactly what AdvancedTextView
80
+ // itself applies for rendering -- see updateTextWithHighlights().
81
+ builder.setLineSpacing(0f, if (lineHeightMultiplier > 0f) lineHeightMultiplier else 1f)
82
+ val layout = builder.build()
83
+ return layout.height.toFloat()
84
+ }
85
+ }
@@ -11,6 +11,7 @@
11
11
 
12
12
  #include <algorithm>
13
13
  #include <cctype>
14
+ #include <cmath>
14
15
  #include <string>
15
16
 
16
17
  #include <react/renderer/attributedstring/AttributedStringBox.h>
@@ -21,6 +22,10 @@
21
22
  #include <react/renderer/textlayoutmanager/TextLayoutContext.h>
22
23
  #include <react/renderer/textlayoutmanager/TextLayoutManager.h>
23
24
 
25
+ #if defined(ANDROID)
26
+ #include <fbjni/fbjni.h>
27
+ #endif
28
+
24
29
  namespace facebook::react {
25
30
 
26
31
  namespace {
@@ -42,7 +47,7 @@ AdvancedTextViewShadowNode::AdvancedTextViewShadowNode(
42
47
  static_cast<const AdvancedTextViewShadowNode&>(sourceShadowNode)
43
48
  .textLayoutManager_) {}
44
49
 
45
- TextAttributes AdvancedTextViewShadowNode::baseTextAttributes() const {
50
+ AttributedString AdvancedTextViewShadowNode::getAttributedString() const {
46
51
  const auto& props = getConcreteProps();
47
52
 
48
53
  auto textAttributes = TextAttributes::defaultTextAttributes();
@@ -68,87 +73,82 @@ TextAttributes AdvancedTextViewShadowNode::baseTextAttributes() const {
68
73
  textAttributes.letterSpacing = props.letterSpacing;
69
74
  }
70
75
 
71
- return textAttributes;
72
- }
76
+ // Deliberately never sets `textAttributes.lineHeight`: the component's
77
+ // `lineHeight` prop is a *multiple* of the font's natural line height (see
78
+ // `setLineSpacing(0, multiplier)` in AdvancedTextView.kt / the
79
+ // NSParagraphStyle-based spacing in AdvancedTextView.mm on iOS), not the
80
+ // absolute per-line value `TextAttributes::lineHeight` expects. Forcing an
81
+ // absolute value here would make Fabric apply RN's own CSS-style
82
+ // forced-line-height algorithm, which is a *different* algorithm from
83
+ // `setLineSpacing`'s multiplicative one and drifts from it by an amount
84
+ // that compounds with every wrapped line. Measuring naturally instead and
85
+ // scaling the *result* in `measureContent()` uses the same "multiply the
86
+ // natural total" math `setLineSpacing` itself uses, so they agree exactly
87
+ // regardless of paragraph length.
73
88
 
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,
89
+ auto attributedString = AttributedString{};
90
+ attributedString.appendFragment(AttributedString::Fragment{
91
+ .string = props.text,
92
+ .textAttributes = textAttributes,
85
93
  .parentShadowView = ShadowView(*this),
86
94
  });
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;
95
+ return attributedString;
106
96
  }
107
- #endif
108
97
 
109
- AttributedString AdvancedTextViewShadowNode::getAttributedString(
110
- const TextAttributes& baseTextAttributes,
111
- const TextLayoutContext& textLayoutContext) const {
98
+ #if defined(ANDROID)
99
+ Float AdvancedTextViewShadowNode::measureHeightViaAndroidNative(
100
+ Float widthDp,
101
+ Float pointScaleFactor) const {
102
+ if (!std::isfinite(widthDp) || widthDp <= 0 || pointScaleFactor <= 0) {
103
+ return -1;
104
+ }
105
+
112
106
  const auto& props = getConcreteProps();
113
107
 
114
- auto textAttributes = baseTextAttributes;
115
-
116
- // The component treats `lineHeight` as a multiple of the font's own
117
- // natural line height (see CssLineHeightSpan.kt on Android / the
118
- // NSParagraphStyle-based spacing in AdvancedTextView.mm on iOS) -- NOT the
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).
124
- if (props.lineHeight > 0 && props.lineHeight != 1.0) {
125
- #if defined(ANDROID)
126
- // Android's natural single-line height depends on the resolved
127
- // font/size/weight and isn't a fixed ratio of fontSize, so a constant
128
- // approximation drifts from the real value CssLineHeightSpan.kt derives
129
- // from Paint.getFontMetricsInt() -- by an amount that compounds with
130
- // every wrapped line, which is why the error scaled with paragraph
131
- // length. Measure the real natural height for these exact attributes
132
- // instead, the same way the view derives it.
133
- auto naturalLineHeight =
134
- measureNaturalLineHeight(textAttributes, textLayoutContext);
135
- textAttributes.lineHeight = naturalLineHeight > 0
136
- ? props.lineHeight * naturalLineHeight
137
- : props.lineHeight * textAttributes.fontSize * 1.2;
138
- #else
139
- textAttributes.lineHeight =
140
- props.lineHeight * textAttributes.fontSize * 1.2;
141
- #endif
108
+ const auto weight = toLowerAscii(props.fontWeight);
109
+ // Matches Typeface.NORMAL/BOLD/ITALIC -- the same three-way mapping
110
+ // AdvancedTextView.kt uses for `typeface = when (fontWeight) { ... }`.
111
+ int typefaceStyle = 0; // Typeface.NORMAL
112
+ if (weight == "bold") {
113
+ typefaceStyle = 1; // Typeface.BOLD
114
+ } else if (weight == "italic") {
115
+ typefaceStyle = 2; // Typeface.ITALIC
142
116
  }
143
117
 
144
- auto attributedString = AttributedString{};
145
- attributedString.appendFragment(AttributedString::Fragment{
146
- .string = props.text,
147
- .textAttributes = textAttributes,
148
- .parentShadowView = ShadowView(*this),
149
- });
150
- return attributedString;
118
+ std::string fontFamily =
119
+ props.fontFamily.empty() ? "sans-serif" : props.fontFamily;
120
+ Float fontSize = props.fontSize > 0 ? props.fontSize : 16.0;
121
+ Float lineHeightMultiplier = props.lineHeight > 0 ? props.lineHeight : 1.0;
122
+ Float widthPx = widthDp * pointScaleFactor;
123
+
124
+ static const auto cls =
125
+ facebook::jni::findClassStatic("com/advancedtext/NativeTextMeasurement");
126
+ static const auto method = cls->getStaticMethod<jfloat(
127
+ std::string,
128
+ std::string,
129
+ jint,
130
+ jfloat,
131
+ jfloat,
132
+ jfloat,
133
+ jfloat)>("measureHeight");
134
+
135
+ Float heightPx = method(
136
+ cls,
137
+ props.text,
138
+ fontFamily,
139
+ typefaceStyle,
140
+ static_cast<jfloat>(fontSize),
141
+ static_cast<jfloat>(lineHeightMultiplier),
142
+ static_cast<jfloat>(props.letterSpacing),
143
+ static_cast<jfloat>(widthPx));
144
+
145
+ if (heightPx <= 0) {
146
+ return -1;
147
+ }
148
+
149
+ return heightPx / pointScaleFactor;
151
150
  }
151
+ #endif
152
152
 
153
153
  Size AdvancedTextViewShadowNode::measureContent(
154
154
  const LayoutContext& layoutContext,
@@ -173,13 +173,40 @@ Size AdvancedTextViewShadowNode::measureContent(
173
173
  paragraphAttributes.adjustsFontSizeToFit = false;
174
174
 
175
175
  auto measurement = textLayoutManager_->measure(
176
- AttributedStringBox{
177
- getAttributedString(baseTextAttributes(), textLayoutContext)},
176
+ AttributedStringBox{getAttributedString()},
178
177
  paragraphAttributes,
179
178
  textLayoutContext,
180
179
  layoutConstraints);
181
180
 
182
- return layoutConstraints.clamp(measurement.size);
181
+ auto size = measurement.size;
182
+
183
+ #if defined(ANDROID)
184
+ // Prefer asking Android's own `StaticLayout` for the height directly (see
185
+ // `measureHeightViaAndroidNative()`) -- it's the same class
186
+ // `AdvancedTextView`'s real rendering is built on, so there's no
187
+ // measurement-vs-render drift to compound over long paragraphs. `size`
188
+ // (from RN's own TextLayoutManager, above) still supplies the width --
189
+ // this only replaces the height. Falls back to the `lineHeight`-scaling
190
+ // math below when the width isn't a finite, positive number (e.g. an
191
+ // unconstrained/intrinsic-width measure pass).
192
+ auto nativeHeight =
193
+ measureHeightViaAndroidNative(size.width, textLayoutContext.pointScaleFactor);
194
+ if (nativeHeight > 0) {
195
+ size.height = nativeHeight;
196
+ return layoutConstraints.clamp(size);
197
+ }
198
+ #endif
199
+
200
+ // `lineHeight` scales the font's own natural line height (see
201
+ // `getAttributedString()`); since `size.height` above is the *natural*
202
+ // (multiplier == 1) total, `setLineSpacing(0, multiplier)` scaling the
203
+ // same natural total on the view side means multiplying it here
204
+ // reproduces exactly the same number, for any paragraph length.
205
+ if (props.lineHeight > 0) {
206
+ size.height *= props.lineHeight;
207
+ }
208
+
209
+ return layoutConstraints.clamp(size);
183
210
  }
184
211
 
185
212
  } // namespace facebook::react
@@ -34,7 +34,6 @@
34
34
 
35
35
  #include <jsi/jsi.h>
36
36
  #include <react/renderer/attributedstring/AttributedString.h>
37
- #include <react/renderer/attributedstring/TextAttributes.h>
38
37
  #include <react/renderer/components/AdvancedTextViewSpec/EventEmitters.h>
39
38
  #include <react/renderer/components/AdvancedTextViewSpec/Props.h>
40
39
  #include <react/renderer/components/AdvancedTextViewSpec/States.h>
@@ -43,7 +42,6 @@
43
42
  #include <react/renderer/core/LayoutContext.h>
44
43
  #include <react/renderer/core/ShadowNode.h>
45
44
  #include <react/renderer/core/ShadowNodeFragment.h>
46
- #include <react/renderer/textlayoutmanager/TextLayoutContext.h>
47
45
 
48
46
  namespace facebook::react {
49
47
 
@@ -85,41 +83,25 @@ class AdvancedTextViewShadowNode final : public ConcreteViewShadowNode<
85
83
 
86
84
  private:
87
85
  /*
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.
86
+ * Builds a single-fragment `AttributedString` from the component props
87
+ * (text + font attributes), mirroring how the native views render it.
88
+ * Deliberately never sets `lineHeight` -- see the .cpp for why.
92
89
  */
93
- TextAttributes baseTextAttributes() const;
90
+ AttributedString getAttributedString() const;
94
91
 
95
- /*
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).
109
- */
110
92
  #if defined(ANDROID)
111
- Float measureNaturalLineHeight(
112
- const TextAttributes& baseTextAttributes,
113
- const TextLayoutContext& textLayoutContext) const;
114
- #endif
115
-
116
93
  /*
117
- * Builds a single-fragment `AttributedString` from the component props
118
- * (text + font attributes), mirroring how the native views render it.
94
+ * Asks Android's own `StaticLayout` (via a JNI call into
95
+ * `NativeTextMeasurement.measureHeight()`) for this text's real rendered
96
+ * height at the given width, instead of approximating it through RN's own
97
+ * `TextLayoutManager`. Returns height in dp, or a negative value if it
98
+ * couldn't be computed (e.g. unconstrained width), signaling the caller to
99
+ * fall back to the `TextLayoutManager`-based path.
119
100
  */
120
- AttributedString getAttributedString(
121
- const TextAttributes& baseTextAttributes,
122
- const TextLayoutContext& textLayoutContext) const;
101
+ Float measureHeightViaAndroidNative(
102
+ Float widthDp,
103
+ Float pointScaleFactor) const;
104
+ #endif
123
105
 
124
106
  // Lazily created on first measure, then reused / copied across clones.
125
107
  mutable std::shared_ptr<const TextLayoutManager> textLayoutManager_;
@@ -9,6 +9,21 @@
9
9
 
10
10
  using namespace facebook::react;
11
11
 
12
+ // Resolves a PostScript name, a family name, or an expo-font alias (expo-font
13
+ // only hooks +fontNamesForFamilyName:, which is also what RN's <Text> uses).
14
+ static UIFont *ATResolveFont(NSString *family, CGFloat size)
15
+ {
16
+ if (family.length == 0) {
17
+ return nil;
18
+ }
19
+ UIFont *font = [UIFont fontWithName:family size:size];
20
+ if (font) {
21
+ return font;
22
+ }
23
+ NSString *fontName = [UIFont fontNamesForFamilyName:family].firstObject;
24
+ return fontName ? [UIFont fontWithName:fontName size:size] : nil;
25
+ }
26
+
12
27
 
13
28
  @class AdvancedTextView;
14
29
 
@@ -446,10 +461,7 @@ using namespace facebook::react;
446
461
  if (_lineHeight > 0) {
447
462
  NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
448
463
 
449
- UIFont *font = nil;
450
- if (_fontFamily && _fontFamily.length > 0) {
451
- font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
452
- }
464
+ UIFont *font = ATResolveFont(_fontFamily, _fontSize > 0 ? _fontSize : 16.0);
453
465
  if (!font) {
454
466
  font = [UIFont systemFontOfSize:_fontSize > 0 ? _fontSize : 16.0];
455
467
  }
@@ -462,11 +474,7 @@ using namespace facebook::react;
462
474
  range:NSMakeRange(0, attributedString.length)];
463
475
  }
464
476
 
465
- UIFont *font = nil;
466
-
467
- if (_fontFamily && _fontFamily.length > 0) {
468
- font = [UIFont fontWithName:_fontFamily size:_fontSize > 0 ? _fontSize : 16.0];
469
- }
477
+ UIFont *font = ATResolveFont(_fontFamily, _fontSize > 0 ? _fontSize : 16.0);
470
478
 
471
479
  if (!font) {
472
480
  if (_fontWeight && [_fontWeight.lowercaseString isEqualToString:@"bold"]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.2.8",
3
+ "version": "0.2.11",
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,54 +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
- * `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
- *
31
- * See: https://www.w3.org/TR/css-inline-3/#inline-height
32
- */
33
- internal class CssLineHeightSpan(private val lineHeightPx: Int) : LineHeightSpan {
34
-
35
- override fun chooseHeight(
36
- text: CharSequence,
37
- start: Int,
38
- end: Int,
39
- spanstartv: Int,
40
- v: Int,
41
- fm: Paint.FontMetricsInt
42
- ) {
43
- val leading = lineHeightPx - ((-fm.ascent) + fm.descent)
44
- fm.ascent -= ceil(leading / 2.0f).toInt()
45
- fm.descent += floor(leading / 2.0f).toInt()
46
-
47
- if (start == 0) {
48
- fm.top = fm.ascent
49
- }
50
- if (end == text.length) {
51
- fm.bottom = fm.descent
52
- }
53
- }
54
- }