diff --git a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt index 8b5a2296b..286a19b7b 100644 --- a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +++ b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt @@ -279,7 +279,8 @@ class EnrichedTextInputViewManager : SimpleViewManager(), heightMode: YogaMeasureMode?, attachmentsPositions: FloatArray? ): Long { - return MeasurementStore.getMeasureById(localData?.getInt("viewTag"), width) + val id = localData?.getInt("viewTag") + return MeasurementStore.getMeasureById(context, id, width, props) } companion object { diff --git a/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt index ed4df3b3a..a58477754 100644 --- a/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +++ b/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt @@ -1,14 +1,23 @@ package com.swmansion.enriched +import android.content.Context import android.graphics.Typeface import android.graphics.text.LineBreaker import android.os.Build import android.text.Spannable import android.text.StaticLayout import android.text.TextPaint +import android.util.Log +import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles +import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle +import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight import com.facebook.yoga.YogaMeasureOutput +import com.swmansion.enriched.styles.HtmlStyle +import com.swmansion.enriched.utils.EnrichedParser import java.util.concurrent.ConcurrentHashMap +import kotlin.math.ceil object MeasurementStore { data class PaintParams( @@ -17,10 +26,12 @@ object MeasurementStore { ) data class MeasurementParams( + val initialized: Boolean, + val cachedWidth: Float, val cachedSize: Long, - val spannable: Spannable?, + val spannable: CharSequence?, val paintParams: PaintParams, ) @@ -29,10 +40,12 @@ object MeasurementStore { fun store(id: Int, spannable: Spannable?, paint: TextPaint): Boolean { val cachedWidth = data[id]?.cachedWidth ?: 0f val cachedSize = data[id]?.cachedSize ?: 0L + val initialized = data[id]?.initialized ?: true + val size = measure(cachedWidth, spannable, paint) val paintParams = PaintParams(paint.typeface, paint.textSize) - data[id] = MeasurementParams(cachedWidth, size, spannable, paintParams) + data[id] = MeasurementParams(initialized, cachedWidth, size, spannable, paintParams) return cachedSize != size } @@ -40,7 +53,7 @@ object MeasurementStore { data.remove(id) } - fun measure(maxWidth: Float, spannable: Spannable?, paintParams: PaintParams): Long { + private fun measure(maxWidth: Float, spannable: CharSequence?, paintParams: PaintParams): Long { val paint = TextPaint().apply { typeface = paintParams.typeface textSize = paintParams.fontSize @@ -49,7 +62,7 @@ object MeasurementStore { return measure(maxWidth, spannable, paint) } - fun measure(maxWidth: Float, spannable: Spannable?, paint: TextPaint): Long { + private fun measure(maxWidth: Float, spannable: CharSequence?, paint: TextPaint): Long { val text = spannable ?: "" val textLength = text.length val builder = StaticLayout.Builder @@ -71,9 +84,63 @@ object MeasurementStore { return YogaMeasureOutput.make(widthInSP, heightInSP) } - fun getMeasureById(id: Int?, width: Float): Long { - val id = id ?: return YogaMeasureOutput.make(0, 0) - val value = data[id] ?: return YogaMeasureOutput.make(0, 0) + // Returns either: Spannable parsed from HTML defaultValue, or plain text defaultValue, or "I" if no defaultValue + private fun getInitialText(defaultView: EnrichedTextInputView, props: ReadableMap?): CharSequence { + val defaultValue = props?.getString("defaultValue") + + // If there is no default value, assume text is one line, "I" is a good approximation of height + if (defaultValue == null) return "I" + + val isHtml = defaultValue.startsWith("") && defaultValue.endsWith("") + if (!isHtml) return defaultValue + + try { + val htmlStyle = HtmlStyle(defaultView, props.getMap("htmlStyle")) + val parsed = EnrichedParser.fromHtml(defaultValue, htmlStyle, null) + return parsed.trimEnd('\n') + } catch (e: Exception) { + Log.w("MeasurementStore", "Error parsing initial HTML text: ${e.message}") + return defaultValue + } + } + + private fun getInitialFontSize(defaultView: EnrichedTextInputView, props: ReadableMap?): Float { + val propsFontSize = props?.getDouble("fontSize")?.toFloat() + if (propsFontSize == null) return defaultView.textSize + + return ceil(PixelUtil.toPixelFromSP(propsFontSize)) + } + + // Called when view measurements are not available in the store + // Most likely first measurement, we can use defaultValue, as no native state is set yet + private fun initialMeasure(context: Context, id: Int?, width: Float, props: ReadableMap?): Long { + val defaultView = EnrichedTextInputView(context) + + val text = getInitialText(defaultView, props) + val fontSize = getInitialFontSize(defaultView, props) + + val fontFamily = props?.getString("fontFamily") + val fontStyle = parseFontStyle(props?.getString("fontStyle")) + val fontWeight = parseFontWeight(props?.getString("fontWeight")) + + val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets) + val paintParams = PaintParams(typeface, fontSize) + val size = measure(width, text, PaintParams(typeface, fontSize)) + + if (id != null) { + data[id] = MeasurementParams(true, width, size, text, paintParams) + } + + return size + } + + fun getMeasureById(context: Context, id: Int?, width: Float, props: ReadableMap?): Long { + val id = id ?: return initialMeasure(context, id, width, props) + val value = data[id] ?: return initialMeasure(context, id, width, props) + + // First measure has to be done using initialMeasure + // That way it's free of any side effects and async initializations + if (!value.initialized) return initialMeasure(context, id, width, props) if (width == value.cachedWidth) { return value.cachedSize @@ -83,8 +150,9 @@ object MeasurementStore { typeface = value.paintParams.typeface textSize = value.paintParams.fontSize } + val size = measure(width, value.spannable, paint) - data[id] = MeasurementParams(width, size, value.spannable, value.paintParams) + data[id] = MeasurementParams(true, width, size, value.spannable, value.paintParams) return size } } diff --git a/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp b/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp index 03aa14d56..01f391e45 100644 --- a/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +++ b/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp @@ -1,4 +1,5 @@ #include "EnrichedTextInputMeasurementManager.h" +#include "conversions.h" #include #include @@ -11,6 +12,7 @@ namespace facebook::react { Size EnrichedTextInputMeasurementManager::measure( SurfaceId surfaceId, int viewTag, + const EnrichedTextInputViewProps& props, LayoutConstraints layoutConstraints) const { const jni::global_ref& fabricUIManager = contextContainer_->at>("FabricUIManager"); @@ -33,17 +35,23 @@ namespace facebook::react { local_ref componentName = make_jstring("EnrichedTextInputView"); - folly::dynamic extra = folly::dynamic::object(); - extra["viewTag"] = viewTag; - local_ref extraData = ReadableNativeMap::newObjectCxxArgs(extra); - local_ref extraDataRM = make_local(reinterpret_cast(extraData.get())); + // Prepare extraData map with viewTag + folly::dynamic extraData = folly::dynamic::object(); + extraData["viewTag"] = viewTag; + local_ref extraDataRNM = ReadableNativeMap::newObjectCxxArgs(extraData); + local_ref extraDataRM = make_local(reinterpret_cast(extraDataRNM.get())); + + // Prepare layout metrics affecting props + auto serializedProps = toDynamic(props); + local_ref propsRNM = ReadableNativeMap::newObjectCxxArgs(serializedProps); + local_ref propsRM = make_local(reinterpret_cast(propsRNM.get())); auto measurement = yogaMeassureToSize(measure( fabricUIManager, surfaceId, componentName.get(), extraDataRM.get(), - nullptr, + propsRM.get(), nullptr, minimumSize.width, maximumSize.width, diff --git a/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h b/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h index 4e2600ce3..775a3fc43 100644 --- a/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +++ b/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h @@ -17,6 +17,7 @@ namespace facebook::react { Size measure( SurfaceId surfaceId, int viewTag, + const EnrichedTextInputViewProps& props, LayoutConstraints layoutConstraints) const; private: diff --git a/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp b/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp index 3af21edb6..a2632ad15 100644 --- a/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +++ b/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp @@ -27,7 +27,7 @@ extern const char EnrichedTextInputComponentName[] = "EnrichedTextInputView"; Size EnrichedTextInputShadowNode::measureContent( const LayoutContext &layoutContext, const LayoutConstraints &layoutConstraints) const { - return measurementsManager_->measure(getSurfaceId(), getTag(), layoutConstraints); + return measurementsManager_->measure(getSurfaceId(), getTag(), getConcreteProps(), layoutConstraints); } } // namespace facebook::react diff --git a/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h b/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h new file mode 100644 index 000000000..0b82fdcec --- /dev/null +++ b/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +#ifdef RN_SERIALIZABLE_STATE +inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props) +{ + // Serialize only metrics affecting props + folly::dynamic serializedProps = folly::dynamic::object(); + serializedProps["defaultValue"] = props.defaultValue; + serializedProps["placeholder"] = props.placeholder; + serializedProps["fontSize"] = props.fontSize; + serializedProps["fontWeight"] = props.fontWeight; + serializedProps["fontStyle"] = props.fontStyle; + serializedProps["fontFamily"] = props.fontFamily; + serializedProps["htmlStyle"] = toDynamic(props.htmlStyle); + + return serializedProps; +} +#endif + +} // namespace facebook::react