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 + } }