DylanVann / react-native-fast-image

🚩 FastImage, performant React Native image component.
MIT License
8.21k stars 1.51k forks source link

Flashing/flickering occurs when changing image sources #747

Open jamesxabregas opened 3 years ago

jamesxabregas commented 3 years ago

I am having an issue with flashing when I have a FastImage componenet on which I am changing the source dynamically. My use case is that I am creating an image picker. At first I thought this was a pre-caching issue but it doesn't appear to be as the issue occurs even with pre-caching configured. It seems the issue is that when the image source changes, FastImage renders a blank image momentarily and then renders the new image. The flashing only occurs for a fraction of a second but is quite distracting. It also occurs when loading local resources stored within the React Native app. In comparison React Native's default Image component waits for the Image to finish caching before swapping in the new image so no flashing occurs on change however the image caching on React Native is not great which is why I would prefer to use Fast Image.

I've made a screen recording to demonstrate the issue. All of these images were local files btw, and the video was recorded on a physical iPhone 12 (not emulated) running a production build (it was not connected to Metro).

It seems the flash is not visible on every transition in this recording likely because of the frame rate, but when viewing in person the flash does occur on each transition.

Environment

System:
    OS: macOS 10.15.7
    CPU: (16) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 348.77 MB / 16.00 GB
    Shell: 3.2.57 - /bin/bash
  Binaries:
    Node: 10.21.0 - /usr/local/bin/node
    Yarn: 1.19.1 - /usr/local/bin/yarn
    npm: 6.14.5 - /usr/local/bin/npm
    Watchman: 4.9.0 - /usr/local/bin/watchman
  Managers:
    CocoaPods: 1.9.3 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: iOS 14.2, DriverKit 20.0, macOS 11.0, tvOS 14.2, watchOS 7.1
    Android SDK: Not Found
  IDEs:
    Android Studio: 4.0 AI-193.6911.18.40.6626763
    Xcode: 12.2/12B45b - /usr/bin/xcodebuild
  Languages:
    Java: 12.0.1 - /usr/bin/javac
    Python: 2.7.16 - /usr/bin/python
  npmPackages:
    @react-native-community/cli: Not Found
    react: 16.13.1 => 16.13.1 
    react-native: 0.63.3 => 0.63.3 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found
ahmadkhudeish commented 3 years ago

Hi @jamesxabregas I had a similar issue recently with an image carousel flashing quickly before changing the image dynamically via swipe. My issue was that I was trying to add the key property on the encompassing view. Changing the key value to some random value other than the current carousel index made it work for me.

jamesxabregas commented 3 years ago

Yeah this isn't an issue with keys or anything like that. I looked into that. The issue is that until an image is within FastImage's cache this sort of flashing will occur when changing the image source. The solution as per the documentation is to preload the images of course, but realistically this is flawed for two reasons:

  1. If you have dynamic images you can't realistically cache every image. Imagine scrolling through a list of Unsplash thumbnails and then you click on one and it loads the large image into an existing image component. Each large image might be 500KB, you're not going to preload 50 of these on the chance that one of them might be clicked.
  2. There is no callback or promise on the preload function, it just returns void, so you can't even program around this issue and change the image source after the preload has completed because there is no way of determining that.

I ended up devloping a hacky solution within my own project where I had two images laid on top of each other with the lower level one loading the image first and then firing an onLoad event that set the source of the upper layer image. This meant that image sources could be changed without the flash occuring during the loading phase. However, as stated this is hacky and not particularly performant. This really should be fixed at the native level.

If someone could point me in the right direction I'd be more than happy to tinker with this to get it right.

hugoh59 commented 3 years ago

I am also experiencing this issue and looking for a solution. Thank you

james11a commented 2 years ago

It's 2022 and this isn't fixed yet. Thanks @jamesxabregas , the hack works.

StationSoen commented 2 years ago

same problem here.

lanceliamll commented 2 years ago

Any updates for this ticket?

ahmadzraiq commented 2 years ago

Any update?

hannesuw commented 2 years ago

Any update?

tomskopek commented 2 years ago

Please leave an upvote on this discussion item if you need this. https://github.com/DylanVann/react-native-fast-image/discussions/825

In the meantime @jamesxabregas hack seems to work pretty well for me - here's a (simplified) code snippet for how I've implemented it

import { Dimensions, StyleSheet, View } from 'react-native';
import FastImage from 'react-native-fast-image';

type ImageSource = { uri: string; headers: { [key: string]: string } };

const { width, height } = Dimensions.get('window');

type Props = {
  imageSource: ImageSource
}

