react-native-advanced-text 0.2.8 → 0.2.10
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.
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +21 -41
- package/android/src/main/java/com/advancedtext/NativeTextMeasurement.kt +69 -0
- package/common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.cpp +102 -75
- package/common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h +14 -32
- package/package.json +1 -1
- package/android/src/main/java/com/advancedtext/CssLineHeightSpan.kt +0 -54
|
@@ -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
|
|
|
@@ -278,7 +287,7 @@ class AdvancedTextView : TextView {
|
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
|
|
281
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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,69 @@
|
|
|
1
|
+
package com.advancedtext
|
|
2
|
+
|
|
3
|
+
import android.content.res.Resources
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import android.text.StaticLayout
|
|
6
|
+
import android.text.TextPaint
|
|
7
|
+
import android.util.TypedValue
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* JNI bridge so the Fabric ShadowNode (C++ `measureContent()`) can ask
|
|
11
|
+
* Android's own `StaticLayout` -- the exact class `AdvancedTextView`'s real
|
|
12
|
+
* rendering is built on -- for this text's real height, instead of going
|
|
13
|
+
* through React Native's own text-measurement engine (`TextLayoutManager`).
|
|
14
|
+
*
|
|
15
|
+
* RN's own engine and Android's native `TextView` rendering are two
|
|
16
|
+
* independently-implemented paths that can drift apart by a few px per line
|
|
17
|
+
* (verified: RN's `TextLayoutManager` takes a different code path --
|
|
18
|
+
* `BoringLayout` vs `StaticLayout` -- for short/simple text than for real,
|
|
19
|
+
* multi-line paragraphs, and even StaticLayout-to-StaticLayout the two
|
|
20
|
+
* engines' resolved `Paint`/`Typeface` don't perfectly agree). Since this
|
|
21
|
+
* component doesn't reuse RN's own prepared text layout for drawing anyway
|
|
22
|
+
* (it re-renders through its own `TextView`), measuring with the same
|
|
23
|
+
* `StaticLayout` call the view itself is built on removes the drift
|
|
24
|
+
* entirely, rather than approximating it from the C++ side.
|
|
25
|
+
*
|
|
26
|
+
* Every parameter below is chosen to mirror exactly what `AdvancedTextView`
|
|
27
|
+
* itself configures on its own `TextView`/`Paint` in `updateTextWithHighlights()`
|
|
28
|
+
* -- font resolution, text size (SP, not raw px), `setLineSpacing`, and
|
|
29
|
+
* letter spacing. Spans (highlight/click color) are omitted: they don't
|
|
30
|
+
* affect font metrics or line breaking, only drawing.
|
|
31
|
+
*/
|
|
32
|
+
object NativeTextMeasurement {
|
|
33
|
+
|
|
34
|
+
@JvmStatic
|
|
35
|
+
fun measureHeight(
|
|
36
|
+
text: String,
|
|
37
|
+
fontFamily: String,
|
|
38
|
+
typefaceStyle: Int,
|
|
39
|
+
fontSizeSp: Float,
|
|
40
|
+
lineHeightMultiplier: Float,
|
|
41
|
+
letterSpacingDp: Float,
|
|
42
|
+
widthPx: Float
|
|
43
|
+
): Float {
|
|
44
|
+
if (text.isEmpty() || widthPx <= 0f) {
|
|
45
|
+
return 0f
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
val displayMetrics = Resources.getSystem().displayMetrics
|
|
49
|
+
val fontSizePx =
|
|
50
|
+
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, fontSizeSp, displayMetrics)
|
|
51
|
+
|
|
52
|
+
val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG)
|
|
53
|
+
paint.typeface = Typeface.create(fontFamily, typefaceStyle)
|
|
54
|
+
paint.textSize = fontSizePx
|
|
55
|
+
// Mirrors AdvancedTextView's own letterSpacing conversion: dp/points
|
|
56
|
+
// prop -> em, scale-invariant so no density conversion is needed.
|
|
57
|
+
if (letterSpacingDp != 0f && fontSizeSp > 0f) {
|
|
58
|
+
paint.letterSpacing = letterSpacingDp / fontSizeSp
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
val width = widthPx.toInt().coerceAtLeast(1)
|
|
62
|
+
val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
|
|
63
|
+
// `setLineSpacing(0, multiplier)` is exactly what AdvancedTextView
|
|
64
|
+
// itself applies for rendering -- see updateTextWithHighlights().
|
|
65
|
+
builder.setLineSpacing(0f, if (lineHeightMultiplier > 0f) lineHeightMultiplier else 1f)
|
|
66
|
+
val layout = builder.build()
|
|
67
|
+
return layout.height.toFloat()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
* (
|
|
90
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
118
|
-
* (
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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_;
|
package/package.json
CHANGED
|
@@ -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
|
-
}
|