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.
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +5 -58
- package/common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.cpp +8 -79
- package/common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h +1 -14
- package/package.json +1 -1
- package/android/src/main/java/com/advancedtext/CssLineHeightSpan.kt +0 -47
|
@@ -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 =
|
|
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" ->
|
|
293
|
-
"italic" ->
|
|
294
|
-
else ->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 *
|
|
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(
|
|
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(
|
|
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,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
|
-
}
|