react-native-advanced-text 0.2.13 → 0.2.14

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.
@@ -12,6 +12,7 @@ import android.text.Spanned
12
12
  import android.text.TextPaint
13
13
  import android.text.method.ArrowKeyMovementMethod
14
14
  import android.text.style.ClickableSpan
15
+ import android.text.style.ForegroundColorSpan
15
16
  import android.util.AttributeSet
16
17
  import android.util.Log
17
18
  import android.view.ActionMode
@@ -53,6 +54,9 @@ class AdvancedTextView : TextView {
53
54
  private var indicatorMode: String = "Highlight"
54
55
 
55
56
  private var wordPositions: List<WordPosition> = emptyList()
57
+ // Each word's color span, kept so color changes can be applied to the live
58
+ // text. setText() would clear any selection the user is making.
59
+ private var wordColorSpans: Array<ForegroundColorSpan?> = emptyArray()
56
60
 
57
61
  constructor(context: Context?) : super(context) { init() }
58
62
  constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
@@ -133,8 +137,10 @@ class AdvancedTextView : TextView {
133
137
  }
134
138
 
135
139
  fun setAdvancedTextColor(colorInt: Int) {
136
- textColor = String.format("#%06X", 0xFFFFFF and colorInt)
137
- updateTextWithHighlights()
140
+ val color = String.format("#%06X", 0xFFFFFF and colorInt)
141
+ if (textColor == color) return
142
+ textColor = color
143
+ updateWordColors()
138
144
  }
139
145
 
140
146
  fun setAdvancedTextSize(size: Float) {
@@ -202,7 +208,7 @@ class AdvancedTextView : TextView {
202
208
  if (indicatorWordIndex == index) return
203
209
  indicatorWordIndex = index
204
210
  if (indicatorMode == "fill") {
205
- updateTextWithHighlights()
211
+ updateWordColors()
206
212
  } else {
207
213
  invalidate()
208
214
  }
@@ -212,7 +218,7 @@ class AdvancedTextView : TextView {
212
218
  if (indicatorColor == color) return
213
219
  indicatorColor = color
214
220
  if (indicatorMode == "fill") {
215
- updateTextWithHighlights()
221
+ updateWordColors()
216
222
  } else {
217
223
  invalidate()
218
224
  }
@@ -221,7 +227,7 @@ class AdvancedTextView : TextView {
221
227
  fun setIndicatorMode(mode: String) {
222
228
  if (indicatorMode == mode) return
223
229
  indicatorMode = mode
224
- updateTextWithHighlights()
230
+ updateWordColors()
225
231
  invalidate()
226
232
  }
227
233
 
@@ -268,25 +274,18 @@ class AdvancedTextView : TextView {
268
274
  val spannableString = SpannableString(currentText)
269
275
 
270
276
  wordPositions.forEach { wordPos ->
271
- val wordColor = if (indicatorMode == "fill" &&
272
- indicatorWordIndex >= 0 &&
273
- wordPos.index <= indicatorWordIndex &&
274
- indicatorColor.isNotEmpty()) {
275
- parseColor(indicatorColor)
276
- } else {
277
- parseColor(textColor)
278
- }
279
277
  spannableString.setSpan(
280
278
  WordClickableSpan(
281
279
  wordIndex = wordPos.index,
282
- word = wordPos.word,
283
- wordColor = wordColor
280
+ word = wordPos.word
284
281
  ),
285
282
  wordPos.start,
286
283
  wordPos.end,
287
284
  Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
288
285
  )
289
286
  }
287
+ wordColorSpans = arrayOfNulls(wordPositions.size)
288
+ applyWordColors(spannableString)
290
289
 
291
290
  applyTextAlign()
292
291
 
@@ -322,6 +321,50 @@ class AdvancedTextView : TextView {
322
321
  Log.d(TAG, "Text updated with ${wordPositions.size} spans")
323
322
  }
324
323
 
324
+ // Recolors words on the live text instead of rebuilding it, so the
325
+ // indicator can advance while the user is selecting text. Span changes go
326
+ // through TextView's ChangeWatcher, which redraws just the changed region
327
+ // and keeps the selection and its action mode.
328
+ private fun updateWordColors() {
329
+ if (currentText.isEmpty()) return
330
+ val spannable = text as? Spannable
331
+ if (spannable == null ||
332
+ spannable.length != currentText.length ||
333
+ wordColorSpans.size != wordPositions.size) {
334
+ updateTextWithHighlights()
335
+ return
336
+ }
337
+ applyWordColors(spannable)
338
+ }
339
+
340
+ // Only words whose color changed get a new span, so an indicator step
341
+ // touches one or two words rather than the whole paragraph.
342
+ private fun applyWordColors(spannable: Spannable) {
343
+ val baseColor = parseColor(textColor)
344
+ val fillColor = if (indicatorMode == "fill" &&
345
+ indicatorWordIndex >= 0 &&
346
+ indicatorColor.isNotEmpty()) {
347
+ parseColor(indicatorColor)
348
+ } else {
349
+ null
350
+ }
351
+
352
+ wordPositions.forEach { wordPos ->
353
+ val color = if (fillColor != null && wordPos.index <= indicatorWordIndex) {
354
+ fillColor
355
+ } else {
356
+ baseColor
357
+ }
358
+ val current = wordColorSpans[wordPos.index]
359
+ if (current?.foregroundColor == color) return@forEach
360
+
361
+ current?.let { spannable.removeSpan(it) }
362
+ val span = ForegroundColorSpan(color)
363
+ spannable.setSpan(span, wordPos.start, wordPos.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
364
+ wordColorSpans[wordPos.index] = span
365
+ }
366
+ }
367
+
325
368
  // Every word is a ClickableSpan so a tap can report it, but TalkBack treats
326
369
  // each span as a link: it reads the text one word at a time and lists every
327
370
  // word under Actions. Plain text keeps it reading as prose; granularity
@@ -448,8 +491,7 @@ class AdvancedTextView : TextView {
448
491
 
449
492
  private inner class WordClickableSpan(
450
493
  private val wordIndex: Int,
451
- private val word: String,
452
- private val wordColor: Int
494
+ private val word: String
453
495
  ) : ClickableSpan() {
454
496
 
455
497
  override fun onClick(widget: View) {
@@ -457,10 +499,10 @@ class AdvancedTextView : TextView {
457
499
  sendWordPressEvent(word, wordIndex)
458
500
  }
459
501
 
502
+ // Skips super, which would paint the word in the link color and
503
+ // underline it. The word's color comes from its ForegroundColorSpan.
460
504
  override fun updateDrawState(ds: TextPaint) {
461
- super.updateDrawState(ds)
462
505
  ds.isUnderlineText = false
463
- ds.color = wordColor
464
506
  }
465
507
  }
466
508
 
@@ -268,6 +268,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
268
268
  }
269
269
 
270
270
  BOOL indicatorChanged = NO;
271
+ BOOL indicatorModeChanged = NO;
271
272
 
272
273
  if (oldViewProps.indicatorMode != newViewProps.indicatorMode) {
273
274
  NSLog(@"[AdvancedTextView] Updating indicatorMode to: %s", newViewProps.indicatorMode.c_str());
@@ -275,6 +276,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
275
276
  ? @"Highlight"
276
277
  : [NSString stringWithUTF8String:newViewProps.indicatorMode.c_str()];
277
278
  indicatorChanged = YES;
279
+ indicatorModeChanged = YES;
278
280
  }
279
281
 
280
282
  if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
@@ -295,11 +297,10 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
295
297
  }
296
298
 
297
299
  if (indicatorChanged) {
298
- if ([_indicatorMode isEqualToString:@"fill"]) {
299
- [self updateTextAppearance]; // rebuilds attributed text with fill colors; calls setNeedsDisplay
300
- } else {
301
- [_textView setNeedsDisplay];
300
+ if ([_indicatorMode isEqualToString:@"fill"] || indicatorModeChanged) {
301
+ [self updateWordColors];
302
302
  }
303
+ [_textView setNeedsDisplay];
303
304
  }
304
305
 
305
306
  if (textChanged) {
@@ -497,22 +498,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
497
498
  }
498
499
 
499
500
 
500
- UIColor *color = _textColor ?: [UIColor labelColor];
501
- [attributedString addAttribute:NSForegroundColorAttributeName
502
- value:color
503
- range:NSMakeRange(0, attributedString.length)];
504
-
505
- if ([_indicatorMode isEqualToString:@"fill"] && _indicatorWordIndex >= 0 && _indicatorColor) {
506
- for (NSDictionary *wordInfo in _wordRanges) {
507
- NSInteger idx = [wordInfo[@"index"] integerValue];
508
- if (idx <= _indicatorWordIndex) {
509
- NSRange wordRange = [wordInfo[@"range"] rangeValue];
510
- [attributedString addAttribute:NSForegroundColorAttributeName
511
- value:_indicatorColor
512
- range:wordRange];
513
- }
514
- }
515
- }
501
+ [self applyWordColorsToString:attributedString];
516
502
 
517
503
  _textView.attributedText = attributedString;
518
504
  [_textView setNeedsDisplay];
@@ -534,6 +520,50 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
534
520
  }
535
521
  }
536
522
 
523
+ - (void)applyWordColorsToString:(NSMutableAttributedString *)string
524
+ {
525
+ UIColor *color = _textColor ?: [UIColor labelColor];
526
+ [string addAttribute:NSForegroundColorAttributeName
527
+ value:color
528
+ range:NSMakeRange(0, string.length)];
529
+
530
+ if (![_indicatorMode isEqualToString:@"fill"] || _indicatorWordIndex < 0 || !_indicatorColor) {
531
+ return;
532
+ }
533
+
534
+ for (NSDictionary *wordInfo in _wordRanges) {
535
+ if ([wordInfo[@"index"] integerValue] > _indicatorWordIndex) {
536
+ break;
537
+ }
538
+ NSRange wordRange = [wordInfo[@"range"] rangeValue];
539
+ if (NSMaxRange(wordRange) > string.length) {
540
+ break;
541
+ }
542
+ [string addAttribute:NSForegroundColorAttributeName
543
+ value:_indicatorColor
544
+ range:wordRange];
545
+ }
546
+ }
547
+
548
+ // Recolors the live textStorage instead of reassigning attributedText, so the
549
+ // indicator can advance while the user is selecting text: attribute-only
550
+ // edits keep selectedRange and the edit menu, assigning attributedText resets them.
551
+ - (void)updateWordColors
552
+ {
553
+ @try {
554
+ NSTextStorage *storage = _textView.textStorage;
555
+ if (storage.length == 0) {
556
+ return;
557
+ }
558
+
559
+ [storage beginEditing];
560
+ [self applyWordColorsToString:storage];
561
+ [storage endEditing];
562
+ } @catch (NSException *exception) {
563
+ NSLog(@"[AdvancedTextView] Exception in updateWordColors: %@", exception.reason);
564
+ }
565
+ }
566
+
537
567
  - (void)drawHighlightedBackgroundsInTextView:(UITextView *)textView
538
568
  {
539
569
  if (!textView.layoutManager || !textView.textContainer) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": " Advanced text component for React Native with custom select options.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",