react-native-advanced-text 0.2.12 → 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,9 +12,11 @@ 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
|
|
19
|
+
import android.view.Gravity
|
|
18
20
|
import android.view.Menu
|
|
19
21
|
import android.view.MenuItem
|
|
20
22
|
import android.view.View
|
|
@@ -40,7 +42,8 @@ class AdvancedTextView : TextView {
|
|
|
40
42
|
private var textColor: String = "#000000"
|
|
41
43
|
private var fontSize: Float = 16f
|
|
42
44
|
private var fontWeight: String = "normal"
|
|
43
|
-
|
|
45
|
+
// "" = unset: natural alignment (start of the text's own direction).
|
|
46
|
+
private var textAlign: String = ""
|
|
44
47
|
private var fontFamily: String = "sans-serif"
|
|
45
48
|
private var lineHeightMultiplier: Float = 1.0f
|
|
46
49
|
// Value from JS, in dp/points (0 = unset). Converted to em for TextView.
|
|
@@ -51,6 +54,9 @@ class AdvancedTextView : TextView {
|
|
|
51
54
|
private var indicatorMode: String = "Highlight"
|
|
52
55
|
|
|
53
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()
|
|
54
60
|
|
|
55
61
|
constructor(context: Context?) : super(context) { init() }
|
|
56
62
|
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
|
|
@@ -131,8 +137,10 @@ class AdvancedTextView : TextView {
|
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
fun setAdvancedTextColor(colorInt: Int) {
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
val color = String.format("#%06X", 0xFFFFFF and colorInt)
|
|
141
|
+
if (textColor == color) return
|
|
142
|
+
textColor = color
|
|
143
|
+
updateWordColors()
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
fun setAdvancedTextSize(size: Float) {
|
|
@@ -150,10 +158,24 @@ class AdvancedTextView : TextView {
|
|
|
150
158
|
fun setAdvancedTextAlign(align: String) {
|
|
151
159
|
if (textAlign == align) return
|
|
152
160
|
textAlign = align
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
applyTextAlign()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// "left"/"right" are absolute edges, as on iOS. TEXT_START/TEXT_END follow
|
|
165
|
+
// the paragraph's direction, so RTL text (Arabic) with "right" would land
|
|
166
|
+
// on the left. Only TEXT_ALIGNMENT_GRAVITY with Gravity.LEFT/RIGHT is absolute.
|
|
167
|
+
private fun applyTextAlign() {
|
|
168
|
+
val vertical = gravity and Gravity.VERTICAL_GRAVITY_MASK
|
|
169
|
+
when (textAlign) {
|
|
170
|
+
"left" -> {
|
|
171
|
+
gravity = vertical or Gravity.LEFT
|
|
172
|
+
textAlignment = View.TEXT_ALIGNMENT_GRAVITY
|
|
173
|
+
}
|
|
174
|
+
"right" -> {
|
|
175
|
+
gravity = vertical or Gravity.RIGHT
|
|
176
|
+
textAlignment = View.TEXT_ALIGNMENT_GRAVITY
|
|
177
|
+
}
|
|
155
178
|
"center" -> textAlignment = View.TEXT_ALIGNMENT_CENTER
|
|
156
|
-
"right" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_END
|
|
157
179
|
else -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
|
|
158
180
|
}
|
|
159
181
|
}
|
|
@@ -186,7 +208,7 @@ class AdvancedTextView : TextView {
|
|
|
186
208
|
if (indicatorWordIndex == index) return
|
|
187
209
|
indicatorWordIndex = index
|
|
188
210
|
if (indicatorMode == "fill") {
|
|
189
|
-
|
|
211
|
+
updateWordColors()
|
|
190
212
|
} else {
|
|
191
213
|
invalidate()
|
|
192
214
|
}
|
|
@@ -196,7 +218,7 @@ class AdvancedTextView : TextView {
|
|
|
196
218
|
if (indicatorColor == color) return
|
|
197
219
|
indicatorColor = color
|
|
198
220
|
if (indicatorMode == "fill") {
|
|
199
|
-
|
|
221
|
+
updateWordColors()
|
|
200
222
|
} else {
|
|
201
223
|
invalidate()
|
|
202
224
|
}
|
|
@@ -205,7 +227,7 @@ class AdvancedTextView : TextView {
|
|
|
205
227
|
fun setIndicatorMode(mode: String) {
|
|
206
228
|
if (indicatorMode == mode) return
|
|
207
229
|
indicatorMode = mode
|
|
208
|
-
|
|
230
|
+
updateWordColors()
|
|
209
231
|
invalidate()
|
|
210
232
|
}
|
|
211
233
|
|
|
@@ -252,32 +274,20 @@ class AdvancedTextView : TextView {
|
|
|
252
274
|
val spannableString = SpannableString(currentText)
|
|
253
275
|
|
|
254
276
|
wordPositions.forEach { wordPos ->
|
|
255
|
-
val wordColor = if (indicatorMode == "fill" &&
|
|
256
|
-
indicatorWordIndex >= 0 &&
|
|
257
|
-
wordPos.index <= indicatorWordIndex &&
|
|
258
|
-
indicatorColor.isNotEmpty()) {
|
|
259
|
-
parseColor(indicatorColor)
|
|
260
|
-
} else {
|
|
261
|
-
parseColor(textColor)
|
|
262
|
-
}
|
|
263
277
|
spannableString.setSpan(
|
|
264
278
|
WordClickableSpan(
|
|
265
279
|
wordIndex = wordPos.index,
|
|
266
|
-
word = wordPos.word
|
|
267
|
-
wordColor = wordColor
|
|
280
|
+
word = wordPos.word
|
|
268
281
|
),
|
|
269
282
|
wordPos.start,
|
|
270
283
|
wordPos.end,
|
|
271
284
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
272
285
|
)
|
|
273
286
|
}
|
|
287
|
+
wordColorSpans = arrayOfNulls(wordPositions.size)
|
|
288
|
+
applyWordColors(spannableString)
|
|
274
289
|
|
|
275
|
-
|
|
276
|
-
"left" -> View.TEXT_ALIGNMENT_TEXT_START
|
|
277
|
-
"center" -> View.TEXT_ALIGNMENT_CENTER
|
|
278
|
-
"right" -> View.TEXT_ALIGNMENT_TEXT_END
|
|
279
|
-
else -> View.TEXT_ALIGNMENT_TEXT_START
|
|
280
|
-
}
|
|
290
|
+
applyTextAlign()
|
|
281
291
|
|
|
282
292
|
setTextSize(fontSize)
|
|
283
293
|
|
|
@@ -311,6 +321,50 @@ class AdvancedTextView : TextView {
|
|
|
311
321
|
Log.d(TAG, "Text updated with ${wordPositions.size} spans")
|
|
312
322
|
}
|
|
313
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
|
+
|
|
314
368
|
// Every word is a ClickableSpan so a tap can report it, but TalkBack treats
|
|
315
369
|
// each span as a link: it reads the text one word at a time and lists every
|
|
316
370
|
// word under Actions. Plain text keeps it reading as prose; granularity
|
|
@@ -437,8 +491,7 @@ class AdvancedTextView : TextView {
|
|
|
437
491
|
|
|
438
492
|
private inner class WordClickableSpan(
|
|
439
493
|
private val wordIndex: Int,
|
|
440
|
-
private val word: String
|
|
441
|
-
private val wordColor: Int
|
|
494
|
+
private val word: String
|
|
442
495
|
) : ClickableSpan() {
|
|
443
496
|
|
|
444
497
|
override fun onClick(widget: View) {
|
|
@@ -446,10 +499,10 @@ class AdvancedTextView : TextView {
|
|
|
446
499
|
sendWordPressEvent(word, wordIndex)
|
|
447
500
|
}
|
|
448
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.
|
|
449
504
|
override fun updateDrawState(ds: TextPaint) {
|
|
450
|
-
super.updateDrawState(ds)
|
|
451
505
|
ds.isUnderlineText = false
|
|
452
|
-
ds.color = wordColor
|
|
453
506
|
}
|
|
454
507
|
}
|
|
455
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