From 06fbb6857c6e262b41a6889389b18d881a2f4b48 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Mon, 7 Sep 2026 00:50:28 +0200 Subject: [PATCH 1/2] fix: sync img placeholder colors with current text color --- .../enriched/common/AsyncDrawable.kt | 19 +++++++++++++- .../common/parser/EnrichedSpanFactory.kt | 2 ++ .../common/spans/EnrichedImageSpan.kt | 25 +++++++++++++++++-- .../enriched/text/EnrichedTextSpanFactory.kt | 5 +++- .../enriched/text/EnrichedTextView.kt | 18 +++++++++---- .../text/spans/EnrichedTextImageSpan.kt | 12 ++++++--- .../EnrichedTextInputSpannableFactory.kt | 5 +++- .../textinput/EnrichedTextInputView.kt | 18 +++++++++---- .../textinput/spans/EnrichedInputImageSpan.kt | 12 ++++++--- .../textinput/styles/ParametrizedStyles.kt | 2 +- ios/utils/AttachmentLayoutUtils.mm | 5 +++- 11 files changed, 100 insertions(+), 23 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt b/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt index 6198eadd3..55cb96bf5 100644 --- a/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt +++ b/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt @@ -13,6 +13,7 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.toDrawable import com.swmansion.enriched.R import java.net.URL @@ -21,11 +22,14 @@ import java.util.concurrent.Executors class AsyncDrawable( private val url: String, + private var placeholderTintColor: Int, ) : Drawable() { private var internalDrawable: Drawable = Color.TRANSPARENT.toDrawable() private val mainHandler = Handler(Looper.getMainLooper()) private val executor = Executors.newSingleThreadExecutor() var isLoaded = false + var isShowingPlaceholder = false + private set init { internalDrawable.bounds = bounds @@ -94,7 +98,20 @@ class AsyncDrawable( } private fun loadPlaceholderImage() { - internalDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image) + val drawable = ResourceManager.getDrawableResource(R.drawable.broken_image) + + DrawableCompat.setTint(drawable, placeholderTintColor) + + isShowingPlaceholder = true + internalDrawable = drawable + } + + fun applyPlaceholderTint(color: Int) { + placeholderTintColor = color + + if (!isShowingPlaceholder) return + + DrawableCompat.setTint(internalDrawable, color) } override fun draw(canvas: Canvas) { diff --git a/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt b/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt index 38fb167b1..ef8f578e4 100644 --- a/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt +++ b/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt @@ -22,6 +22,8 @@ import com.swmansion.enriched.common.spans.EnrichedUnderlineSpan import com.swmansion.enriched.common.spans.EnrichedUnorderedListSpan interface EnrichedSpanFactory { + var textColor: Int + fun createAlignmentSpan(cssValue: String): EnrichedAlignmentSpan fun createBoldSpan(style: T): EnrichedBoldSpan 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..4aeeb06f8 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 @@ -13,6 +13,7 @@ import android.os.Looper import android.text.Spannable import android.text.style.ImageSpan import android.util.Log +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.withSave import com.swmansion.enriched.common.AsyncDrawable @@ -26,9 +27,28 @@ open class EnrichedImageSpan : private var width: Int = 0 private var height: Int = 0 - constructor(drawable: Drawable, source: String, width: Int, height: Int) : super(drawable, source, ALIGN_BASELINE) { + private var isStaticPlaceholder: Boolean = false + + constructor( + drawable: Drawable, + source: String, + width: Int, + height: Int, + isStaticPlaceholder: Boolean = false, + ) : super(drawable, source, ALIGN_BASELINE) { this.width = width this.height = height + this.isStaticPlaceholder = isStaticPlaceholder + } + + fun refreshPlaceholderTint(color: Int) { + val d = drawable + + if (d is AsyncDrawable) { + d.applyPlaceholderTint(color) + } else if (isStaticPlaceholder) { + DrawableCompat.setTint(d, color) + } } override fun draw( @@ -134,11 +154,12 @@ open class EnrichedImageSpan : src: String, width: Int, height: Int, + placeholderTintColor: Int, ): Drawable? { var cleanPath = src if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) { - return AsyncDrawable(cleanPath) + return AsyncDrawable(cleanPath, placeholderTintColor) } if (cleanPath.startsWith("file://")) { diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextSpanFactory.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextSpanFactory.kt index f61ac812b..539ddd39e 100644 --- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextSpanFactory.kt +++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextSpanFactory.kt @@ -1,5 +1,6 @@ package com.swmansion.enriched.text +import android.graphics.Color import com.swmansion.enriched.common.parser.EnrichedSpanFactory import com.swmansion.enriched.text.spans.EnrichedTextAlignmentSpan import com.swmansion.enriched.text.spans.EnrichedTextBlockQuoteSpan @@ -23,6 +24,8 @@ import com.swmansion.enriched.text.spans.EnrichedTextUnderlineSpan import com.swmansion.enriched.text.spans.EnrichedTextUnorderedListSpan class EnrichedTextSpanFactory : EnrichedSpanFactory { + override var textColor: Int = Color.BLACK + override fun createAlignmentSpan(cssValue: String) = EnrichedTextAlignmentSpan(cssValue) override fun createBoldSpan(style: EnrichedTextStyle) = EnrichedTextBoldSpan(style) @@ -52,7 +55,7 @@ class EnrichedTextSpanFactory : EnrichedSpanFactory { source: String, width: Int, height: Int, - ) = EnrichedTextImageSpan.createEnrichedImageSpan(source, width, height) + ) = EnrichedTextImageSpan.createEnrichedImageSpan(source, width, height, textColor) override fun createH1Span(style: EnrichedTextStyle) = EnrichedTextH1Span(style) 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..7f68f163f 100644 --- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt +++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt @@ -292,12 +292,20 @@ class EnrichedTextView : AppCompatTextView { } fun setColor(colorInt: Int?) { - if (colorInt == null) { - setTextColor(Color.BLACK) - return - } + val resolvedColor = colorInt ?: Color.BLACK + + setTextColor(resolvedColor) + spannableFactory.textColor = resolvedColor + refreshImagePlaceholderTints(resolvedColor) + } - setTextColor(colorInt) + private fun refreshImagePlaceholderTints(color: Int) { + val spanned = text as? Spanned ?: return + val spans = spanned.getSpans(0, spanned.length, EnrichedTextImageSpan::class.java) + + for (span in spans) { + span.refreshPlaceholderTint(color) + } } fun setFontSize(size: Float) { diff --git a/android/src/main/java/com/swmansion/enriched/text/spans/EnrichedTextImageSpan.kt b/android/src/main/java/com/swmansion/enriched/text/spans/EnrichedTextImageSpan.kt index 21f2c9fe3..d2803f876 100644 --- a/android/src/main/java/com/swmansion/enriched/text/spans/EnrichedTextImageSpan.kt +++ b/android/src/main/java/com/swmansion/enriched/text/spans/EnrichedTextImageSpan.kt @@ -3,6 +3,7 @@ package com.swmansion.enriched.text.spans import android.graphics.drawable.Drawable import android.os.Handler import android.os.Looper +import androidx.core.graphics.drawable.DrawableCompat import com.swmansion.enriched.R import com.swmansion.enriched.common.AsyncDrawable import com.swmansion.enriched.common.ResourceManager @@ -15,7 +16,8 @@ class EnrichedTextImageSpan( source: String, width: Int, height: Int, -) : EnrichedImageSpan(drawable, source, width, height), + isStaticPlaceholder: Boolean = false, +) : EnrichedImageSpan(drawable, source, width, height, isStaticPlaceholder), EnrichedTextSpan { override val dependsOnHtmlStyle = false @@ -44,14 +46,18 @@ class EnrichedTextImageSpan( src: String, width: Int, height: Int, + placeholderTintColor: Int, ): EnrichedImageSpan { - var imgDrawable = prepareDrawableForImage(src, width, height) + var imgDrawable = prepareDrawableForImage(src, width, height, placeholderTintColor) + var isStaticPlaceholder = false if (imgDrawable == null) { imgDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image) + isStaticPlaceholder = true + DrawableCompat.setTint(imgDrawable, placeholderTintColor) } - return EnrichedTextImageSpan(imgDrawable, src, width, height) + return EnrichedTextImageSpan(imgDrawable, src, width, height, isStaticPlaceholder) } } } diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt index fadeb6e1b..eeb414b56 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt @@ -1,5 +1,6 @@ package com.swmansion.enriched.textinput +import android.graphics.Color import com.swmansion.enriched.common.parser.EnrichedSpanFactory import com.swmansion.enriched.common.spans.EnrichedImageSpan import com.swmansion.enriched.textinput.spans.EnrichedInputAlignmentSpan @@ -25,6 +26,8 @@ import com.swmansion.enriched.textinput.spans.EnrichedInputUnorderedListSpan import com.swmansion.enriched.textinput.styles.HtmlStyle class EnrichedTextInputSpannableFactory : EnrichedSpanFactory { + override var textColor: Int = Color.BLACK + override fun createAlignmentSpan(cssValue: String) = EnrichedInputAlignmentSpan(cssValue) override fun createBoldSpan(style: HtmlStyle) = EnrichedInputBoldSpan(style) @@ -54,7 +57,7 @@ class EnrichedTextInputSpannableFactory : EnrichedSpanFactory { source: String, width: Int, height: Int, - ): EnrichedImageSpan = EnrichedInputImageSpan.createEnrichedImageSpan(source, width, height) + ): EnrichedImageSpan = EnrichedInputImageSpan.createEnrichedImageSpan(source, width, height, textColor) override fun createH1Span(style: HtmlStyle) = EnrichedInputH1Span(style) 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 f4aed71ea..68688358c 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt @@ -545,12 +545,20 @@ class EnrichedTextInputView : } fun setColor(colorInt: Int?) { - if (colorInt == null) { - setTextColor(Color.BLACK) - return - } + val resolvedColor = colorInt ?: Color.BLACK - setTextColor(colorInt) + setTextColor(resolvedColor) + spannableFactory.textColor = resolvedColor + refreshImagePlaceholderTints(resolvedColor) + } + + private fun refreshImagePlaceholderTints(color: Int) { + val liveText = text ?: return + val spans = liveText.getSpans(0, liveText.length, EnrichedInputImageSpan::class.java) + + for (span in spans) { + span.refreshPlaceholderTint(color) + } } fun setFontSize(size: Float) { diff --git a/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputImageSpan.kt b/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputImageSpan.kt index 94a3ca24d..4630ec705 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputImageSpan.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputImageSpan.kt @@ -1,6 +1,7 @@ package com.swmansion.enriched.textinput.spans import android.graphics.drawable.Drawable +import androidx.core.graphics.drawable.DrawableCompat import com.swmansion.enriched.R import com.swmansion.enriched.common.ResourceManager import com.swmansion.enriched.common.spans.EnrichedImageSpan @@ -12,7 +13,8 @@ class EnrichedInputImageSpan( source: String, width: Int, height: Int, -) : EnrichedImageSpan(drawable, source, width, height), + isStaticPlaceholder: Boolean = false, +) : EnrichedImageSpan(drawable, source, width, height, isStaticPlaceholder), EnrichedInputSpan { override val dependsOnHtmlStyle: Boolean = false @@ -23,14 +25,18 @@ class EnrichedInputImageSpan( src: String, width: Int, height: Int, + placeholderTintColor: Int, ): EnrichedInputImageSpan { - var imgDrawable = prepareDrawableForImage(src, width, height) + var imgDrawable = prepareDrawableForImage(src, width, height, placeholderTintColor) + var isStaticPlaceholder = false if (imgDrawable == null) { imgDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image) + isStaticPlaceholder = true + DrawableCompat.setTint(imgDrawable, placeholderTintColor) } - return EnrichedInputImageSpan(imgDrawable, src, width, height) + return EnrichedInputImageSpan(imgDrawable, src, width, height, isStaticPlaceholder) } } } diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt index 387eec351..6ee6597c8 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt @@ -355,7 +355,7 @@ class ParametrizedStyles( } val (imageStart, imageEnd) = spannable.getSafeSpanBoundaries(start, start + 1) - val span = EnrichedInputImageSpan.createEnrichedImageSpan(src, width.toInt(), height.toInt()) + val span = EnrichedInputImageSpan.createEnrichedImageSpan(src, width.toInt(), height.toInt(), view.currentTextColor) span.observeAsyncDrawableLoaded(view.text) spannable.setSpan(span, imageStart, imageEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) diff --git a/ios/utils/AttachmentLayoutUtils.mm b/ios/utils/AttachmentLayoutUtils.mm index ace6ee346..4948f319a 100644 --- a/ios/utils/AttachmentLayoutUtils.mm +++ b/ios/utils/AttachmentLayoutUtils.mm @@ -66,7 +66,6 @@ + (void)handleAttachmentUpdate:(MediaAttachment *)attachment imgView = [[UIImageView alloc] initWithFrame:rect]; imgView.contentMode = UIViewContentModeScaleAspectFit; - imgView.tintColor = [UIColor labelColor]; // Add it directly to the TextView [textView addSubview:imgView]; @@ -76,6 +75,10 @@ + (void)handleAttachmentUpdate:(MediaAttachment *)attachment if (!CGRectEqualToRect(imgView.frame, rect)) { imgView.frame = rect; } + + // Keep the placeholder tint in sync with the + // current font color + imgView.tintColor = [config primaryColor]; UIImage *targetImage = attachment.storedAnimatedImage ?: attachment.image; From d253bb494d3c8015fc94b319a980e2360e60f51e Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Mon, 7 Sep 2026 14:56:21 +0200 Subject: [PATCH 2/2] fix: update asyncDrawable internal state on the main thread --- .../main/java/com/swmansion/enriched/common/AsyncDrawable.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt b/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt index 55cb96bf5..648d6312a 100644 --- a/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt +++ b/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt @@ -57,7 +57,9 @@ class AsyncDrawable( } catch (e: Exception) { Log.e("AsyncDrawable", "Failed to load: $url", e) - loadPlaceholderImage() + mainHandler.post { + loadPlaceholderImage() + } } finally { isLoaded = true onLoaded?.invoke()