diff --git a/ReactNativeFastImageExample/src/LocalImagesExample.tsx b/ReactNativeFastImageExample/src/LocalImagesExample.tsx index c6beae94..b476d2b7 100644 --- a/ReactNativeFastImageExample/src/LocalImagesExample.tsx +++ b/ReactNativeFastImageExample/src/LocalImagesExample.tsx @@ -16,11 +16,11 @@ import BulletText from './BulletText'; // @ts-ignore import FieldsImage from './images/fields.jpg'; // @ts-ignore -import FieldsWebP from './images/fields.webp'; +import FieldsWebP from './images/fields_alt.webp'; // @ts-ignore import JellyfishGIF from './images/jellyfish.gif'; // @ts-ignore -import JellyfishWebP from './images/jellyfish.webp'; +import JellyfishWebP from './images/jellyfish_anim.webp'; // @ts-ignore import ReactSvg from './images/react-native.svg'; diff --git a/ReactNativeFastImageExample/src/images/fields.webp b/ReactNativeFastImageExample/src/images/fields_alt.webp similarity index 100% rename from ReactNativeFastImageExample/src/images/fields.webp rename to ReactNativeFastImageExample/src/images/fields_alt.webp diff --git a/ReactNativeFastImageExample/src/images/jellyfish.webp b/ReactNativeFastImageExample/src/images/jellyfish_anim.webp similarity index 100% rename from ReactNativeFastImageExample/src/images/jellyfish.webp rename to ReactNativeFastImageExample/src/images/jellyfish_anim.webp diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java b/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java index 86ee08e6..4ab49234 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java @@ -17,6 +17,10 @@ public class FastImageSource { private static final String ANDROID_RESOURCE_SCHEME = "android.resource"; private static final String ANDROID_CONTENT_SCHEME = "content"; private static final String LOCAL_FILE_SCHEME = "file"; + private static final String RAW_RESOURCE_FOLDER = "raw"; + private static final String HTTP_SCHEME_PREFIX = "http"; + private static final String DATA_URI_PREFIX = "data:"; + private final Headers mHeaders; private Uri mUri; private String mSource; @@ -53,19 +57,95 @@ public FastImageSource(Context context, String source, double width, double heig ImageSource imageSource = new ImageSource(context, source, width, height); mSource = imageSource.getSource(); mHeaders = headers == null ? Headers.DEFAULT : headers; - mUri = imageSource.getUri(); + mUri = resolveResourceUri(context, source, imageSource.getUri()); + + boolean isUriEmpty = mUri == null || TextUtils.isEmpty(mUri.toString()); + boolean isResourceMissing = isResource() && isUriEmpty; - if (isResource() && TextUtils.isEmpty(mUri.toString())) { + if (isResourceMissing) { throw new Resources.NotFoundException("Local Resource Not Found. Resource: '" + getSource() + "'."); } - if (isLocalResourceUri(mUri)) { - // Convert res:/ scheme to android.resource:// so - // glide can understand the uri. - mUri = Uri.parse(mUri.toString().replace("res:/", ANDROID_RESOURCE_SCHEME + "://" + context.getPackageName() + "/")); + boolean shouldConvertLocalResourceScheme = mUri != null && isLocalResourceUri(mUri); + + if (shouldConvertLocalResourceScheme) { + // Convert res:/ scheme to android.resource:// so glide can understand the uri. + String convertedUri = mUri.toString().replace("res:/", ANDROID_RESOURCE_SCHEME + "://" + context.getPackageName() + "/"); + mUri = Uri.parse(convertedUri); + } + } + + /** + * Attempts to find a resource URI for the given source. + * First uses the URI from ImageSource (checks drawable folder), + * then falls back to raw folder for assets like SVGs in release builds. + * + * @param context Application context + * @param source Original source string + * @param initialUri URI resolved by ImageSource + * @return Resolved URI or null if not found + */ + @Nullable + private Uri resolveResourceUri(Context context, String source, Uri initialUri) { + boolean isInitialUriValid = initialUri != null && !TextUtils.isEmpty(initialUri.toString()); + + if (isInitialUriValid) { + return initialUri; + } + + boolean isSourceNull = source == null; + boolean isRemoteUrl = source != null && source.startsWith(HTTP_SCHEME_PREFIX); + boolean isDataUri = source != null && source.startsWith(DATA_URI_PREFIX); + boolean shouldSkipRawLookup = isSourceNull || isRemoteUrl || isDataUri; + + if (shouldSkipRawLookup) { + return initialUri; + } + + return tryResolveFromRawFolder(context, source); + } + + /** + * Attempts to find a resource in the res/raw folder. + * This is needed because SVG and some other assets are bundled to res/raw/ + * in Android release builds, but ImageSource only checks res/drawable/. + * + * @param context Application context + * @param source Original source string + * @return URI pointing to raw resource, or null if not found + */ + @Nullable + private Uri tryResolveFromRawFolder(Context context, String source) { + String rawResourceName = toAndroidResourceName(source); + int rawResId = context.getResources().getIdentifier( + rawResourceName, + RAW_RESOURCE_FOLDER, + context.getPackageName() + ); + + boolean resourceNotFound = rawResId == 0; + + if (resourceNotFound) { + return null; } + + String rawResourceUri = ANDROID_RESOURCE_SCHEME + "://" + + context.getPackageName() + "/" + RAW_RESOURCE_FOLDER + "/" + rawResourceName; + + return Uri.parse(rawResourceUri); } + /** + * Converts a source name to Android resource name format. + * Android resources must be lowercase and cannot contain hyphens. + * Hyphens are removed to match React Native's bundling behavior. + * + * @param source Original source name + * @return Android-compatible resource name + */ + private String toAndroidResourceName(String source) { + return source.toLowerCase().replace("-", ""); + } public boolean isBase64Resource() { return mUri != null && FastImageSource.isBase64Uri(mUri);