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
- private var textAlign: String = "left"
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
- textColor = String.format("#%06X", 0xFFFFFF and colorInt)
135
- updateTextWithHighlights()
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
- when (align) {
154
- "left" -> textAlignment = View.TEXT_ALIGNMENT_TEXT_START
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
- updateTextWithHighlights()
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
- updateTextWithHighlights()
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
- updateTextWithHighlights()
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
- textAlignment = when (textAlign) {
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
 
@@ -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.12",
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",