react-native-highlight-text-view 0.1.24 → 0.1.26
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/README.md +1 -0
- package/android/src/main/java/com/highlighttext/HighlightTextView.kt +216 -525
- package/android/src/main/java/com/highlighttext/HighlightTextViewManager.kt +7 -0
- package/ios/HighlightTextView.mm +16 -0
- package/lib/module/HighlightTextViewNativeComponent.ts +2 -0
- package/lib/typescript/src/HighlightTextViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/HighlightTextViewNativeComponent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/HighlightTextViewNativeComponent.ts +2 -0
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ export default function App() {
|
|
|
48
48
|
| `verticalAlign` | `'top' \| 'center' \| 'middle' \| 'bottom'` | - | Vertical alignment (iOS only). Alternative to using combined `textAlign` values. **Note:** Android does not support vertical alignment and will use default vertical positioning. |
|
|
49
49
|
| `fontFamily` | `string` | - | Font family name |
|
|
50
50
|
| `fontSize` | `string` | `32` | Font size in points |
|
|
51
|
+
| `letterSpacing` | `string` | `0` | Extra space between characters, in layout points (same semantics as React Native's `letterSpacing`). |
|
|
51
52
|
| `lineHeight` | `string` | `0` | Line height override (0 means use default line height) |
|
|
52
53
|
| `highlightBorderRadius` | `string` | `0` | Border radius for the highlight background |
|
|
53
54
|
| `padding` | `string` | `4` | Padding around each character highlight (expands background outward) |
|
|
@@ -7,197 +7,68 @@ import android.graphics.Paint
|
|
|
7
7
|
import android.graphics.RectF
|
|
8
8
|
import android.graphics.Typeface
|
|
9
9
|
import android.text.Editable
|
|
10
|
-
import android.text.Spannable
|
|
11
|
-
import android.text.SpannableString
|
|
12
10
|
import android.text.TextWatcher
|
|
13
|
-
import android.text.style.ReplacementSpan
|
|
14
11
|
import android.util.AttributeSet
|
|
15
12
|
import android.util.TypedValue
|
|
16
13
|
import android.view.Gravity
|
|
17
14
|
import androidx.appcompat.widget.AppCompatEditText
|
|
15
|
+
import com.facebook.react.common.assets.ReactFontManager
|
|
18
16
|
|
|
19
17
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Custom EditText that mimics the iOS implementation by drawing per-character
|
|
19
|
+
* rounded highlights directly in onDraw(), instead of using spans.
|
|
20
|
+
*
|
|
21
|
+
* This avoids layout thrashing/flicker when lines auto-wrap and keeps padding
|
|
22
|
+
* logic independent from Android's line breaking.
|
|
22
23
|
*/
|
|
23
|
-
class RoundedBackgroundSpan(
|
|
24
|
-
internal val backgroundColor: Int,
|
|
25
|
-
internal val textColor: Int,
|
|
26
|
-
internal val paddingLeft: Float,
|
|
27
|
-
internal val paddingRight: Float,
|
|
28
|
-
internal val paddingTop: Float,
|
|
29
|
-
internal val paddingBottom: Float,
|
|
30
|
-
internal val backgroundInsetTop: Float,
|
|
31
|
-
internal val backgroundInsetBottom: Float,
|
|
32
|
-
internal val backgroundInsetLeft: Float,
|
|
33
|
-
internal val backgroundInsetRight: Float,
|
|
34
|
-
internal val cornerRadius: Float,
|
|
35
|
-
internal val isLineStart: Boolean = false,
|
|
36
|
-
internal val isLineEnd: Boolean = false,
|
|
37
|
-
internal val isFirstLine: Boolean = false,
|
|
38
|
-
internal val isLastLine: Boolean = false
|
|
39
|
-
) : ReplacementSpan() {
|
|
40
|
-
|
|
41
|
-
override fun getSize(
|
|
42
|
-
paint: Paint,
|
|
43
|
-
text: CharSequence?,
|
|
44
|
-
start: Int,
|
|
45
|
-
end: Int,
|
|
46
|
-
fm: Paint.FontMetricsInt?
|
|
47
|
-
): Int {
|
|
48
|
-
// Only add padding at line boundaries (matches iOS behavior)
|
|
49
|
-
val width = paint.measureText(text, start, end)
|
|
50
|
-
val leftPad = if (isLineStart) paddingLeft else 0f
|
|
51
|
-
val rightPad = if (isLineEnd) paddingRight else 0f
|
|
52
|
-
return (width + leftPad + rightPad).toInt()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
override fun draw(
|
|
56
|
-
canvas: Canvas,
|
|
57
|
-
text: CharSequence?,
|
|
58
|
-
start: Int,
|
|
59
|
-
end: Int,
|
|
60
|
-
x: Float,
|
|
61
|
-
top: Int,
|
|
62
|
-
y: Int,
|
|
63
|
-
bottom: Int,
|
|
64
|
-
paint: Paint
|
|
65
|
-
) {
|
|
66
|
-
if (text == null) return
|
|
67
|
-
|
|
68
|
-
val bgPaint = Paint().apply {
|
|
69
|
-
color = backgroundColor
|
|
70
|
-
style = Paint.Style.FILL
|
|
71
|
-
isAntiAlias = true
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
val width = paint.measureText(text, start, end)
|
|
75
|
-
|
|
76
|
-
// Use font metrics for consistent height (matches iOS)
|
|
77
|
-
val fontMetrics = paint.fontMetrics
|
|
78
|
-
val textHeight = fontMetrics.descent - fontMetrics.ascent
|
|
79
|
-
val textTop = y + fontMetrics.ascent
|
|
80
|
-
|
|
81
|
-
// Apply background insets first (shrinks from line box - EXACTLY like iOS line 45-48)
|
|
82
|
-
val insetTop = textTop + backgroundInsetTop
|
|
83
|
-
val insetHeight = textHeight - (backgroundInsetTop + backgroundInsetBottom)
|
|
84
|
-
|
|
85
|
-
// Only apply padding at line boundaries (matches iOS behavior)
|
|
86
|
-
val leftPad = if (isLineStart) paddingLeft else 0f
|
|
87
|
-
val rightPad = if (isLineEnd) paddingRight else 0f
|
|
88
|
-
|
|
89
|
-
// SELECTIVE ROUNDING STRATEGY:
|
|
90
|
-
// 1. Line Start: Round Left corners.
|
|
91
|
-
// 2. Line End: Round Right corners.
|
|
92
|
-
// 3. Middle: Square (no rounding).
|
|
93
|
-
// 4. Overlap: Minimal (1px) to seal seams.
|
|
94
|
-
|
|
95
|
-
val overlapExtension = 1f
|
|
96
|
-
|
|
97
|
-
// No extension needed for start/end boundaries
|
|
98
|
-
val leftExtend = 0f
|
|
99
|
-
|
|
100
|
-
// Extend right slightly for middle characters to seal the gap
|
|
101
|
-
val rightExtend = if (!isLineEnd) {
|
|
102
|
-
if (isLineStart) leftPad + overlapExtension else overlapExtension
|
|
103
|
-
} else {
|
|
104
|
-
0f
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Vertical overlap to eliminate gaps (reduced to prevent descender clipping)
|
|
108
|
-
val topExtend = 0f
|
|
109
|
-
val bottomExtend = 0f
|
|
110
|
-
|
|
111
|
-
// Calculate background rect
|
|
112
|
-
// NOTE: Since this is a ReplacementSpan, 'x' is the start of the span (including padding).
|
|
113
|
-
// So we draw from 'x', not 'x - leftPad'.
|
|
114
|
-
val rect = RectF(
|
|
115
|
-
x + backgroundInsetLeft - leftExtend,
|
|
116
|
-
insetTop - paddingTop - topExtend,
|
|
117
|
-
x + leftPad + width + rightPad - backgroundInsetRight + rightExtend,
|
|
118
|
-
insetTop + insetHeight + paddingBottom + bottomExtend
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
// Draw based on position
|
|
122
|
-
when {
|
|
123
|
-
isLineStart && isLineEnd -> {
|
|
124
|
-
// Single character - round all corners
|
|
125
|
-
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, bgPaint)
|
|
126
|
-
}
|
|
127
|
-
isLineStart -> {
|
|
128
|
-
// Line START - round LEFT corners (top and bottom)
|
|
129
|
-
val path = android.graphics.Path()
|
|
130
|
-
path.addRoundRect(
|
|
131
|
-
rect,
|
|
132
|
-
floatArrayOf(
|
|
133
|
-
cornerRadius, cornerRadius, // top-left
|
|
134
|
-
0f, 0f, // top-right
|
|
135
|
-
0f, 0f, // bottom-right
|
|
136
|
-
cornerRadius, cornerRadius // bottom-left
|
|
137
|
-
),
|
|
138
|
-
android.graphics.Path.Direction.CW
|
|
139
|
-
)
|
|
140
|
-
canvas.drawPath(path, bgPaint)
|
|
141
|
-
}
|
|
142
|
-
isLineEnd -> {
|
|
143
|
-
// Line END - round RIGHT corners (top and bottom)
|
|
144
|
-
val path = android.graphics.Path()
|
|
145
|
-
path.addRoundRect(
|
|
146
|
-
rect,
|
|
147
|
-
floatArrayOf(
|
|
148
|
-
0f, 0f, // top-left
|
|
149
|
-
cornerRadius, cornerRadius, // top-right
|
|
150
|
-
cornerRadius, cornerRadius, // bottom-right
|
|
151
|
-
0f, 0f // bottom-left
|
|
152
|
-
),
|
|
153
|
-
android.graphics.Path.Direction.CW
|
|
154
|
-
)
|
|
155
|
-
canvas.drawPath(path, bgPaint)
|
|
156
|
-
}
|
|
157
|
-
else -> {
|
|
158
|
-
// Middle characters - NO rounded corners (square) for smooth edges
|
|
159
|
-
canvas.drawRect(rect, bgPaint)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Draw text offset by left padding only if at line start
|
|
164
|
-
val textPaint = Paint(paint).apply {
|
|
165
|
-
color = textColor
|
|
166
|
-
isAntiAlias = true
|
|
167
|
-
}
|
|
168
|
-
canvas.drawText(text, start, end, x + leftPad, y.toFloat(), textPaint)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
24
|
class HighlightTextView : AppCompatEditText {
|
|
25
|
+
// Visual props
|
|
173
26
|
private var characterBackgroundColor: Int = Color.parseColor("#FFFF00")
|
|
174
27
|
private var textColorValue: Int = Color.BLACK
|
|
175
28
|
private var cornerRadius: Float = 4f
|
|
176
29
|
private var highlightBorderRadius: Float = 0f
|
|
30
|
+
|
|
31
|
+
// Per-character padding
|
|
177
32
|
private var charPaddingLeft: Float = 4f
|
|
178
33
|
private var charPaddingRight: Float = 4f
|
|
179
34
|
private var charPaddingTop: Float = 4f
|
|
180
35
|
private var charPaddingBottom: Float = 4f
|
|
36
|
+
|
|
37
|
+
// Background insets (shrink from line box)
|
|
181
38
|
private var backgroundInsetTop: Float = 0f
|
|
182
39
|
private var backgroundInsetBottom: Float = 0f
|
|
183
40
|
private var backgroundInsetLeft: Float = 0f
|
|
184
41
|
private var backgroundInsetRight: Float = 0f
|
|
42
|
+
|
|
43
|
+
// Line height control
|
|
185
44
|
private var customLineHeight: Float = 0f
|
|
45
|
+
|
|
46
|
+
// Font + alignment state
|
|
186
47
|
private var currentFontFamily: String? = null
|
|
187
48
|
private var currentFontWeight: String = "normal"
|
|
188
49
|
private var currentVerticalAlign: String? = null
|
|
50
|
+
// Letter spacing in layout points (same semantics as React Native's letterSpacing prop)
|
|
51
|
+
private var letterSpacingPoints: Float = 0f
|
|
52
|
+
|
|
53
|
+
// Internal flags
|
|
189
54
|
private var isUpdatingText: Boolean = false
|
|
190
|
-
|
|
55
|
+
|
|
56
|
+
// Drawing helpers
|
|
57
|
+
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
58
|
+
style = Paint.Style.FILL
|
|
59
|
+
}
|
|
60
|
+
private val backgroundRect = RectF()
|
|
61
|
+
|
|
191
62
|
var onTextChangeListener: ((String) -> Unit)? = null
|
|
192
63
|
|
|
193
64
|
constructor(context: Context?) : super(context!!) {
|
|
194
65
|
init()
|
|
195
66
|
}
|
|
196
|
-
|
|
67
|
+
|
|
197
68
|
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {
|
|
198
69
|
init()
|
|
199
70
|
}
|
|
200
|
-
|
|
71
|
+
|
|
201
72
|
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
|
202
73
|
context!!,
|
|
203
74
|
attrs,
|
|
@@ -212,42 +83,120 @@ class HighlightTextView : AppCompatEditText {
|
|
|
212
83
|
gravity = Gravity.CENTER
|
|
213
84
|
setPadding(20, 20, 20, 20)
|
|
214
85
|
textColorValue = currentTextColor
|
|
215
|
-
|
|
86
|
+
|
|
216
87
|
// Enable text wrapping
|
|
217
88
|
maxLines = Int.MAX_VALUE
|
|
218
89
|
isSingleLine = false
|
|
219
90
|
setHorizontallyScrolling(false)
|
|
220
|
-
|
|
91
|
+
|
|
92
|
+
applyLineHeightAndSpacing()
|
|
93
|
+
|
|
221
94
|
addTextChangedListener(object : TextWatcher {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
|
226
|
-
changeStart = start
|
|
227
|
-
changeEnd = start + after
|
|
228
|
-
}
|
|
229
|
-
|
|
95
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
96
|
+
|
|
230
97
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
231
|
-
|
|
98
|
+
|
|
232
99
|
override fun afterTextChanged(s: Editable?) {
|
|
233
100
|
if (!isUpdatingText) {
|
|
234
101
|
onTextChangeListener?.invoke(s?.toString() ?: "")
|
|
235
|
-
|
|
236
|
-
|
|
102
|
+
// Text changed → redraw backgrounds
|
|
103
|
+
invalidate()
|
|
237
104
|
}
|
|
238
105
|
}
|
|
239
106
|
})
|
|
240
107
|
}
|
|
241
108
|
|
|
109
|
+
// --- Drawing -----------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
override fun onDraw(canvas: Canvas) {
|
|
112
|
+
val layout = layout
|
|
113
|
+
val text = text
|
|
114
|
+
|
|
115
|
+
if (layout != null && text != null && text.isNotEmpty()) {
|
|
116
|
+
val save = canvas.save()
|
|
117
|
+
|
|
118
|
+
// Canvas is already translated by scrollX/scrollY in View.draw().
|
|
119
|
+
// Here we only need to account for view padding so our coordinates
|
|
120
|
+
// match the Layout's internal coordinate system.
|
|
121
|
+
val translateX = totalPaddingLeft
|
|
122
|
+
val translateY = totalPaddingTop
|
|
123
|
+
canvas.translate(translateX.toFloat(), translateY.toFloat())
|
|
124
|
+
|
|
125
|
+
drawCharacterBackgrounds(canvas, text, layout)
|
|
126
|
+
|
|
127
|
+
canvas.restoreToCount(save)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Let EditText draw text, cursor, selection, etc. on top of backgrounds
|
|
131
|
+
super.onDraw(canvas)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun drawCharacterBackgrounds(canvas: Canvas, text: CharSequence, layout: android.text.Layout) {
|
|
135
|
+
backgroundPaint.color = characterBackgroundColor
|
|
136
|
+
val paint = paint
|
|
137
|
+
val radius = if (highlightBorderRadius > 0f) highlightBorderRadius else cornerRadius
|
|
138
|
+
|
|
139
|
+
val length = text.length
|
|
140
|
+
if (length == 0) return
|
|
141
|
+
|
|
142
|
+
for (i in 0 until length) {
|
|
143
|
+
val ch = text[i]
|
|
144
|
+
// Match iOS: skip spaces and control characters for background
|
|
145
|
+
if (ch == '\n' || ch == '\t' || ch == ' ') continue
|
|
146
|
+
|
|
147
|
+
val line = layout.getLineForOffset(i)
|
|
148
|
+
val lineStart = layout.getLineStart(line)
|
|
149
|
+
val lineEnd = layout.getLineEnd(line)
|
|
150
|
+
|
|
151
|
+
// Horizontal bounds based on layout positions
|
|
152
|
+
val xStart = layout.getPrimaryHorizontal(i)
|
|
153
|
+
val isLastCharInLine = i == lineEnd - 1
|
|
154
|
+
val xEnd = if (!isLastCharInLine && i + 1 < length) {
|
|
155
|
+
layout.getPrimaryHorizontal(i + 1)
|
|
156
|
+
} else {
|
|
157
|
+
// Fallback for last character in the line/text
|
|
158
|
+
xStart + paint.measureText(text, i, i + 1)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Vertical bounds based on line box (includes line spacing)
|
|
162
|
+
val lineTop = layout.getLineTop(line).toFloat()
|
|
163
|
+
val lineBottom = layout.getLineBottom(line).toFloat()
|
|
164
|
+
|
|
165
|
+
var left = xStart
|
|
166
|
+
var right = xEnd
|
|
167
|
+
var top = lineTop
|
|
168
|
+
var bottom = lineBottom
|
|
169
|
+
|
|
170
|
+
// First shrink by background insets (from the line box)
|
|
171
|
+
top += backgroundInsetTop
|
|
172
|
+
bottom -= backgroundInsetBottom
|
|
173
|
+
left += backgroundInsetLeft
|
|
174
|
+
right -= backgroundInsetRight
|
|
175
|
+
|
|
176
|
+
// Then expand outward by per-character padding
|
|
177
|
+
left -= charPaddingLeft
|
|
178
|
+
right += charPaddingRight
|
|
179
|
+
top -= charPaddingTop
|
|
180
|
+
bottom += charPaddingBottom
|
|
181
|
+
|
|
182
|
+
if (right <= left || bottom <= top) continue
|
|
183
|
+
|
|
184
|
+
backgroundRect.set(left, top, right, bottom)
|
|
185
|
+
canvas.drawRoundRect(backgroundRect, radius, radius, backgroundPaint)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Public API used from the ViewManager ------------------------------------
|
|
190
|
+
|
|
242
191
|
fun setCharacterBackgroundColor(color: Int) {
|
|
243
192
|
characterBackgroundColor = color
|
|
244
|
-
|
|
193
|
+
invalidate()
|
|
245
194
|
}
|
|
246
|
-
|
|
195
|
+
|
|
247
196
|
override fun setTextColor(color: Int) {
|
|
248
197
|
super.setTextColor(color)
|
|
249
198
|
textColorValue = color
|
|
250
|
-
|
|
199
|
+
invalidate()
|
|
251
200
|
}
|
|
252
201
|
|
|
253
202
|
fun setCharPadding(left: Float, top: Float, right: Float, bottom: Float) {
|
|
@@ -256,35 +205,40 @@ class HighlightTextView : AppCompatEditText {
|
|
|
256
205
|
charPaddingRight = right
|
|
257
206
|
charPaddingBottom = bottom
|
|
258
207
|
updateViewPadding()
|
|
259
|
-
|
|
208
|
+
applyLineHeightAndSpacing()
|
|
209
|
+
invalidate()
|
|
260
210
|
}
|
|
261
|
-
|
|
211
|
+
|
|
262
212
|
fun setCharPaddingLeft(padding: Float) {
|
|
263
213
|
charPaddingLeft = padding
|
|
264
214
|
updateViewPadding()
|
|
265
|
-
|
|
215
|
+
applyLineHeightAndSpacing()
|
|
216
|
+
invalidate()
|
|
266
217
|
}
|
|
267
|
-
|
|
218
|
+
|
|
268
219
|
fun setCharPaddingRight(padding: Float) {
|
|
269
220
|
charPaddingRight = padding
|
|
270
221
|
updateViewPadding()
|
|
271
|
-
|
|
222
|
+
applyLineHeightAndSpacing()
|
|
223
|
+
invalidate()
|
|
272
224
|
}
|
|
273
|
-
|
|
225
|
+
|
|
274
226
|
fun setCharPaddingTop(padding: Float) {
|
|
275
227
|
charPaddingTop = padding
|
|
276
228
|
updateViewPadding()
|
|
277
|
-
|
|
229
|
+
applyLineHeightAndSpacing()
|
|
230
|
+
invalidate()
|
|
278
231
|
}
|
|
279
|
-
|
|
232
|
+
|
|
280
233
|
fun setCharPaddingBottom(padding: Float) {
|
|
281
234
|
charPaddingBottom = padding
|
|
282
235
|
updateViewPadding()
|
|
283
|
-
|
|
236
|
+
applyLineHeightAndSpacing()
|
|
237
|
+
invalidate()
|
|
284
238
|
}
|
|
285
|
-
|
|
239
|
+
|
|
286
240
|
private fun updateViewPadding() {
|
|
287
|
-
//
|
|
241
|
+
// Keep view padding in sync so backgrounds are not clipped at the edges
|
|
288
242
|
setPadding(
|
|
289
243
|
charPaddingLeft.toInt(),
|
|
290
244
|
charPaddingTop.toInt(),
|
|
@@ -292,27 +246,27 @@ class HighlightTextView : AppCompatEditText {
|
|
|
292
246
|
charPaddingBottom.toInt()
|
|
293
247
|
)
|
|
294
248
|
}
|
|
295
|
-
|
|
249
|
+
|
|
296
250
|
fun setCornerRadius(radius: Float) {
|
|
297
251
|
cornerRadius = radius
|
|
298
|
-
|
|
252
|
+
invalidate()
|
|
299
253
|
}
|
|
300
|
-
|
|
254
|
+
|
|
301
255
|
fun setHighlightBorderRadius(radius: Float) {
|
|
302
256
|
highlightBorderRadius = radius
|
|
303
|
-
|
|
257
|
+
invalidate()
|
|
304
258
|
}
|
|
305
|
-
|
|
259
|
+
|
|
306
260
|
fun setFontWeight(weight: String) {
|
|
307
261
|
currentFontWeight = weight
|
|
308
262
|
updateFont()
|
|
309
263
|
}
|
|
310
|
-
|
|
264
|
+
|
|
311
265
|
fun setFontFamilyProp(family: String?) {
|
|
312
266
|
currentFontFamily = family
|
|
313
267
|
updateFont()
|
|
314
268
|
}
|
|
315
|
-
|
|
269
|
+
|
|
316
270
|
private fun updateFont() {
|
|
317
271
|
// Parse font weight to integer (100-900)
|
|
318
272
|
val weight = when (currentFontWeight) {
|
|
@@ -327,16 +281,21 @@ class HighlightTextView : AppCompatEditText {
|
|
|
327
281
|
"900" -> 900
|
|
328
282
|
else -> 400
|
|
329
283
|
}
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
val
|
|
333
|
-
|
|
284
|
+
|
|
285
|
+
// Capture currentFontFamily as local variable to avoid smart cast issues
|
|
286
|
+
val fontFamily = currentFontFamily
|
|
287
|
+
|
|
288
|
+
// Get base typeface using ReactFontManager for custom fonts
|
|
289
|
+
val baseTypeface = if (fontFamily != null) {
|
|
290
|
+
when (fontFamily.lowercase()) {
|
|
334
291
|
"system" -> Typeface.DEFAULT
|
|
335
292
|
"sans-serif" -> Typeface.SANS_SERIF
|
|
336
293
|
"serif" -> Typeface.SERIF
|
|
337
294
|
"monospace" -> Typeface.MONOSPACE
|
|
338
295
|
else -> try {
|
|
339
|
-
|
|
296
|
+
// Use ReactFontManager to load custom fonts from assets
|
|
297
|
+
val style = if (weight >= 600) Typeface.BOLD else Typeface.NORMAL
|
|
298
|
+
ReactFontManager.getInstance().getTypeface(fontFamily, style, context.assets)
|
|
340
299
|
} catch (e: Exception) {
|
|
341
300
|
Typeface.DEFAULT
|
|
342
301
|
}
|
|
@@ -344,7 +303,7 @@ class HighlightTextView : AppCompatEditText {
|
|
|
344
303
|
} else {
|
|
345
304
|
Typeface.DEFAULT
|
|
346
305
|
}
|
|
347
|
-
|
|
306
|
+
|
|
348
307
|
// Apply font weight - use API 28+ method for better weight support
|
|
349
308
|
val typeface = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
|
350
309
|
Typeface.create(baseTypeface, weight, false)
|
|
@@ -353,16 +312,18 @@ class HighlightTextView : AppCompatEditText {
|
|
|
353
312
|
val style = if (weight >= 600) Typeface.BOLD else Typeface.NORMAL
|
|
354
313
|
Typeface.create(baseTypeface, style)
|
|
355
314
|
}
|
|
356
|
-
|
|
315
|
+
|
|
357
316
|
this.typeface = typeface
|
|
358
|
-
|
|
317
|
+
applyLineHeightAndSpacing()
|
|
318
|
+
invalidate()
|
|
359
319
|
}
|
|
360
|
-
|
|
320
|
+
|
|
361
321
|
fun setVerticalAlign(align: String?) {
|
|
362
322
|
currentVerticalAlign = align
|
|
363
323
|
updateVerticalAlignment()
|
|
324
|
+
invalidate()
|
|
364
325
|
}
|
|
365
|
-
|
|
326
|
+
|
|
366
327
|
private fun updateVerticalAlignment() {
|
|
367
328
|
// Preserve horizontal alignment when updating vertical
|
|
368
329
|
val horizontalGravity = gravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
|
@@ -371,50 +332,63 @@ class HighlightTextView : AppCompatEditText {
|
|
|
371
332
|
"bottom" -> Gravity.BOTTOM
|
|
372
333
|
else -> Gravity.CENTER_VERTICAL
|
|
373
334
|
}
|
|
374
|
-
|
|
335
|
+
|
|
375
336
|
gravity = horizontalGravity or verticalGravity
|
|
376
337
|
}
|
|
377
|
-
|
|
338
|
+
|
|
378
339
|
fun setBackgroundInsetTop(inset: Float) {
|
|
379
340
|
backgroundInsetTop = inset
|
|
380
|
-
|
|
341
|
+
invalidate()
|
|
381
342
|
}
|
|
382
|
-
|
|
343
|
+
|
|
383
344
|
fun setBackgroundInsetBottom(inset: Float) {
|
|
384
345
|
backgroundInsetBottom = inset
|
|
385
|
-
|
|
346
|
+
invalidate()
|
|
386
347
|
}
|
|
387
|
-
|
|
348
|
+
|
|
388
349
|
fun setBackgroundInsetLeft(inset: Float) {
|
|
389
350
|
backgroundInsetLeft = inset
|
|
390
|
-
|
|
351
|
+
invalidate()
|
|
391
352
|
}
|
|
392
|
-
|
|
353
|
+
|
|
393
354
|
fun setBackgroundInsetRight(inset: Float) {
|
|
394
355
|
backgroundInsetRight = inset
|
|
395
|
-
|
|
356
|
+
invalidate()
|
|
396
357
|
}
|
|
397
|
-
|
|
358
|
+
|
|
398
359
|
fun setCustomLineHeight(lineHeight: Float) {
|
|
399
360
|
customLineHeight = lineHeight
|
|
400
|
-
|
|
361
|
+
applyLineHeightAndSpacing()
|
|
362
|
+
invalidate()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
fun setLetterSpacingProp(points: Float) {
|
|
366
|
+
letterSpacingPoints = points
|
|
367
|
+
applyLetterSpacing()
|
|
368
|
+
invalidate()
|
|
401
369
|
}
|
|
402
370
|
|
|
403
|
-
fun
|
|
404
|
-
|
|
371
|
+
override fun setTextSize(unit: Int, size: Float) {
|
|
372
|
+
super.setTextSize(unit, size)
|
|
373
|
+
applyLineHeightAndSpacing()
|
|
374
|
+
applyLetterSpacing()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fun setTextProp(newText: String) {
|
|
378
|
+
if (this.text?.toString() != newText) {
|
|
405
379
|
isUpdatingText = true
|
|
406
|
-
setText(
|
|
407
|
-
applyCharacterBackgrounds()
|
|
380
|
+
setText(newText)
|
|
408
381
|
// Move cursor to end of text after setting
|
|
409
382
|
post {
|
|
410
383
|
if (hasFocus()) {
|
|
411
|
-
text
|
|
384
|
+
text?.length?.let { setSelection(it) }
|
|
412
385
|
}
|
|
413
386
|
}
|
|
414
387
|
isUpdatingText = false
|
|
388
|
+
invalidate()
|
|
415
389
|
}
|
|
416
390
|
}
|
|
417
|
-
|
|
391
|
+
|
|
418
392
|
fun setAutoFocus(autoFocus: Boolean) {
|
|
419
393
|
if (autoFocus && isFocusable && isFocusableInTouchMode) {
|
|
420
394
|
postDelayed({
|
|
@@ -426,319 +400,36 @@ class HighlightTextView : AppCompatEditText {
|
|
|
426
400
|
}, 100)
|
|
427
401
|
}
|
|
428
402
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// Check if a newline was inserted - if so, expand region to include char before it
|
|
442
|
-
val hasNewline = textStr.substring(start, minOf(end, textStr.length)).contains('\n')
|
|
443
|
-
|
|
444
|
-
// Expand the region to include entire lines that were affected
|
|
445
|
-
val layout = layout
|
|
446
|
-
val expandedStart: Int
|
|
447
|
-
val expandedEnd: Int
|
|
448
|
-
|
|
449
|
-
if (layout != null && textStr.isNotEmpty()) {
|
|
450
|
-
val startLine = layout.getLineForOffset(minOf(start, textStr.length - 1))
|
|
451
|
-
val endLine = layout.getLineForOffset(minOf(end, textStr.length - 1))
|
|
452
|
-
expandedStart = layout.getLineStart(startLine)
|
|
453
|
-
expandedEnd = layout.getLineEnd(endLine)
|
|
454
|
-
} else {
|
|
455
|
-
// If newline inserted, include character before it
|
|
456
|
-
expandedStart = if (hasNewline) maxOf(0, start - 2) else maxOf(0, start - 1)
|
|
457
|
-
expandedEnd = minOf(textStr.length, end + 1)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Remove existing spans in the affected lines
|
|
461
|
-
val existingSpans = editable.getSpans(expandedStart, expandedEnd, RoundedBackgroundSpan::class.java)
|
|
462
|
-
for (span in existingSpans) {
|
|
463
|
-
editable.removeSpan(span)
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Apply spans with correct line boundary flags immediately
|
|
467
|
-
val radius = if (highlightBorderRadius > 0) highlightBorderRadius else cornerRadius
|
|
468
|
-
|
|
469
|
-
for (i in expandedStart until expandedEnd) {
|
|
470
|
-
if (i >= textStr.length) break
|
|
471
|
-
|
|
472
|
-
val char = textStr[i]
|
|
473
|
-
val shouldHighlight = when {
|
|
474
|
-
char == '\n' || char == '\t' -> false
|
|
475
|
-
char == ' ' -> {
|
|
476
|
-
val hasSpaceBefore = i > 0 && textStr[i - 1] == ' '
|
|
477
|
-
val hasSpaceAfter = i < textStr.length - 1 && textStr[i + 1] == ' '
|
|
478
|
-
!hasSpaceBefore && !hasSpaceAfter
|
|
479
|
-
}
|
|
480
|
-
else -> true
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (shouldHighlight) {
|
|
484
|
-
// ALWAYS check newlines first (for manual line breaks)
|
|
485
|
-
val hasNewlineBefore = i > 0 && textStr[i - 1] == '\n'
|
|
486
|
-
val hasNewlineAfter = i + 1 < textStr.length && textStr[i + 1] == '\n'
|
|
487
|
-
|
|
488
|
-
var isAtLineStart = i == 0 || hasNewlineBefore
|
|
489
|
-
var isAtLineEnd = i == textStr.length - 1 || hasNewlineAfter
|
|
490
|
-
|
|
491
|
-
// Determine if this is the first or last line
|
|
492
|
-
var isOnFirstLine = i == 0 || hasNewlineBefore
|
|
493
|
-
var isOnLastLine = i == textStr.length - 1 || hasNewlineAfter
|
|
494
|
-
|
|
495
|
-
// Only use layout for auto-wrapped lines (not manual newlines)
|
|
496
|
-
if (!hasNewlineBefore && !hasNewlineAfter && layout != null && i < textStr.length) {
|
|
497
|
-
try {
|
|
498
|
-
val line = layout.getLineForOffset(i)
|
|
499
|
-
val lineStart = layout.getLineStart(line)
|
|
500
|
-
val lineEnd = layout.getLineEnd(line)
|
|
501
|
-
// Check if this is the first or last line
|
|
502
|
-
isOnFirstLine = line == 0
|
|
503
|
-
isOnLastLine = line == layout.lineCount - 1
|
|
504
|
-
// Only override if this is an auto-wrapped boundary
|
|
505
|
-
if (i == lineStart && textStr.getOrNull(i - 1) != '\n') {
|
|
506
|
-
isAtLineStart = true
|
|
507
|
-
}
|
|
508
|
-
if ((i + 1) == lineEnd && textStr.getOrNull(i + 1) != '\n') {
|
|
509
|
-
isAtLineEnd = true
|
|
510
|
-
}
|
|
511
|
-
} catch (e: Exception) {
|
|
512
|
-
// Layout might not be ready, keep newline-based detection
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
val span = RoundedBackgroundSpan(
|
|
517
|
-
characterBackgroundColor,
|
|
518
|
-
textColorValue,
|
|
519
|
-
charPaddingLeft,
|
|
520
|
-
charPaddingRight,
|
|
521
|
-
charPaddingTop,
|
|
522
|
-
charPaddingBottom,
|
|
523
|
-
backgroundInsetTop,
|
|
524
|
-
backgroundInsetBottom,
|
|
525
|
-
backgroundInsetLeft,
|
|
526
|
-
backgroundInsetRight,
|
|
527
|
-
radius,
|
|
528
|
-
isAtLineStart,
|
|
529
|
-
isAtLineEnd,
|
|
530
|
-
isOnFirstLine,
|
|
531
|
-
isOnLastLine
|
|
532
|
-
)
|
|
533
|
-
editable.setSpan(span, i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
403
|
+
|
|
404
|
+
// --- Layout helpers ----------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
private fun applyLineHeightAndSpacing() {
|
|
407
|
+
if (customLineHeight > 0f) {
|
|
408
|
+
// customLineHeight comes from JS as "points"; convert to px using scaledDensity
|
|
409
|
+
val metrics = resources.displayMetrics
|
|
410
|
+
val desiredLineHeightPx = customLineHeight * metrics.scaledDensity
|
|
411
|
+
val textHeightPx = textSize
|
|
412
|
+
if (textHeightPx > 0f) {
|
|
413
|
+
val multiplier = desiredLineHeightPx / textHeightPx
|
|
414
|
+
setLineSpacing(0f, multiplier)
|
|
534
415
|
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
isUpdatingText = false
|
|
538
|
-
|
|
539
|
-
// JERK FIX: Skip the post-update during fast typing to prevent layout thrashing
|
|
540
|
-
// Only update boundaries when user stops typing (reduces update frequency)
|
|
541
|
-
removeCallbacks(boundaryUpdateCheck)
|
|
542
|
-
postDelayed(boundaryUpdateCheck, 200)
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Runnable for delayed boundary check
|
|
546
|
-
private val boundaryUpdateCheck = Runnable {
|
|
547
|
-
if (!isUpdatingText) {
|
|
548
|
-
updateAutoWrappedLineBoundaries()
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Full re-application of spans (used when props change, not during typing).
|
|
554
|
-
* iOS-style: Work directly with editable, no setText() call.
|
|
555
|
-
*/
|
|
556
|
-
private fun applyCharacterBackgrounds() {
|
|
557
|
-
val editable = editableText ?: return
|
|
558
|
-
val textStr = editable.toString()
|
|
559
|
-
if (textStr.isEmpty()) return
|
|
560
|
-
|
|
561
|
-
// Apply line height if specified, or add spacing for padding
|
|
562
|
-
if (customLineHeight > 0) {
|
|
563
|
-
val lineSpacingMultiplier = customLineHeight / textSize
|
|
564
|
-
setLineSpacing(0f, lineSpacingMultiplier)
|
|
565
416
|
} else {
|
|
566
|
-
//
|
|
417
|
+
// Default: add extra spacing equal to vertical padding so backgrounds don't collide
|
|
567
418
|
val extraSpacing = charPaddingTop + charPaddingBottom
|
|
568
419
|
setLineSpacing(extraSpacing, 1.0f)
|
|
569
420
|
}
|
|
570
|
-
|
|
571
|
-
isUpdatingText = true
|
|
572
|
-
|
|
573
|
-
// Remove all existing spans
|
|
574
|
-
val existingSpans = editable.getSpans(0, editable.length, RoundedBackgroundSpan::class.java)
|
|
575
|
-
for (span in existingSpans) {
|
|
576
|
-
editable.removeSpan(span)
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Apply spans to all characters with correct line boundary flags
|
|
580
|
-
val radius = if (highlightBorderRadius > 0) highlightBorderRadius else cornerRadius
|
|
581
|
-
val layoutObj = layout
|
|
582
|
-
|
|
583
|
-
for (i in textStr.indices) {
|
|
584
|
-
val char = textStr[i]
|
|
585
|
-
|
|
586
|
-
val shouldHighlight = when {
|
|
587
|
-
char == '\n' || char == '\t' -> false
|
|
588
|
-
char == ' ' -> {
|
|
589
|
-
val hasSpaceBefore = i > 0 && textStr[i - 1] == ' '
|
|
590
|
-
val hasSpaceAfter = i < textStr.length - 1 && textStr[i + 1] == ' '
|
|
591
|
-
!hasSpaceBefore && !hasSpaceAfter
|
|
592
|
-
}
|
|
593
|
-
else -> true
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (shouldHighlight) {
|
|
597
|
-
// ALWAYS check newlines first (for manual line breaks)
|
|
598
|
-
val hasNewlineBefore = i > 0 && textStr[i - 1] == '\n'
|
|
599
|
-
val hasNewlineAfter = i + 1 < textStr.length && textStr[i + 1] == '\n'
|
|
600
|
-
|
|
601
|
-
var isAtLineStart = i == 0 || hasNewlineBefore
|
|
602
|
-
var isAtLineEnd = i == textStr.length - 1 || hasNewlineAfter
|
|
603
|
-
|
|
604
|
-
// Determine if this is the first or last line
|
|
605
|
-
var isOnFirstLine = i == 0 || hasNewlineBefore
|
|
606
|
-
var isOnLastLine = i == textStr.length - 1 || hasNewlineAfter
|
|
607
|
-
|
|
608
|
-
// Only use layout for auto-wrapped lines (not manual newlines)
|
|
609
|
-
if (!hasNewlineBefore && !hasNewlineAfter && layoutObj != null && i < textStr.length) {
|
|
610
|
-
try {
|
|
611
|
-
val line = layoutObj.getLineForOffset(i)
|
|
612
|
-
val lineStart = layoutObj.getLineStart(line)
|
|
613
|
-
val lineEnd = layoutObj.getLineEnd(line)
|
|
614
|
-
// Check if this is the first or last line
|
|
615
|
-
isOnFirstLine = line == 0
|
|
616
|
-
isOnLastLine = line == layoutObj.lineCount - 1
|
|
617
|
-
// Only override if this is an auto-wrapped boundary
|
|
618
|
-
if (i == lineStart && textStr.getOrNull(i - 1) != '\n') {
|
|
619
|
-
isAtLineStart = true
|
|
620
|
-
}
|
|
621
|
-
if ((i + 1) == lineEnd && textStr.getOrNull(i + 1) != '\n') {
|
|
622
|
-
isAtLineEnd = true
|
|
623
|
-
}
|
|
624
|
-
} catch (e: Exception) {
|
|
625
|
-
// Layout might not be ready, keep newline-based detection
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
val span = RoundedBackgroundSpan(
|
|
630
|
-
characterBackgroundColor,
|
|
631
|
-
textColorValue,
|
|
632
|
-
charPaddingLeft,
|
|
633
|
-
charPaddingRight,
|
|
634
|
-
charPaddingTop,
|
|
635
|
-
charPaddingBottom,
|
|
636
|
-
backgroundInsetTop,
|
|
637
|
-
backgroundInsetBottom,
|
|
638
|
-
backgroundInsetLeft,
|
|
639
|
-
backgroundInsetRight,
|
|
640
|
-
radius,
|
|
641
|
-
isAtLineStart,
|
|
642
|
-
isAtLineEnd,
|
|
643
|
-
isOnFirstLine,
|
|
644
|
-
isOnLastLine
|
|
645
|
-
)
|
|
646
|
-
editable.setSpan(span, i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
isUpdatingText = false
|
|
651
|
-
|
|
652
|
-
// Schedule a post-layout update to ensure corner rounding is correct
|
|
653
|
-
// This is needed because layout might not be ready when this method is called
|
|
654
|
-
post {
|
|
655
|
-
if (!isUpdatingText) {
|
|
656
|
-
updateAutoWrappedLineBoundaries()
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
421
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
val editable = editableText ?: return
|
|
672
|
-
val textStr = editable.toString()
|
|
673
|
-
if (textStr.isEmpty()) return
|
|
674
|
-
|
|
675
|
-
// Validate that layout is ready and has valid dimensions
|
|
676
|
-
if (width <= 0 || layout.lineCount == 0) return
|
|
677
|
-
|
|
678
|
-
val spans = editable.getSpans(0, editable.length, RoundedBackgroundSpan::class.java)
|
|
679
|
-
if (spans.isEmpty()) return
|
|
680
|
-
|
|
681
|
-
isUpdatingText = true
|
|
682
|
-
var hasChanges = false
|
|
683
|
-
|
|
684
|
-
for (span in spans) {
|
|
685
|
-
val spanStart = editable.getSpanStart(span)
|
|
686
|
-
val spanEnd = editable.getSpanEnd(span)
|
|
687
|
-
|
|
688
|
-
if (spanStart < 0 || spanStart >= textStr.length) continue
|
|
689
|
-
|
|
690
|
-
try {
|
|
691
|
-
val line = layout.getLineForOffset(spanStart)
|
|
692
|
-
val lineStart = layout.getLineStart(line)
|
|
693
|
-
val lineEnd = layout.getLineEnd(line)
|
|
694
|
-
|
|
695
|
-
// Determine actual line boundaries (includes auto-wrap)
|
|
696
|
-
val isAtLineStart = spanStart == lineStart
|
|
697
|
-
val isAtLineEnd = spanEnd == lineEnd
|
|
698
|
-
|
|
699
|
-
// Check if this is the first or last line
|
|
700
|
-
val isOnFirstLine = line == 0
|
|
701
|
-
val isOnLastLine = line == layout.lineCount - 1
|
|
702
|
-
|
|
703
|
-
// Only update if this is an auto-wrapped line boundary (not a newline boundary)
|
|
704
|
-
val isNewlineBoundary = (spanStart > 0 && textStr[spanStart - 1] == '\n') ||
|
|
705
|
-
(spanEnd < textStr.length && textStr[spanEnd] == '\n')
|
|
706
|
-
|
|
707
|
-
// Only recreate span if it's at an auto-wrapped boundary and flags are wrong
|
|
708
|
-
if (!isNewlineBoundary && (isAtLineStart != span.isLineStart || isAtLineEnd != span.isLineEnd ||
|
|
709
|
-
isOnFirstLine != span.isFirstLine || isOnLastLine != span.isLastLine)) {
|
|
710
|
-
val newSpan = RoundedBackgroundSpan(
|
|
711
|
-
span.backgroundColor,
|
|
712
|
-
span.textColor,
|
|
713
|
-
span.paddingLeft,
|
|
714
|
-
span.paddingRight,
|
|
715
|
-
span.paddingTop,
|
|
716
|
-
span.paddingBottom,
|
|
717
|
-
span.backgroundInsetTop,
|
|
718
|
-
span.backgroundInsetBottom,
|
|
719
|
-
span.backgroundInsetLeft,
|
|
720
|
-
span.backgroundInsetRight,
|
|
721
|
-
span.cornerRadius,
|
|
722
|
-
isAtLineStart,
|
|
723
|
-
isAtLineEnd,
|
|
724
|
-
isOnFirstLine,
|
|
725
|
-
isOnLastLine
|
|
726
|
-
)
|
|
727
|
-
editable.removeSpan(span)
|
|
728
|
-
editable.setSpan(newSpan, spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
729
|
-
hasChanges = true
|
|
730
|
-
}
|
|
731
|
-
} catch (e: Exception) {
|
|
732
|
-
// Layout state is invalid, skip this update
|
|
733
|
-
continue
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
isUpdatingText = false
|
|
738
|
-
|
|
739
|
-
// Only invalidate if we actually made changes
|
|
740
|
-
if (hasChanges) {
|
|
741
|
-
invalidate()
|
|
422
|
+
|
|
423
|
+
private fun applyLetterSpacing() {
|
|
424
|
+
// React Native's letterSpacing is specified in layout points. Convert that to
|
|
425
|
+
// Android's "em" units: pxSpacing / textSizePx.
|
|
426
|
+
val metrics = resources.displayMetrics
|
|
427
|
+
val pxSpacing = letterSpacingPoints * metrics.scaledDensity
|
|
428
|
+
val textPx = textSize
|
|
429
|
+
if (textPx > 0f) {
|
|
430
|
+
super.setLetterSpacing(pxSpacing / textPx)
|
|
431
|
+
} else {
|
|
432
|
+
super.setLetterSpacing(0f)
|
|
742
433
|
}
|
|
743
434
|
}
|
|
744
435
|
}
|
|
@@ -134,6 +134,13 @@ class HighlightTextViewManager : SimpleViewManager<HighlightTextView>(),
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
@ReactProp(name = "letterSpacing")
|
|
138
|
+
override fun setLetterSpacing(view: HighlightTextView?, value: String?) {
|
|
139
|
+
value?.toFloatOrNull()?.let { spacing ->
|
|
140
|
+
view?.setLetterSpacingProp(spacing)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
137
144
|
@ReactProp(name = "padding")
|
|
138
145
|
override fun setPadding(view: HighlightTextView?, value: String?) {
|
|
139
146
|
value?.toFloatOrNull()?.let { padding ->
|
package/ios/HighlightTextView.mm
CHANGED
|
@@ -90,6 +90,7 @@ using namespace facebook::react;
|
|
|
90
90
|
CGFloat _backgroundInsetRight;
|
|
91
91
|
CGFloat _lineHeight;
|
|
92
92
|
CGFloat _fontSize;
|
|
93
|
+
CGFloat _letterSpacing; // in layout points, matches React Native's letterSpacing
|
|
93
94
|
NSString * _fontFamily;
|
|
94
95
|
NSString * _fontWeight;
|
|
95
96
|
BOOL _isUpdatingText;
|
|
@@ -122,6 +123,7 @@ using namespace facebook::react;
|
|
|
122
123
|
_backgroundInsetRight = 0.0;
|
|
123
124
|
_lineHeight = 0.0; // 0 means use default line height
|
|
124
125
|
_fontSize = 32.0; // Default font size
|
|
126
|
+
_letterSpacing = 0.0; // Default: no extra spacing
|
|
125
127
|
_fontFamily = nil;
|
|
126
128
|
_fontWeight = @"normal";
|
|
127
129
|
_currentVerticalAlignment = nil;
|
|
@@ -296,6 +298,13 @@ using namespace facebook::react;
|
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
if (oldViewProps.letterSpacing != newViewProps.letterSpacing) {
|
|
302
|
+
NSString *spacingStr = [[NSString alloc] initWithUTF8String: newViewProps.letterSpacing.c_str()];
|
|
303
|
+
CGFloat spacing = [spacingStr floatValue];
|
|
304
|
+
_letterSpacing = spacing;
|
|
305
|
+
[self applyCharacterBackgrounds]; // Reapply to update glyph layout
|
|
306
|
+
}
|
|
307
|
+
|
|
299
308
|
if (oldViewProps.highlightBorderRadius != newViewProps.highlightBorderRadius) {
|
|
300
309
|
NSString *radiusStr = [[NSString alloc] initWithUTF8String: newViewProps.highlightBorderRadius.c_str()];
|
|
301
310
|
CGFloat radius = [radiusStr floatValue];
|
|
@@ -537,6 +546,13 @@ Class<RCTComponentViewProtocol> HighlightTextViewCls(void)
|
|
|
537
546
|
value:_textView.font
|
|
538
547
|
range:NSMakeRange(0, text.length)];
|
|
539
548
|
|
|
549
|
+
// Apply letter spacing (kern) if specified
|
|
550
|
+
if (_letterSpacing != 0) {
|
|
551
|
+
[attributedString addAttribute:NSKernAttributeName
|
|
552
|
+
value:@(_letterSpacing)
|
|
553
|
+
range:NSMakeRange(0, text.length)];
|
|
554
|
+
}
|
|
555
|
+
|
|
540
556
|
// Apply text color if available
|
|
541
557
|
if (_textView.textColor) {
|
|
542
558
|
[attributedString addAttribute:NSForegroundColorAttributeName
|
|
@@ -47,6 +47,8 @@ export interface HighlightTextViewProps extends ViewProps {
|
|
|
47
47
|
fontFamily?: string;
|
|
48
48
|
fontSize?: string;
|
|
49
49
|
fontWeight?: string;
|
|
50
|
+
/** Additional space between characters, in layout points (matches React Native's letterSpacing). */
|
|
51
|
+
letterSpacing?: string;
|
|
50
52
|
lineHeight?: string;
|
|
51
53
|
highlightBorderRadius?: string;
|
|
52
54
|
padding?: string;
|
|
@@ -29,6 +29,8 @@ export interface HighlightTextViewProps extends ViewProps {
|
|
|
29
29
|
fontFamily?: string;
|
|
30
30
|
fontSize?: string;
|
|
31
31
|
fontWeight?: string;
|
|
32
|
+
/** Additional space between characters, in layout points (matches React Native's letterSpacing). */
|
|
33
|
+
letterSpacing?: string;
|
|
32
34
|
lineHeight?: string;
|
|
33
35
|
highlightBorderRadius?: string;
|
|
34
36
|
padding?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HighlightTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/HighlightTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,OAAO,GACP,SAAS,GACT,YAAY,GACZ,UAAU,GACV,KAAK,GACL,QAAQ,GACR,UAAU,GACV,YAAY,GACZ,WAAW,GACX,aAAa,GACb,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,WAAW,sBAAuB,SAAQ,SAAS;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iFAAiF;IACjF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CACpD;;AAED,wBAEE"}
|
|
1
|
+
{"version":3,"file":"HighlightTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/HighlightTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,OAAO,GACP,SAAS,GACT,YAAY,GACZ,UAAU,GACV,KAAK,GACL,QAAQ,GACR,UAAU,GACV,YAAY,GACZ,WAAW,GACX,aAAa,GACb,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,WAAW,sBAAuB,SAAQ,SAAS;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iFAAiF;IACjF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CACpD;;AAED,wBAEE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-highlight-text-view",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "A native text input for React Native that supports inline text highlighting",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -47,6 +47,8 @@ export interface HighlightTextViewProps extends ViewProps {
|
|
|
47
47
|
fontFamily?: string;
|
|
48
48
|
fontSize?: string;
|
|
49
49
|
fontWeight?: string;
|
|
50
|
+
/** Additional space between characters, in layout points (matches React Native's letterSpacing). */
|
|
51
|
+
letterSpacing?: string;
|
|
50
52
|
lineHeight?: string;
|
|
51
53
|
highlightBorderRadius?: string;
|
|
52
54
|
padding?: string;
|