@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, using TextView's layout so metrics match RN <Text/> as closely as
75
- // possible (especially for bold fonts).
76
- val layout = layout
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
- layout.draw(canvas)
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: resolvedText, color: effectiveColor, fontSize: effectiveFontSize, fontWeight: effectiveFontWeight, fontFamily: effectiveFontFamily, fontStyle: effectiveFontStyle, lineHeight: effectiveLineHeight, letterSpacing: effectiveLetterSpacing, textAlign: effectiveTextAlign, textAlignVertical: effectiveTextAlignVertical, textDecorationLine: effectiveTextDecorationLine, textTransform: effectiveTextTransform, 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: [
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
- strokeInset === 0
139
- ? null
140
- : {
141
- top: -strokeInset,
142
- right: -strokeInset,
143
- bottom: -strokeInset,
144
- left: -strokeInset,
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.10",
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
+ }