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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/ios/AdvancedTextView.mm
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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