11package com.swmansion.enriched
22
3+ import android.content.Context
34import android.graphics.Typeface
45import android.graphics.text.LineBreaker
56import android.os.Build
67import android.text.Spannable
78import android.text.StaticLayout
89import android.text.TextPaint
10+ import android.util.Log
11+ import com.facebook.react.bridge.ReadableMap
912import 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
1016import com.facebook.yoga.YogaMeasureOutput
17+ import com.swmansion.enriched.styles.HtmlStyle
18+ import com.swmansion.enriched.utils.EnrichedParser
1119import java.util.concurrent.ConcurrentHashMap
20+ import kotlin.math.ceil
1221
1322object MeasurementStore {
1423 data class PaintParams (
@@ -17,10 +26,12 @@ object MeasurementStore {
1726 )
1827
1928 data class MeasurementParams (
29+ val initialized : Boolean ,
30+
2031 val cachedWidth : Float ,
2132 val cachedSize : Long ,
2233
23- val spannable : Spannable ? ,
34+ val spannable : CharSequence ? ,
2435 val paintParams : PaintParams ,
2536 )
2637
@@ -29,18 +40,20 @@ object MeasurementStore {
2940 fun store (id : Int , spannable : Spannable ? , paint : TextPaint ): Boolean {
3041 val cachedWidth = data[id]?.cachedWidth ? : 0f
3142 val cachedSize = data[id]?.cachedSize ? : 0L
43+ val initialized = data[id]?.initialized ? : true
44+
3245 val size = measure(cachedWidth, spannable, paint)
3346 val paintParams = PaintParams (paint.typeface, paint.textSize)
3447
35- data[id] = MeasurementParams (cachedWidth, size, spannable, paintParams)
48+ data[id] = MeasurementParams (initialized, cachedWidth, size, spannable, paintParams)
3649 return cachedSize != size
3750 }
3851
3952 fun release (id : Int ) {
4053 data.remove(id)
4154 }
4255
43- fun measure (maxWidth : Float , spannable : Spannable ? , paintParams : PaintParams ): Long {
56+ private fun measure (maxWidth : Float , spannable : CharSequence ? , paintParams : PaintParams ): Long {
4457 val paint = TextPaint ().apply {
4558 typeface = paintParams.typeface
4659 textSize = paintParams.fontSize
@@ -49,7 +62,7 @@ object MeasurementStore {
4962 return measure(maxWidth, spannable, paint)
5063 }
5164
52- fun measure (maxWidth : Float , spannable : Spannable ? , paint : TextPaint ): Long {
65+ private fun measure (maxWidth : Float , spannable : CharSequence ? , paint : TextPaint ): Long {
5366 val text = spannable ? : " "
5467 val textLength = text.length
5568 val builder = StaticLayout .Builder
@@ -71,9 +84,63 @@ object MeasurementStore {
7184 return YogaMeasureOutput .make(widthInSP, heightInSP)
7285 }
7386
74- fun getMeasureById (id : Int? , width : Float ): Long {
75- val id = id ? : return YogaMeasureOutput .make(0 , 0 )
76- val value = data[id] ? : return YogaMeasureOutput .make(0 , 0 )
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)
77144
78145 if (width == value.cachedWidth) {
79146 return value.cachedSize
@@ -83,8 +150,9 @@ object MeasurementStore {
83150 typeface = value.paintParams.typeface
84151 textSize = value.paintParams.fontSize
85152 }
153+
86154 val size = measure(width, value.spannable, paint)
87- data[id] = MeasurementParams (width, size, value.spannable, value.paintParams)
155+ data[id] = MeasurementParams (true , width, size, value.spannable, value.paintParams)
88156 return size
89157 }
90158}
0 commit comments