diff --git a/BlastedImage.podspec b/BlastedImage.podspec index 4ec1a75..5ed1c69 100644 --- a/BlastedImage.podspec +++ b/BlastedImage.podspec @@ -19,5 +19,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" s.dependency "SDWebImage" + s.dependency "SDWebImageSVGCoder" + s.dependency 'SDWebImageAVIFCoder' end diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6f6ba..153aa02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,37 @@ -## [1.0.8] (2025-01-12) +## [1.1.0] (2025-01-31) + +#### 🚨 Breaking Changes (Android Only) +- **Upgraded Glide from 4.12.0 to 4.16.0** which may cause duplicate class conflicts if another package in your project also includes Glide. + +If you encounter a build error stating `com.bumptech.glide.GeneratedAppGlideModuleImpl is defined multiple times` it means another package in your project is also bundling Glide. + +To resolve this check the error message for the conflicting package and remove it if not needed. + +Alternatively ensure all dependencies use the same Glide version by adding the following to your android/app/build.gradle file: +``` +dependencies { + implementation 'com.github.bumptech.glide:glide:4.16.0' +} +``` +#### ✨ New Features + +- Added support for returning image dimensions via `onLoad` in combination by setting the `returnSize` param. ([#32](https://github.com/xerdnu/react-native-blasted-image/issues/32)) +- Added support for `SVG` images. +- Added support for `AVIF` images. ([#20](https://github.com/xerdnu/react-native-blasted-image/issues/20)) +- Added support for changing color with `tintColor` parameter. ([#11](https://github.com/xerdnu/react-native-blasted-image/issues/11)) + +#### 🔥 Improvements + +- Reworked parts of the code and created renderKey that will make sure images that failed in retry operation re-renders. ([#31](https://github.com/xerdnu/react-native-blasted-image/issues/31)) +- Reworked other parts of the code for increased performance. + +#### 🔄 Changes + +- Updated documentation. +- Changed repo for Android buildscript. +- Increased caching limits. + +## [1.0.9] (2025-01-12) #### New Features @@ -223,6 +256,7 @@ - Initial release. +[1.1.0]: https://github.com/xerdnu/react-native-blasted-image/compare/v1.0.9...v1.1.0 [1.0.9]: https://github.com/xerdnu/react-native-blasted-image/compare/v1.0.8...v1.0.9 [1.0.8]: https://github.com/xerdnu/react-native-blasted-image/compare/v1.0.7...v1.0.8 [1.0.7]: https://github.com/xerdnu/react-native-blasted-image/compare/v1.0.6...v1.0.7 diff --git a/README.md b/README.md index 008dd5f..b84d082 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,15 @@ A simple yet powerful image component for React Native, powered by [Glide](https://github.com/bumptech/glide) (Android) and [SDWebImage](https://github.com/SDWebImage/SDWebImage) (iOS). +## Support My Work! 🎉 +I truly appreciate your support! If you'd like to help me out, the best way is to check out my latest app — **LogoDuel**. + +**LogoDuel** is a fun, turn-based multiplayer trivia game where you challenge friends (or foes!) to guess famous logos. Test your brand knowledge and see who comes out on top! + +🚀 **Powered by BlastedImage** for performant and optimized image handling.

