@whetware/react-native-stroke-text 0.0.10 → 0.0.12
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.
|
@@ -5,6 +5,7 @@ import android.graphics.Color
|
|
|
5
5
|
import android.graphics.Paint
|
|
6
6
|
import android.graphics.Paint.FontMetricsInt
|
|
7
7
|
import android.graphics.Typeface
|
|
8
|
+
import android.content.res.ColorStateList
|
|
8
9
|
import android.os.Build
|
|
9
10
|
import android.text.Layout
|
|
10
11
|
import android.text.TextDirectionHeuristics
|
|
@@ -71,9 +72,9 @@ internal class StrokeTextView(context: ThemedReactContext) : TextView(context) {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
override fun onDraw(canvas: Canvas) {
|
|
74
|
-
// Draw stroke behind fill
|
|
75
|
-
//
|
|
76
|
-
|
|
75
|
+
// Draw stroke behind fill through TextView's normal drawing path. Calling super.onDraw()
|
|
76
|
+
// for both passes keeps Android's TextView layout, glyph shaping, and color state handling
|
|
77
|
+
// consistent with the fill render while still letting us switch the paint style for outline.
|
|
77
78
|
if (layout != null && strokeWidthPx > 0f && strokeColor != Color.TRANSPARENT) {
|
|
78
79
|
val textPaint = paint
|
|
79
80
|
val prevStyle = textPaint.style
|
|
@@ -81,29 +82,23 @@ internal class StrokeTextView(context: ThemedReactContext) : TextView(context) {
|
|
|
81
82
|
val prevStrokeJoin = textPaint.strokeJoin
|
|
82
83
|
val prevStrokeCap = textPaint.strokeCap
|
|
83
84
|
val prevColor = textPaint.color
|
|
85
|
+
val prevTextColors: ColorStateList = textColors
|
|
84
86
|
val prevUnderline = textPaint.isUnderlineText
|
|
85
87
|
val prevStrike = textPaint.isStrikeThruText
|
|
86
88
|
|
|
87
|
-
val saveCount = canvas.save()
|
|
88
|
-
val compoundPaddingLeft = compoundPaddingLeft
|
|
89
|
-
val extendedPaddingTop = extendedPaddingTop
|
|
90
|
-
|
|
91
|
-
canvas.translate(compoundPaddingLeft.toFloat(), extendedPaddingTop.toFloat())
|
|
92
|
-
canvas.translate(-scrollX.toFloat(), -scrollY.toFloat())
|
|
93
|
-
|
|
94
89
|
// Only stroke the glyph outlines; keep underline/strike in the fill pass.
|
|
95
90
|
textPaint.isUnderlineText = false
|
|
96
91
|
textPaint.isStrikeThruText = false
|
|
97
92
|
|
|
93
|
+
setTextColor(strokeColor)
|
|
98
94
|
textPaint.style = Paint.Style.STROKE
|
|
99
95
|
textPaint.strokeJoin = Paint.Join.ROUND
|
|
100
96
|
textPaint.strokeCap = Paint.Cap.ROUND
|
|
101
97
|
textPaint.strokeWidth = strokeWidthPx
|
|
102
98
|
textPaint.color = strokeColor
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
canvas.restoreToCount(saveCount)
|
|
99
|
+
super.onDraw(canvas)
|
|
106
100
|
|
|
101
|
+
setTextColor(prevTextColors)
|
|
107
102
|
textPaint.style = prevStyle
|
|
108
103
|
textPaint.strokeWidth = prevStrokeWidth
|
|
109
104
|
textPaint.strokeJoin = prevStrokeJoin
|
package/lib/StrokeText.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { StyleSheet, Text, View } from 'react-native';
|
|
2
|
+
import { Platform, StyleSheet, Text, View } from 'react-native';
|
|
3
3
|
import { callback, getHostComponent } from 'react-native-nitro-modules';
|
|
4
4
|
import StrokeTextViewConfig from '../nitrogen/generated/shared/json/StrokeTextViewConfig.json';
|
|
5
5
|
const NativeStrokeTextView = getHostComponent('StrokeTextView', () => StrokeTextViewConfig);
|
|
@@ -21,6 +21,22 @@ function firstNumber(...values) {
|
|
|
21
21
|
}
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
24
|
+
function toNativeMeasuredLineText(line) {
|
|
25
|
+
return line.text.replace(/ /g, '\u00a0');
|
|
26
|
+
}
|
|
27
|
+
function toNativeMeasuredText(lines) {
|
|
28
|
+
if (lines.length === 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
let result = '';
|
|
31
|
+
lines.forEach((line, index) => {
|
|
32
|
+
const lineText = toNativeMeasuredLineText(line);
|
|
33
|
+
result += lineText;
|
|
34
|
+
if (index < lines.length - 1 && !lineText.endsWith('\n')) {
|
|
35
|
+
result += '\n';
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
24
40
|
function toFontWeightString(value) {
|
|
25
41
|
if (typeof value === 'string')
|
|
26
42
|
return value;
|
|
@@ -94,7 +110,56 @@ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
|
|
|
94
110
|
const effectiveTextDecorationLine = nativeProps.textDecorationLine ?? styleTextDecorationLine;
|
|
95
111
|
const effectiveTextTransform = nativeProps.textTransform ?? styleTextTransform;
|
|
96
112
|
const effectiveColor = nativeProps.color ?? toColorString(styleColor);
|
|
113
|
+
const shouldSyncMeasuredLineBreaks = Platform.OS === 'android' && effectiveNumberOfLines == null;
|
|
114
|
+
const [measuredNativeText, setMeasuredNativeText] = React.useState(undefined);
|
|
115
|
+
const nativeText = measuredNativeText ?? resolvedText;
|
|
116
|
+
const nativeTextTransform = measuredNativeText == null ? effectiveTextTransform : 'none';
|
|
117
|
+
// RN's hidden Text owns measurement, but Android TextView gets its own layout pass.
|
|
118
|
+
// Compensate for the native stroke inset on every edge, then give that native
|
|
119
|
+
// pass extra right-side room so one-pixel/font-metric drift cannot turn an RN
|
|
120
|
+
// single line into a native soft wrap.
|
|
121
|
+
const nativeOverlayInset = Platform.OS === 'android' && strokeInset > 0 ? strokeInset : 0;
|
|
122
|
+
const nativeOverlayRightInset = nativeOverlayInset > 0 ? nativeOverlayInset * 2 : 0;
|
|
97
123
|
const wrappedHybridRef = React.useMemo(() => (hybridRef ? callback(hybridRef) : undefined), [hybridRef]);
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
setMeasuredNativeText(undefined);
|
|
126
|
+
}, [
|
|
127
|
+
resolvedText,
|
|
128
|
+
effectiveNumberOfLines,
|
|
129
|
+
effectiveEllipsizeMode,
|
|
130
|
+
effectiveFontSize,
|
|
131
|
+
effectiveFontWeight,
|
|
132
|
+
effectiveFontFamily,
|
|
133
|
+
effectiveFontStyle,
|
|
134
|
+
effectiveLineHeight,
|
|
135
|
+
effectiveLetterSpacing,
|
|
136
|
+
effectiveTextAlign,
|
|
137
|
+
effectiveTextAlignVertical,
|
|
138
|
+
effectiveTextDecorationLine,
|
|
139
|
+
effectiveTextTransform,
|
|
140
|
+
effectiveIncludeFontPadding,
|
|
141
|
+
baseTop,
|
|
142
|
+
baseRight,
|
|
143
|
+
baseBottom,
|
|
144
|
+
baseLeft,
|
|
145
|
+
strokeInset,
|
|
146
|
+
]);
|
|
147
|
+
const handleTextLayout = React.useCallback((event) => {
|
|
148
|
+
if (!shouldSyncMeasuredLineBreaks)
|
|
149
|
+
return;
|
|
150
|
+
const lineCount = event.nativeEvent.lines.length;
|
|
151
|
+
if (lineCount <= 1) {
|
|
152
|
+
setMeasuredNativeText((currentText) => currentText == null ? currentText : undefined);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const nextText = toNativeMeasuredText(event.nativeEvent.lines);
|
|
156
|
+
if (nextText == null)
|
|
157
|
+
return;
|
|
158
|
+
const nextMeasuredNativeText = nextText === resolvedText ? undefined : nextText;
|
|
159
|
+
setMeasuredNativeText((currentText) => currentText === nextMeasuredNativeText
|
|
160
|
+
? currentText
|
|
161
|
+
: nextMeasuredNativeText);
|
|
162
|
+
}, [resolvedText, shouldSyncMeasuredLineBreaks]);
|
|
98
163
|
const measurerTextStyle = React.useMemo(() => ({
|
|
99
164
|
color: effectiveColor,
|
|
100
165
|
fontSize: effectiveFontSize,
|
|
@@ -123,26 +188,28 @@ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
|
|
|
123
188
|
effectiveIncludeFontPadding,
|
|
124
189
|
]);
|
|
125
190
|
return (React.createElement(View, { style: [styles.container, containerStyle] },
|
|
126
|
-
React.createElement(Text, { accessible: false, pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, allowFontScaling: nativeProps.allowFontScaling, maxFontSizeMultiplier: nativeProps.maxFontSizeMultiplier, style: [
|
|
191
|
+
React.createElement(Text, { accessible: false, pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, onTextLayout: handleTextLayout, allowFontScaling: nativeProps.allowFontScaling, maxFontSizeMultiplier: nativeProps.maxFontSizeMultiplier, style: [
|
|
127
192
|
measurerTextStyle,
|
|
193
|
+
// The hidden RN Text is the layout source of truth, so reserve the same
|
|
194
|
+
// inset that the native TextView uses to keep the outline inside bounds.
|
|
128
195
|
{
|
|
129
|
-
paddingTop: baseTop,
|
|
130
|
-
paddingRight: baseRight,
|
|
131
|
-
paddingBottom: baseBottom,
|
|
132
|
-
paddingLeft: baseLeft,
|
|
196
|
+
paddingTop: baseTop + strokeInset,
|
|
197
|
+
paddingRight: baseRight + strokeInset,
|
|
198
|
+
paddingBottom: baseBottom + strokeInset,
|
|
199
|
+
paddingLeft: baseLeft + strokeInset,
|
|
133
200
|
},
|
|
134
201
|
styles.hiddenText,
|
|
135
202
|
] }, resolvedText),
|
|
136
|
-
React.createElement(NativeStrokeTextView, { ...nativeProps, text:
|
|
203
|
+
React.createElement(NativeStrokeTextView, { ...nativeProps, text: nativeText, color: effectiveColor, fontSize: effectiveFontSize, fontWeight: effectiveFontWeight, fontFamily: effectiveFontFamily, fontStyle: effectiveFontStyle, lineHeight: effectiveLineHeight, letterSpacing: effectiveLetterSpacing, textAlign: effectiveTextAlign, textAlignVertical: effectiveTextAlignVertical, textDecorationLine: effectiveTextDecorationLine, textTransform: nativeTextTransform, opacity: nativeProps.opacity ?? toNumber(styleOpacity), includeFontPadding: effectiveIncludeFontPadding, numberOfLines: nativeProps.numberOfLines, ellipsizeMode: effectiveEllipsizeMode, paddingTop: baseTop, paddingRight: baseRight, paddingBottom: baseBottom, paddingLeft: baseLeft, hybridRef: wrappedHybridRef, pointerEvents: "none", style: [
|
|
137
204
|
styles.overlay,
|
|
138
|
-
|
|
139
|
-
?
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
205
|
+
nativeOverlayInset > 0
|
|
206
|
+
? {
|
|
207
|
+
top: -nativeOverlayInset,
|
|
208
|
+
right: -nativeOverlayRightInset,
|
|
209
|
+
bottom: -nativeOverlayInset,
|
|
210
|
+
left: -nativeOverlayInset,
|
|
211
|
+
}
|
|
212
|
+
: null,
|
|
146
213
|
] })));
|
|
147
214
|
}
|
|
148
215
|
const styles = StyleSheet.create({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whetware/react-native-stroke-text",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Stroke/outline text for React Native (New Architecture) on iOS, Android, and Web.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -121,4 +121,4 @@
|
|
|
121
121
|
"useTabs": false,
|
|
122
122
|
"semi": false
|
|
123
123
|
}
|
|
124
|
-
}
|
|
124
|
+
}
|