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.CENTER
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 line box (includes line spacing)
198
- val lineTop = layout.getLineTop(line).toFloat()
199
- val lineBottom = layout.getLineBottom(line).toFloat()
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 = lineTop
204
- var bottom = lineBottom
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
- // Left Edge Logic
251
- if (!hasLeftNeighbor) {
252
- // Top-Left: Round if first line of paragraph
253
- tl = if (isFirstLineOfParagraph) radius else 0f
254
- // Bottom-Left: Round if last line of paragraph
255
- bl = if (isLastLineOfParagraph) radius else 0f
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
- // Right Edge Logic
259
- if (!hasRightNeighbor) {
260
- val currentLineWidth = layout.getLineMax(line)
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
- // Top-Right
263
- if (isFirstLineOfParagraph) {
264
- tr = radius
265
- } else {
266
- val prevLineWidth = layout.getLineMax(line - 1)
267
- // Round Top-Right if we stick out further than the line above
268
- tr = if (currentLineWidth > prevLineWidth) radius else 0f
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
- // Bottom-Right
272
- if (isLastLineOfParagraph) {
273
- br = radius
274
- } else {
275
- val nextLineWidth = layout.getLineMax(line + 1)
276
- // Round Bottom-Right if we overhang the line below
277
- br = if (currentLineWidth > nextLineWidth) radius else 0f
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.31",
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",