@whetware/react-native-stroke-text 0.0.2 → 0.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @whetware/react-native-stroke-text
2
2
 
3
+ ## 0.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`80ded08`](https://github.com/whetware/react-native-stroke-text/commit/80ded08eb3acf84344f56e12b24f74f771726ec1) Thanks [@evelant](https://github.com/evelant)! - Fix handles leak on Android causing crash
8
+
9
+ ## 0.0.3
10
+
11
+ ### Patch Changes
12
+
13
+ - [`dd724fd`](https://github.com/whetware/react-native-stroke-text/commit/dd724fd18055ec28e173029a6d3aa0a5744af919) Thanks [@evelant](https://github.com/evelant)! - fix bounding boxes
14
+
3
15
  ## 0.0.2
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -39,6 +39,12 @@ export function Example() {
39
39
  }
40
40
  ```
41
41
 
42
+ ## Layout notes
43
+
44
+ To avoid the outline getting clipped during animations (especially on Android), `StrokeText` keeps the
45
+ stroke inside the component bounds by applying an internal inset of `ceil(strokeWidth) / 2`. It then
46
+ uses matching negative margins so the layout footprint matches a normal `<Text />`.
47
+
42
48
  ## Development
43
49
 
44
50
  - Generate Nitro bindings: `pnpm specs`
@@ -1,12 +1,16 @@
1
1
  package com.margelo.nitro.stroketext
2
2
 
3
3
  import android.view.View
4
+ import com.facebook.jni.HybridData
4
5
  import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.margelo.nitro.views.RecyclableView
5
7
 
6
- class HybridStrokeTextView(context: ThemedReactContext) : HybridStrokeTextViewSpec() {
8
+ class HybridStrokeTextView(context: ThemedReactContext) : HybridStrokeTextViewSpec(), RecyclableView {
7
9
  private val strokeTextView = StrokeTextView(context)
8
10
  override val view: View = strokeTextView
9
11
 
12
+ private var isDisposed = false
13
+
10
14
  override var text: String = ""
11
15
  override var color: String? = null
12
16
  override var strokeColor: String? = null
@@ -34,6 +38,54 @@ class HybridStrokeTextView(context: ThemedReactContext) : HybridStrokeTextViewSp
34
38
  override var paddingBottom: Double? = null
35
39
  override var paddingLeft: Double? = null
36
40
 
41
+ override fun dispose() {
42
+ if (isDisposed) return
43
+ isDisposed = true
44
+ super.dispose()
45
+
46
+ try {
47
+ val hybridDataField = HybridStrokeTextViewSpec::class.java.getDeclaredField("mHybridData")
48
+ hybridDataField.isAccessible = true
49
+ val hybridData = hybridDataField.get(this) as? HybridData ?: return
50
+
51
+ val resetNativeMethod = HybridData::class.java.getDeclaredMethod("resetNative")
52
+ resetNativeMethod.isAccessible = true
53
+ resetNativeMethod.invoke(hybridData)
54
+ } catch (_: Throwable) {
55
+ }
56
+ }
57
+
58
+ override fun prepareForRecycle() {
59
+ text = ""
60
+ color = null
61
+ strokeColor = null
62
+ strokeWidth = null
63
+ fontSize = null
64
+ fontWeight = null
65
+ fontFamily = null
66
+ fontStyle = null
67
+ lineHeight = null
68
+ letterSpacing = null
69
+ textAlign = null
70
+ textDecorationLine = null
71
+ textTransform = null
72
+ opacity = null
73
+ allowFontScaling = null
74
+ maxFontSizeMultiplier = null
75
+ includeFontPadding = null
76
+ numberOfLines = null
77
+ ellipsizeMode = null
78
+ padding = null
79
+ paddingVertical = null
80
+ paddingHorizontal = null
81
+ paddingTop = null
82
+ paddingRight = null
83
+ paddingBottom = null
84
+ paddingLeft = null
85
+
86
+ afterUpdate()
87
+ }
88
+
37
89
  override fun afterUpdate() {
38
90
  val displayMetrics = strokeTextView.resources.displayMetrics
39
91
  val resolvedAllowFontScaling = allowFontScaling ?: true
@@ -7,12 +7,12 @@ import com.facebook.react.BaseReactPackage
7
7
  import com.facebook.react.ViewManagerOnDemandReactPackage
8
8
  import com.facebook.react.bridge.ModuleSpec
9
9
  import com.facebook.react.uimanager.ViewManager
10
- import com.margelo.nitro.stroketext.views.HybridStrokeTextViewManager
10
+ import com.margelo.nitro.stroketext.views.StrokeTextViewManager
11
11
 
12
12
  class NitroStrokeTextPackage : BaseReactPackage(), ViewManagerOnDemandReactPackage {
13
13
  private val viewManagers: Map<String, ModuleSpec> by lazy {
14
14
  mapOf(
15
- "StrokeTextView" to ModuleSpec.viewManagerSpec { HybridStrokeTextViewManager() }
15
+ "StrokeTextView" to ModuleSpec.viewManagerSpec { StrokeTextViewManager() }
16
16
  )
17
17
  }
18
18
 
@@ -113,7 +113,7 @@ internal class StrokeTextView(context: ThemedReactContext) : TextView(context) {
113
113
  }
114
114
 
115
115
  private fun applyProps() {
116
- // Padding: apply stroke inset in native to compensate for the overlay expansion in JS.
116
+ // Padding: apply a stroke inset so the outline stays within the view bounds.
117
117
  val inset = strokeInsetPx()
118
118
  val left = floorToInt(resolvePadding(paddingLeftPx, paddingHorizontalPx, paddingAllPx) + inset)
119
119
  val top = floorToInt(resolvePadding(paddingTopPx, paddingVerticalPx, paddingAllPx) + inset)
@@ -0,0 +1,15 @@
1
+ package com.margelo.nitro.stroketext.views
2
+
3
+ import android.view.View
4
+ import com.margelo.nitro.R.id.associated_hybrid_view_tag
5
+ import com.margelo.nitro.stroketext.HybridStrokeTextView
6
+
7
+ class StrokeTextViewManager : HybridStrokeTextViewManager() {
8
+ override fun onDropViewInstance(view: View) {
9
+ val hybridView = view.getTag(associated_hybrid_view_tag) as? HybridStrokeTextView
10
+ view.setTag(associated_hybrid_view_tag, null)
11
+ hybridView?.dispose()
12
+ super.onDropViewInstance(view)
13
+ }
14
+ }
15
+
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 { I18nManager, 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);
@@ -63,6 +63,24 @@ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
63
63
  const baseRight = firstNumber(paddingRight, stylePaddingRight, paddingHorizontal, stylePaddingHorizontal, padding, stylePadding) ?? 0;
64
64
  const baseBottom = firstNumber(paddingBottom, stylePaddingBottom, paddingVertical, stylePaddingVertical, padding, stylePadding) ?? 0;
65
65
  const baseLeft = firstNumber(paddingLeft, stylePaddingLeft, paddingHorizontal, stylePaddingHorizontal, padding, stylePadding) ?? 0;
66
+ const baseMarginTop = firstNumber(containerStyle.marginTop, containerStyle.marginVertical, containerStyle.margin) ?? 0;
67
+ const baseMarginRight = firstNumber(containerStyle.marginRight, I18nManager.isRTL ? containerStyle.marginStart : containerStyle.marginEnd, containerStyle.marginHorizontal, containerStyle.margin) ?? 0;
68
+ const baseMarginBottom = firstNumber(containerStyle.marginBottom, containerStyle.marginVertical, containerStyle.margin) ?? 0;
69
+ const baseMarginLeft = firstNumber(containerStyle.marginLeft, I18nManager.isRTL ? containerStyle.marginEnd : containerStyle.marginStart, containerStyle.marginHorizontal, containerStyle.margin) ?? 0;
70
+ const baseMarginStart = toNumber(containerStyle.marginStart);
71
+ const baseMarginEnd = toNumber(containerStyle.marginEnd);
72
+ const strokeInsetMarginStyle = strokeInset === 0
73
+ ? null
74
+ : {
75
+ marginTop: baseMarginTop - strokeInset,
76
+ marginRight: baseMarginRight - strokeInset,
77
+ marginBottom: baseMarginBottom - strokeInset,
78
+ marginLeft: baseMarginLeft - strokeInset,
79
+ ...(baseMarginStart == null
80
+ ? {}
81
+ : { marginStart: baseMarginStart - strokeInset }),
82
+ ...(baseMarginEnd == null ? {} : { marginEnd: baseMarginEnd - strokeInset }),
83
+ };
66
84
  const effectiveNumberOfLines = nativeProps.numberOfLines != null && nativeProps.numberOfLines > 0
67
85
  ? nativeProps.numberOfLines
68
86
  : undefined;
@@ -70,31 +88,22 @@ export function StrokeText({ text, children, style, hybridRef, ...rest }) {
70
88
  ? undefined
71
89
  : nativeProps.ellipsizeMode ?? 'tail';
72
90
  const effectiveIncludeFontPadding = nativeProps.includeFontPadding ?? styleIncludeFontPadding ?? false;
73
- return (React.createElement(View, { style: [styles.container, containerStyle] },
91
+ const wrappedHybridRef = React.useMemo(() => (hybridRef ? callback(hybridRef) : undefined), [hybridRef]);
92
+ return (React.createElement(View, { style: [styles.container, containerStyle, strokeInsetMarginStyle] },
74
93
  React.createElement(Text, { accessible: false, pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, allowFontScaling: nativeProps.allowFontScaling, maxFontSizeMultiplier: nativeProps.maxFontSizeMultiplier, style: [
75
94
  style,
76
95
  {
77
- paddingTop: baseTop,
78
- paddingRight: baseRight,
79
- paddingBottom: baseBottom,
80
- paddingLeft: baseLeft,
96
+ paddingTop: baseTop + strokeInset,
97
+ paddingRight: baseRight + strokeInset,
98
+ paddingBottom: baseBottom + strokeInset,
99
+ paddingLeft: baseLeft + strokeInset,
81
100
  },
82
101
  effectiveIncludeFontPadding == null
83
102
  ? null
84
103
  : { includeFontPadding: effectiveIncludeFontPadding },
85
104
  styles.hiddenText,
86
105
  ] }, resolvedText),
87
- React.createElement(NativeStrokeTextView, { ...nativeProps, text: resolvedText, color: nativeProps.color ?? toColorString(styleColor), fontSize: nativeProps.fontSize ?? toNumber(styleFontSize), fontWeight: nativeProps.fontWeight ?? toFontWeightString(styleFontWeight), fontFamily: nativeProps.fontFamily ?? styleFontFamily, fontStyle: nativeProps.fontStyle ?? styleFontStyle, lineHeight: nativeProps.lineHeight ?? toNumber(styleLineHeight), letterSpacing: nativeProps.letterSpacing ?? toNumber(styleLetterSpacing), textAlign: nativeProps.textAlign ?? styleTextAlign, textDecorationLine: nativeProps.textDecorationLine ?? styleTextDecorationLine, textTransform: nativeProps.textTransform ?? styleTextTransform, opacity: nativeProps.opacity ?? toNumber(styleOpacity), includeFontPadding: effectiveIncludeFontPadding, numberOfLines: nativeProps.numberOfLines, ellipsizeMode: effectiveEllipsizeMode, paddingTop: baseTop, paddingRight: baseRight, paddingBottom: baseBottom, paddingLeft: baseLeft, hybridRef: hybridRef ? callback(hybridRef) : undefined, pointerEvents: "none", style: [
88
- styles.overlay,
89
- strokeInset === 0
90
- ? null
91
- : {
92
- top: -strokeInset,
93
- right: -strokeInset,
94
- bottom: -strokeInset,
95
- left: -strokeInset,
96
- },
97
- ] })));
106
+ React.createElement(NativeStrokeTextView, { ...nativeProps, text: resolvedText, color: nativeProps.color ?? toColorString(styleColor), fontSize: nativeProps.fontSize ?? toNumber(styleFontSize), fontWeight: nativeProps.fontWeight ?? toFontWeightString(styleFontWeight), fontFamily: nativeProps.fontFamily ?? styleFontFamily, fontStyle: nativeProps.fontStyle ?? styleFontStyle, lineHeight: nativeProps.lineHeight ?? toNumber(styleLineHeight), letterSpacing: nativeProps.letterSpacing ?? toNumber(styleLetterSpacing), textAlign: nativeProps.textAlign ?? styleTextAlign, textDecorationLine: nativeProps.textDecorationLine ?? styleTextDecorationLine, textTransform: nativeProps.textTransform ?? styleTextTransform, 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 })));
98
107
  }
99
108
  const styles = StyleSheet.create({
100
109
  container: {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { StyleSheet, Text, View } from 'react-native';
2
+ import { I18nManager, StyleSheet, Text, View } from 'react-native';
3
3
  function resolveText(text, children) {
4
4
  if (typeof text === 'string')
5
5
  return text;
@@ -38,6 +38,24 @@ export function StrokeText({ text, children, style, ...rest }) {
38
38
  const strokeColor = rest.strokeColor ?? 'transparent';
39
39
  const strokeWidth = Math.max(0, rest.strokeWidth ?? 0);
40
40
  const strokeInset = Math.ceil(strokeWidth) / 2;
41
+ const baseMarginTop = firstNumber(containerStyle.marginTop, containerStyle.marginVertical, containerStyle.margin) ?? 0;
42
+ const baseMarginRight = firstNumber(containerStyle.marginRight, I18nManager.isRTL ? containerStyle.marginStart : containerStyle.marginEnd, containerStyle.marginHorizontal, containerStyle.margin) ?? 0;
43
+ const baseMarginBottom = firstNumber(containerStyle.marginBottom, containerStyle.marginVertical, containerStyle.margin) ?? 0;
44
+ const baseMarginLeft = firstNumber(containerStyle.marginLeft, I18nManager.isRTL ? containerStyle.marginEnd : containerStyle.marginStart, containerStyle.marginHorizontal, containerStyle.margin) ?? 0;
45
+ const baseMarginStart = toNumber(containerStyle.marginStart);
46
+ const baseMarginEnd = toNumber(containerStyle.marginEnd);
47
+ const strokeInsetMarginStyle = strokeInset === 0
48
+ ? null
49
+ : {
50
+ marginTop: baseMarginTop - strokeInset,
51
+ marginRight: baseMarginRight - strokeInset,
52
+ marginBottom: baseMarginBottom - strokeInset,
53
+ marginLeft: baseMarginLeft - strokeInset,
54
+ ...(baseMarginStart == null
55
+ ? {}
56
+ : { marginStart: baseMarginStart - strokeInset }),
57
+ ...(baseMarginEnd == null ? {} : { marginEnd: baseMarginEnd - strokeInset }),
58
+ };
41
59
  const baseTop = firstNumber(rest.paddingTop, stylePaddingTop, rest.paddingVertical, stylePaddingVertical, rest.padding, stylePadding) ?? 0;
42
60
  const baseRight = firstNumber(rest.paddingRight, stylePaddingRight, rest.paddingHorizontal, stylePaddingHorizontal, rest.padding, stylePadding) ?? 0;
43
61
  const baseBottom = firstNumber(rest.paddingBottom, stylePaddingBottom, rest.paddingVertical, stylePaddingVertical, rest.padding, stylePadding) ?? 0;
@@ -46,15 +64,17 @@ export function StrokeText({ text, children, style, ...rest }) {
46
64
  ? rest.numberOfLines
47
65
  : undefined;
48
66
  const effectiveEllipsizeMode = effectiveNumberOfLines == null ? undefined : rest.ellipsizeMode ?? 'tail';
49
- return (React.createElement(View, { style: [styles.container, containerStyle] },
67
+ return (React.createElement(View, { style: [styles.container, containerStyle, strokeInsetMarginStyle] },
50
68
  React.createElement(Text, { accessible: false, pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, style: [
51
69
  textStyle,
52
70
  {
53
- paddingTop: baseTop,
54
- paddingRight: baseRight,
55
- paddingBottom: baseBottom,
56
- paddingLeft: baseLeft,
71
+ paddingTop: baseTop + strokeInset,
72
+ paddingRight: baseRight + strokeInset,
73
+ paddingBottom: baseBottom + strokeInset,
74
+ paddingLeft: baseLeft + strokeInset,
57
75
  },
76
+ strokeInset === 0 ? null : { maxWidth: 'none' },
77
+ strokeInset === 0 ? null : { boxSizing: 'content-box' },
58
78
  styles.hiddenText,
59
79
  ] }, resolvedText),
60
80
  React.createElement(Text, { pointerEvents: "none", numberOfLines: effectiveNumberOfLines, ellipsizeMode: effectiveEllipsizeMode, style: [
@@ -83,14 +103,6 @@ export function StrokeText({ text, children, style, ...rest }) {
83
103
  }
84
104
  : null,
85
105
  styles.overlay,
86
- strokeInset === 0
87
- ? null
88
- : {
89
- top: -strokeInset,
90
- right: -strokeInset,
91
- bottom: -strokeInset,
92
- left: -strokeInset,
93
- },
94
106
  ] }, resolvedText)));
95
107
  }
96
108
  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.2",
3
+ "version": "0.0.4",
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",
@@ -28,19 +28,6 @@
28
28
  "CHANGELOG.md",
29
29
  "README.md"
30
30
  ],
31
- "scripts": {
32
- "typecheck": "tsc --noEmit",
33
- "clean": "rm -rf android/build node_modules/**/android/build lib",
34
- "lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
35
- "lint-ci": "eslint \"**/*.{js,ts,tsx}\"",
36
- "typescript": "tsc",
37
- "prepare": "npm run prepack",
38
- "prepack": "tsc",
39
- "changeset": "changeset",
40
- "version-packages": "changeset version",
41
- "release": "changeset publish",
42
- "specs": "tsc --noEmit false && nitrogen --logLevel=\"debug\""
43
- },
44
31
  "keywords": [
45
32
  "react-native",
46
33
  "nitro",
@@ -120,5 +107,16 @@
120
107
  "trailingComma": "es5",
121
108
  "useTabs": false,
122
109
  "semi": false
110
+ },
111
+ "scripts": {
112
+ "typecheck": "tsc --noEmit",
113
+ "clean": "rm -rf android/build node_modules/**/android/build lib",
114
+ "lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
115
+ "lint-ci": "eslint \"**/*.{js,ts,tsx}\"",
116
+ "typescript": "tsc",
117
+ "changeset": "changeset",
118
+ "version-packages": "changeset version",
119
+ "release": "changeset publish",
120
+ "specs": "tsc --noEmit false && nitrogen --logLevel=\"debug\""
123
121
  }
124
- }
122
+ }