diff --git a/.maestro/enrichedInput/flows/image_width_clamping.yaml b/.maestro/enrichedInput/flows/image_width_clamping.yaml
new file mode 100644
index 000000000..68a43bf23
--- /dev/null
+++ b/.maestro/enrichedInput/flows/image_width_clamping.yaml
@@ -0,0 +1,21 @@
+appId: swmansion.enriched.example
+---
+# verifies the image width is clamped down to the total available space, and wraps properly
+- launchApp
+
+- tapOn:
+ id: "toggle-screen-button"
+
+- runFlow:
+ file: "../subflows/set_editor_value.yaml"
+ env:
+ VALUE: >
+




+
+- tapOn:
+ id: "size-max-button"
+
+- runFlow:
+ file: "../subflows/capture_or_assert_screenshot.yaml"
+ env:
+ SCREENSHOT_NAME: "image_width_clamping"
diff --git a/.maestro/enrichedInput/screenshots/android/image_width_clamping.png b/.maestro/enrichedInput/screenshots/android/image_width_clamping.png
new file mode 100644
index 000000000..58bbf14d4
Binary files /dev/null and b/.maestro/enrichedInput/screenshots/android/image_width_clamping.png differ
diff --git a/.maestro/enrichedInput/screenshots/ios/image_width_clamping.png b/.maestro/enrichedInput/screenshots/ios/image_width_clamping.png
new file mode 100644
index 000000000..f22bc0e90
Binary files /dev/null and b/.maestro/enrichedInput/screenshots/ios/image_width_clamping.png differ
diff --git a/.maestro/enrichedText/flows/image_wrap.yaml b/.maestro/enrichedText/flows/image_wrap.yaml
new file mode 100644
index 000000000..01bccee7a
--- /dev/null
+++ b/.maestro/enrichedText/flows/image_wrap.yaml
@@ -0,0 +1,26 @@
+appId: swmansion.enriched.example
+---
+# Validates that images wrap correctly
+- launchApp
+
+- tapOn:
+ id: 'toggle-screen-button'
+
+- tapOn:
+ id: 'toggle-enriched-text-screen-button'
+
+- runFlow:
+ file: '../subflows/set_enriched_text_value.yaml'
+ env:
+ VALUE: >
+ 


