Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
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 {
Expand Down
84 changes: 76 additions & 8 deletions android/src/main/java/com/swmansion/enriched/MeasurementStore.kt
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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,
)

Expand All @@ -29,18 +40,20 @@ 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
}

fun release(id: Int) {
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
Expand All @@ -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
Expand All @@ -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("<html>") && defaultValue.endsWith("</html>")
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
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "EnrichedTextInputMeasurementManager.h"
#include "conversions.h"

#include <fbjni/fbjni.h>
#include <react/jni/ReadableNativeMap.h>
Expand All @@ -11,6 +12,7 @@ namespace facebook::react {
Size EnrichedTextInputMeasurementManager::measure(
SurfaceId surfaceId,
int viewTag,
const EnrichedTextInputViewProps& props,
LayoutConstraints layoutConstraints) const {
const jni::global_ref<jobject>& fabricUIManager =
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
Expand All @@ -33,17 +35,23 @@ namespace facebook::react {

local_ref<JString> componentName = make_jstring("EnrichedTextInputView");

folly::dynamic extra = folly::dynamic::object();
extra["viewTag"] = viewTag;
local_ref<ReadableNativeMap::javaobject> extraData = ReadableNativeMap::newObjectCxxArgs(extra);
local_ref<ReadableMap::javaobject> extraDataRM = make_local(reinterpret_cast<ReadableMap::javaobject>(extraData.get()));
// Prepare extraData map with viewTag
folly::dynamic extraData = folly::dynamic::object();
extraData["viewTag"] = viewTag;
local_ref<ReadableNativeMap::javaobject> extraDataRNM = ReadableNativeMap::newObjectCxxArgs(extraData);
local_ref<ReadableMap::javaobject> extraDataRM = make_local(reinterpret_cast<ReadableMap::javaobject>(extraDataRNM.get()));

// Prepare layout metrics affecting props
auto serializedProps = toDynamic(props);
local_ref<ReadableNativeMap::javaobject> propsRNM = ReadableNativeMap::newObjectCxxArgs(serializedProps);
local_ref<ReadableMap::javaobject> propsRM = make_local(reinterpret_cast<ReadableMap::javaobject>(propsRNM.get()));

auto measurement = yogaMeassureToSize(measure(
fabricUIManager,
surfaceId,
componentName.get(),
extraDataRM.get(),
nullptr,
propsRM.get(),
nullptr,
minimumSize.width,
maximumSize.width,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace facebook::react {
Size measure(
SurfaceId surfaceId,
int viewTag,
const EnrichedTextInputViewProps& props,
LayoutConstraints layoutConstraints) const;

private:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include <folly/dynamic.h>
#include <react/renderer/components/FBReactNativeSpec/Props.h>
#include <react/renderer/core/propsConversions.h>
#include <react/renderer/components/RNEnrichedTextInputViewSpec/Props.h>

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
Loading