react-native-enriched 0.1.4 → 0.1.6

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.
Files changed (44) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +3 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +1 -0
  3. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  4. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -0
  5. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +65 -28
  6. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -46
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +16 -19
  8. package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
  9. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +6 -2
  10. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +9 -5
  11. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +10 -5
  12. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +11 -1
  13. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +61 -46
  14. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +11 -1
  15. package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +1 -2
  16. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +29 -12
  17. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +23 -3
  18. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +0 -1
  19. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
  20. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
  21. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
  22. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
  23. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
  24. package/ios/EnrichedTextInputView.h +0 -1
  25. package/ios/EnrichedTextInputView.mm +72 -36
  26. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  27. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -0
  28. package/ios/inputParser/InputParser.mm +32 -7
  29. package/ios/inputTextView/InputTextView.mm +3 -5
  30. package/ios/styles/LinkStyle.mm +7 -7
  31. package/ios/styles/OrderedListStyle.mm +3 -8
  32. package/ios/styles/UnorderedListStyle.mm +3 -8
  33. package/ios/utils/StringExtension.h +1 -1
  34. package/ios/utils/StringExtension.mm +17 -8
  35. package/lib/module/EnrichedTextInput.js +2 -0
  36. package/lib/module/EnrichedTextInput.js.map +1 -1
  37. package/lib/module/EnrichedTextInputNativeComponent.ts +1 -0
  38. package/lib/typescript/src/EnrichedTextInput.d.ts +2 -1
  39. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  40. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +1 -0
  41. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  42. package/package.json +3 -2
  43. package/src/EnrichedTextInput.tsx +3 -0
  44. package/src/EnrichedTextInputNativeComponent.ts +1 -0
@@ -55,6 +55,9 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
55
55
  case "htmlStyle":
56
56
  mViewManager.setHtmlStyle(view, (ReadableMap) value);
57
57
  break;
58
+ case "scrollEnabled":
59
+ mViewManager.setScrollEnabled(view, value == null ? false : (boolean) value);
60
+ break;
58
61
  case "color":
59
62
  mViewManager.setColor(view, ColorPropConverter.getColor(value, view.getContext()));
60
63
  break;
@@ -26,6 +26,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
26
26
  void setSelectionColor(T view, @Nullable Integer value);
27
27
  void setAutoCapitalize(T view, @Nullable String value);
28
28
  void setHtmlStyle(T view, @Nullable ReadableMap value);
29
+ void setScrollEnabled(T view, boolean value);
29
30
  void setColor(T view, @Nullable Integer value);
30
31
  void setFontSize(T view, float value);
31
32
  void setFontFamily(T view, @Nullable String value);
