react-native-highlight-text-view 0.1.20 → 0.1.21

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.
@@ -16,6 +16,10 @@ import android.util.TypedValue
16
16
  import android.view.Gravity
17
17
  import androidx.appcompat.widget.AppCompatEditText
18
18
 
19
+ /**
20
+ * iOS-style span: Draws rounded background for each character.
21
+ * Padding is only applied at line boundaries (first/last character of each line).
22
+ */
19
23
  class RoundedBackgroundSpan(
20
24
  internal val backgroundColor: Int,
21
25
  internal val textColor: Int,
@@ -28,10 +32,8 @@ class RoundedBackgroundSpan(
28
32
  internal val backgroundInsetLeft: Float,
29
33
  internal val backgroundInsetRight: Float,
30
34
  internal val cornerRadius: Float,
31
- internal val isFirstInGroup: Boolean = false,
32
- internal val isLastInGroup: Boolean = false,
33
- private val isStartOfLine: Boolean = false,
34
- private val isEndOfLine: Boolean = false
35
+ internal val isLineStart: Boolean = false,
36
+ internal val isLineEnd: Boolean = false
35
37
  ) : ReplacementSpan() {
36
38
 
37
39
  override fun getSize(
@@ -41,10 +43,10 @@ class RoundedBackgroundSpan(
41
43
  end: Int,
42
44
  fm: Paint.FontMetricsInt?
43
45
  ): Int {
46
+ // Only add padding at line boundaries (matches iOS behavior)
44
47
  val width = paint.measureText(text, start, end)
45
- // Add padding for word boundaries AND line boundaries (for consistent alignment)
46
- val leftPad = if (isFirstInGroup || isStartOfLine) paddingLeft else 0f
47
- val rightPad = if (isLastInGroup || isEndOfLine) paddingRight else 0f
48
+ val leftPad = if (isLineStart) paddingLeft else 0f
49
+ val rightPad = if (isLineEnd) paddingRight else 0f
48
50
  return (width + leftPad + rightPad).toInt()
49
51
  }
50
52
 
@@ -59,7 +61,8 @@ class RoundedBackgroundSpan(
59
61
  bottom: Int,
60
62
  paint: Paint
61
63
  ) {
62
- // Draw background with padding
64
+ if (text == null) return
65
+
63
66
  val bgPaint = Paint().apply {
64
67
  color = backgroundColor
65
68
  style = Paint.Style.FILL
@@ -68,83 +71,51 @@ class RoundedBackgroundSpan(
68
71
 
69
72
  val width = paint.measureText(text, start, end)
70
73
 
71
- // Use font metrics for consistent height (matches iOS behavior)
74
+ // Use font metrics for consistent height (matches iOS)
72
75
  val fontMetrics = paint.fontMetrics
73
76
  val textHeight = fontMetrics.descent - fontMetrics.ascent
74
77
  val textTop = y + fontMetrics.ascent
75
78
 
76
- // Add padding for word AND line boundaries (consistent alignment)
77
- val isReallyFirst = isFirstInGroup || isStartOfLine
78
- val isReallyLast = isLastInGroup || isEndOfLine
79
- val leftPad = if (isReallyFirst) paddingLeft else 0f
80
- val rightPad = if (isReallyLast) paddingRight else 0f
81
-
82
- // Small overlap to eliminate gaps between characters
83
- val overlapExtension = 2f
84
- val leftOverlap = if (!isReallyFirst) overlapExtension else 0f
85
- val rightOverlap = if (!isReallyLast) overlapExtension else 0f
86
-
87
- // Apply background insets first (shrinks from line box)
79
+ // Apply background insets first (shrinks from line box - EXACTLY like iOS line 45-48)
88
80
  val insetTop = textTop + backgroundInsetTop
89
81
  val insetHeight = textHeight - (backgroundInsetTop + backgroundInsetBottom)
90
82
 
91
- // Calculate background rect
83
+ // Only apply padding at line boundaries (matches iOS behavior)
84
+ val leftPad = if (isLineStart) paddingLeft else 0f
85
+ val rightPad = if (isLineEnd) paddingRight else 0f
86
+
87
+ // Aggressive overlap to ensure completely seamless connection (no gaps)
88
+ // Extended both horizontally AND vertically for complete coverage
89
+ val overlapExtension = 4f
90
+ val leftExtend = if (!isLineStart) overlapExtension else 0f
91
+ val rightExtend = if (!isLineEnd) {
92
+ // If this is line start, extend by padding amount to bridge the gap
93
+ if (isLineStart) leftPad + overlapExtension else overlapExtension
94
+ } else {
95
+ 0f
96
+ }
97
+
98
+ // Vertical overlap to eliminate gaps at top/bottom edges
99
+ val topExtend = overlapExtension
100
+ val bottomExtend = overlapExtension
101
+
102
+ // Calculate background rect with padding only at line boundaries
92
103
  val rect = RectF(
93
- x - leftOverlap + backgroundInsetLeft,
94
- insetTop - paddingTop,
95
- x + width + leftPad + rightPad + rightOverlap - backgroundInsetRight,
96
- insetTop + insetHeight + paddingBottom
104
+ x - leftPad + backgroundInsetLeft - leftExtend,
105
+ insetTop - paddingTop - topExtend,
106
+ x + width + rightPad - backgroundInsetRight + rightExtend,
107
+ insetTop + insetHeight + paddingBottom + bottomExtend
97
108
  )
98
109
 
99
- // Draw background with selective corner rounding (matches iOS behavior)
100
- // iOS draws per-character backgrounds with full corner radius, so we do the same
101
- when {
102
- isReallyFirst && isReallyLast -> {
103
- // Single character or isolated group - round all corners (matches iOS)
104
- canvas.drawRoundRect(rect, cornerRadius, cornerRadius, bgPaint)
105
- }
106
- isReallyFirst -> {
107
- // First character (word start or line start) - round left corners only
108
- val path = android.graphics.Path()
109
- path.addRoundRect(
110
- rect,
111
- floatArrayOf(
112
- cornerRadius, cornerRadius, // top-left
113
- 0f, 0f, // top-right (flat for connection)
114
- 0f, 0f, // bottom-right (flat for connection)
115
- cornerRadius, cornerRadius // bottom-left
116
- ),
117
- android.graphics.Path.Direction.CW
118
- )
119
- canvas.drawPath(path, bgPaint)
120
- }
121
- isReallyLast -> {
122
- // Last character (word end or line end) - round right corners only
123
- val path = android.graphics.Path()
124
- path.addRoundRect(
125
- rect,
126
- floatArrayOf(
127
- 0f, 0f, // top-left (flat for connection)
128
- cornerRadius, cornerRadius, // top-right
129
- cornerRadius, cornerRadius, // bottom-right
130
- 0f, 0f // bottom-left (flat for connection)
131
- ),
132
- android.graphics.Path.Direction.CW
133
- )
134
- canvas.drawPath(path, bgPaint)
135
- }
136
- else -> {
137
- // Middle character - no rounded corners for seamless connection
138
- canvas.drawRect(rect, bgPaint)
139
- }
140
- }
110
+ // Draw rounded rect (full corners for all characters - they overlap seamlessly)
111
+ canvas.drawRoundRect(rect, cornerRadius, cornerRadius, bgPaint)
141
112
 
142
- // Draw text with left padding offset only if first in group
113
+ // Draw text offset by left padding only if at line start
143
114
  val textPaint = Paint(paint).apply {
144
115
  color = textColor
145
116
  isAntiAlias = true
146
117
  }
147
- canvas.drawText(text!!, start, end, x + leftPad, y.toFloat(), textPaint)
118
+ canvas.drawText(text, start, end, x + leftPad, y.toFloat(), textPaint)
148
119
  }
149
120
  }
150
121
 
@@ -198,12 +169,21 @@ class HighlightTextView : AppCompatEditText {
198
169
  setHorizontallyScrolling(false)
199
170
 
200
171
  addTextChangedListener(object : TextWatcher {
201
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
172
+ private var changeStart = 0
173
+ private var changeEnd = 0
174
+
175
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
176
+ changeStart = start
177
+ changeEnd = start + after
178
+ }
179
+
202
180
  override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
181
+
203
182
  override fun afterTextChanged(s: Editable?) {
204
183
  if (!isUpdatingText) {
205
184
  onTextChangeListener?.invoke(s?.toString() ?: "")
206
- applyCharacterBackgrounds()
185
+
186
+ applyCharacterBackgroundsIncremental(s, changeStart, changeEnd)
207
187
  }
208
188
  }
209
189
  })
@@ -360,25 +340,149 @@ class HighlightTextView : AppCompatEditText {
360
340
  isUpdatingText = true
361
341
  setText(text)
362
342
  applyCharacterBackgrounds()
343
+ // Move cursor to end of text after setting
344
+ post {
345
+ if (hasFocus()) {
346
+ text.length.let { setSelection(it) }
347
+ }
348
+ }
363
349
  isUpdatingText = false
364
350
  }
365
351
  }
366
352
 
367
353
  fun setAutoFocus(autoFocus: Boolean) {
368
354
  if (autoFocus && isFocusable && isFocusableInTouchMode) {
369
- post {
355
+ postDelayed({
370
356
  requestFocus()
357
+ // Move cursor to end of text
358
+ text?.length?.let { setSelection(it) }
371
359
  val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager
372
- imm?.showSoftInput(this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
360
+ imm?.showSoftInput(this, android.view.inputmethod.InputMethodManager.SHOW_FORCED)
361
+ }, 100)
362
+ }
363
+ }
364
+
365
+ /**
366
+ * iOS-style incremental update: Only update spans for changed region.
367
+ * This is called during typing and only touches the modified characters.
368
+ */
369
+ private fun applyCharacterBackgroundsIncremental(editable: Editable?, start: Int, end: Int) {
370
+ if (editable == null) return
371
+ val textStr = editable.toString()
372
+ if (textStr.isEmpty()) return
373
+
374
+ isUpdatingText = true
375
+
376
+ // Check if a newline was inserted - if so, expand region to include char before it
377
+ val hasNewline = textStr.substring(start, minOf(end, textStr.length)).contains('\n')
378
+
379
+ // Expand the region to include entire lines that were affected
380
+ val layout = layout
381
+ val expandedStart: Int
382
+ val expandedEnd: Int
383
+
384
+ if (layout != null && textStr.isNotEmpty()) {
385
+ val startLine = layout.getLineForOffset(minOf(start, textStr.length - 1))
386
+ val endLine = layout.getLineForOffset(minOf(end, textStr.length - 1))
387
+ expandedStart = layout.getLineStart(startLine)
388
+ expandedEnd = layout.getLineEnd(endLine)
389
+ } else {
390
+ // If newline inserted, include character before it
391
+ expandedStart = if (hasNewline) maxOf(0, start - 2) else maxOf(0, start - 1)
392
+ expandedEnd = minOf(textStr.length, end + 1)
393
+ }
394
+
395
+ // Remove existing spans in the affected lines
396
+ val existingSpans = editable.getSpans(expandedStart, expandedEnd, RoundedBackgroundSpan::class.java)
397
+ for (span in existingSpans) {
398
+ editable.removeSpan(span)
399
+ }
400
+
401
+ // Apply spans with correct line boundary flags immediately
402
+ val radius = if (highlightBorderRadius > 0) highlightBorderRadius else cornerRadius
403
+
404
+ for (i in expandedStart until expandedEnd) {
405
+ if (i >= textStr.length) break
406
+
407
+ val char = textStr[i]
408
+ val shouldHighlight = when {
409
+ char == '\n' || char == '\t' -> false
410
+ char == ' ' -> {
411
+ val hasSpaceBefore = i > 0 && textStr[i - 1] == ' '
412
+ val hasSpaceAfter = i < textStr.length - 1 && textStr[i + 1] == ' '
413
+ !hasSpaceBefore && !hasSpaceAfter
414
+ }
415
+ else -> true
373
416
  }
417
+
418
+ if (shouldHighlight) {
419
+ // ALWAYS check newlines first (for manual line breaks)
420
+ val hasNewlineBefore = i > 0 && textStr[i - 1] == '\n'
421
+ val hasNewlineAfter = i + 1 < textStr.length && textStr[i + 1] == '\n'
422
+
423
+ var isAtLineStart = i == 0 || hasNewlineBefore
424
+ var isAtLineEnd = i == textStr.length - 1 || hasNewlineAfter
425
+
426
+ // Only use layout for auto-wrapped lines (not manual newlines)
427
+ if (!hasNewlineBefore && !hasNewlineAfter && layout != null && i < textStr.length) {
428
+ try {
429
+ val line = layout.getLineForOffset(i)
430
+ val lineStart = layout.getLineStart(line)
431
+ val lineEnd = layout.getLineEnd(line)
432
+ // Only override if this is an auto-wrapped boundary
433
+ if (i == lineStart && textStr.getOrNull(i - 1) != '\n') {
434
+ isAtLineStart = true
435
+ }
436
+ if ((i + 1) == lineEnd && textStr.getOrNull(i + 1) != '\n') {
437
+ isAtLineEnd = true
438
+ }
439
+ } catch (e: Exception) {
440
+ // Layout might not be ready, keep newline-based detection
441
+ }
442
+ }
443
+
444
+ val span = RoundedBackgroundSpan(
445
+ characterBackgroundColor,
446
+ textColorValue,
447
+ charPaddingLeft,
448
+ charPaddingRight,
449
+ charPaddingTop,
450
+ charPaddingBottom,
451
+ backgroundInsetTop,
452
+ backgroundInsetBottom,
453
+ backgroundInsetLeft,
454
+ backgroundInsetRight,
455
+ radius,
456
+ isAtLineStart,
457
+ isAtLineEnd
458
+ )
459
+ editable.setSpan(span, i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
460
+ }
461
+ }
462
+
463
+ isUpdatingText = false
464
+
465
+ // JERK FIX: Skip the post-update during fast typing to prevent layout thrashing
466
+ // Only update boundaries when user stops typing (reduces update frequency)
467
+ removeCallbacks(boundaryUpdateCheck)
468
+ postDelayed(boundaryUpdateCheck, 200)
469
+ }
470
+
471
+ // Runnable for delayed boundary check
472
+ private val boundaryUpdateCheck = Runnable {
473
+ if (!isUpdatingText) {
474
+ updateAutoWrappedLineBoundaries()
374
475
  }
375
476
  }
376
477
 
478
+ /**
479
+ * Full re-application of spans (used when props change, not during typing).
480
+ * iOS-style: Work directly with editable, no setText() call.
481
+ */
377
482
  private fun applyCharacterBackgrounds() {
378
- val text = text?.toString() ?: return
379
- if (text.isEmpty()) return
380
-
381
- val spannable = SpannableString(text)
483
+ val editable = editableText ?: return
484
+ val textStr = editable.toString()
485
+ if (textStr.isEmpty()) return
382
486
 
383
487
  // Apply line height if specified
384
488
  if (customLineHeight > 0) {
@@ -386,29 +490,56 @@ class HighlightTextView : AppCompatEditText {
386
490
  setLineSpacing(0f, lineSpacingMultiplier)
387
491
  }
388
492
 
389
- // Apply character-by-character for proper line wrapping
390
- for (i in text.indices) {
391
- val char = text[i]
493
+ isUpdatingText = true
494
+
495
+ // Remove all existing spans
496
+ val existingSpans = editable.getSpans(0, editable.length, RoundedBackgroundSpan::class.java)
497
+ for (span in existingSpans) {
498
+ editable.removeSpan(span)
499
+ }
500
+
501
+ // Apply spans to all characters with correct line boundary flags
502
+ val radius = if (highlightBorderRadius > 0) highlightBorderRadius else cornerRadius
503
+ val layoutObj = layout
504
+
505
+ for (i in textStr.indices) {
506
+ val char = textStr[i]
392
507
 
393
- // Check if this is a space that should be highlighted
394
508
  val shouldHighlight = when {
395
- char == '\n' || char == '\t' -> false // Never highlight newlines or tabs
509
+ char == '\n' || char == '\t' -> false
396
510
  char == ' ' -> {
397
- // Highlight space only if it's a single space (not multiple consecutive)
398
- val hasSpaceBefore = i > 0 && text[i - 1] == ' '
399
- val hasSpaceAfter = i < text.length - 1 && text[i + 1] == ' '
511
+ val hasSpaceBefore = i > 0 && textStr[i - 1] == ' '
512
+ val hasSpaceAfter = i < textStr.length - 1 && textStr[i + 1] == ' '
400
513
  !hasSpaceBefore && !hasSpaceAfter
401
514
  }
402
- else -> true // Highlight all other characters
515
+ else -> true
403
516
  }
404
517
 
405
518
  if (shouldHighlight) {
406
- // Determine if this is the first or last character in a word group
407
- val isFirst = i == 0 || !shouldHighlightChar(text, i - 1)
408
- val isLast = i == text.length - 1 || !shouldHighlightChar(text, i + 1)
519
+ // ALWAYS check newlines first (for manual line breaks)
520
+ val hasNewlineBefore = i > 0 && textStr[i - 1] == '\n'
521
+ val hasNewlineAfter = i + 1 < textStr.length && textStr[i + 1] == '\n'
522
+
523
+ var isAtLineStart = i == 0 || hasNewlineBefore
524
+ var isAtLineEnd = i == textStr.length - 1 || hasNewlineAfter
409
525
 
410
- // Use highlightBorderRadius if specified, otherwise use cornerRadius (matches iOS)
411
- val radius = if (highlightBorderRadius > 0) highlightBorderRadius else cornerRadius
526
+ // Only use layout for auto-wrapped lines (not manual newlines)
527
+ if (!hasNewlineBefore && !hasNewlineAfter && layoutObj != null && i < textStr.length) {
528
+ try {
529
+ val line = layoutObj.getLineForOffset(i)
530
+ val lineStart = layoutObj.getLineStart(line)
531
+ val lineEnd = layoutObj.getLineEnd(line)
532
+ // Only override if this is an auto-wrapped boundary
533
+ if (i == lineStart && textStr.getOrNull(i - 1) != '\n') {
534
+ isAtLineStart = true
535
+ }
536
+ if ((i + 1) == lineEnd && textStr.getOrNull(i + 1) != '\n') {
537
+ isAtLineEnd = true
538
+ }
539
+ } catch (e: Exception) {
540
+ // Layout might not be ready, keep newline-based detection
541
+ }
542
+ }
412
543
 
413
544
  val span = RoundedBackgroundSpan(
414
545
  characterBackgroundColor,
@@ -422,62 +553,60 @@ class HighlightTextView : AppCompatEditText {
422
553
  backgroundInsetLeft,
423
554
  backgroundInsetRight,
424
555
  radius,
425
- isFirst,
426
- isLast
556
+ isAtLineStart,
557
+ isAtLineEnd
427
558
  )
428
- spannable.setSpan(span, i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
559
+ editable.setSpan(span, i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
429
560
  }
430
561
  }
431
562
 
432
- // Save current selection to prevent cursor jumping (smooth editing)
433
- val currentSelection = selectionStart
434
-
435
- isUpdatingText = true
436
- setText(spannable)
437
-
438
- // Restore cursor position if valid (prevents jerking during editing)
439
- if (currentSelection >= 0 && currentSelection <= text.length) {
440
- setSelection(currentSelection)
441
- }
442
563
  isUpdatingText = false
443
-
444
- // Detect line wraps after layout is ready
445
- post { detectLineWraps() }
446
564
  }
447
565
 
448
- private fun detectLineWraps() {
566
+ /**
567
+ * Update line boundary flags only for auto-wrapped lines.
568
+ * This is called after layout completes to handle text wrapping.
569
+ * Only updates spans that are at auto-wrapped line boundaries.
570
+ * Optimized to skip updates when layout hasn't changed.
571
+ */
572
+ private fun updateAutoWrappedLineBoundaries() {
573
+ if (isUpdatingText) return
574
+
449
575
  val layout = layout ?: return
450
- val text = text as? Spannable ?: return
451
- val textStr = text.toString()
452
- val spans = text.getSpans(0, text.length, RoundedBackgroundSpan::class.java)
576
+ val editable = editableText ?: return
577
+ val textStr = editable.toString()
578
+ if (textStr.isEmpty()) return
579
+
580
+ // Validate that layout is ready and has valid dimensions
581
+ if (width <= 0 || layout.lineCount == 0) return
582
+
583
+ val spans = editable.getSpans(0, editable.length, RoundedBackgroundSpan::class.java)
584
+ if (spans.isEmpty()) return
585
+
586
+ isUpdatingText = true
587
+ var hasChanges = false
453
588
 
454
589
  for (span in spans) {
455
- val spanStart = text.getSpanStart(span)
456
- val spanEnd = text.getSpanEnd(span)
590
+ val spanStart = editable.getSpanStart(span)
591
+ val spanEnd = editable.getSpanEnd(span)
592
+
593
+ if (spanStart < 0 || spanStart >= textStr.length) continue
457
594
 
458
- if (spanStart >= 0 && spanStart < text.length) {
595
+ try {
459
596
  val line = layout.getLineForOffset(spanStart)
460
597
  val lineStart = layout.getLineStart(line)
461
598
  val lineEnd = layout.getLineEnd(line)
462
599
 
463
- // Check for manual line break (\n) before this character
464
- val hasNewlineBefore = spanStart > 0 && textStr[spanStart - 1] == '\n'
465
- // Check for manual line break (\n) after this character
466
- val hasNewlineAfter = spanEnd < textStr.length && textStr[spanEnd] == '\n'
600
+ // Determine actual line boundaries (includes auto-wrap)
601
+ val isAtLineStart = spanStart == lineStart
602
+ val isAtLineEnd = spanEnd == lineEnd
467
603
 
468
- // Check if this is the last line of text
469
- val isLastLine = line == layout.lineCount - 1
604
+ // Only update if this is an auto-wrapped line boundary (not a newline boundary)
605
+ val isNewlineBoundary = (spanStart > 0 && textStr[spanStart - 1] == '\n') ||
606
+ (spanEnd < textStr.length && textStr[spanEnd] == '\n')
470
607
 
471
- // Check if this char is at start of visual line (wrapped OR after \n)
472
- val isAtLineStart = (spanStart == lineStart && !span.isFirstInGroup) || hasNewlineBefore
473
-
474
- // Check if this char is at end of visual line (wrapped OR before \n OR end of last line)
475
- // CRITICAL: Ensure last character of entire text gets rounded corners
476
- val isAtLineEnd = (spanEnd == lineEnd && !span.isLastInGroup) || hasNewlineAfter ||
477
- (isLastLine && spanEnd == lineEnd)
478
-
479
- if (isAtLineStart || isAtLineEnd) {
480
- // Create new span with line boundary flags
608
+ // Only recreate span if it's at an auto-wrapped boundary and flags are wrong
609
+ if (!isNewlineBoundary && (isAtLineStart != span.isLineStart || isAtLineEnd != span.isLineEnd)) {
481
610
  val newSpan = RoundedBackgroundSpan(
482
611
  span.backgroundColor,
483
612
  span.textColor,
@@ -490,31 +619,24 @@ class HighlightTextView : AppCompatEditText {
490
619
  span.backgroundInsetLeft,
491
620
  span.backgroundInsetRight,
492
621
  span.cornerRadius,
493
- span.isFirstInGroup,
494
- span.isLastInGroup,
495
622
  isAtLineStart,
496
623
  isAtLineEnd
497
624
  )
498
- text.removeSpan(span)
499
- text.setSpan(newSpan, spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
625
+ editable.removeSpan(span)
626
+ editable.setSpan(newSpan, spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
627
+ hasChanges = true
500
628
  }
629
+ } catch (e: Exception) {
630
+ // Layout state is invalid, skip this update
631
+ continue
501
632
  }
502
633
  }
503
- invalidate()
504
- }
505
-
506
- private fun shouldHighlightChar(text: String, index: Int): Boolean {
507
- if (index < 0 || index >= text.length) return false
508
- val char = text[index]
509
-
510
- return when {
511
- char == '\n' || char == '\t' -> false
512
- char == ' ' -> {
513
- val hasSpaceBefore = index > 0 && text[index - 1] == ' '
514
- val hasSpaceAfter = index < text.length - 1 && text[index + 1] == ' '
515
- !hasSpaceBefore && !hasSpaceAfter
516
- }
517
- else -> true
634
+
635
+ isUpdatingText = false
636
+
637
+ // Only invalidate if we actually made changes
638
+ if (hasChanges) {
639
+ invalidate()
518
640
  }
519
641
  }
520
642
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-highlight-text-view",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
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",