@whetware/react-native-stroke-text 0.0.9 → 0.0.11

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.
@@ -3,6 +3,7 @@ package com.margelo.nitro.stroketext
3
3
  import android.view.View
4
4
  import com.facebook.jni.HybridData
5
5
  import com.facebook.react.uimanager.ThemedReactContext
6
+ import kotlin.math.ceil
6
7
 
7
8
  class HybridStrokeTextView(context: ThemedReactContext) : HybridStrokeTextViewSpec() {
8
9
  private val strokeTextView = StrokeTextView(context)
@@ -77,12 +78,14 @@ class HybridStrokeTextView(context: ThemedReactContext) : HybridStrokeTextViewSp
77
78
  )
78
79
 
79
80
  strokeTextView.fontSizePx =
80
- StrokeTextView.textToPx(
81
- fontSize ?: 14.0,
82
- resolvedAllowFontScaling,
83
- resolvedMaxFontSizeMultiplier,
84
- displayMetrics,
85
- )
81
+ ceil(
82
+ StrokeTextView.textToPx(
83
+ fontSize ?: 14.0,
84
+ resolvedAllowFontScaling,
85
+ resolvedMaxFontSizeMultiplier,
86
+ displayMetrics,
87
+ ).toDouble()
88
+ ).toFloat()
86
89
  strokeTextView.fontWeight = fontWeight
87
90
  strokeTextView.fontFamily = fontFamily
88
91
  strokeTextView.fontStyle = fontStyle ?: StrokeTextFontStyle.NORMAL
