@matiks/rn-stroke-text 0.0.1
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/NitroRnStrokeText.podspec +31 -0
- package/README.md +38 -0
- package/android/CMakeLists.txt +29 -0
- package/android/build.gradle +138 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/rnstroketext/FontUtil.kt +48 -0
- package/android/src/main/java/com/margelo/nitro/rnstroketext/HybridMatiksStrokeText.kt +80 -0
- package/android/src/main/java/com/margelo/nitro/rnstroketext/NitroRnStrokeTextPackage.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/rnstroketext/StrokeTextView.kt +348 -0
- package/ios/Bridge.h +8 -0
- package/ios/HybridMatiksStrokeText.swift +97 -0
- package/ios/StrokeTextView.swift +174 -0
- package/ios/StrokedTextLabel.swift +62 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +23 -0
- package/lib/specs/Stroke.nitro.d.ts +23 -0
- package/lib/specs/Stroke.nitro.js +1 -0
- package/nitro.json +24 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroRnStrokeText+autolinking.cmake +83 -0
- package/nitrogen/generated/android/NitroRnStrokeText+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroRnStrokeTextOnLoad.cpp +46 -0
- package/nitrogen/generated/android/NitroRnStrokeTextOnLoad.hpp +25 -0
- package/nitrogen/generated/android/c++/JDimensions.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridMatiksStrokeTextSpec.cpp +156 -0
- package/nitrogen/generated/android/c++/JHybridMatiksStrokeTextSpec.hpp +85 -0
- package/nitrogen/generated/android/c++/JTextAlign.hpp +61 -0
- package/nitrogen/generated/android/c++/views/JHybridMatiksStrokeTextStateUpdater.cpp +92 -0
- package/nitrogen/generated/android/c++/views/JHybridMatiksStrokeTextStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnstroketext/Dimensions.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnstroketext/HybridMatiksStrokeTextSpec.kt +115 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnstroketext/NitroRnStrokeTextOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnstroketext/TextAlign.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnstroketext/views/HybridMatiksStrokeTextManager.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnstroketext/views/HybridMatiksStrokeTextStateUpdater.kt +23 -0
- package/nitrogen/generated/ios/NitroRnStrokeText+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroRnStrokeText-Swift-Cxx-Bridge.cpp +33 -0
- package/nitrogen/generated/ios/NitroRnStrokeText-Swift-Cxx-Bridge.hpp +119 -0
- package/nitrogen/generated/ios/NitroRnStrokeText-Swift-Cxx-Umbrella.hpp +51 -0
- package/nitrogen/generated/ios/NitroRnStrokeTextAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroRnStrokeTextAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridMatiksStrokeTextSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMatiksStrokeTextSpecSwift.hpp +157 -0
- package/nitrogen/generated/ios/c++/views/HybridMatiksStrokeTextComponent.mm +147 -0
- package/nitrogen/generated/ios/swift/Dimensions.swift +35 -0
- package/nitrogen/generated/ios/swift/HybridMatiksStrokeTextSpec.swift +65 -0
- package/nitrogen/generated/ios/swift/HybridMatiksStrokeTextSpec_cxx.swift +341 -0
- package/nitrogen/generated/ios/swift/TextAlign.swift +44 -0
- package/nitrogen/generated/shared/c++/Dimensions.hpp +87 -0
- package/nitrogen/generated/shared/c++/HybridMatiksStrokeTextSpec.cpp +41 -0
- package/nitrogen/generated/shared/c++/HybridMatiksStrokeTextSpec.hpp +87 -0
- package/nitrogen/generated/shared/c++/TextAlign.hpp +80 -0
- package/nitrogen/generated/shared/c++/views/HybridMatiksStrokeTextComponent.cpp +196 -0
- package/nitrogen/generated/shared/c++/views/HybridMatiksStrokeTextComponent.hpp +118 -0
- package/nitrogen/generated/shared/json/MatiksStrokeTextConfig.json +19 -0
- package/package.json +107 -0
- package/react-native.config.js +16 -0
- package/src/index.tsx +44 -0
- package/src/specs/Stroke.nitro.ts +34 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
package com.margelo.nitro.rnstroketext
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.Typeface
|
|
8
|
+
import android.text.Layout
|
|
9
|
+
import android.text.StaticLayout
|
|
10
|
+
import android.text.TextPaint
|
|
11
|
+
import android.text.TextUtils
|
|
12
|
+
import android.util.TypedValue
|
|
13
|
+
import android.view.View
|
|
14
|
+
import kotlin.math.ceil
|
|
15
|
+
import kotlin.math.max
|
|
16
|
+
|
|
17
|
+
class StrokeTextView(
|
|
18
|
+
context: Context
|
|
19
|
+
) : View(context) {
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────
|
|
22
|
+
// Props
|
|
23
|
+
// ─────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
private var text: String = ""
|
|
26
|
+
private var fontSizePx: Float = sp(14f)
|
|
27
|
+
private var textColor: Int = Color.BLACK
|
|
28
|
+
private var strokeColor: Int = Color.WHITE
|
|
29
|
+
private var strokeWidthPx: Float = dp(1f)
|
|
30
|
+
private var fontFamily: String = "sans-serif"
|
|
31
|
+
private var numberOfLines: Int = 0
|
|
32
|
+
private var ellipsis: Boolean = false
|
|
33
|
+
private var alignment: Layout.Alignment = Layout.Alignment.ALIGN_CENTER
|
|
34
|
+
private var customWidthPx: Float = 0f
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────
|
|
37
|
+
// Paints
|
|
38
|
+
// ─────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
41
|
+
style = Paint.Style.FILL
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private val strokePaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
45
|
+
style = Paint.Style.STROKE
|
|
46
|
+
strokeJoin = Paint.Join.ROUND
|
|
47
|
+
strokeCap = Paint.Cap.ROUND
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────
|
|
51
|
+
// Layout
|
|
52
|
+
// ─────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
private var textLayout: StaticLayout? = null
|
|
55
|
+
private var strokeLayout: StaticLayout? = null
|
|
56
|
+
private var layoutDirty = true
|
|
57
|
+
|
|
58
|
+
private val fontCache = HashMap<String, Typeface?>()
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────
|
|
61
|
+
// Measurement (RN / Nitro safe)
|
|
62
|
+
// ─────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
65
|
+
updatePaints()
|
|
66
|
+
|
|
67
|
+
val measuredWidth = resolveMeasuredWidth(widthMeasureSpec)
|
|
68
|
+
ensureLayout(measuredWidth)
|
|
69
|
+
|
|
70
|
+
val desiredHeight = (textLayout?.height ?: 0).coerceAtLeast(1)
|
|
71
|
+
|
|
72
|
+
setMeasuredDimension(
|
|
73
|
+
measuredWidth,
|
|
74
|
+
resolveSize(desiredHeight, heightMeasureSpec)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun resolveMeasuredWidth(spec: Int): Int {
|
|
79
|
+
val mode = MeasureSpec.getMode(spec)
|
|
80
|
+
val size = MeasureSpec.getSize(spec)
|
|
81
|
+
|
|
82
|
+
// Width explicitly provided from JS prop
|
|
83
|
+
if (customWidthPx > 0f) {
|
|
84
|
+
return ceil(customWidthPx).toInt().coerceAtLeast(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Exact width from Yoga
|
|
88
|
+
if (mode == MeasureSpec.EXACTLY && size > 0) {
|
|
89
|
+
return size
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Intrinsic width from text
|
|
93
|
+
val textWidth = ceil(measureTextWidth()).toInt().coerceAtLeast(1)
|
|
94
|
+
|
|
95
|
+
return when (mode) {
|
|
96
|
+
MeasureSpec.AT_MOST -> minOf(size, textWidth)
|
|
97
|
+
else -> textWidth
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─────────────────────────────────────────────
|
|
102
|
+
// Layout creation
|
|
103
|
+
// ─────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
private fun updatePaints() {
|
|
106
|
+
val typeface = getFont(fontFamily)
|
|
107
|
+
|
|
108
|
+
textPaint.apply {
|
|
109
|
+
this.typeface = typeface
|
|
110
|
+
textSize = fontSizePx
|
|
111
|
+
color = textColor
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
strokePaint.apply {
|
|
115
|
+
this.typeface = typeface
|
|
116
|
+
textSize = fontSizePx
|
|
117
|
+
strokeWidth = strokeWidthPx
|
|
118
|
+
color = strokeColor
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun ensureLayout(width: Int) {
|
|
123
|
+
if (!layoutDirty && textLayout != null) return
|
|
124
|
+
|
|
125
|
+
val safeWidth = width.coerceAtLeast(1)
|
|
126
|
+
updatePaints()
|
|
127
|
+
|
|
128
|
+
var displayText: CharSequence =
|
|
129
|
+
if (ellipsis) {
|
|
130
|
+
TextUtils.ellipsize(
|
|
131
|
+
text,
|
|
132
|
+
textPaint,
|
|
133
|
+
safeWidth.toFloat(),
|
|
134
|
+
TextUtils.TruncateAt.END
|
|
135
|
+
)
|
|
136
|
+
} else {
|
|
137
|
+
text
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var layout = StaticLayout(
|
|
141
|
+
displayText,
|
|
142
|
+
textPaint,
|
|
143
|
+
safeWidth,
|
|
144
|
+
alignment,
|
|
145
|
+
1f,
|
|
146
|
+
0f,
|
|
147
|
+
false
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if (numberOfLines > 0 && layout.lineCount > numberOfLines) {
|
|
151
|
+
val end = layout.getLineEnd(numberOfLines - 1)
|
|
152
|
+
displayText = displayText.subSequence(0, end)
|
|
153
|
+
|
|
154
|
+
layout = StaticLayout(
|
|
155
|
+
displayText,
|
|
156
|
+
textPaint,
|
|
157
|
+
safeWidth,
|
|
158
|
+
alignment,
|
|
159
|
+
1f,
|
|
160
|
+
0f,
|
|
161
|
+
false
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
textLayout = layout
|
|
166
|
+
strokeLayout = StaticLayout(
|
|
167
|
+
displayText,
|
|
168
|
+
strokePaint,
|
|
169
|
+
safeWidth,
|
|
170
|
+
alignment,
|
|
171
|
+
1f,
|
|
172
|
+
0f,
|
|
173
|
+
false
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
layoutDirty = false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fun getTextDimensions(): Pair<Double, Double> {
|
|
180
|
+
updatePaints()
|
|
181
|
+
|
|
182
|
+
// Ensure we have a valid layout for measurement.
|
|
183
|
+
// If width is 0/unspecified, we measure intrinsic width.
|
|
184
|
+
val measureWidth = if (customWidthPx > 0f) {
|
|
185
|
+
ceil(customWidthPx).toInt()
|
|
186
|
+
} else {
|
|
187
|
+
ceil(measureTextWidth()).toInt()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// We create a temporary layout if needed to get accurate height for the given width
|
|
191
|
+
// But since ensureLayout caches the layout based on last width, we might want to just ensure layout exists.
|
|
192
|
+
// For accurate "intrinsic" measurement, we usually want the width to be the text width if not constrained.
|
|
193
|
+
|
|
194
|
+
ensureLayout(measureWidth)
|
|
195
|
+
|
|
196
|
+
val w = textLayout?.width?.toDouble() ?: 0.0
|
|
197
|
+
val h = textLayout?.height?.toDouble() ?: 0.0
|
|
198
|
+
|
|
199
|
+
return Pair(w, h)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private fun measureTextWidth(): Float {
|
|
203
|
+
var maxWidth = 0f
|
|
204
|
+
for (line in text.split("\n")) {
|
|
205
|
+
maxWidth = max(maxWidth, textPaint.measureText(line))
|
|
206
|
+
}
|
|
207
|
+
return maxWidth + strokeWidthPx * 2
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─────────────────────────────────────────────
|
|
211
|
+
// Drawing
|
|
212
|
+
// ─────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
override fun onDraw(canvas: Canvas) {
|
|
215
|
+
super.onDraw(canvas)
|
|
216
|
+
ensureLayout(width)
|
|
217
|
+
strokeLayout?.draw(canvas)
|
|
218
|
+
textLayout?.draw(canvas)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─────────────────────────────────────────────
|
|
222
|
+
// Props setters (Nitro)
|
|
223
|
+
// ─────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
fun setText(value: String) {
|
|
226
|
+
if (text != value) {
|
|
227
|
+
text = value
|
|
228
|
+
invalidateLayout()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fun setFontSize(value: Float) {
|
|
233
|
+
val px = sp(value)
|
|
234
|
+
if (fontSizePx != px) {
|
|
235
|
+
fontSizePx = px
|
|
236
|
+
invalidateLayout()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fun setTextColor(value: String) {
|
|
241
|
+
val c = parseColor(value)
|
|
242
|
+
if (textColor != c) {
|
|
243
|
+
textColor = c
|
|
244
|
+
invalidate()
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fun setStrokeColor(value: String) {
|
|
249
|
+
val c = parseColor(value)
|
|
250
|
+
if (strokeColor != c) {
|
|
251
|
+
strokeColor = c
|
|
252
|
+
invalidate()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fun setStrokeWidth(value: Float) {
|
|
257
|
+
val px = dp(value)
|
|
258
|
+
if (strokeWidthPx != px) {
|
|
259
|
+
strokeWidthPx = px
|
|
260
|
+
invalidateLayout()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fun setFontFamily(value: String) {
|
|
265
|
+
if (fontFamily != value) {
|
|
266
|
+
fontFamily = value
|
|
267
|
+
invalidateLayout()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fun setTextAlignment(value: String) {
|
|
272
|
+
val newAlignment = when (value) {
|
|
273
|
+
"left" -> Layout.Alignment.ALIGN_NORMAL
|
|
274
|
+
"right" -> Layout.Alignment.ALIGN_OPPOSITE
|
|
275
|
+
"center" -> Layout.Alignment.ALIGN_CENTER
|
|
276
|
+
else -> alignment
|
|
277
|
+
}
|
|
278
|
+
if (alignment != newAlignment) {
|
|
279
|
+
alignment = newAlignment
|
|
280
|
+
invalidateLayout()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fun setNumberOfLines(value: Int) {
|
|
285
|
+
if (numberOfLines != value) {
|
|
286
|
+
numberOfLines = value
|
|
287
|
+
invalidateLayout()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fun setEllipsis(value: Boolean) {
|
|
292
|
+
if (ellipsis != value) {
|
|
293
|
+
ellipsis = value
|
|
294
|
+
invalidateLayout()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fun setCustomWidth(value: Float) {
|
|
299
|
+
val px = dp(value)
|
|
300
|
+
if (customWidthPx != px) {
|
|
301
|
+
customWidthPx = px
|
|
302
|
+
invalidateLayout()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─────────────────────────────────────────────
|
|
307
|
+
// Helpers
|
|
308
|
+
// ─────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
private fun invalidateLayout() {
|
|
311
|
+
layoutDirty = true
|
|
312
|
+
requestLayout()
|
|
313
|
+
invalidate()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private fun sp(v: Float): Float =
|
|
317
|
+
TypedValue.applyDimension(
|
|
318
|
+
TypedValue.COMPLEX_UNIT_SP,
|
|
319
|
+
v,
|
|
320
|
+
resources.displayMetrics
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
private fun dp(v: Float): Float =
|
|
324
|
+
TypedValue.applyDimension(
|
|
325
|
+
TypedValue.COMPLEX_UNIT_DIP,
|
|
326
|
+
v,
|
|
327
|
+
resources.displayMetrics
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
private fun parseColor(color: String): Int =
|
|
331
|
+
try {
|
|
332
|
+
Color.parseColor(color)
|
|
333
|
+
} catch (_: Exception) {
|
|
334
|
+
Color.BLACK
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private fun getFont(fontFamily: String): Typeface? {
|
|
338
|
+
return fontCache[fontFamily] ?: run {
|
|
339
|
+
val tf = FontUtil.getFont(context, fontFamily)
|
|
340
|
+
fontCache[fontFamily] = tf
|
|
341
|
+
tf
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
init {
|
|
346
|
+
setWillNotDraw(false)
|
|
347
|
+
}
|
|
348
|
+
}
|
package/ios/Bridge.h
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
class HybridMatiksStrokeText: HybridMatiksStrokeTextSpec {
|
|
6
|
+
var view: UIView {
|
|
7
|
+
return strokeTextView
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
private let strokeTextView: StrokeTextView
|
|
11
|
+
|
|
12
|
+
public override init() {
|
|
13
|
+
self.strokeTextView = StrokeTextView(frame: .zero)
|
|
14
|
+
super.init()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// MARK: - Props
|
|
18
|
+
|
|
19
|
+
var width: Double? = 0 {
|
|
20
|
+
didSet {
|
|
21
|
+
strokeTextView.width = NSNumber(value: width ?? 0)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var text: String = "" {
|
|
26
|
+
didSet {
|
|
27
|
+
strokeTextView.text = text
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var fontSize: Double? = 0 {
|
|
32
|
+
didSet {
|
|
33
|
+
strokeTextView.fontSize = NSNumber(value: fontSize ?? 0)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var color: String? = "" {
|
|
38
|
+
didSet {
|
|
39
|
+
strokeTextView.color = color ?? "#000000"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var strokeColor: String? = "" {
|
|
44
|
+
didSet {
|
|
45
|
+
strokeTextView.strokeColor = strokeColor ?? "#FFFFFF"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var strokeWidth: Double? = 0 {
|
|
50
|
+
didSet {
|
|
51
|
+
strokeTextView.strokeWidth = NSNumber(value: strokeWidth ?? 0)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var fontFamily: String? = "" {
|
|
56
|
+
didSet {
|
|
57
|
+
strokeTextView.fontFamily = fontFamily ?? ""
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var align: TextAlign? = .center {
|
|
62
|
+
didSet {
|
|
63
|
+
switch align {
|
|
64
|
+
case .left: strokeTextView.align = "left"
|
|
65
|
+
case .right: strokeTextView.align = "right"
|
|
66
|
+
case .center: strokeTextView.align = "center"
|
|
67
|
+
case .none: strokeTextView.align = "center"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var numberOfLines: Double? = 0 {
|
|
73
|
+
didSet {
|
|
74
|
+
strokeTextView.numberOfLines = NSNumber(value: numberOfLines ?? 0)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var ellipsis: Bool? = false {
|
|
79
|
+
didSet {
|
|
80
|
+
strokeTextView.ellipsis = ellipsis ?? false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - Methods
|
|
85
|
+
|
|
86
|
+
func measureDimensions() -> Dimensions {
|
|
87
|
+
// StrokeTextView.measureDimensions returns [String: NSNumber]
|
|
88
|
+
// We need to return Dimensions struct which is generated by Nitro
|
|
89
|
+
|
|
90
|
+
let dims = strokeTextView.measureDimensions()
|
|
91
|
+
let w = dims["width"]?.doubleValue ?? 0.0
|
|
92
|
+
let h = dims["height"]?.doubleValue ?? 0.0
|
|
93
|
+
|
|
94
|
+
// Assuming Dimensions is a generated struct with init(width: Double, height: Double)
|
|
95
|
+
return Dimensions(width: w, height: h)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
class StrokeTextView: UIView {
|
|
5
|
+
|
|
6
|
+
private let label = StrokedTextLabel()
|
|
7
|
+
private var fontCache: [String: UIFont] = [:]
|
|
8
|
+
|
|
9
|
+
override init(frame: CGRect) {
|
|
10
|
+
super.init(frame: frame)
|
|
11
|
+
|
|
12
|
+
label.translatesAutoresizingMaskIntoConstraints = false
|
|
13
|
+
addSubview(label)
|
|
14
|
+
|
|
15
|
+
NSLayoutConstraint.activate([
|
|
16
|
+
label.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
17
|
+
label.centerYAnchor.constraint(equalTo: centerYAnchor)
|
|
18
|
+
])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
required init?(coder: NSCoder) {
|
|
22
|
+
fatalError("init(coder:) has not been implemented")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// MARK: - Layout (THIS replaces UIManager.setSize)
|
|
26
|
+
|
|
27
|
+
override var intrinsicContentSize: CGSize {
|
|
28
|
+
return label.intrinsicContentSize
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - Props (Nitro-style)
|
|
32
|
+
|
|
33
|
+
@objc var text: String = "" {
|
|
34
|
+
didSet {
|
|
35
|
+
label.text = text
|
|
36
|
+
invalidateIntrinsicContentSize()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc var fontSize: NSNumber = 14 {
|
|
41
|
+
didSet {
|
|
42
|
+
label.font = label.font.withSize(CGFloat(truncating: fontSize))
|
|
43
|
+
invalidateIntrinsicContentSize()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@objc var fontFamily: String = "Helvetica" {
|
|
48
|
+
didSet {
|
|
49
|
+
let key = "\(fontFamily)-\(fontSize)"
|
|
50
|
+
if let cached = fontCache[key] {
|
|
51
|
+
label.font = cached
|
|
52
|
+
} else {
|
|
53
|
+
let size = CGFloat(truncating: fontSize)
|
|
54
|
+
let font =
|
|
55
|
+
UIFont(name: fontFamily, size: size)
|
|
56
|
+
?? UIFont.systemFont(ofSize: size)
|
|
57
|
+
|
|
58
|
+
fontCache[key] = font
|
|
59
|
+
label.font = font
|
|
60
|
+
}
|
|
61
|
+
invalidateIntrinsicContentSize()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@objc var color: String = "#000000" {
|
|
66
|
+
didSet {
|
|
67
|
+
label.textColor = colorStringToUIColor(color)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@objc var strokeColor: String = "#FFFFFF" {
|
|
72
|
+
didSet {
|
|
73
|
+
label.outlineColor = colorStringToUIColor(strokeColor)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@objc var strokeWidth: NSNumber = 0 {
|
|
78
|
+
didSet {
|
|
79
|
+
label.outlineWidth = CGFloat(truncating: strokeWidth)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@objc var align: String = "center" {
|
|
84
|
+
didSet {
|
|
85
|
+
label.align =
|
|
86
|
+
align == "left" ? .left :
|
|
87
|
+
align == "right" ? .right : .center
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@objc var ellipsis: Bool = false {
|
|
92
|
+
didSet {
|
|
93
|
+
label.ellipsis = ellipsis
|
|
94
|
+
invalidateIntrinsicContentSize()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@objc var numberOfLines: NSNumber = 0 {
|
|
99
|
+
didSet {
|
|
100
|
+
label.numberOfLines = Int(truncating: numberOfLines)
|
|
101
|
+
invalidateIntrinsicContentSize()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@objc var width: NSNumber = 0 {
|
|
106
|
+
didSet {
|
|
107
|
+
label.customWidth = CGFloat(truncating: width)
|
|
108
|
+
invalidateIntrinsicContentSize()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MARK: - Hybrid Methods (optional but powerful)
|
|
113
|
+
|
|
114
|
+
@objc func measureDimensions() -> [String: NSNumber] {
|
|
115
|
+
let size = intrinsicContentSize
|
|
116
|
+
return [
|
|
117
|
+
"width": NSNumber(value: Double(size.width)),
|
|
118
|
+
"height": NSNumber(value: Double(size.height))
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// MARK: - Helpers
|
|
123
|
+
|
|
124
|
+
private func colorStringToUIColor(_ colorString: String) -> UIColor {
|
|
125
|
+
let cString = colorString.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
126
|
+
|
|
127
|
+
// Handle named colors
|
|
128
|
+
switch cString {
|
|
129
|
+
case "black": return .black
|
|
130
|
+
case "darkgray", "darkgrey": return .darkGray
|
|
131
|
+
case "lightgray", "lightgrey": return .lightGray
|
|
132
|
+
case "white": return .white
|
|
133
|
+
case "gray", "grey": return .gray
|
|
134
|
+
case "red": return .red
|
|
135
|
+
case "green": return .green
|
|
136
|
+
case "blue": return .blue
|
|
137
|
+
case "cyan": return .cyan
|
|
138
|
+
case "yellow": return .yellow
|
|
139
|
+
case "magenta": return .magenta
|
|
140
|
+
case "orange": return .orange
|
|
141
|
+
case "purple": return .purple
|
|
142
|
+
case "brown": return .brown
|
|
143
|
+
case "clear": return .clear
|
|
144
|
+
default: break
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle Hex
|
|
148
|
+
var hex = cString.uppercased()
|
|
149
|
+
if hex.hasPrefix("#") {
|
|
150
|
+
hex.remove(at: hex.startIndex)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var rgbValue: UInt64 = 0
|
|
154
|
+
Scanner(string: hex).scanHexInt64(&rgbValue)
|
|
155
|
+
|
|
156
|
+
if hex.count == 6 {
|
|
157
|
+
return UIColor(
|
|
158
|
+
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
|
|
159
|
+
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
|
|
160
|
+
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
|
|
161
|
+
alpha: 1.0
|
|
162
|
+
)
|
|
163
|
+
} else if hex.count == 8 {
|
|
164
|
+
return UIColor(
|
|
165
|
+
red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0,
|
|
166
|
+
green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255.0,
|
|
167
|
+
blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255.0,
|
|
168
|
+
alpha: CGFloat(rgbValue & 0x000000FF) / 255.0
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return .black
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
class StrokedTextLabel: UILabel {
|
|
4
|
+
required init?(coder: NSCoder) {
|
|
5
|
+
fatalError("init(coder:) has not been implemented")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
override init(frame: CGRect) {
|
|
9
|
+
super.init(frame: frame)
|
|
10
|
+
self.numberOfLines = 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private var textInsets: UIEdgeInsets = .zero
|
|
14
|
+
|
|
15
|
+
public func updateTextInsets() {
|
|
16
|
+
textInsets = UIEdgeInsets(top: outlineWidth, left: outlineWidth, bottom: outlineWidth, right: outlineWidth)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
var outlineWidth: CGFloat = 0
|
|
20
|
+
var outlineColor: UIColor = .clear
|
|
21
|
+
var align: NSTextAlignment = .center
|
|
22
|
+
var customWidth: CGFloat = 0
|
|
23
|
+
var ellipsis: Bool = false
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
override func drawText(in rect: CGRect) {
|
|
27
|
+
let shadowOffset = self.shadowOffset
|
|
28
|
+
let textColor = self.textColor
|
|
29
|
+
|
|
30
|
+
self.lineBreakMode = ellipsis ? .byTruncatingTail : .byWordWrapping
|
|
31
|
+
|
|
32
|
+
let adjustedRect = rect.inset(by: textInsets)
|
|
33
|
+
|
|
34
|
+
let context = UIGraphicsGetCurrentContext()
|
|
35
|
+
context?.setLineWidth(outlineWidth)
|
|
36
|
+
context?.setLineJoin(.round)
|
|
37
|
+
context?.setTextDrawingMode(.stroke)
|
|
38
|
+
self.textAlignment = align
|
|
39
|
+
self.textColor = outlineColor
|
|
40
|
+
|
|
41
|
+
super.drawText(in: adjustedRect)
|
|
42
|
+
|
|
43
|
+
context?.setTextDrawingMode(.fill)
|
|
44
|
+
self.textColor = textColor
|
|
45
|
+
self.shadowOffset = CGSize(width: 0, height: 0)
|
|
46
|
+
super.drawText(in: adjustedRect)
|
|
47
|
+
|
|
48
|
+
self.shadowOffset = shadowOffset
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override var intrinsicContentSize: CGSize {
|
|
52
|
+
var contentSize = super.intrinsicContentSize
|
|
53
|
+
if customWidth > 0 {
|
|
54
|
+
contentSize.width = customWidth
|
|
55
|
+
} else {
|
|
56
|
+
contentSize.width += outlineWidth
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
contentSize.height += outlineWidth
|
|
60
|
+
return contentSize
|
|
61
|
+
}
|
|
62
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type MatiksStrokeTextProps } from "./specs/Stroke.nitro";
|
|
3
|
+
interface StrokeTextViewProps extends MatiksStrokeTextProps {
|
|
4
|
+
styles?: {
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
declare const _default: React.MemoExoticComponent<({ styles, ...props }: StrokeTextViewProps) => React.JSX.Element>;
|
|
10
|
+
export default _default;
|