export default function Image({ imageSource } : Props) {
  const [visibleImageSource, setVisibleImageSource] = useState<ImageSource>();

  // This is a hack to get around FastImage's cache-ing limitations
  // There can be a short black flash when changing image source before the next image is
  // fully loaded. Ideally we'd have FastImage.preload be promise based so we can know
  // when the images are cached, but this is not implemented in the library.
  // The hack is to have one hidden image component which tries to load
  // the image. Once the image loads and onLoad fires, we set the visible image component with the
  // right source.
  // Credit for this hack: https://github.com/DylanVann/react-native-fast-image/issues/747

  return (
    <View style={{ flex: 1, width, height }} >
      <FastImage
        source={imageSource}
        style={{ height: 1, width: 1, opacity: 0 }} // height and width must be non-zero or else onLoad does not fire on Android
        onLoad={() => {
          setVisibleImageSource(imageSource);
        }}
      />
      <FastImage
        source={visibleImageSource}
        style={StyleSheet.absoluteFillObject}
      />
    </View>
  );
}
knro commented 2 years ago

I tried above fix by @tomskopek but I am still getting flickering on Android.

bruteforks commented 1 year ago

anyone find a reason or workaround for this white flashing issue? The above hack didn't work for me

edit: adding example video, this is from a flatlist using the preload feature, one is the debug apk, one is the release apk. So might not completely fix it, but it fixed it enough to satisfy me. I would love for the image to just be there but this is close enough! images are 1080 resolution. lowering the image resolution made the image look horrible but fixed the issue. Wasn't an option for my app though cause it's largely image based. hope it helps someone.

cacheflash.webm kindafixed-release.webm

cristiangu commented 1 year ago

Hi everyone, I also encountered this and I wrote an article and a patch for this lib, maybe it's useful. Basically I'm keeping the current UIImage/Drawable as a placeholder until the new one is loaded.

appsgenie commented 1 year ago

@cristiangu is your patch basically keeping the old image until the new one is loaded and displays it? I think it is a good use case for you example where a low res image is replaced with a higher res after some time. but for a carousel-like UI (like what @bruteforks has), how would this work since swiping to the next item will still be showing blank until the next image is loaded? we would not want the same image to be showing on the next item as that might be confusing..

jamesxabregas commented 1 year ago

Hi everyone, I also encountered this and I wrote an article and a patch for this lib, maybe it's useful. Basically I'm keeping the current UIImage/Drawable as a placeholder until the new one is loaded.

@cristiangu thanks for providing this patch. This is exactly what I was looking to do when I encountered this issue but I wasn't sure where in the native code it would need to be updated. This patch should really become part of this library.

jamesxabregas commented 1 year ago

@cristiangu is your patch basically keeping the old image until the new one is loaded and displays it? I think it is a good use case for you example where a low res image is replaced with a higher res after some time. but for a carousel-like UI (like what @bruteforks has), how would this work since swiping to the next item will still be showing blank until the next image is loaded? we would not want the same image to be showing on the next item as that might be confusing..

@appsgenie the carousel use case you are referring to is a different issue, as it is related to preloading the image. The initial issue I brought up was related to when the image source is already loaded and is then changed.

jamieGardyn commented 1 year ago
<FastImage
        source={imageSource}
        style={{ height: 1, width: 1, opacity: 0 }} // height and width must be non-zero or else onLoad does not fire on Android
        onLoad={() => {
          setVisibleImageSource(imageSource);
        }}
      />

This didn't work, nor did the patch work for me - though my use case might be slightly different - but what did work was changing onLoad= in this code to onLoadEnd

kulakdev commented 11 months ago

@cristiangu thank you for your patch!

phungthanhdong commented 8 months ago

Hi everyone, I also encountered this and I wrote an article and a patch for this lib, maybe it's useful. Basically I'm keeping the current UIImage/Drawable as a placeholder until the new one is loaded.

Thanks @cristiangu, I used your work around and it actually solve the flickering problem, but sometimes when images updating so fast it could cause crashing because bitmap was recycled before the draw event. The safer way I found and use as below:

Instead of setting placeHolder directly using this.getDrawable()

...
         .placeholder(mUseLastImageAsDefaultSource ? this.getDrawable() : mDefaultSource) // show until loaded

Copy to new BitmapDrawable and use for default source

public void setSource(@Nullable ReadableMap source) {
        mNeedsReload = true;
        mSource = source;

        if (mUseLastImageAsDefaultSource) {
            BitmapDrawable currentDrawable = (BitmapDrawable) this.getDrawable();
            Bitmap toBmp = currentDrawable == null ? null : currentDrawable .getBitmap();
            if (toBmp != null && !toBmp.isRecycled()) {
                this.setDefaultSource(new BitmapDrawable(getResources(), toBmp.copy(toBmp.getConfig(), false)));
            }
        }
    }
cristiangu commented 6 months ago

@phungthanhdong, it makes sense! Thank you for this!