@@ -85,6 +85,8 @@ internal class StrokeTextView(context: ThemedReactContext) : TextView(context) {
85
85
  val prevStrike = textPaint.isStrikeThruText
86
86
 
87
87
  val saveCount = canvas.save()
88
+ val compoundPaddingLeft = compoundPaddingLeft
89
+ val extendedPaddingTop = extendedPaddingTop
88
90
 
89
91
  canvas.translate(compoundPaddingLeft.toFloat(), extendedPaddingTop.toFloat())
90
92
  canvas.translate(-scrollX.toFloat(), -scrollY.toFloat())
@@ -194,6 +196,10 @@ internal class StrokeTextView(context: ThemedReactContext) : TextView(context) {
194
196
 
195
197
  // Line limits / ellipsizing
196
198
  val maxLines = if (numberOfLines > 0) numberOfLines else Int.MAX_VALUE
199
+ // React Native's single-line Text does not wrap at spaces. Without horizontal scrolling,
200
+ // Android TextView can create an internal second line inside a host view that was measured
201
+ // as one line by the hidden RN Text used for layout.
202
+ setHorizontallyScrolling(numberOfLines == 1)
197
203
  setMaxLines(maxLines)
198
204
  ellipsize =
199
205
  if (numberOfLines <= 0) {
package/lib/StrokeText.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { I18nManager, 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;
@@ -71,32 +87,12 @@ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
71
87
  const baseRight = firstNumber(paddingRight, stylePaddingRight, paddingHorizontal, stylePaddingHorizontal, padding, stylePadding) ?? 0;
72
88
  const baseBottom = firstNumber(paddingBottom, stylePaddingBottom, paddingVertical, stylePaddingVertical, padding, stylePadding) ?? 0;
73
89
  const baseLeft = firstNumber(paddingLeft, stylePaddingLeft, paddingHorizontal, stylePaddingHorizontal, padding, stylePadding) ?? 0;
74
- const baseMarginTop = firstNumber(containerStyle.marginTop, containerStyle.marginVertical, containerStyle.margin) ?? 0;
75
- const baseMarginRight = firstNumber(containerStyle.marginRight, I18nManager.isRTL ? containerStyle.marginStart : containerStyle.marginEnd, containerStyle.marginHorizontal, containerStyle.margin) ?? 0;
76
- const baseMarginBottom = firstNumber(containerStyle.marginBottom, containerStyle.marginVertical, containerStyle.margin) ?? 0;
77
- const baseMarginLeft = firstNumber(containerStyle.marginLeft, I18nManager.isRTL ? containerStyle.marginEnd : containerStyle.marginStart, containerStyle.marginHorizontal, containerStyle.margin) ?? 0;
78
- const baseMarginStart = toNumber(containerStyle.marginStart);
79
- const baseMarginEnd = toNumber(containerStyle.marginEnd);
80
- const strokeInsetMarginStyle = strokeInset === 0
81
- ? null
82
- : {
83
- marginTop: baseMarginTop - strokeInset,
84
- marginRight: baseMarginRight - strokeInset,
85
- marginBottom: baseMarginBottom - strokeInset,
86
- marginLeft: baseMarginLeft - strokeInset,
87
- ...(baseMarginStart == null
88
- ? {}
89
- : { marginStart: baseMarginStart - strokeInset }),
90
- ...(baseMarginEnd == null
91
- ? {}
92
- : { marginEnd: baseMarginEnd - strokeInset }),
93
- };
94
90
  const effectiveNumberOfLines = nativeProps.numberOfLines != null && nativeProps.numberOfLines > 0
95
91
  ? nativeProps.numberOfLines
96
92
  : undefined;
97
93
  const effectiveEllipsizeMode = effectiveNumberOfLines == null
98
94
  ? undefined
99
- : (nativeProps.ellipsizeMode ?? 'tail');
95
+ : nativeProps.ellipsizeMode ?? 'tail';
100
96
  const mappedTextAlignVerticalFromVerticalAlign = styleVerticalAlign === 'middle'
101
97
  ? 'center'
102
98
  : toTextAlignVertical(styleVerticalAlign);
@@ -114,7 +110,45 @@ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
114
110
  const effectiveTextDecorationLine = nativeProps.textDecorationLine ?? styleTextDecorationLine;
115
111
  const effectiveTextTransform = nativeProps.textTransform ?? styleTextTransform;
116
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
117
  const wrappedHybridRef = React.useMemo(() => (hybridRef ? callback(hybridRef) : undefined), [hybridRef]);
118
+ React.useEffect(() => {
119
+ setMeasuredNativeText(undefined);
120
+ }, [
121
+ resolvedText,
122
+ effectiveNumberOfLines,
123
+ effectiveEllipsizeMode,
124
+ effectiveFontSize,
125
+ effectiveFontWeight,
126
+ effectiveFontFamily,
127
+ effectiveFontStyle,
128
+ effectiveLineHeight,
129
+ effectiveLetterSpacing,
130
+ effectiveTextAlign,
131
+ effectiveTextAlignVertical,
132
+ effectiveTextDecorationLine,
133
+ effectiveTextTransform,
134
+ effectiveIncludeFontPadding,
135
+ baseTop,
136
+ baseRight,
137
+ baseBottom,
138
+ baseLeft,
139
+ strokeInset,
140
+ ]);
141
+ const handleTextLayout = React.useCallback((event) => {
142
+ if (!shouldSyncMeasuredLineBreaks)
143
+ return;
144
+ const nextText = toNativeMeasuredText(event.nativeEvent.lines);
145
+ if (nextText == null)
146
+ return;
147
+ const nextMeasuredNativeText = nextText === resolvedText ? undefined : nextText;
148
+ setMeasuredNativeText((currentText) => currentText === nextMeasuredNativeText
149
+ ? currentText
150
+ : nextMeasuredNativeText);
151
+ }, [resolvedText, shouldSyncMeasuredLineBreaks]);
118
152
  const measurerTextStyle = React.useMemo(() => ({
119
153
  color: effectiveColor,
120
154
  fontSize: effectiveFontSize,
@@ -142,9 +176,11 @@ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
142
176
  effectiveTextTransform,
143
177
  effectiveIncludeFontPadding,
144
178
  ]);
145
- return (React.createElement(View, { style: [styles.container, containerStyle, strokeInsetMarginStyle] },
146
- React.createElement(Text, { accessible: false, pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, allowFontScaling: nativeProps.allowFontScaling, maxFontSizeMultiplier: nativeProps.maxFontSizeMultiplier, style: [
179
+ return (React.createElement(View, { style: [styles.container, containerStyle] },
180
+ React.createElement(Text, { accessible: false, pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, onTextLayout: handleTextLayout, allowFontScaling: nativeProps.allowFontScaling, maxFontSizeMultiplier: nativeProps.maxFontSizeMultiplier, style: [
147
181
  measurerTextStyle,
182
+ // The hidden RN Text is the layout source of truth, so reserve the same
183
+ // inset that the native TextView uses to keep the outline inside bounds.
148
184
  {
149
185
  paddingTop: baseTop + strokeInset,
150
186
  paddingRight: baseRight + strokeInset,
@@ -153,7 +189,7 @@ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
153
189
  },
154
190
  styles.hiddenText,
155
191
  ] }, resolvedText),
156
- 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: styles.overlay })));
192
+ 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: styles.overlay })));
157
193
  }
158
194
  const styles = StyleSheet.create({
159
195
  container: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whetware/react-native-stroke-text",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
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",