From 1f0de6895c11d4f26398769f7e501e4fd9d28768 Mon Sep 17 00:00:00 2001 From: franruiztech Date: Mon, 5 Oct 2026 10:21:39 +0200 Subject: [PATCH] fix(android): stop Fresco "Don't know how to round that drawable" warnings on images ReactImageDownloadListener doubles as the progress bar drawable of the image hierarchy, wrapping a private EmptyDrawable. ReactImageView builds its hierarchy with RoundingParams, and Fresco applies those to the leaf of every child drawable (WrappingUtils.maybeApplyLeafRounding). It only knows how to round bitmap, nine-patch and color drawables, so for any other leaf it logs "Don't know how to round that drawable" at warn level, once for the progress bar plus once per later update. Wrap a transparent ColorDrawable instead, which Fresco can round and which still renders nothing. ReactImageView also called setProgressBarImage with the same listener on every update. After the first call the leaf is already a RoundedColorDrawable, which is not a ColorDrawable, so Fresco would warn again. Install the progress bar only when the listener changes. --- .../views/image/ReactImageDownloadListener.kt | 22 +++-------- .../react/views/image/ReactImageView.kt | 6 ++- .../views/image/ReactImagePropertyTest.kt | 38 +++++++++++++++++++ 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.kt index 281a7382c9bb..da7689adf3c0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.kt @@ -7,16 +7,16 @@ package com.facebook.react.views.image -import android.graphics.Canvas -import android.graphics.ColorFilter -import android.graphics.PixelFormat +import android.graphics.Color import android.graphics.drawable.Animatable -import android.graphics.drawable.Drawable +import android.graphics.drawable.ColorDrawable import com.facebook.drawee.controller.ControllerListener import com.facebook.drawee.drawable.ForwardingDrawable +// The wrapped drawable has to be one that Fresco knows how to round (a ColorDrawable here), because +// the hierarchy applies the rounding params to the leaf of the progress bar drawable. internal open class ReactImageDownloadListener : - ForwardingDrawable(EmptyDrawable()), ControllerListener { + ForwardingDrawable(ColorDrawable(Color.TRANSPARENT)), ControllerListener { open fun onProgressChange(loaded: Int, total: Int) = Unit override fun onLevelChange(level: Int): Boolean { @@ -36,18 +36,6 @@ internal open class ReactImageDownloadListener : override fun onRelease(id: String) = Unit - /** A [Drawable] that renders nothing. */ - private class EmptyDrawable : Drawable() { - override fun draw(canvas: Canvas) = Unit - - override fun setAlpha(alpha: Int) = Unit - - override fun setColorFilter(colorFilter: ColorFilter?) = Unit - - @Deprecated("Deprecated in Java", ReplaceWith("PixelFormat.OPAQUE")) - override fun getOpacity(): Int = PixelFormat.OPAQUE - } - companion object { private const val MAX_LEVEL = 10000 } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 0b3e78db9b3e..11ee7b8e79de 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -100,6 +100,7 @@ public class ReactImageView( private var tilePostprocessor: TilePostprocessor? = null private var iterativeBoxBlurPostProcessor: IterativeBoxBlurPostProcessor? = null private var downloadListener: ReactImageDownloadListener? = null + private var progressBarListener: ReactImageDownloadListener? = null private var controllerForTesting: ControllerListener? = null private var fadeDurationMs = -1 private var progressiveRenderingEnabled = false @@ -523,8 +524,11 @@ public class ReactImageView( builder.setControllerListener(downloadListener) } - if (downloadListener != null) { + // Install the progress bar once per listener: setting it again would make Fresco try to round + // the already rounded leaf drawable and log a warning on every update. + if (downloadListener != null && downloadListener !== progressBarListener) { hierarchy.setProgressBarImage(downloadListener) + progressBarListener = downloadListener } controller = builder.build() diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt index c3b78c3f3834..99d73560b148 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt @@ -11,6 +11,7 @@ package com.facebook.react.views.image +import android.content.Context import android.graphics.Color import android.util.DisplayMetrics import com.facebook.common.logging.FLog @@ -29,6 +30,9 @@ import com.facebook.react.uimanager.LengthPercentage import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.ReactStylesDiffMap import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.events.BlackHoleEventDispatcher +import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.uimanager.events.EventDispatcherProvider import com.facebook.react.uimanager.style.BorderRadiusProp import com.facebook.react.util.RNLog import com.facebook.react.views.imagehelper.ImageSource @@ -43,6 +47,7 @@ import org.mockito.MockedStatic import org.mockito.Mockito.anyString import org.mockito.Mockito.mockStatic import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.reset @@ -146,6 +151,33 @@ class ReactImagePropertyTest { .isEqualTo(view.imageSource) } + @Test + fun testProgressBarImageDoesNotWarnAboutRounding() { + val reactContext = + ReactContextWithEventDispatcher(RuntimeEnvironment.getApplication()).apply { + initializeWithInstance(createMockCatalystInstance()) + } + val viewManager = ReactImageManager() + val themedContext = ThemedReactContext(reactContext, reactContext, null, -1) + val view = viewManager.createViewInstance(themedContext) + view.setShouldNotifyLoadEvents(true) + + // Every update installs the progress bar image again unless it is already in place. + for (uri in listOf("https://example.com/a.png", "https://example.com/b.png")) { + val sources = Arguments.createArray() + val source = Arguments.createMap() + source.putString("uri", uri) + sources.pushMap(source) + viewManager.setSource(view, sources) + view.maybeUpdateView() + } + + flogMock.verify( + { FLog.w(any(), eq("Don't know how to round that drawable: %s"), any()) }, + never(), + ) + } + @Test fun testBorderRadius() { val viewManager = ReactImageManager() @@ -281,4 +313,10 @@ class ReactImagePropertyTest { viewManager.setHeaders(mockView, headers) verify(mockView).setHeaders(headers) } + + /** A [BridgeReactContext] that can also hand out an [EventDispatcher], as the real ones do. */ + private class ReactContextWithEventDispatcher(context: Context) : + BridgeReactContext(context), EventDispatcherProvider { + override fun getEventDispatcher(): EventDispatcher = BlackHoleEventDispatcher + } }