@@ -30,6 +30,7 @@ EnrichedTextInputViewProps::EnrichedTextInputViewProps(
30
30
  selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
31
31
  autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
32
32
  htmlStyle(convertRawProp(context, rawProps, "htmlStyle", sourceProps.htmlStyle, {})),
33
+ scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
33
34
  color(convertRawProp(context, rawProps, "color", sourceProps.color, {})),
34
35
  fontSize(convertRawProp(context, rawProps, "fontSize", sourceProps.fontSize, {0.0})),
35
36
  fontFamily(convertRawProp(context, rawProps, "fontFamily", sourceProps.fontFamily, {})),
@@ -94,6 +95,10 @@ folly::dynamic EnrichedTextInputViewProps::getDiffProps(
94
95
  result["htmlStyle"] = toDynamic(htmlStyle);
95
96
  }
96
97
 
98
+ if (scrollEnabled != oldProps->scrollEnabled) {
99
+ result["scrollEnabled"] = scrollEnabled;
100
+ }
101
+
97
102
  if (color != oldProps->color) {
98
103
  result["color"] = *color;
99
104
  }
@@ -559,6 +559,7 @@ class EnrichedTextInputViewProps final : public ViewProps {
559
559
  SharedColor selectionColor{};
560
560
  std::string autoCapitalize{};
561
561
  EnrichedTextInputViewHtmlStyleStruct htmlStyle{};
562
+ bool scrollEnabled{false};
562
563
  SharedColor color{};
563
564
  Float fontSize{0.0};
564
565
  std::string fontFamily{};
@@ -7,6 +7,7 @@ import android.graphics.BlendMode
7
7
  import android.graphics.BlendModeColorFilter
8
8
  import android.graphics.Color
9
9
  import android.graphics.Rect
10
+ import android.graphics.text.LineBreaker
10
11
  import android.os.Build
11
12
  import android.text.InputType
12
13
  import android.text.Spannable
@@ -51,11 +52,9 @@ class EnrichedTextInputView : AppCompatEditText {
51
52
  val paragraphStyles: ParagraphStyles? = ParagraphStyles(this)
52
53
  val listStyles: ListStyles? = ListStyles(this)
53
54
  val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
54
- // Sometimes setting up style triggers many changes in sequence
55
- // Eg. removing conflicting styles -> changing text -> applying spans
56
- // In such scenario we want to prevent from handling side effects (eg. onTextChanged)
57
55
  var isDuringTransaction: Boolean = false
58
56
  var isRemovingMany: Boolean = false
57
+ var scrollEnabled: Boolean = true
59
58
 
60
59
  val mentionHandler: MentionHandler? = MentionHandler(this)
61
60
  var htmlStyle: HtmlStyle = HtmlStyle(this, null)
@@ -72,6 +71,8 @@ class EnrichedTextInputView : AppCompatEditText {
72
71
  private var fontFamily: String? = null
73
72
  private var fontStyle: Int = ReactConstants.UNSET
74
73
  private var fontWeight: Int = ReactConstants.UNSET
74
+ private var defaultValue: CharSequence? = null
75
+ private var defaultValueDirty: Boolean = false
75
76
 
76
77
  private var inputMethodManager: InputMethodManager? = null
77
78
 
@@ -102,6 +103,10 @@ class EnrichedTextInputView : AppCompatEditText {
102
103
  gravity = Gravity.TOP or Gravity.START
103
104
  inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
104
105
 
106
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
107
+ breakStrategy = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
108
+ }
109
+
105
110
  setPadding(0, 0, 0, 0)
106
111
  setBackgroundColor(Color.TRANSPARENT)
107
112
 
@@ -135,6 +140,14 @@ class EnrichedTextInputView : AppCompatEditText {
135
140
  return super.onTouchEvent(ev)
136
141
  }
137
142
 
143
+ override fun canScrollVertically(direction: Int): Boolean {
144
+ return scrollEnabled
145
+ }
146
+
147
+ override fun canScrollHorizontally(direction: Int): Boolean {
148
+ return scrollEnabled
149
+ }
150
+
138
151
  override fun onSelectionChanged(selStart: Int, selEnd: Int) {
139
152
  super.onSelectionChanged(selStart, selEnd)
140
153
  selection?.onSelection(selStart, selEnd)
@@ -236,18 +249,17 @@ class EnrichedTextInputView : AppCompatEditText {
236
249
 
237
250
  fun setValue(value: CharSequence?) {
238
251
  if (value == null) return
239
- isDuringTransaction = true
240
-
241
- val newText = parseText(value)
242
- setText(newText)
243
252
 
244
- // Assign SpanWatcher one more time as our previous spannable has been replaced
245
- addSpanWatcher(EnrichedSpanWatcher(this))
253
+ runAsATransaction {
254
+ val newText = parseText(value)
255
+ setText(newText)
246
256
 
247
- // Scroll to the last line of text
248
- setSelection(text?.length ?: 0)
257
+ // Assign SpanWatcher one more time as our previous spannable has been replaced
258
+ addSpanWatcher(EnrichedSpanWatcher(this))
249
259
 
250
- isDuringTransaction = false
260
+ // Scroll to the last line of text
261
+ setSelection(text?.length ?: 0)
262
+ }
251
263
  }
252
264
 
253
265
  fun setAutoFocus(autoFocus: Boolean) {
@@ -305,7 +317,7 @@ class EnrichedTextInputView : AppCompatEditText {
305
317
 
306
318
  // This ensured that newly created spans will take the new font size into account
307
319
  htmlStyle.invalidateStyles()
308
- layoutManager.invalidateLayout(text)
320
+ layoutManager.invalidateLayout()
309
321
  }
310
322
 
311
323
  fun setFontFamily(family: String?) {
@@ -359,7 +371,24 @@ class EnrichedTextInputView : AppCompatEditText {
359
371
  return false
360
372
  }
361
373
 
362
- fun updateTypeface() {
374
+ fun afterUpdateTransaction() {
375
+ updateTypeface()
376
+ updateDefaultValue()
377
+ }
378
+
379
+ fun setDefaultValue(value: CharSequence?) {
380
+ defaultValue = value
381
+ defaultValueDirty = true
382
+ }
383
+
384
+ private fun updateDefaultValue() {
385
+ if (!defaultValueDirty) return
386
+
387
+ defaultValueDirty = false
388
+ setValue(defaultValue ?: "")
389
+ }
390
+
391
+ private fun updateTypeface() {
363
392
  if (!typefaceDirty) return
364
393
  typefaceDirty = false
365
394
 
@@ -367,7 +396,7 @@ class EnrichedTextInputView : AppCompatEditText {
367
396
  typeface = newTypeface
368
397
  paint.typeface = newTypeface
369
398
 
370
- layoutManager.invalidateLayout(text)
399
+ layoutManager.invalidateLayout()
371
400
  }
372
401
 
373
402
  private fun toggleStyle(name: String) {
@@ -387,7 +416,7 @@ class EnrichedTextInputView : AppCompatEditText {
387
416
  else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
388
417
  }
389
418
 
390
- layoutManager.invalidateLayout(text)
419
+ layoutManager.invalidateLayout()
391
420
  }
392
421
 
393
422
  private fun removeStyle(name: String, start: Int, end: Int): Boolean {
@@ -437,7 +466,7 @@ class EnrichedTextInputView : AppCompatEditText {
437
466
  }
438
467
 
439
468
  private fun verifyStyle(name: String): Boolean {
440
- val mergingConfig = EnrichedSpans.mergingConfig[name] ?: return true
469
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(name, htmlStyle) ?: return true
441
470
  val conflictingStyles = mergingConfig.conflictingStyles
442
471
  val blockingStyles = mergingConfig.blockingStyles
443
472
  val isEnabling = spanState?.getStart(name) == null
@@ -455,13 +484,13 @@ class EnrichedTextInputView : AppCompatEditText {
455
484
  val end = selection?.end ?: 0
456
485
  val lengthBefore = text?.length ?: 0
457
486
 
458
- isDuringTransaction = true
459
- val targetRange = getTargetRange(name)
460
- val removed = removeStyle(style, targetRange.first, targetRange.second)
461
- if (removed) {
462
- spanState?.setStart(style, null)
487
+ runAsATransaction {
488
+ val targetRange = getTargetRange(name)
489
+ val removed = removeStyle(style, targetRange.first, targetRange.second)
490
+ if (removed) {
491
+ spanState?.setStart(style, null)
492
+ }
463
493
  }
464
- isDuringTransaction = false
465
494
 
466
495
  val lengthAfter = text?.length ?: 0
467
496
  val charactersRemoved = lengthBefore - lengthAfter
@@ -503,6 +532,7 @@ class EnrichedTextInputView : AppCompatEditText {
503
532
  if (!isValid) return
504
533
 
505
534
  parametrizedStyles?.setImageSpan(src)
535
+ layoutManager.invalidateLayout()
506
536
  }
507
537
 
508
538
  fun startMention(indicator: String) {
@@ -519,6 +549,18 @@ class EnrichedTextInputView : AppCompatEditText {
519
549
  parametrizedStyles?.setMentionSpan(text, indicator, attributes)
520
550
  }
521
551
 
552
+ // Sometimes setting up style triggers many changes in sequence
553
+ // Eg. removing conflicting styles -> changing text -> applying spans
554
+ // In such scenario we want to prevent from handling side effects (eg. onTextChanged)
555
+ fun runAsATransaction(block: () -> Unit) {
556
+ try {
557
+ isDuringTransaction = true
558
+ block()
559
+ } finally {
560
+ isDuringTransaction = false
561
+ }
562
+ }
563
+
522
564
  override fun onAttachedToWindow() {
523
565
  super.onAttachedToWindow()
524
566
 
@@ -529,11 +571,6 @@ class EnrichedTextInputView : AppCompatEditText {
529
571
  didAttachToWindow = true
530
572
  }
531
573
 
532
- override fun onDetachedFromWindow() {
533
- layoutManager.cleanup()
534
- super.onDetachedFromWindow()
535
- }
536
-
537
574
  companion object {
538
575
  const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
539
576
  }
@@ -1,22 +1,16 @@
1
1
  package com.swmansion.enriched
2
2
 
3
- import android.text.Editable
4
- import android.text.StaticLayout
5
3
  import com.facebook.react.bridge.Arguments
6
- import com.facebook.react.uimanager.PixelUtil
7
4
 
8
5
  class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView) {
9
- private var cachedSize: Pair<Float, Float> = Pair(0f, 0f)
10
- private var cachedYogaWidth: Float = 0f
11
6
  private var forceHeightRecalculationCounter: Int = 0
12
7
 
13
- fun cleanup() {
14
- forceHeightRecalculationCounter = 0
15
- }
8
+ fun invalidateLayout() {
9
+ val text = view.text
10
+ val paint = view.paint
16
11
 
17
- // Update shadow node's state in order to recalculate layout
18
- fun invalidateLayout(text: Editable?) {
19
- measureSize(text ?: "")
12
+ val needUpdate = MeasurementStore.store(view.id, text, paint)
13
+ if (!needUpdate) return
20
14
 
21
15
  val counter = forceHeightRecalculationCounter
22
16
  forceHeightRecalculationCounter++
@@ -25,40 +19,7 @@ class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView
25
19
  view.stateWrapper?.updateState(state)
26
20
  }
27
21
 
28
- fun getMeasuredSize(maxWidth: Float): Pair<Float, Float> {
29
- if (maxWidth == cachedYogaWidth) {
30
- return cachedSize
31
- }
32
-
33
- val text = view.text ?: ""
34
- val result = measureAndCacheSize(text, maxWidth)
35
- cachedYogaWidth = maxWidth
36
- return result
37
- }
38
-
39
- fun measureSize(text: CharSequence): Pair<Float, Float> {
40
- return measureAndCacheSize(text, cachedYogaWidth)
41
- }
42
-
43
- private fun measureAndCacheSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
44
- val result = measureSize(text, maxWidth)
45
- cachedSize = result
46
- return result
47
- }
48
-
49
- private fun measureSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
50
- val paint = view.paint
51
- val textLength = text.length
52
-
53
- val staticLayout = StaticLayout.Builder
54
- .obtain(text, 0, textLength, paint, maxWidth.toInt())
55
- .setIncludePad(true)
56
- .setLineSpacing(0f, 1f)
57
- .build()
58
-
59
- val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
60
- val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
61
-
62
- return Pair(widthInSP, heightInSP)
22
+ fun releaseMeasurementStore() {
23
+ MeasurementStore.release(view.id)
63
24
  }
64
25
  }
@@ -15,7 +15,6 @@ import com.facebook.react.uimanager.annotations.ReactProp
15
15
  import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerDelegate
16
16
  import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerInterface
17
17
  import com.facebook.yoga.YogaMeasureMode
18
- import com.facebook.yoga.YogaMeasureOutput
19
18
  import com.swmansion.enriched.events.OnInputBlurEvent
20
19
  import com.swmansion.enriched.events.OnChangeHtmlEvent
21
20
  import com.swmansion.enriched.events.OnChangeSelectionEvent
@@ -32,12 +31,8 @@ import com.swmansion.enriched.utils.jsonStringToStringMap
32
31
  @ReactModule(name = EnrichedTextInputViewManager.NAME)
33
32
  class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
34
33
  EnrichedTextInputViewManagerInterface<EnrichedTextInputView> {
35
- private val mDelegate: ViewManagerDelegate<EnrichedTextInputView>
36
- private var view: EnrichedTextInputView? = null
37
-
38
- init {
39
- mDelegate = EnrichedTextInputViewManagerDelegate(this)
40
- }
34
+ private val mDelegate: ViewManagerDelegate<EnrichedTextInputView> =
35
+ EnrichedTextInputViewManagerDelegate(this)
41
36
 
42
37
  override fun getDelegate(): ViewManagerDelegate<EnrichedTextInputView>? {
43
38
  return mDelegate
@@ -48,10 +43,12 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
48
43
  }
49
44
 
50
45
  public override fun createViewInstance(context: ThemedReactContext): EnrichedTextInputView {
51
- val view = EnrichedTextInputView(context)
52
- this.view = view
46
+ return EnrichedTextInputView(context)
47
+ }
53
48
 
54
- return view
49
+ override fun onDropViewInstance(view: EnrichedTextInputView) {
50
+ super.onDropViewInstance(view)
51
+ view.layoutManager.releaseMeasurementStore()
55
52
  }
56
53
 
57
54
  override fun updateState(
@@ -80,7 +77,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
80
77
 
81
78
  @ReactProp(name = "defaultValue")
82
79
  override fun setDefaultValue(view: EnrichedTextInputView?, value: String?) {
83
- view?.setValue(value)
80
+ view?.setDefaultValue(value)
84
81
  }
85
82
 
86
83
  @ReactProp(name = "placeholder")
@@ -157,9 +154,14 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
157
154
  view?.setFontStyle(style)
158
155
  }
159
156
 
157
+ @ReactProp(name = "scrollEnabled")
158
+ override fun setScrollEnabled(view: EnrichedTextInputView, scrollEnabled: Boolean) {
159
+ view.scrollEnabled = scrollEnabled
160
+ }
161
+
160
162
  override fun onAfterUpdateTransaction(view: EnrichedTextInputView) {
161
163
  super.onAfterUpdateTransaction(view)
162
- view.updateTypeface()
164
+ view.afterUpdateTransaction()
163
165
  }
164
166
 
165
167
  override fun setPadding(
@@ -277,13 +279,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
277
279
  heightMode: YogaMeasureMode?,
278
280
  attachmentsPositions: FloatArray?
279
281
  ): Long {
280
- val size = this.view?.layoutManager?.getMeasuredSize(width)
281
-
282
- if (size != null) {
283
- return YogaMeasureOutput.make(size.first, size.second)
284
- }
285
-
286
- return YogaMeasureOutput.make(0, 0)
282
+ val id = localData?.getInt("viewTag")
283
+ return MeasurementStore.getMeasureById(context, id, width, props)
287
284
  }
288
285
 
289
286
  companion object {
@@ -0,0 +1,158 @@
1
+ package com.swmansion.enriched
2
+
3
+ import android.content.Context
4
+ import android.graphics.Typeface
5
+ import android.graphics.text.LineBreaker
6
+ import android.os.Build
7
+ import android.text.Spannable
8
+ import android.text.StaticLayout
9
+ import android.text.TextPaint
10
+ import android.util.Log
11
+ import com.facebook.react.bridge.ReadableMap
12
+ import com.facebook.react.uimanager.PixelUtil
13
+ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
14
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
15
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
16
+ import com.facebook.yoga.YogaMeasureOutput
17
+ import com.swmansion.enriched.styles.HtmlStyle
18
+ import com.swmansion.enriched.utils.EnrichedParser
19
+ import java.util.concurrent.ConcurrentHashMap
20
+ import kotlin.math.ceil
21
+
22
+ object MeasurementStore {
23
+ data class PaintParams(
24
+ val typeface: Typeface,
25
+ val fontSize: Float,
26
+ )
27
+
28
+ data class MeasurementParams(
29
+ val initialized: Boolean,
30
+
31
+ val cachedWidth: Float,
32
+ val cachedSize: Long,
33
+
34
+ val spannable: CharSequence?,
35
+ val paintParams: PaintParams,
36
+ )
37
+
38
+ private val data = ConcurrentHashMap<Int, MeasurementParams>()
39
+
40
+ fun store(id: Int, spannable: Spannable?, paint: TextPaint): Boolean {
41
+ val cachedWidth = data[id]?.cachedWidth ?: 0f
42
+ val cachedSize = data[id]?.cachedSize ?: 0L
43
+ val initialized = data[id]?.initialized ?: true
44
+
45
+ val size = measure(cachedWidth, spannable, paint)
46
+ val paintParams = PaintParams(paint.typeface, paint.textSize)
47
+
48
+ data[id] = MeasurementParams(initialized, cachedWidth, size, spannable, paintParams)
49
+ return cachedSize != size
50
+ }
51
+
52
+ fun release(id: Int) {
53
+ data.remove(id)
54
+ }
55
+
56
+ private fun measure(maxWidth: Float, spannable: CharSequence?, paintParams: PaintParams): Long {
57
+ val paint = TextPaint().apply {
58
+ typeface = paintParams.typeface
59
+ textSize = paintParams.fontSize
60
+ }
61
+
62
+ return measure(maxWidth, spannable, paint)
63
+ }
64
+
65
+ private fun measure(maxWidth: Float, spannable: CharSequence?, paint: TextPaint): Long {
66
+ val text = spannable ?: ""
67
+ val textLength = text.length
68
+ val builder = StaticLayout.Builder
69
+ .obtain(text, 0, textLength, paint, maxWidth.toInt())
70
+ .setIncludePad(true)
71
+ .setLineSpacing(0f, 1f)
72
+
73
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
74
+ builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
75
+ }
76
+
77
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
78
+ builder.setUseLineSpacingFromFallbacks(true)
79
+ }
80
+
81
+ val staticLayout = builder.build()
82
+ val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
83
+ val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
84
+ return YogaMeasureOutput.make(widthInSP, heightInSP)
85
+ }
86
+
87
+ // Returns either: Spannable parsed from HTML defaultValue, or plain text defaultValue, or "I" if no defaultValue
88
+ private fun getInitialText(defaultView: EnrichedTextInputView, props: ReadableMap?): CharSequence {
89
+ val defaultValue = props?.getString("defaultValue")
90
+
91
+ // If there is no default value, assume text is one line, "I" is a good approximation of height
92
+ if (defaultValue == null) return "I"
93
+
94
+ val isHtml = defaultValue.startsWith("<html>") && defaultValue.endsWith("</html>")
95
+ if (!isHtml) return defaultValue
96
+
97
+ try {
98
+ val htmlStyle = HtmlStyle(defaultView, props.getMap("htmlStyle"))
99
+ val parsed = EnrichedParser.fromHtml(defaultValue, htmlStyle, null)
100
+ return parsed.trimEnd('\n')
101
+ } catch (e: Exception) {
102
+ Log.w("MeasurementStore", "Error parsing initial HTML text: ${e.message}")
103
+ return defaultValue
104
+ }
105
+ }
106
+
107
+ private fun getInitialFontSize(defaultView: EnrichedTextInputView, props: ReadableMap?): Float {
108
+ val propsFontSize = props?.getDouble("fontSize")?.toFloat()
109
+ if (propsFontSize == null) return defaultView.textSize
110
+
111
+ return ceil(PixelUtil.toPixelFromSP(propsFontSize))
112
+ }
113
+
114
+ // Called when view measurements are not available in the store
115
+ // Most likely first measurement, we can use defaultValue, as no native state is set yet
116
+ private fun initialMeasure(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
117
+ val defaultView = EnrichedTextInputView(context)
118
+
119
+ val text = getInitialText(defaultView, props)
120
+ val fontSize = getInitialFontSize(defaultView, props)
121
+
122
+ val fontFamily = props?.getString("fontFamily")
123
+ val fontStyle = parseFontStyle(props?.getString("fontStyle"))
124
+ val fontWeight = parseFontWeight(props?.getString("fontWeight"))
125
+
126
+ val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
127
+ val paintParams = PaintParams(typeface, fontSize)
128
+ val size = measure(width, text, PaintParams(typeface, fontSize))
129
+
130
+ if (id != null) {
131
+ data[id] = MeasurementParams(true, width, size, text, paintParams)
132
+ }
133
+
134
+ return size
135
+ }
136
+
137
+ fun getMeasureById(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
138
+ val id = id ?: return initialMeasure(context, id, width, props)
139
+ val value = data[id] ?: return initialMeasure(context, id, width, props)
140
+
141
+ // First measure has to be done using initialMeasure
142
+ // That way it's free of any side effects and async initializations
143
+ if (!value.initialized) return initialMeasure(context, id, width, props)
144
+
145
+ if (width == value.cachedWidth) {
146
+ return value.cachedSize
147
+ }
148
+
149
+ val paint = TextPaint().apply {
150
+ typeface = value.paintParams.typeface
151
+ textSize = value.paintParams.fontSize
152
+ }
153
+
154
+ val size = measure(width, value.spannable, paint)
155
+ data[id] = MeasurementParams(true, width, size, value.spannable, value.paintParams)
156
+ return size
157
+ }
158
+ }
@@ -4,13 +4,17 @@ import android.graphics.Canvas
4
4
  import android.graphics.Paint
5
5
  import android.text.Layout
6
6
  import android.text.TextPaint
7
- import android.text.style.CharacterStyle
8
7
  import android.text.style.LeadingMarginSpan
8
+ import android.text.style.MetricAffectingSpan
9
9
  import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
10
10
  import com.swmansion.enriched.styles.HtmlStyle
11
11
 
12
12
  // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/QuoteSpan.java
13
- class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : CharacterStyle(), LeadingMarginSpan, EnrichedBlockSpan {
13
+ class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedBlockSpan {
14
+ override fun updateMeasureState(p0: TextPaint) {
15
+ // Do nothing, but inform layout that this span affects text metrics
16
+ }
17
+
14
18
  override fun getLeadingMargin(p0: Boolean): Int {
15
19
  return htmlStyle.blockquoteStripeWidth + htmlStyle.blockquoteGapWidth
16
20
  }
@@ -5,15 +5,19 @@ import android.graphics.Paint
5
5
  import android.graphics.RectF
6
6
  import android.graphics.Typeface
7
7
  import android.text.TextPaint
8
- import android.text.style.CharacterStyle
9
8
  import android.text.style.LineBackgroundSpan
9
+ import android.text.style.MetricAffectingSpan
10
10
  import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
11
11
  import com.swmansion.enriched.styles.HtmlStyle
12
12
 
13
- class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : CharacterStyle(), LineBackgroundSpan, EnrichedBlockSpan {
14
- override fun updateDrawState(paint: TextPaint?) {
15
- paint?.typeface = Typeface.MONOSPACE
16
- paint?.color = htmlStyle.codeBlockColor
13
+ class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LineBackgroundSpan, EnrichedBlockSpan {
14
+ override fun updateDrawState(paint: TextPaint) {
15
+ paint.typeface = Typeface.MONOSPACE
16
+ paint.color = htmlStyle.codeBlockColor
17
+ }
18
+
19
+ override fun updateMeasureState(paint: TextPaint) {
20
+ paint.typeface = Typeface.MONOSPACE
17
21
  }
18
22
 
19
23
  override fun drawBackground(
@@ -2,15 +2,20 @@ package com.swmansion.enriched.spans
2
2
 
3
3
  import android.graphics.Typeface
4
4
  import android.text.TextPaint
5
- import android.text.style.BackgroundColorSpan
5
+ import android.text.style.MetricAffectingSpan
6
6
  import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
7
7
  import com.swmansion.enriched.styles.HtmlStyle
8
8
 
9
- class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : BackgroundColorSpan(htmlStyle.inlineCodeBackgroundColor), EnrichedInlineSpan {
9
+ class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), EnrichedInlineSpan {
10
10
  override fun updateDrawState(textPaint: TextPaint) {
11
- super.updateDrawState(textPaint)
12
-
11
+ val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
12
+ textPaint.typeface = typeface
13
13
  textPaint.color = htmlStyle.inlineCodeColor
14
- textPaint.typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
14
+ textPaint.bgColor = htmlStyle.inlineCodeBackgroundColor
15
+ }
16
+
17
+ override fun updateMeasureState(textPaint: TextPaint) {
18
+ val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
19
+ textPaint.typeface = typeface
15
20
  }
16
21
  }
@@ -4,11 +4,21 @@ import android.graphics.Canvas
4
4
  import android.graphics.Paint
5
5
  import android.graphics.Typeface
6
6
  import android.text.Layout
7
+ import android.text.TextPaint
7
8
  import android.text.style.LeadingMarginSpan
9
+ import android.text.style.MetricAffectingSpan
8
10
  import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
9
11
  import com.swmansion.enriched.styles.HtmlStyle
10
12
 
11
- class EnrichedOrderedListSpan(private var index: Int, private val htmlStyle: HtmlStyle) : LeadingMarginSpan, EnrichedParagraphSpan {
13
+ class EnrichedOrderedListSpan(private var index: Int, private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedParagraphSpan {
14
+ override fun updateMeasureState(p0: TextPaint) {
15
+ // Do nothing, but inform layout that this span affects text metrics
16
+ }
17
+
18
+ override fun updateDrawState(p0: TextPaint?) {
19
+ // Do nothing, but inform layout that this span affects text metrics
20
+ }
21
+
12
22
  override fun getLeadingMargin(first: Boolean): Int {
13
23
  return htmlStyle.olMarginLeft + htmlStyle.olGapWidth
14
24
  }