diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 875c4dc00415..8b3da2113f40 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6463,6 +6463,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 395a90a6dc3b..66e668e12393 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 7626b6017134..12f24b5aa892 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/build.gradle.kts b/packages/rn-tester/android/app/build.gradle.kts index 31336dba2383..6af90f8bf683 100644 --- a/packages/rn-tester/android/app/build.gradle.kts +++ b/packages/rn-tester/android/app/build.gradle.kts @@ -147,6 +147,11 @@ android { java.srcDirs( "$reactNativeDirPath/ReactCommon/react/nativemodule/samples/platform/android", ) + res.setSrcDirs( + listOf( + "src/main/res", + "src/main/public_res", + )) } } diff --git a/packages/rn-tester/android/app/src/main/public_res/drawable/ic_android.xml b/packages/rn-tester/android/app/src/main/public_res/drawable/ic_android.xml new file mode 100644 index 000000000000..ad4f1531ac31 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/public_res/drawable/ic_android.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/rn-tester/android/app/src/main/res/drawable/legacy_image.png b/packages/rn-tester/android/app/src/main/public_res/drawable/legacy_image.png similarity index 100% rename from packages/rn-tester/android/app/src/main/res/drawable/legacy_image.png rename to packages/rn-tester/android/app/src/main/public_res/drawable/legacy_image.png diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index 3a43e9a1a648..cacfc9d07db6 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -12,6 +12,8 @@ import type {LayoutEvent} from 'react-native/Libraries/Types/CoreEventTypes'; +import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags'; + const ImageCapInsetsExample = require('./ImageCapInsetsExample'); const React = require('react'); const { @@ -601,6 +603,27 @@ class OnPartialLoadExample extends React.Component< } } +type VectorDrawableExampleState = {||}; + +type VectorDrawableExampleProps = $ReadOnly<{||}>; + +class VectorDrawableExample extends React.Component< + VectorDrawableExampleProps, + VectorDrawableExampleState, +> { + state: VectorDrawableExampleState = {}; + + render(): React.Node { + const isEnabled = ReactNativeFeatureFlags.loadVectorDrawablesOnImages(); + return ( + + Enabled: {isEnabled ? 'true' : 'false'} + + + ); + } +} + const fullImage: ImageSource = { uri: IMAGE2, }; @@ -1511,4 +1534,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', + }, ];