From 9a8ae8c54f7c5c0a21410be7151cb9bc5139b68b Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Fri, 12 Jul 2024 15:09:22 -0700 Subject: [PATCH 1/2] Support loading vector drawables in ImageView Summary: Fresco has indicated that they have no plans to support loading vector assets and similar drawable types in Drawee-backed views ([issue](https://github.com/facebook/fresco/issues/329), [issue](https://github.com/facebook/fresco/issues/1463), [issue](https://github.com/facebook/fresco/issues/2463)). Guidance has been to instead load the vector drawable onto the backing image view ourselves. On the React Native side, having the ability to load vector drawables has been requested many times ([issue](https://github.com/facebook/react-native/issues/16651), [issue](https://github.com/facebook/react-native/issues/27502)). I went this route over using a custom Fresco decoder for XML assets because vector drawables are compiled down into binary XML and I couldn't find a trivial, performant way to parse those files in a context-aware manner. This change only accounts for vector drawables, not any of the other XML-based drawable types (layer lists, level lists, state lists, 9-patch, etc.). Support could be added easily in the future by expanding the `getDrawableIfUnsupported` function. ## Changelog [Android] [Added] - Added support for rendering XML assets provide to `Image` Differential Revision: D59530172 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../react/views/image/ReactImageView.java | 67 ++++++++++++++++++- .../imagehelper/ResourceDrawableIdHelper.kt | 35 ++++++++++ .../app/src/main/res/drawable/ic_android.xml | 10 +++ .../js/examples/Image/ImageExample.js | 26 +++++++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 packages/rn-tester/android/app/src/main/res/drawable/ic_android.xml 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/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 000000000000..ad4f1531ac31 --- /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 3a43e9a1a648..f73225338df8 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', + }, ]; From 36bed4baf56d1b8003953fd27b3d1e3a692e71d7 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Fri, 12 Jul 2024 16:32:55 -0700 Subject: [PATCH 2/2] Declare public resources from images that are loaded by name (#45421) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45421 RNTester contains Android resources that are loaded by name and not resolved by Metro. As a result, these assets are not automatically linked when RNTester JS code is embedded in other projects. This is considered "legacy" loading and is generally discouraged, but is still showcased as an alernative way of loading resources. I also modified the Image test to ensure that flag status is printed so it's obvious why the vector drawable hasn't loaded. Changelog: [Internal] Reviewed By: javache Differential Revision: D59585555 --- packages/rn-tester/android/app/build.gradle.kts | 5 +++++ .../{res => public_res}/drawable/ic_android.xml | 0 .../{res => public_res}/drawable/legacy_image.png | Bin .../rn-tester/js/examples/Image/ImageExample.js | 6 ++++++ 4 files changed, 11 insertions(+) rename packages/rn-tester/android/app/src/main/{res => public_res}/drawable/ic_android.xml (100%) rename packages/rn-tester/android/app/src/main/{res => public_res}/drawable/legacy_image.png (100%) 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/res/drawable/ic_android.xml b/packages/rn-tester/android/app/src/main/public_res/drawable/ic_android.xml similarity index 100% rename from packages/rn-tester/android/app/src/main/res/drawable/ic_android.xml rename to packages/rn-tester/android/app/src/main/public_res/drawable/ic_android.xml 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 f73225338df8..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 { @@ -609,9 +611,13 @@ class VectorDrawableExample extends React.Component< VectorDrawableExampleProps, VectorDrawableExampleState, > { + state: VectorDrawableExampleState = {}; + render(): React.Node { + const isEnabled = ReactNativeFeatureFlags.loadVectorDrawablesOnImages(); return ( + Enabled: {isEnabled ? 'true' : 'false'} );