react-native-highlight-text-view 0.1.31 → 0.1.33
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.
|
@@ -14,6 +14,7 @@ import android.util.TypedValue
|
|
|
14
14
|
import android.view.Gravity
|
|
15
15
|
import androidx.appcompat.widget.AppCompatEditText
|
|
16
16
|
import com.facebook.react.common.assets.ReactFontManager
|
|
17
|
+
import kotlin.math.abs
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Custom EditText that mimics the iOS implementation by drawing per-character
|
|
@@ -84,7 +85,7 @@ class HighlightTextView : AppCompatEditText {
|
|
|
84
85
|
private fun init() {
|
|
85
86
|
setBackgroundColor(Color.TRANSPARENT)
|
|
86
87
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 32f)
|
|
87
|
-
gravity = Gravity.
|
|
88
|
+
gravity = Gravity.START or Gravity.CENTER_VERTICAL
|
|
88
89
|
setPadding(20, 20, 20, 20)
|
|
89
90
|
textColorValue = currentTextColor
|
|
90
91
|
|
|
@@ -141,6 +142,12 @@ class HighlightTextView : AppCompatEditText {
|
|
|
141
142
|
val paint = paint
|
|
142
143
|
val radius = if (highlightBorderRadius > 0f) highlightBorderRadius else cornerRadius
|
|
143
144
|
|
|
145
|
+
// Precompute horizontal alignment flags once per draw pass
|
|
146
|
+
val horizontalGravity = gravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
|
147
|
+
val isLeftAlignedView = horizontalGravity == Gravity.START || horizontalGravity == Gravity.LEFT
|
|
148
|
+
val isRightAlignedView = horizontalGravity == Gravity.END || horizontalGravity == Gravity.RIGHT
|
|
149
|
+
val isCenterAlignedView = horizontalGravity == Gravity.CENTER_HORIZONTAL
|
|
150
|
+
|
|
144
151
|
val length = text.length
|
|
145
152
|
if (length == 0) return
|
|
146
153
|
|
|
@@ -194,14 +201,22 @@ class HighlightTextView : AppCompatEditText {
|
|
|
194
201
|
xStart + paint.measureText(text, i, i + 1)
|
|
195
202
|
}
|
|
196
203
|
|
|
197
|
-
// Vertical bounds based on
|
|
198
|
-
|
|
199
|
-
val
|
|
204
|
+
// Vertical bounds based on font metrics around the baseline, so
|
|
205
|
+
// they are independent from Android's line spacing mechanics.
|
|
206
|
+
val baseline = layout.getLineBaseline(line).toFloat()
|
|
207
|
+
val fm = paint.fontMetrics
|
|
200
208
|
|
|
201
209
|
var left = xStart
|
|
202
210
|
var right = xEnd
|
|
203
|
-
var top =
|
|
204
|
-
var bottom =
|
|
211
|
+
var top = baseline + fm.ascent
|
|
212
|
+
var bottom = baseline + fm.descent
|
|
213
|
+
|
|
214
|
+
// For right-aligned text, ensure the outermost character on each line
|
|
215
|
+
// snaps to the line's visual right edge so the highlight's right side
|
|
216
|
+
// forms a clean vertical column across wrapped lines.
|
|
217
|
+
if (isRightAlignedView && !hasRightNeighbor) {
|
|
218
|
+
right = layout.getLineRight(line)
|
|
219
|
+
}
|
|
205
220
|
|
|
206
221
|
// First shrink by background insets (from the line box)
|
|
207
222
|
top += backgroundInsetTop
|
|
@@ -215,25 +230,6 @@ class HighlightTextView : AppCompatEditText {
|
|
|
215
230
|
top -= charPaddingTop
|
|
216
231
|
bottom += charPaddingBottom
|
|
217
232
|
|
|
218
|
-
if (customLineSpacing < 0f) {
|
|
219
|
-
val originalLineTop = layout.getLineTop(line).toFloat()
|
|
220
|
-
val originalLineBottom = layout.getLineBottom(line).toFloat()
|
|
221
|
-
|
|
222
|
-
if (line > 0 && top < originalLineTop) {
|
|
223
|
-
val prevLineBottom = layout.getLineBottom(line - 1).toFloat()
|
|
224
|
-
if (top < prevLineBottom) {
|
|
225
|
-
top = prevLineBottom
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (line < layout.lineCount - 1 && bottom > originalLineBottom) {
|
|
230
|
-
val nextLineTop = layout.getLineTop(line + 1).toFloat()
|
|
231
|
-
if (bottom > nextLineTop) {
|
|
232
|
-
bottom = nextLineTop
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
233
|
if (right <= left || bottom <= top) continue
|
|
238
234
|
|
|
239
235
|
backgroundRect.set(left, top, right, bottom)
|
|
@@ -242,39 +238,146 @@ class HighlightTextView : AppCompatEditText {
|
|
|
242
238
|
val isFirstLineOfParagraph = line == 0 || isLineEmpty(text, layout, line - 1)
|
|
243
239
|
val isLastLineOfParagraph = line == layout.lineCount - 1 || isLineEmpty(text, layout, line + 1)
|
|
244
240
|
|
|
241
|
+
// Use precomputed text alignment flags
|
|
242
|
+
val isLeftAligned = isLeftAlignedView
|
|
243
|
+
val isRightAligned = isRightAlignedView
|
|
244
|
+
val isCenterAligned = isCenterAlignedView
|
|
245
|
+
|
|
245
246
|
var tl = 0f
|
|
246
247
|
var tr = 0f
|
|
247
248
|
var br = 0f
|
|
248
249
|
var bl = 0f
|
|
249
250
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
when {
|
|
252
|
+
isLeftAligned -> {
|
|
253
|
+
// LEFT ALIGNMENT (default behavior)
|
|
254
|
+
// Left Edge Logic
|
|
255
|
+
if (!hasLeftNeighbor) {
|
|
256
|
+
// Top-Left: Round if first line of paragraph
|
|
257
|
+
tl = if (isFirstLineOfParagraph) radius else 0f
|
|
258
|
+
// Bottom-Left: Round if last line of paragraph
|
|
259
|
+
bl = if (isLastLineOfParagraph) radius else 0f
|
|
260
|
+
}
|
|
257
261
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
// Right Edge Logic
|
|
263
|
+
if (!hasRightNeighbor) {
|
|
264
|
+
val currentLineWidth = layout.getLineMax(line)
|
|
265
|
+
|
|
266
|
+
// Top-Right
|
|
267
|
+
if (isFirstLineOfParagraph) {
|
|
268
|
+
tr = radius
|
|
269
|
+
} else {
|
|
270
|
+
val prevLineWidth = layout.getLineMax(line - 1)
|
|
271
|
+
// Round Top-Right only if this line extends further than the line above
|
|
272
|
+
tr = if (!lineWidthsEqual(currentLineWidth, prevLineWidth) &&
|
|
273
|
+
currentLineWidth > prevLineWidth
|
|
274
|
+
) radius else 0f
|
|
275
|
+
}
|
|
261
276
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
277
|
+
// Bottom-Right
|
|
278
|
+
if (isLastLineOfParagraph) {
|
|
279
|
+
br = radius
|
|
280
|
+
} else {
|
|
281
|
+
val nextLineWidth = layout.getLineMax(line + 1)
|
|
282
|
+
// Round Bottom-Right only if this line extends further than the line below
|
|
283
|
+
br = if (!lineWidthsEqual(currentLineWidth, nextLineWidth) &&
|
|
284
|
+
currentLineWidth > nextLineWidth
|
|
285
|
+
) radius else 0f
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
isRightAligned -> {
|
|
291
|
+
// RIGHT ALIGNMENT (mirror of left alignment)
|
|
292
|
+
// Right Edge Logic
|
|
293
|
+
if (!hasRightNeighbor) {
|
|
294
|
+
// Top-Right: Round if first line of paragraph
|
|
295
|
+
tr = if (isFirstLineOfParagraph) radius else 0f
|
|
296
|
+
// Bottom-Right: Round if last line of paragraph
|
|
297
|
+
br = if (isLastLineOfParagraph) radius else 0f
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Left Edge Logic
|
|
301
|
+
if (!hasLeftNeighbor) {
|
|
302
|
+
val currentLineWidth = layout.getLineMax(line)
|
|
303
|
+
|
|
304
|
+
// Top-Left
|
|
305
|
+
if (isFirstLineOfParagraph) {
|
|
306
|
+
tl = radius
|
|
307
|
+
} else {
|
|
308
|
+
val prevLineWidth = layout.getLineMax(line - 1)
|
|
309
|
+
// Round Top-Left only if this line extends further than the line above
|
|
310
|
+
tl = if (!lineWidthsEqual(currentLineWidth, prevLineWidth) &&
|
|
311
|
+
currentLineWidth > prevLineWidth
|
|
312
|
+
) radius else 0f
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Bottom-Left
|
|
316
|
+
if (isLastLineOfParagraph) {
|
|
317
|
+
bl = radius
|
|
318
|
+
} else {
|
|
319
|
+
val nextLineWidth = layout.getLineMax(line + 1)
|
|
320
|
+
// Round Bottom-Left only if this line extends further than the line below
|
|
321
|
+
bl = if (!lineWidthsEqual(currentLineWidth, nextLineWidth) &&
|
|
322
|
+
currentLineWidth > nextLineWidth
|
|
323
|
+
) radius else 0f
|
|
324
|
+
}
|
|
325
|
+
}
|
|
269
326
|
}
|
|
327
|
+
|
|
328
|
+
isCenterAligned -> {
|
|
329
|
+
// CENTER ALIGNMENT
|
|
330
|
+
val currentLineWidth = layout.getLineMax(line)
|
|
331
|
+
|
|
332
|
+
// Left Edge Logic
|
|
333
|
+
if (!hasLeftNeighbor) {
|
|
334
|
+
// Top-Left
|
|
335
|
+
if (isFirstLineOfParagraph) {
|
|
336
|
+
tl = radius
|
|
337
|
+
} else {
|
|
338
|
+
val prevLineWidth = layout.getLineMax(line - 1)
|
|
339
|
+
// Round Top-Left only if this line extends further than the line above
|
|
340
|
+
tl = if (!lineWidthsEqual(currentLineWidth, prevLineWidth) &&
|
|
341
|
+
currentLineWidth > prevLineWidth
|
|
342
|
+
) radius else 0f
|
|
343
|
+
}
|
|
270
344
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
345
|
+
// Bottom-Left
|
|
346
|
+
if (isLastLineOfParagraph) {
|
|
347
|
+
bl = radius
|
|
348
|
+
} else {
|
|
349
|
+
val nextLineWidth = layout.getLineMax(line + 1)
|
|
350
|
+
// Round Bottom-Left only if this line extends further than the line below
|
|
351
|
+
bl = if (!lineWidthsEqual(currentLineWidth, nextLineWidth) &&
|
|
352
|
+
currentLineWidth > nextLineWidth
|
|
353
|
+
) radius else 0f
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Right Edge Logic
|
|
358
|
+
if (!hasRightNeighbor) {
|
|
359
|
+
// Top-Right
|
|
360
|
+
if (isFirstLineOfParagraph) {
|
|
361
|
+
tr = radius
|
|
362
|
+
} else {
|
|
363
|
+
val prevLineWidth = layout.getLineMax(line - 1)
|
|
364
|
+
// Round Top-Right only if this line extends further than the line above
|
|
365
|
+
tr = if (!lineWidthsEqual(currentLineWidth, prevLineWidth) &&
|
|
366
|
+
currentLineWidth > prevLineWidth
|
|
367
|
+
) radius else 0f
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Bottom-Right
|
|
371
|
+
if (isLastLineOfParagraph) {
|
|
372
|
+
br = radius
|
|
373
|
+
} else {
|
|
374
|
+
val nextLineWidth = layout.getLineMax(line + 1)
|
|
375
|
+
// Round Bottom-Right only if this line extends further than the line below
|
|
376
|
+
br = if (!lineWidthsEqual(currentLineWidth, nextLineWidth) &&
|
|
377
|
+
currentLineWidth > nextLineWidth
|
|
378
|
+
) radius else 0f
|
|
379
|
+
}
|
|
380
|
+
}
|
|
278
381
|
}
|
|
279
382
|
}
|
|
280
383
|
|
|
@@ -290,6 +393,11 @@ class HighlightTextView : AppCompatEditText {
|
|
|
290
393
|
}
|
|
291
394
|
}
|
|
292
395
|
|
|
396
|
+
private fun lineWidthsEqual(w1: Float, w2: Float): Boolean {
|
|
397
|
+
// Small tolerance so lines that should visually match are treated as equal
|
|
398
|
+
return abs(w1 - w2) < 0.5f
|
|
399
|
+
}
|
|
400
|
+
|
|
293
401
|
private fun isLineEmpty(text: CharSequence, layout: android.text.Layout, line: Int): Boolean {
|
|
294
402
|
if (line < 0 || line >= layout.lineCount) return false
|
|
295
403
|
|
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.33",
|
|
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",
|