react-native-fast-image - No flickering when changing the image source


What is react-native-fast-image?
Before diving into the details of the patch, let’s briefly review what react-native-fast-image is and why it’s widely used.
react-native-fast-image
is a library that enhances image loading and rendering in React Native applications. It leverages the powerful Glide library for Android and SDWebImage for iOS to provide a highly optimized image loading experience. This means faster image loading, better caching, and smoother transitions when displaying images in your app.
Introducing a new prop
The patch in question is adding a new prop, useLastImageAsDefaultSource
, by making changes to both the Android and iOS implementations of react-native-fast-image
. Let’s break down the changes step by step.
Android Changes
In the Android part of the patch, we see a new method added to the FastImageViewManager
class:
@ReactProp(name = "useLastImageAsDefaultSource") | |
public void useLastImageAsDefaultSource(FastImageViewWithUrl view, @Nullable Boolean isActivated) { | |
view.useLastImageAsDefaultSource(isActivated); | |
} |
This method is used to set a property called useLastImageAsDefaultSource
on an instance of FastImageViewWithUrl
, which is the Android equivalent of the react-native-fast-image component
.
This is how the new property is being used in FastImageViewWithUrl
, line 23 is the most important one:
private Boolean mUseLastImageAsDefaultSource = false; | |
[...] | |
public void useLastImageAsDefaultSource(@Nullable Boolean isActivated) { | |
mUseLastImageAsDefaultSource = isActivated; | |
} | |
[...] | |
if (requestManager != null) { | |
RequestBuilder<Drawable> builder = | |
requestManager | |
// This will make this work for remote and local images. e.g. | |
// - file:/// | |
// - content:// | |
// - res:/ | |
// - android.resource:// | |
// - data:image/png;base64 | |
.load(imageSource == null ? null : imageSource.getSourceForLoad()) | |
.apply(FastImageViewConverter | |
.getOptions(context, imageSource, mSource) | |
.placeholder(mUseLastImageAsDefaultSource ? this.getDrawable() : mDefaultSource) // show until loaded | |
.fallback(mDefaultSource)); // null will not be treated as error | |
if (key != null) | |
builder.listener(new FastImageRequestListener(key)); | |
builder.into(this); | |
} | |
} |
iOS Changes
In the iOS part of the patch, we see a similar addition in the FFFastImageView.h
:
@property (nonatomic, assign) BOOL useLastImageAsDefaultSource; |
Then this property is being used in FFFastImageView.m
, check out line 15:
[...] | |
- (void) setUseLastImageAsDefaultSource: (BOOL*)useLastImageAsDefaultSource { | |
if (useLastImageAsDefaultSource != _useLastImageAsDefaultSource) { | |
_useLastImageAsDefaultSource = useLastImageAsDefaultSource; | |
} | |
} | |
[...] | |
- (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context { | |
__weak typeof(self) weakSelf = self; // Always use a weak reference to self in blocks | |
[self sd_setImageWithURL: _source.url | |
placeholderImage: _useLastImageAsDefaultSource == YES ? [super image] : _defaultSource | |
options: options | |
context: context | |
progress: ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) { | |
if (weakSelf.onFastImageProgress) { | |
weakSelf.onFastImageProgress(@{ | |
@"loaded": @(receivedSize), | |
@"total": @(expectedSize) | |
}); | |
} | |
} |
What Does It Do
The purpose of this patch is to give you more control over the default image source displayed while an image is loading. When useLastImageAsDefaultSource
is set, the component will use the last loaded image as the placeholder while the new image is being fetched. When set to false (or not set at all), the defaultSource
prop will be used as the placeholder. By doing this you can avoid the flicker that is occuring during the image change.
How to Use It
To take advantage of this patch, you need to set the useLastImageAsDefaultSource
prop on your FastImage
component to a truthy value.
import FastImage from 'react-native-fast-image'; | |
return ( | |
<FastImage | |
useLastImageAsDefaultSource | |
source={source} | |
/> | |
); |
The Patch Diff
If you’re already using patch-package you can create a new file (react-native-fast-image+8.6.3.patch
) in your patches
directory and add this diff:
diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java | |
index c7a7954..ca2b394 100644 | |
--- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java | |
+++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java | |
@@ -68,6 +68,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple | |
.getResourceDrawable(view.getContext(), source)); | |
} | |
+ @ReactProp(name = "useLastImageAsDefaultSource") | |
+ public void useLastImageAsDefaultSource(FastImageViewWithUrl view, @Nullable Boolean isActivated) { | |
+ view.useLastImageAsDefaultSource(isActivated); | |
+ } | |
+ | |
@ReactProp(name = "tintColor", customType = "Color") | |
public void setTintColor(FastImageViewWithUrl view, @Nullable Integer color) { | |
if (color == null) { | |
diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java | |
index 34fcf89..4e3c633 100644 | |
--- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java | |
+++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java | |
@@ -1,5 +1,6 @@ | |
package com.dylanvann.fastimage; | |
+import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; | |
import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT; | |
import android.annotation.SuppressLint; | |
@@ -9,10 +10,12 @@ import android.graphics.drawable.Drawable; | |
import androidx.annotation.Nullable; | |
import androidx.appcompat.widget.AppCompatImageView; | |
+import com.bumptech.glide.GenericTransitionOptions; | |
import com.bumptech.glide.RequestBuilder; | |
import com.bumptech.glide.RequestManager; | |
import com.bumptech.glide.load.model.GlideUrl; | |
import com.bumptech.glide.request.Request; | |
+import com.bumptech.glide.request.transition.DrawableCrossFadeTransition; | |
import com.facebook.react.bridge.ReadableMap; | |
import com.facebook.react.bridge.WritableMap; | |
import com.facebook.react.bridge.WritableNativeMap; | |
@@ -30,6 +33,7 @@ class FastImageViewWithUrl extends AppCompatImageView { | |
private boolean mNeedsReload = false; | |
private ReadableMap mSource = null; | |
private Drawable mDefaultSource = null; | |
+ private Boolean mUseLastImageAsDefaultSource = false; | |
public GlideUrl glideUrl; | |
@@ -47,6 +51,10 @@ class FastImageViewWithUrl extends AppCompatImageView { | |
mDefaultSource = source; | |
} | |
+ public void useLastImageAsDefaultSource(@Nullable Boolean isActivated) { | |
+ mUseLastImageAsDefaultSource = isActivated; | |
+ } | |
+ | |
private boolean isNullOrEmpty(final String url) { | |
return url == null || url.trim().isEmpty(); | |
} | |
@@ -141,12 +149,11 @@ class FastImageViewWithUrl extends AppCompatImageView { | |
.load(imageSource == null ? null : imageSource.getSourceForLoad()) | |
.apply(FastImageViewConverter | |
.getOptions(context, imageSource, mSource) | |
- .placeholder(mDefaultSource) // show until loaded | |
+ .placeholder(mUseLastImageAsDefaultSource ? this.getDrawable() : mDefaultSource) // show until loaded | |
.fallback(mDefaultSource)); // null will not be treated as error | |
if (key != null) | |
builder.listener(new FastImageRequestListener(key)); | |
- | |
builder.into(this); | |
} | |
} | |
diff --git a/node_modules/react-native-fast-image/dist/index.d.ts b/node_modules/react-native-fast-image/dist/index.d.ts | |
index 5abb7c9..7173cde 100644 | |
--- a/node_modules/react-native-fast-image/dist/index.d.ts | |
+++ b/node_modules/react-native-fast-image/dist/index.d.ts | |
@@ -89,6 +89,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps { | |
* Render children within the image. | |
*/ | |
children?: React.ReactNode; | |
+ useLastImageAsDefaultSource?: boolean; | |
} | |
export interface FastImageStaticProperties { | |
resizeMode: typeof resizeMode; | |
diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.h b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.h | |
index e52fca7..08a0a6d 100644 | |
--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.h | |
+++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.h | |
@@ -19,6 +19,6 @@ | |
@property (nonatomic, strong) FFFastImageSource *source; | |
@property (nonatomic, strong) UIImage *defaultSource; | |
@property (nonatomic, strong) UIColor *imageColor; | |
- | |
+@property (nonatomic, assign) BOOL useLastImageAsDefaultSource; | |
@end | |
diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m | |
index f710081..4a9e486 100644 | |
--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m | |
+++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m | |
@@ -113,6 +113,12 @@ - (void) setDefaultSource: (UIImage*)defaultSource { | |
} | |
} | |
+- (void) setUseLastImageAsDefaultSource: (BOOL*)useLastImageAsDefaultSource { | |
+ if (useLastImageAsDefaultSource != _useLastImageAsDefaultSource) { | |
+ _useLastImageAsDefaultSource = useLastImageAsDefaultSource; | |
+ } | |
+} | |
+ | |
- (void) didSetProps: (NSArray<NSString*>*)changedProps { | |
if (_needsReload) { | |
[self reloadImage]; | |
@@ -205,7 +211,7 @@ - (void) reloadImage { | |
- (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context { | |
__weak typeof(self) weakSelf = self; // Always use a weak reference to self in blocks | |
[self sd_setImageWithURL: _source.url | |
- placeholderImage: _defaultSource | |
+ placeholderImage: _useLastImageAsDefaultSource ? [super image] : _defaultSource | |
options: options | |
context: context | |
progress: ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) { | |
diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m | |
index 84ca94e..9b8ff8c 100644 | |
--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m | |
+++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m | |
@@ -20,6 +20,7 @@ - (FFFastImageView*)view { | |
RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock) | |
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock) | |
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadEnd, RCTDirectEventBlock) | |
+RCT_EXPORT_VIEW_PROPERTY(useLastImageAsDefaultSource, BOOL) | |
RCT_REMAP_VIEW_PROPERTY(tintColor, imageColor, UIColor) | |
RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources) |
Demo
Conclusion
The introduction of the useLastImageAsDefaultSource
is a subtle enhancement that can significantly elevate the user experience within image-rich applications. Feel free to write your question in the comments section bellow. 👇
Comments