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.
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +4 -21
- package/android/src/main/java/com/advancedtext/CssLineHeightSpan.kt +7 -0
- package/common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.cpp +60 -66
- package/common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h +32 -12
- package/package.json +1 -1
|
@@ -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 =
|
|
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" ->
|
|
293
|
-
"italic" ->
|
|
294
|
-
else ->
|
|
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
|
-
|
|
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
|
|
127
|
-
// of 1 *is* the natural line height, so there is nothing to
|
|
128
|
-
// that case (overriding it with an approximation, as this
|
|
129
|
-
// unconditionally, made even the "no lineHeight override" case
|
|
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
|
-
|
|
134
|
+
measureNaturalLineHeight(textAttributes, textLayoutContext);
|
|
142
135
|
textAttributes.lineHeight = naturalLineHeight > 0
|
|
143
136
|
? props.lineHeight * naturalLineHeight
|
|
144
|
-
: props.lineHeight * textAttributes.fontSize *
|
|
137
|
+
: props.lineHeight * textAttributes.fontSize * 1.2;
|
|
145
138
|
#else
|
|
146
139
|
textAttributes.lineHeight =
|
|
147
|
-
props.lineHeight * textAttributes.fontSize *
|
|
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{
|
|
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
|
|
87
|
-
* (
|
|
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
|
-
|
|
93
|
+
TextAttributes baseTextAttributes() const;
|
|
90
94
|
|
|
91
|
-
#if defined(ANDROID)
|
|
92
95
|
/*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
const TextAttributes& baseTextAttributes
|
|
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