diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 9c0fe19eff8377..88077b2e3e98ce 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6459,6 +6459,7 @@ public final class com/facebook/react/views/imagehelper/ResourceDrawableIdHelper public final fun getResourceDrawable (Landroid/content/Context;Ljava/lang/String;)Landroid/graphics/drawable/Drawable; public final fun getResourceDrawableId (Landroid/content/Context;Ljava/lang/String;)I public final fun getResourceDrawableUri (Landroid/content/Context;Ljava/lang/String;)Landroid/net/Uri; + public final fun isVectorDrawable (Landroid/content/Context;Ljava/lang/String;)Z } public final class com/facebook/react/views/imagehelper/ResourceDrawableIdHelper$Companion { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java index 395a90a6dc3b8f..66e668e123935a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -496,6 +496,17 @@ public void maybeUpdateView() { ? mFadeDurationMs : mImageSource.isResource() ? 0 : REMOTE_IMAGE_FADE_DURATION_MS); + Drawable drawable = getDrawableIfUnsupported(mImageSource); + if (drawable != null) { + maybeUpdateViewFromDrawable(drawable); + } else { + maybeUpdateViewFromRequest(doResize); + } + + mIsDirty = false; + } + + private void maybeUpdateViewFromRequest(boolean doResize) { List postprocessors = new LinkedList<>(); if (mIterativeBoxBlurPostProcessor != null) { postprocessors.add(mIterativeBoxBlurPostProcessor); @@ -553,17 +564,45 @@ public void maybeUpdateView() { } if (mDownloadListener != null) { - hierarchy.setProgressBarImage(mDownloadListener); + getHierarchy().setProgressBarImage(mDownloadListener); } setController(mDraweeControllerBuilder.build()); - mIsDirty = false; // Reset again so the DraweeControllerBuilder clears all it's references. Otherwise, this causes // a memory leak. mDraweeControllerBuilder.reset(); } + private void maybeUpdateViewFromDrawable(Drawable drawable) { + final boolean shouldNotify = mDownloadListener != null; + final EventDispatcher mEventDispatcher = + shouldNotify + ? UIManagerHelper.getEventDispatcherForReactTag((ReactContext) getContext(), getId()) + : null; + + if (mEventDispatcher != null) { + mEventDispatcher.dispatchEvent( + ImageLoadEvent.createLoadStartEvent( + UIManagerHelper.getSurfaceId(ReactImageView.this), getId())); + } + + getHierarchy().setImage(drawable, 1, false); + + if (mEventDispatcher != null) { + mEventDispatcher.dispatchEvent( + ImageLoadEvent.createLoadEvent( + UIManagerHelper.getSurfaceId(ReactImageView.this), + getId(), + mImageSource.getSource(), + getWidth(), + getHeight())); + mEventDispatcher.dispatchEvent( + ImageLoadEvent.createLoadEndEvent( + UIManagerHelper.getSurfaceId(ReactImageView.this), getId())); + } + } + // VisibleForTesting public void setControllerListener(ControllerListener controllerListener) { mControllerForTesting = controllerListener; @@ -635,6 +674,30 @@ private boolean shouldResize(ImageSource imageSource) { } } + /** + * Checks if the provided ImageSource should not be requested through Fresco and instead loaded + * directly from the resources table. Fresco explicitly does not support a number of drawable + * types like VectorDrawable but they can still be mounted in the image hierarchy. + * + * @param imageSource + * @return drawable resource if Fresco cannot load the image, null otherwise + */ + private @Nullable Drawable getDrawableIfUnsupported(ImageSource imageSource) { + if (!ReactNativeFeatureFlags.loadVectorDrawablesOnImages()) { + return null; + } + String resourceName = imageSource.getSource(); + if (!imageSource.isResource() || resourceName == null) { + return null; + } + ResourceDrawableIdHelper drawableHelper = ResourceDrawableIdHelper.getInstance(); + boolean isVectorDrawable = drawableHelper.isVectorDrawable(getContext(), resourceName); + if (!isVectorDrawable) { + return null; + } + return drawableHelper.getResourceDrawable(getContext(), resourceName); + } + @Nullable private ResizeOptions getResizeOptions() { int width = Math.round((float) getWidth() * mResizeMultiplier); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/imagehelper/ResourceDrawableIdHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/imagehelper/ResourceDrawableIdHelper.kt index 7626b601713452..12f24b5aa89213 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/imagehelper/ResourceDrawableIdHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/imagehelper/ResourceDrawableIdHelper.kt @@ -8,10 +8,12 @@ package com.facebook.react.views.imagehelper import android.content.Context +import android.content.res.Resources import android.graphics.drawable.Drawable import android.net.Uri import androidx.core.content.res.ResourcesCompat import javax.annotation.concurrent.ThreadSafe +import org.xmlpull.v1.XmlPullParser /** Helper class for obtaining information about local images. */ @ThreadSafe @@ -61,6 +63,39 @@ public class ResourceDrawableIdHelper private constructor() { } } + public fun isVectorDrawable(context: Context, name: String): Boolean { + return getOpeningXmlTag(context, name) == "vector" + } + + /** + * If the provided resource name is a valid drawable resource and is an XML file, returns the root + * XML tag. Skips over the versioning/encoding header. Non-XML files and malformed XML files + * return null. + * + * For example, a vector drawable file would return "vector". + */ + private fun getOpeningXmlTag(context: Context, name: String): String? { + val resId = getResourceDrawableId(context, name).takeIf { it > 0 } ?: return null + return try { + val xmlParser = context.resources.getXml(resId) + xmlParser.use { + var parentTag: String? = null + var eventType = xmlParser.eventType + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + parentTag = xmlParser.name + break + } + eventType = xmlParser.next() + } + parentTag + } + } catch (e: Resources.NotFoundException) { + // Drawable image is not an XML file + null + } + } + public companion object { private const val LOCAL_RESOURCE_SCHEME = "res" private val resourceDrawableIdHelper: ResourceDrawableIdHelper = ResourceDrawableIdHelper() diff --git a/packages/rn-tester/android/app/src/main/res/drawable/ic_android.xml b/packages/rn-tester/android/app/src/main/res/drawable/ic_android.xml new file mode 100644 index 00000000000000..ad4f1531ac3113 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/res/drawable/ic_android.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index 3a43e9a1a6483b..f73225338df8df 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -601,6 +601,23 @@ class OnPartialLoadExample extends React.Component< } } +type VectorDrawableExampleState = {||}; + +type VectorDrawableExampleProps = $ReadOnly<{||}>; + +class VectorDrawableExample extends React.Component< + VectorDrawableExampleProps, + VectorDrawableExampleState, +> { + render(): React.Node { + return ( + + + + ); + } +} + const fullImage: ImageSource = { uri: IMAGE2, }; @@ -1511,4 +1528,13 @@ exports.examples = [ }, platform: 'ios', }, + { + title: 'Vector Drawable', + description: + 'Demonstrating an example of loading a vector drawable asset by name', + render: function (): React.Node { + return ; + }, + platform: 'android', + }, ];