+
+- swipe:
+ start: "5%, 35%"
+ end: "5%, 35%"
+ duration: 3000
+
+- runFlow:
+ file: '../subflows/capture_or_assert_screenshot.yaml'
+ env:
+ SCREENSHOT_NAME: 'image_wrap'
diff --git a/.maestro/enrichedText/screenshots/android/image_wrap.png b/.maestro/enrichedText/screenshots/android/image_wrap.png
new file mode 100644
index 000000000..205bb10f7
Binary files /dev/null and b/.maestro/enrichedText/screenshots/android/image_wrap.png differ
diff --git a/.maestro/enrichedText/screenshots/ios/image_wrap.png b/.maestro/enrichedText/screenshots/ios/image_wrap.png
new file mode 100644
index 000000000..fbba58411
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/image_wrap.png differ
diff --git a/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedImageSpan.kt b/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedImageSpan.kt
index ba09436ce..ecc7877bc 100644
--- a/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedImageSpan.kt
+++ b/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedImageSpan.kt
@@ -23,6 +23,7 @@ import java.io.File
open class EnrichedImageSpan :
ImageSpan,
EnrichedInlineSpan {
+ var containerWidth: Int? = null
private var width: Int = 0
private var height: Int = 0
@@ -53,8 +54,19 @@ open class EnrichedImageSpan :
override fun getDrawable(): Drawable {
val drawable = super.getDrawable()
val scale = Resources.getSystem().displayMetrics.density
+ val maxWidth = this.containerWidth
- drawable.setBounds(0, 0, (width * scale).toInt(), (height * scale).toInt())
+ var targetWidth = (width * scale).toInt()
+ var targetHeight = (height * scale).toInt()
+
+ // clamp down the width to the maximum available container space
+ if (maxWidth != null && targetWidth > maxWidth && maxWidth > 0) {
+ val aspectRatio = targetHeight.toFloat() / targetWidth.toFloat()
+ targetWidth = maxWidth
+ targetHeight = (targetWidth * aspectRatio).toInt()
+ }
+
+ drawable.setBounds(0, 0, targetWidth, targetHeight)
return drawable
}
diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt
index 357016da3..1456010ee 100644
--- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt
+++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt
@@ -24,6 +24,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.swmansion.enriched.common.EnrichedConstants
import com.swmansion.enriched.common.EnrichedSpanFlags
+import com.swmansion.enriched.common.ForceRedrawSpan
import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
@@ -87,6 +88,31 @@ class EnrichedTextView : AppCompatTextView {
setFontSize(EnrichedConstants.TEXT_DEFAULT_FONT_SIZE)
}
+ override fun onMeasure(
+ widthMeasureSpec: Int,
+ heightMeasureSpec: Int,
+ ) {
+ val widthMode = MeasureSpec.getMode(widthMeasureSpec)
+ val widthSize = MeasureSpec.getSize(widthMeasureSpec)
+
+ if (widthMode != MeasureSpec.UNSPECIFIED) {
+ val availableWidth = widthSize - totalPaddingLeft - totalPaddingRight
+
+ if (availableWidth > 0) {
+ val spanned = text as? Spanned
+ val spans = spanned?.getSpans(0, spanned.length, EnrichedTextImageSpan::class.java)
+
+ // we need to update the image bounds, potentially clamping
+ // the width to the available one
+ spans?.forEach {
+ it.containerWidth = availableWidth
+ }
+ }
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+
override fun onTouchEvent(event: MotionEvent): Boolean {
val spanned = text as? Spanned
val action = event.action
diff --git a/android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt
index f2805659c..da5ad64de 100644
--- a/android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt
+++ b/android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt
@@ -23,6 +23,7 @@ import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.allowFontScalingFromProps
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
+import com.swmansion.enriched.text.spans.EnrichedTextImageSpan
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
import kotlin.math.ceil
@@ -165,6 +166,13 @@ object MeasurementStore {
val lineHeight = lineHeightFromProps(props)
val allowFontScaling = allowFontScalingFromProps(props)
+ // we need to update the image bounds, potentially clamping
+ // the width to the available one
+ (rawText as? Spannable)?.let { spannable ->
+ val imageSpans = spannable.getSpans(0, spannable.length, EnrichedTextImageSpan::class.java)
+ imageSpans.forEach { it.containerWidth = width.toInt() }
+ }
+
val measuredText: CharSequence =
if (lineHeight > 0f) {
val spannable = SpannableString(rawText)
diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt
index e61435f35..38b477e75 100644
--- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt
+++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt
@@ -175,6 +175,31 @@ class EnrichedTextInputView :
super.scrollTo(0, y)
}
+ override fun onMeasure(
+ widthMeasureSpec: Int,
+ heightMeasureSpec: Int,
+ ) {
+ val widthMode = MeasureSpec.getMode(widthMeasureSpec)
+ val widthSize = MeasureSpec.getSize(widthMeasureSpec)
+
+ if (widthMode != MeasureSpec.UNSPECIFIED) {
+ val availableWidth = widthSize - totalPaddingLeft - totalPaddingRight
+
+ if (availableWidth > 0) {
+ val spannable = text as? Spannable
+ val spans = spannable?.getSpans(0, spannable.length, EnrichedInputImageSpan::class.java)
+
+ // we need to update the image bounds, potentially clamping
+ // the width to the available one
+ spans?.forEach {
+ it.containerWidth = availableWidth
+ }
+ }
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
var inputConnection = super.onCreateInputConnection(outAttrs)
diff --git a/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt
index ea309ef8a..893388738 100644
--- a/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt
+++ b/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt
@@ -19,6 +19,7 @@ import com.facebook.yoga.YogaMeasureOutput
import com.swmansion.enriched.common.allowFontScalingFromProps
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
+import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
import com.swmansion.enriched.textinput.styles.HtmlStyle
import java.util.concurrent.ConcurrentHashMap
@@ -170,6 +171,13 @@ object MeasurementStore {
rawText
}
+ // we need to update the image bounds, potentially clamping
+ // the width to the available one
+ (text as? Spannable)?.let { spannable ->
+ val imageSpans = spannable.getSpans(0, spannable.length, EnrichedInputImageSpan::class.java)
+ imageSpans.forEach { it.containerWidth = width.toInt() }
+ }
+
val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
val paintParams = PaintParams(typeface, fontSize)
val size = measure(width, text, paintParams)
diff --git a/ios/interfaces/ImageAttachment.mm b/ios/interfaces/ImageAttachment.mm
index 491a20777..6c5893640 100644
--- a/ios/interfaces/ImageAttachment.mm
+++ b/ios/interfaces/ImageAttachment.mm
@@ -1,5 +1,6 @@
#import "ImageAttachment.h"
#import "extensions/ImageExtension.h"
+#import "utils/AttachmentLayoutUtils.h"
// NSTextStorage frequently recreates NSTextAttachment objects during attribute
// invalidation (e.g. on every keystroke). Without this cache each recreation
@@ -49,6 +50,10 @@ - (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer
characterIndex:(NSUInteger)charIndex {
CGRect baseBounds = self.bounds;
+ baseBounds.size =
+ [AttachmentLayoutUtils clampedSize:baseBounds.size
+ toMaxWidth:textContainer.size.width];
+
if (!textContainer.layoutManager.textStorage ||
charIndex >= textContainer.layoutManager.textStorage.length) {
return baseBounds;
diff --git a/ios/utils/AttachmentLayoutUtils.h b/ios/utils/AttachmentLayoutUtils.h
index 6245302c2..84faf3286 100644
--- a/ios/utils/AttachmentLayoutUtils.h
+++ b/ios/utils/AttachmentLayoutUtils.h
@@ -21,4 +21,6 @@
textView:(UITextView *)textView
config:(EnrichedConfig *)config;
++ (CGSize)clampedSize:(CGSize)size toMaxWidth:(CGFloat)maxWidth;
+
@end
diff --git a/ios/utils/AttachmentLayoutUtils.mm b/ios/utils/AttachmentLayoutUtils.mm
index ace6ee346..90bc44cd3 100644
--- a/ios/utils/AttachmentLayoutUtils.mm
+++ b/ios/utils/AttachmentLayoutUtils.mm
@@ -121,7 +121,8 @@ + (CGRect)frameForAttachment:(ImageAttachment *)attachment
CGRect lineRect =
[layoutManager lineFragmentRectForGlyphAtIndex:glyphRange.location
effectiveRange:NULL];
- CGSize attachmentSize = attachment.bounds.size;
+ CGSize attachmentSize = [self clampedSize:attachment.bounds.size
+ toMaxWidth:textContainer.size.width];
UIFont *font = [storage attribute:NSFontAttributeName
atIndex:range.location
@@ -141,4 +142,12 @@ + (CGRect)frameForAttachment:(ImageAttachment *)attachment
return CGRectIntegral(rect);
}
++ (CGSize)clampedSize:(CGSize)size toMaxWidth:(CGFloat)maxWidth {
+ if (size.width > maxWidth && maxWidth > 0) {
+ CGFloat scale = maxWidth / size.width;
+ return CGSizeMake(maxWidth, size.height * scale);
+ }
+ return size;
+}
+
@end