👉 Download now and let the logo battle begin! + +[![Get it on Google Play](https://img.shields.io/badge/Google_Play-Download-green?logo=google-play&style=for-the-badge)](https://play.google.com/store/apps/details?id=se.netblast.logoduellen) [![Download on the App Store](https://img.shields.io/badge/App_Store-Download-blue?logo=apple&style=for-the-badge)](https://apps.apple.com/us/app/logoduel/id6470379520) + ## Description Caching remote images has always been a challenge for me with the Image component in React Native. This simplified, yet powerful component, addresses that issue head-on. It offers a robust and performant mechanism for caching remote images, ensuring they're displayed quickly.

Leveraging the strengths of Glide and SDWebImage, it supports both memory and disk caching for remote images. The newly added Hybrid Assets feature allows you to bundle remote assets in your build, fetching from the network only when necessary. Notably, while it provides these enhanced capabilities for remote images, it seamlessly integrates with the standard React Native Image component when handling local assets using require. @@ -16,6 +25,20 @@ Caching remote images has always been a challenge for me with the Image componen - **Robust Caching**: Benefits from both memory and disk caching for maximum performance. - **Hybrid Assets**: Bundle remote assets within your build and only fetch from the network if assets are not included. +## Supported Image Types + +|  Filetype  |  Android  |    iOS    | +| :--------: | :-----: | :-: | +|PNG|✅|✅| +|APNG|✅|✅| +|JPEG|✅|✅| +|SVG|✅|✅| +|GIF|✅|✅| +|WebP|✅|✅| +|AVIF|✅|✅| +|HEIC|✅|✅| +|ICO|✅|✅| + ## Installation ### With bare React Native #### Using npm: @@ -59,11 +82,13 @@ import BlastedImage from 'react-native-blasted-image'; | `height` | `Number` | (Optional) Specifies the height of the image. `Overrides height in style` | 100 | | `resizeMode` | `String` | (Optional) Resize the image with one of the options: `cover` `contain` `center` `stretch` | cover | | `isBackground` | `Boolean` | (Optional) Makes the image act as a container background similar to the native `ImageBackground` component | false | -| `fallbackSource` | `Object` | (Optional) Object containing a `uri` string for a custom error image. | - | +| `returnSize` | `Boolean` | (Optional) Specifies if `Size` parameters should be returned in `onLoad` callback. | false | | `retries` | `Number` | (Optional) Specifies the number of retry attempts if the image fails to load. | 3 | -| `onLoad` | `Function` | (Optional) Callback function that gets called when the image has loaded succesfully. | - | +| `tintColor` | `String` | (Optional) Specifies tintColor for the image using hexadecimal/named colors. | - | +| `fallbackSource` | `Object` | (Optional) Object containing a `uri` string for a custom error image. | - | +| `onLoad` | `Function` | (Optional) Callback function that gets called when the image has loaded succesfully.
Returns `Size` parameters of the source image if `returnSize` set to `true` | - | | `onError` | `Function` | (Optional) Callback function that gets called when there was an error loading the image. | - | -| `style` | `Object` | (Optional) Styles to be applied to the image, e.g., `{borderRadius:20}`.
See [View Style Props](https://reactnative.dev/docs/view-style-props) for all available styles. +| `style` | `Object` | (Optional) Styles to be applied to the image, e.g., `{borderRadius:20}`.
See [View Style Props](https://reactnative.dev/docs/view-style-props) for all available styles. ### Source Parameter | Parameter | Type | Description | Default | |--------------|-------------------|-----------------------------------------------------------------------------------------------------|---------| @@ -176,11 +201,11 @@ useEffect(() => { This component was created with inspiration from [react-native-fast-image](https://github.com/DylanVann/react-native-fast-image) that also uses [Glide](https://github.com/bumptech/glide) and [SDWebImage](https://github.com/SDWebImage/SDWebImage). But due to its lack of ongoing maintenance i felt the need to develop this new image component to continue providing robust and performant caching functionality. ## Support My Work! 🎉 -I truly appreciate your support! If you'd like to help me out, the best way is to check out my latest app — LogoDuel. +I truly appreciate your support! If you'd like to help me out, the best way is to check out my latest app — **LogoDuel**. -LogoDuel is a fun, fast-paced multiplayer trivia game where you challenge friends (or foes!) to guess famous logos. Test your brand knowledge and see who comes out on top! +**LogoDuel** is a fun, turn-based multiplayer trivia game where you challenge friends (or foes!) to guess famous logos. Test your brand knowledge and see who comes out on top! -👉 Download now and let the logo battle begin! +🚀 **Powered by BlastedImage** for performant and optimized image handling.

👉 Download now and let the logo battle begin! [![Get it on Google Play](https://img.shields.io/badge/Google_Play-Download-green?logo=google-play&style=for-the-badge)](https://play.google.com/store/apps/details?id=se.netblast.logoduellen) [![Download on the App Store](https://img.shields.io/badge/App_Store-Download-blue?logo=apple&style=for-the-badge)](https://apps.apple.com/us/app/logoduel/id6470379520) diff --git a/android/build.gradle b/android/build.gradle index 305d3b3..3a5accb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,7 @@ buildscript { repositories { - jcenter() + google() + mavenCentral() } dependencies { @@ -32,7 +33,15 @@ repositories { dependencies { implementation 'com.facebook.react:react-native:+' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'com.github.bumptech.glide:glide:4.12.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' + + // Glide v4.16.0 + implementation 'com.github.bumptech.glide:glide:4.16.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' + + // AVIF support + implementation 'com.github.bumptech.glide:avif-integration:4.16.0' + + // SVG support + api 'com.caverock:androidsvg-aar:1.4' } \ No newline at end of file diff --git a/android/src/main/java/com/reactlibrary/BlastedImageModule.java b/android/src/main/java/com/reactlibrary/BlastedImageModule.java index 4192963..5c7841e 100644 --- a/android/src/main/java/com/reactlibrary/BlastedImageModule.java +++ b/android/src/main/java/com/reactlibrary/BlastedImageModule.java @@ -64,7 +64,7 @@ public BlastedImageModule(ReactApplicationContext reactContext) { if (!isGlideInitialized) { Glide.init(reactContext, new GlideBuilder() .setDiskCache(new InternalCacheDiskCacheFactory(reactContext, 1024 * 1024 * 1024)) //1gb disk cache - .setMemoryCache(new LruResourceCache(100 * 1024 * 1024)) // 100mb memory cache + .setMemoryCache(new LruResourceCache(256 * 1024 * 1024)) // 256mb memory cache ); isGlideInitialized = true; } @@ -129,6 +129,7 @@ public Object prepareGlideUrl(String imageUrl, boolean hybridAssets, String clou public void loadImage(String imageUrl, boolean skipMemoryCache, boolean hybridAssets, String cloudUrl, Promise promise) { try { + // See BlastedImageModule.m for details regarding NativeEventEmitters (BlastedEventLog etc.) Object glideUrl = prepareGlideUrl(imageUrl, hybridAssets, cloudUrl, true); // Is skip skipMemoryCache set for image and should we store it only to disk? diff --git a/android/src/main/java/com/reactlibrary/BlastedViewManager.java b/android/src/main/java/com/reactlibrary/BlastedViewManager.java index 5cad740..1dfa00b 100644 --- a/android/src/main/java/com/reactlibrary/BlastedViewManager.java +++ b/android/src/main/java/com/reactlibrary/BlastedViewManager.java @@ -7,6 +7,8 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableMap; +import androidx.annotation.Nullable; + import android.widget.ImageView; import android.view.View; import android.view.ViewOutlineProvider; @@ -16,6 +18,10 @@ import android.graphics.drawable.Drawable; import android.graphics.Rect; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; + import android.util.Log; @@ -117,5 +123,22 @@ public void setHeight(ImageView view, int height) { } } + @ReactProp(name = "tintColor") + public void setTintColor(ImageView view, @Nullable String color) { + if (color != null && !color.isEmpty()) { + try { + if (!color.startsWith("#") && color.length() == 6 && color.matches("[0-9A-Fa-f]+")) { + color = "#" + color; + } + int parsedColor = Color.parseColor(color); + view.setColorFilter(new PorterDuffColorFilter(parsedColor, PorterDuff.Mode.SRC_IN)); + } catch (IllegalArgumentException e) { + Log.e("BlastedViewManager", "Invalid color format: " + color); + } + } else { + view.clearColorFilter(); + } + } + // more properties... :) } diff --git a/android/src/main/java/com/reactlibrary/SvgDecoder.java b/android/src/main/java/com/reactlibrary/SvgDecoder.java new file mode 100644 index 0000000..6f444f8 --- /dev/null +++ b/android/src/main/java/com/reactlibrary/SvgDecoder.java @@ -0,0 +1,29 @@ +package com.xerdnu.blastedimage; + +import androidx.annotation.NonNull; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.SimpleResource; +import com.caverock.androidsvg.SVG; +import com.caverock.androidsvg.SVGParseException; +import java.io.IOException; +import java.io.InputStream; + +public class SvgDecoder implements ResourceDecoder { + @Override + public boolean handles(@NonNull InputStream source, @NonNull Options options) { + return true; // Assume all InputStreams are SVG + } + + @Override + public Resource decode(@NonNull InputStream source, int width, int height, @NonNull Options options) + throws IOException { + try { + SVG svg = SVG.getFromInputStream(source); + return new SimpleResource<>(svg); + } catch (SVGParseException e) { + throw new IOException("Failed to parse SVG", e); + } + } +} diff --git a/android/src/main/java/com/reactlibrary/SvgDrawableResource.java b/android/src/main/java/com/reactlibrary/SvgDrawableResource.java new file mode 100644 index 0000000..3df1c8e --- /dev/null +++ b/android/src/main/java/com/reactlibrary/SvgDrawableResource.java @@ -0,0 +1,38 @@ +package com.xerdnu.blastedimage; + +import android.graphics.drawable.PictureDrawable; +import androidx.annotation.NonNull; +import com.bumptech.glide.load.engine.Resource; + +public class SvgDrawableResource implements Resource { + private final PictureDrawable drawable; + + public SvgDrawableResource(PictureDrawable drawable) { + if (drawable == null) { + throw new NullPointerException("PictureDrawable must not be null"); + } + this.drawable = drawable; + } + + @NonNull + @Override + public Class getResourceClass() { + return PictureDrawable.class; + } + + @NonNull + @Override + public PictureDrawable get() { + return drawable; + } + + @Override + public int getSize() { + return 1; + } + + @Override + public void recycle() { + // Do nothing here + } +} \ No newline at end of file diff --git a/android/src/main/java/com/reactlibrary/SvgDrawableTranscoder.java b/android/src/main/java/com/reactlibrary/SvgDrawableTranscoder.java new file mode 100644 index 0000000..5c319e1 --- /dev/null +++ b/android/src/main/java/com/reactlibrary/SvgDrawableTranscoder.java @@ -0,0 +1,27 @@ +package com.xerdnu.blastedimage; + +import android.graphics.Picture; +import android.graphics.drawable.PictureDrawable; + + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.caverock.androidsvg.SVG; + +public class SvgDrawableTranscoder implements ResourceTranscoder { + @Nullable + @Override + public Resource transcode( + @NonNull Resource toTranscode, + @NonNull Options options + ) { + SVG svg = toTranscode.get(); + Picture picture = svg.renderToPicture(); + PictureDrawable drawable = new PictureDrawable(picture); + return new SvgDrawableResource(drawable); + } +} diff --git a/android/src/main/java/com/reactlibrary/SvgModule.java b/android/src/main/java/com/reactlibrary/SvgModule.java new file mode 100644 index 0000000..45c95d9 --- /dev/null +++ b/android/src/main/java/com/reactlibrary/SvgModule.java @@ -0,0 +1,31 @@ +package com.xerdnu.blastedimage; + +import android.content.Context; +import android.graphics.drawable.PictureDrawable; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; +import com.caverock.androidsvg.SVG; + +import java.io.InputStream; + +@GlideModule +public class SvgModule extends AppGlideModule { + @Override + public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + Log.d("SvgModule", "Registering SVG support in Glide"); + registry + .register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder()) + .append(InputStream.class, SVG.class, new SvgDecoder()); + } + + @Override + public boolean isManifestParsingEnabled() { + return false; + } +} \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 2450a6a..a1f7ed0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -11,15 +11,17 @@ declare module 'react-native-blasted-image' { interface BlastedImageProps { resizeMode?: 'cover' | 'contain' | 'stretch' | 'repeat' | 'center'; isBackground?: boolean; + returnSize?: boolean; fallbackSource?: ImageSourcePropType; source: SourceProp | number; width?: number; height?: number; style?: StyleProp; - onLoad?: () => void; + onLoad?: (size?: ImageSize | null) => void; onError?: (error: Error) => void; children?: React.ReactNode; retries?: number; + tintColor?: string; } interface BlastedImageStatic { diff --git a/index.js b/index.js index d4e33d2..0a9653b 100644 --- a/index.js +++ b/index.js @@ -38,13 +38,21 @@ export const loadImage = (imageUrl, skipMemoryCache = false, hybridAssets = fals if (!requestsCache[cacheKey]) { requestsCache[cacheKey] = new Promise(async (resolve, reject) => { + + let wasRetried = false; + for (let attempt = 1; attempt <= retries; attempt++) { + // sleep for 1 second before retrying if attempt > 1 + if (attempt > 1) { + wasRetried = true; + // await new Promise(resolve => setTimeout(resolve, 5000)); Keep for testing purposes + } try { await NativeBlastedImage.loadImage(imageUrl, skipMemoryCache, hybridAssets, cloudUrl); - resolve(); + resolve({ wasRetried }); return; } catch (error) { - console.warn(`Attempt ${attempt} failed for ${imageUrl}`); + console.warn(`[BlastedImage] Attempt ${attempt} failed for ${imageUrl}`); if (attempt === retries) { delete requestsCache[cacheKey]; // Clear failed cache entry reject(error); @@ -60,7 +68,9 @@ export const loadImage = (imageUrl, skipMemoryCache = false, hybridAssets = fals const BlastedImage = ({ resizeMode = "cover", isBackground = false, + returnSize = false, fallbackSource = null, + tintColor = null, retries = 3, source, width, @@ -72,6 +82,8 @@ const BlastedImage = ({ }) => { const [error, setError] = useState(false); const errorRef = useRef({}); + const [renderKey, setRenderKey] = useState(null); + const isDoneRef = useRef(false); if (typeof source === 'object') { source = { @@ -106,9 +118,13 @@ const BlastedImage = ({ setError(true); return; } + + if (isDoneRef.current) { + return; + } fetchImage(); - }, [source, retries]); + }, [source]); // Callback for fetching image to not cause re-renders const fetchImage = useCallback(async () => { @@ -117,6 +133,7 @@ const BlastedImage = ({ return; } + /* try { setError(false); await loadImage(source.uri, false, source.hybridAssets, source.cloudUrl, retries); @@ -127,6 +144,37 @@ const BlastedImage = ({ console.error(`Failed to load image: ${source.uri}`, err); onError?.(err); } + */ + + loadImage(source.uri, false, source.hybridAssets, source.cloudUrl, retries) + .then(({wasRetried}) => { + // Finally succeeded + isDoneRef.current = true; + if (wasRetried) { + const key = Math.random().toString(36).substring(2, 8); + setRenderKey(key); + } + setError(false); + //onLoad?.(); + + if (returnSize) { + Image.getSize(source.uri, (width, height) => { + onLoad?.({ width, height }); + }, (error) => { + console.warn('[BlastedImage] Failed to get image size:', error); + onLoad?.(null); + }); + } else { + onLoad?.(); + } + }) + .catch((err) => { + isDoneRef.current = true; + setError(true); + errorRef.current[source.uri] = true; + console.error(`Failed to load image: ${source.uri}`, err); + onError?.(err); + }); }, [source, retries]); // Flatten styles if provided as an array, otherwise use style as-is @@ -153,12 +201,12 @@ const BlastedImage = ({ } = remainingStyle; if (typeof width === 'string' && width.includes('%')) { - console.log("For maximum performance, BlastedImage does not support width defined as a percentage"); + console.log("[BlastedImage] For maximum performance, BlastedImage does not support width defined as a percentage"); return; } if (typeof height === 'string' && height.includes('%')) { - console.log("For maximum performance, BlastedImage does not support height defined as a percentage"); + console.log("[BlastedImage] For maximum performance, BlastedImage does not support height defined as a percentage"); return; } @@ -187,17 +235,17 @@ const BlastedImage = ({ {isBackground ? ( - {renderImageContent(error, source, fallbackSource, adjustedHeight, adjustedWidth, resizeMode)} + {renderImageContent(error, source, fallbackSource, tintColor, adjustedHeight, adjustedWidth, resizeMode, renderKey)} ) : ( - renderImageContent(error, source, fallbackSource, adjustedHeight, adjustedWidth, resizeMode) + renderImageContent(error, source, fallbackSource, tintColor, adjustedHeight, adjustedWidth, resizeMode, renderKey) )} {isBackground && {children}} ); }; -function renderImageContent(error, source, fallbackSource, adjustedHeight, adjustedWidth, resizeMode) { +function renderImageContent(error, source, fallbackSource, tintColor, adjustedHeight, adjustedWidth, resizeMode, renderKey) { if (error) { if (fallbackSource) { // Error - Fallback specified, use native component return ( @@ -205,6 +253,7 @@ function renderImageContent(error, source, fallbackSource, adjustedHeight, adjus source={fallbackSource} style={{ width: adjustedHeight, height: adjustedHeight }} resizeMode={resizeMode} + tintColor={tintColor} /> ); } else { // Error - No fallback, use native component with static asset @@ -213,6 +262,7 @@ function renderImageContent(error, source, fallbackSource, adjustedHeight, adjus source={require('./assets/image-error.png')} style={{ width: adjustedHeight, height: adjustedHeight }} resizeMode={resizeMode} + tintColor={tintColor} /> ); } @@ -222,6 +272,7 @@ function renderImageContent(error, source, fallbackSource, adjustedHeight, adjus source={source} style={{ width: adjustedWidth, height: adjustedHeight }} resizeMode={resizeMode} + tintColor={tintColor} /> ); } else if (typeof source === 'object' && source.uri && source.uri.startsWith('file://')) { // Success - with local asset (file://android_asset), no need to use cache @@ -230,15 +281,26 @@ function renderImageContent(error, source, fallbackSource, adjustedHeight, adjus source={{ uri: source.uri }} style={{ width: adjustedWidth, height: adjustedHeight }} resizeMode={resizeMode} + tintColor={tintColor} /> ); } else { // Success - with remote asset (http/https), use native component with full cache support - return ( + return renderKey != null ? ( + ) : ( + ); } diff --git a/ios/BlastedImageModule.m b/ios/BlastedImageModule.m index 82d205a..031d300 100644 --- a/ios/BlastedImageModule.m +++ b/ios/BlastedImageModule.m @@ -2,6 +2,8 @@ #import #import #import +#import +#import @implementation BlastedImageModule { @@ -13,10 +15,19 @@ @implementation BlastedImageModule - (instancetype)init { self = [super init]; if (self) { + // Cache configuration SDImageCacheConfig *cacheConfig = [SDImageCache sharedImageCache].config; cacheConfig.maxDiskSize = 1024 * 1024 * 1024; // 1GB cacheConfig.maxDiskAge = NSIntegerMax; // No max time for disk cache - cacheConfig.maxMemoryCost = 100 * 1024 * 1024; // 100MB memory cache + cacheConfig.maxMemoryCost = 256 * 1024 * 1024; // 256MB memory cache + + // Add SVG support + SDImageSVGCoder *svgCoder = [SDImageSVGCoder sharedCoder]; + [[SDImageCodersManager sharedManager] addCoder:svgCoder]; + + // Add AVIF support + SDImageAVIFCoder *avifCoder = [SDImageAVIFCoder sharedCoder]; + [[SDImageCodersManager sharedManager] addCoder:avifCoder]; } return self; } @@ -111,6 +122,7 @@ - (NSURL *)prepareUrl:(NSString *)imageUrl resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + // If showing image right after setting up NativeEventEmitters (BlastedEventLog etc.) the log might not show on iOS. Fix is to add a delay before showing the image but this is not a good solution or an option for production. Lets keep it as it is for now. NSURL *url = [self prepareUrl:imageUrl hybridAssets:hybridAssets cloudUrl:cloudUrl showLog:YES]; // Is skip skipMemoryCache set for image and should we store it only to disk? diff --git a/ios/BlastedViewManager.m b/ios/BlastedViewManager.m index 8788f0c..02eb9ac 100644 --- a/ios/BlastedViewManager.m +++ b/ios/BlastedViewManager.m @@ -5,6 +5,7 @@ #import #import #import +#import @implementation BlastedViewManager @@ -19,6 +20,31 @@ - (BOOL)isEmptyString:(NSString *)str { return (!str || ![str isKindOfClass:[NSString class]] || [str isEqualToString:@""]); } +- (UIColor *)colorFromHexString:(NSString *)hexString { + if ([hexString hasPrefix:@"#"]) { + hexString = [hexString substringFromIndex:1]; + } + + if (hexString.length == 6) { + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner scanHexInt:&rgbValue]; + + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 + green:((rgbValue & 0x00FF00) >> 8) / 255.0 + blue:(rgbValue & 0x0000FF) / 255.0 + alpha:1.0]; + } + + SEL colorSelector = NSSelectorFromString([hexString stringByAppendingString:@"Color"]); + if ([UIColor respondsToSelector:colorSelector]) { + return [UIColor performSelector:colorSelector]; + } + + // Return black if no match is found + return [UIColor blackColor]; +} + RCT_CUSTOM_VIEW_PROPERTY(source, NSDictionary, UIImageView) { BlastedImageModule *blastedImageModule = [self.bridge moduleForClass:[BlastedImageModule class]]; @@ -32,11 +58,28 @@ - (BOOL)isEmptyString:(NSString *)str { NSString *uri = [RCTConvert NSString:json[@"uri"]]; BOOL hybridAssets = [RCTConvert BOOL:json[@"hybridAssets"]]; NSString *cloudUrl = [RCTConvert NSString:json[@"cloudUrl"]]; + NSString *tintColorHex = [RCTConvert NSString:json[@"tintColor"]]; NSURL *url = [blastedImageModule prepareUrl:uri hybridAssets:hybridAssets cloudUrl:cloudUrl showLog:NO]; if (url != nil && ![url.absoluteString isEqualToString:@""]) { - [view sd_setImageWithURL:url]; + //[view sd_setImageWithURL:url]; + UIColor *storedTintColor = objc_getAssociatedObject(view, @selector(tintColor)); + + if (!storedTintColor) { + [view sd_setImageWithURL:url]; + } else { + [view sd_setImageWithURL:url completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { + if (error) return; + + dispatch_async(dispatch_get_main_queue(), ^{ + view.tintColor = storedTintColor; + view.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [view setNeedsDisplay]; + }); + }]; + } + [view setHidden:NO]; } else { [view setHidden:YES]; @@ -78,4 +121,20 @@ - (BOOL)isEmptyString:(NSString *)str { view.frame = frame; } +RCT_CUSTOM_VIEW_PROPERTY(tintColor, NSString, UIImageView) { + if ([self isEmptyString:json]) { + return; + } + + UIColor *uiColor = [self colorFromHexString:json]; + objc_setAssociatedObject(view, @selector(tintColor), uiColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (view.image) { + view.image = [view.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + view.tintColor = uiColor; + } + + [view setNeedsDisplay]; +} + @end diff --git a/package.json b/package.json index 07bba22..f4d3b41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-blasted-image", - "version": "1.0.9", + "version": "1.1.0", "description": "A simple yet powerful image component for React Native, powered by Glide and SDWebImage", "main": "index.js", "types": "index.d.ts",