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

Cristian
Written by Cristian on
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}
/>
);
view raw Example.tsx hosted with ❤ by GitHub

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)
view raw patch.diff hosted with ❤ by GitHub

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

comments powered by Disqus