Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Postprocessor> postprocessors = new LinkedList<>();
if (mIterativeBoxBlurPostProcessor != null) {
postprocessors.add(mIterativeBoxBlurPostProcessor);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions packages/rn-tester/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ android {
java.srcDirs(
"$reactNativeDirPath/ReactCommon/react/nativemodule/samples/platform/android",
)
res.setSrcDirs(
listOf(
"src/main/res",
"src/main/public_res",
))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#a4c639"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z" />
</vector>
32 changes: 32 additions & 0 deletions packages/rn-tester/js/examples/Image/ImageExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 (
<View style={styles.flex}>
<Text>Enabled: {isEnabled ? 'true' : 'false'}</Text>
<Image source={{uri: 'ic_android'}} style={{height: 64, width: 64}} />
</View>
);
}
}

const fullImage: ImageSource = {
uri: IMAGE2,
};
Expand Down Expand Up @@ -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 <VectorDrawableExample />;
},
platform: 'android',
},
];