diff --git a/README.md b/README.md index 314d5afca..6b909e19c 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,13 @@ Headers to load the image with. e.g. `{ Authorization: 'someAuthToken' }`. --- +### `defaultSource?: number` + +- An asset loaded with `require(...)`. +- Note that like the built-in `Image` implementation, on Android `defaultSource` does not work in debug mode. This is due to the fact that assets are sent from the dev server, but RN's functions only know how to load it from `res`. + +--- + ### `resizeMode?: enum` - `FastImage.resizeMode.contain` - Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or less than the corresponding dimension of the view (minus padding). diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java b/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java index e659a616e..811292aa7 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java @@ -32,7 +32,7 @@ @GlideModule public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule { - private static DispatchingProgressListener progressListener = new DispatchingProgressListener(); + private static final DispatchingProgressListener progressListener = new DispatchingProgressListener(); @Override public void registerComponents( diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java b/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java index 361417be6..dbeb81313 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java @@ -16,8 +16,7 @@ public class FastImageRequestListener implements RequestListener { static final String REACT_ON_ERROR_EVENT = "onFastImageError"; static final String REACT_ON_LOAD_EVENT = "onFastImageLoad"; static final String REACT_ON_LOAD_END_EVENT = "onFastImageLoadEnd"; - - private String key; + private final String key; FastImageRequestListener(String key) { this.key = key; diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java b/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java index 888b38e09..d9dbd9933 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageSource.java @@ -17,7 +17,7 @@ public class FastImageSource extends ImageSource { 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 Headers mHeaders; + private final Headers mHeaders; private Uri mUri; public static boolean isBase64Uri(Uri uri) { diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java index d86f66f33..86ca00d01 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java @@ -1,18 +1,16 @@ package com.dylanvann.fastimage; +import static com.bumptech.glide.request.RequestOptions.signatureOf; + import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.media.Image; -import android.net.Uri; -import android.util.Log; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import com.bumptech.glide.Priority; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.Headers; import com.bumptech.glide.load.model.LazyHeaders; import com.bumptech.glide.request.RequestOptions; @@ -21,18 +19,12 @@ import com.facebook.react.bridge.NoSuchKeyException; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; -import com.facebook.react.views.imagehelper.ImageSource; -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; -import static com.bumptech.glide.request.RequestOptions.signatureOf; - class FastImageViewConverter { private static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT); @@ -57,10 +49,13 @@ class FastImageViewConverter { put("stretch", ScaleType.FIT_XY); put("center", ScaleType.CENTER_INSIDE); }}; - + // Resolve the source uri to a file path that android understands. - static FastImageSource getImageSource(Context context, ReadableMap source) { - return new FastImageSource(context, source.getString("uri"), getHeaders(source)); + static @Nullable + FastImageSource getImageSource(Context context, @Nullable ReadableMap source) { + return source == null + ? null + : new FastImageSource(context, source.getString("uri"), getHeaders(source)); } static Headers getHeaders(ReadableMap source) { @@ -90,8 +85,8 @@ static RequestOptions getOptions(Context context, FastImageSource imageSource, R // Get cache control method. final FastImageCacheControl cacheControl = FastImageViewConverter.getCacheControl(source); DiskCacheStrategy diskCacheStrategy = DiskCacheStrategy.AUTOMATIC; - Boolean onlyFromCache = false; - Boolean skipMemoryCache = false; + boolean onlyFromCache = false; + boolean skipMemoryCache = false; switch (cacheControl) { case WEB: // If using none then OkHttp integration should be used for caching. @@ -107,12 +102,12 @@ static RequestOptions getOptions(Context context, FastImageSource imageSource, R } RequestOptions options = new RequestOptions() - .diskCacheStrategy(diskCacheStrategy) - .onlyRetrieveFromCache(onlyFromCache) - .skipMemoryCache(skipMemoryCache) - .priority(priority) - .placeholder(TRANSPARENT_DRAWABLE); - + .diskCacheStrategy(diskCacheStrategy) + .onlyRetrieveFromCache(onlyFromCache) + .skipMemoryCache(skipMemoryCache) + .priority(priority) + .placeholder(TRANSPARENT_DRAWABLE); + if (imageSource.isResource()) { // Every local resource (drawable) in Android has its own unique numeric id, which are // generated at build time. Although these ids are unique, they are not guaranteed unique @@ -123,7 +118,7 @@ static RequestOptions getOptions(Context context, FastImageSource imageSource, R options = options.apply(signatureOf(ApplicationVersionSignature.obtain(context))); } - return options; + return options; } private static FastImageCacheControl getCacheControl(ReadableMap source) { diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java index 094446399..c7a795471 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java @@ -1,15 +1,19 @@ package com.dylanvann.fastimage; +import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT; +import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_LOAD_END_EVENT; +import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_LOAD_EVENT; + import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.graphics.PorterDuff; import android.os.Build; +import androidx.annotation.NonNull; + import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; -import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.request.Request; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; @@ -18,36 +22,33 @@ import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import javax.annotation.Nullable; -import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT; -import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_LOAD_END_EVENT; -import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_LOAD_EVENT; - class FastImageViewManager extends SimpleViewManager implements FastImageProgressListener { - private static final String REACT_CLASS = "FastImageView"; - private static final String REACT_ON_LOAD_START_EVENT = "onFastImageLoadStart"; - private static final String REACT_ON_PROGRESS_EVENT = "onFastImageProgress"; + static final String REACT_CLASS = "FastImageView"; + static final String REACT_ON_LOAD_START_EVENT = "onFastImageLoadStart"; + static final String REACT_ON_PROGRESS_EVENT = "onFastImageProgress"; private static final Map> VIEWS_FOR_URLS = new WeakHashMap<>(); @Nullable private RequestManager requestManager = null; + @NonNull @Override public String getName() { return REACT_CLASS; } + @NonNull @Override - protected FastImageViewWithUrl createViewInstance(ThemedReactContext reactContext) { + protected FastImageViewWithUrl createViewInstance(@NonNull ThemedReactContext reactContext) { if (isValidContextForGlide(reactContext)) { requestManager = Glide.with(reactContext); } @@ -56,76 +57,15 @@ protected FastImageViewWithUrl createViewInstance(ThemedReactContext reactContex } @ReactProp(name = "source") - public void setSrc(FastImageViewWithUrl view, @Nullable ReadableMap source) { - if (source == null || !source.hasKey("uri") || isNullOrEmpty(source.getString("uri"))) { - // Cancel existing requests. - clearView(view); - - if (view.glideUrl != null) { - FastImageOkHttpProgressGlideModule.forget(view.glideUrl.toStringUrl()); - } - // Clear the image. - view.setImageDrawable(null); - return; - } - - //final GlideUrl glideUrl = FastImageViewConverter.getGlideUrl(view.getContext(), source); - final FastImageSource imageSource = FastImageViewConverter.getImageSource(view.getContext(), source); - if (imageSource.getUri().toString().length() == 0) { - ThemedReactContext context = (ThemedReactContext) view.getContext(); - RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); - int viewId = view.getId(); - WritableMap event = new WritableNativeMap(); - event.putString("message", "Invalid source prop:" + source); - eventEmitter.receiveEvent(viewId, REACT_ON_ERROR_EVENT, event); - - // Cancel existing requests. - if (requestManager != null) { - requestManager.clear(view); - } - - if (view.glideUrl != null) { - FastImageOkHttpProgressGlideModule.forget(view.glideUrl.toStringUrl()); - } - // Clear the image. - view.setImageDrawable(null); - return; - } - - final GlideUrl glideUrl = imageSource.getGlideUrl(); - - // Cancel existing request. - view.glideUrl = glideUrl; - clearView(view); - - String key = glideUrl.toStringUrl(); - FastImageOkHttpProgressGlideModule.expect(key, this); - List viewsForKey = VIEWS_FOR_URLS.get(key); - if (viewsForKey != null && !viewsForKey.contains(view)) { - viewsForKey.add(view); - } else if (viewsForKey == null) { - List newViewsForKeys = new ArrayList<>(Collections.singletonList(view)); - VIEWS_FOR_URLS.put(key, newViewsForKeys); - } + public void setSource(FastImageViewWithUrl view, @Nullable ReadableMap source) { + view.setSource(source); + } - ThemedReactContext context = (ThemedReactContext) view.getContext(); - RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); - int viewId = view.getId(); - eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_START_EVENT, new WritableNativeMap()); - - if (requestManager != null) { - requestManager - // This will make this work for remote and local images. e.g. - // - file:/// - // - content:// - // - res:/ - // - android.resource:// - // - data:image/png;base64 - .load(imageSource.getSourceForLoad()) - .apply(FastImageViewConverter.getOptions(context, imageSource, source)) - .listener(new FastImageRequestListener(key)) - .into(view); - } + @ReactProp(name = "defaultSource") + public void setDefaultSource(FastImageViewWithUrl view, @Nullable String source) { + view.setDefaultSource( + ResourceDrawableIdHelper.getInstance() + .getResourceDrawable(view.getContext(), source)); } @ReactProp(name = "tintColor", customType = "Color") @@ -144,9 +84,9 @@ public void setResizeMode(FastImageViewWithUrl view, String resizeMode) { } @Override - public void onDropViewInstance(FastImageViewWithUrl view) { + public void onDropViewInstance(@NonNull FastImageViewWithUrl view) { // This will cancel existing requests. - clearView(view); + view.clearView(requestManager); if (view.glideUrl != null) { final String key = view.glideUrl.toString(); @@ -193,11 +133,6 @@ public float getGranularityPercentage() { return 0.5f; } - private boolean isNullOrEmpty(final String url) { - return url == null || url.trim().isEmpty(); - } - - private static boolean isValidContextForGlide(final Context context) { Activity activity = getActivityFromContext(context); @@ -235,14 +170,14 @@ private static boolean isActivityDestroyed(Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return activity.isDestroyed() || activity.isFinishing(); } else { - return activity.isDestroyed() || activity.isFinishing() || activity.isChangingConfigurations(); + return activity.isFinishing() || activity.isChangingConfigurations(); } } - private void clearView(FastImageViewWithUrl view) { - if (requestManager != null && view != null && view.getTag() != null && view.getTag() instanceof Request) { - requestManager.clear(view); - } + @Override + protected void onAfterUpdateTransaction(@NonNull FastImageViewWithUrl view) { + super.onAfterUpdateTransaction(view); + view.onAfterUpdate(this, requestManager, VIEWS_FOR_URLS); } } diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java index 019032b23..f9d6faad7 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java @@ -2,6 +2,8 @@ import android.app.Activity; +import androidx.annotation.NonNull; + import com.bumptech.glide.Glide; import com.bumptech.glide.load.model.GlideUrl; import com.facebook.react.bridge.Promise; @@ -20,6 +22,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule { super(reactContext); } + @NonNull @Override public String getName() { return REACT_CLASS; diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java index 032f46ca3..0a211c9f5 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java @@ -1,5 +1,7 @@ package com.dylanvann.fastimage; +import androidx.annotation.NonNull; + import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; @@ -9,13 +11,15 @@ import java.util.List; public class FastImageViewPackage implements ReactPackage { + @NonNull @Override - public List createNativeModules(ReactApplicationContext reactContext) { + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { return Collections.singletonList(new FastImageViewModule(reactContext)); } + @NonNull @Override - public List createViewManagers(ReactApplicationContext reactContext) { + public List createViewManagers(@NonNull ReactApplicationContext reactContext) { return Collections.singletonList(new FastImageViewManager()); } } diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java index d0af70547..34fcf898d 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java @@ -1,15 +1,159 @@ package com.dylanvann.fastimage; +import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT; + +import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.drawable.Drawable; +import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; +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.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; class FastImageViewWithUrl extends AppCompatImageView { + private boolean mNeedsReload = false; + private ReadableMap mSource = null; + private Drawable mDefaultSource = null; + public GlideUrl glideUrl; public FastImageViewWithUrl(Context context) { super(context); } + + public void setSource(@Nullable ReadableMap source) { + mNeedsReload = true; + mSource = source; + } + + public void setDefaultSource(@Nullable Drawable source) { + mNeedsReload = true; + mDefaultSource = source; + } + + private boolean isNullOrEmpty(final String url) { + return url == null || url.trim().isEmpty(); + } + + @SuppressLint("CheckResult") + public void onAfterUpdate( + @Nonnull FastImageViewManager manager, + @Nullable RequestManager requestManager, + @Nonnull Map> viewsForUrlsMap) { + if (!mNeedsReload) + return; + + if ((mSource == null || + !mSource.hasKey("uri") || + isNullOrEmpty(mSource.getString("uri"))) && + mDefaultSource == null) { + + // Cancel existing requests. + clearView(requestManager); + + if (glideUrl != null) { + FastImageOkHttpProgressGlideModule.forget(glideUrl.toStringUrl()); + } + + // Clear the image. + setImageDrawable(null); + return; + } + + //final GlideUrl glideUrl = FastImageViewConverter.getGlideUrl(view.getContext(), mSource); + final FastImageSource imageSource = FastImageViewConverter.getImageSource(getContext(), mSource); + + if (imageSource != null && imageSource.getUri().toString().length() == 0) { + ThemedReactContext context = (ThemedReactContext) getContext(); + RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); + int viewId = getId(); + WritableMap event = new WritableNativeMap(); + event.putString("message", "Invalid source prop:" + mSource); + eventEmitter.receiveEvent(viewId, REACT_ON_ERROR_EVENT, event); + + // Cancel existing requests. + clearView(requestManager); + + if (glideUrl != null) { + FastImageOkHttpProgressGlideModule.forget(glideUrl.toStringUrl()); + } + // Clear the image. + setImageDrawable(null); + return; + } + + // `imageSource` may be null and we still continue, if `defaultSource` is not null + final GlideUrl glideUrl = imageSource == null ? null : imageSource.getGlideUrl(); + + // Cancel existing request. + this.glideUrl = glideUrl; + clearView(requestManager); + + String key = glideUrl == null ? null : glideUrl.toStringUrl(); + + if (glideUrl != null) { + FastImageOkHttpProgressGlideModule.expect(key, manager); + List viewsForKey = viewsForUrlsMap.get(key); + if (viewsForKey != null && !viewsForKey.contains(this)) { + viewsForKey.add(this); + } else if (viewsForKey == null) { + List newViewsForKeys = new ArrayList<>(Collections.singletonList(this)); + viewsForUrlsMap.put(key, newViewsForKeys); + } + } + + ThemedReactContext context = (ThemedReactContext) getContext(); + if (imageSource != null) { + // This is an orphan even without a load/loadend when only loading a placeholder + RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); + int viewId = this.getId(); + + eventEmitter.receiveEvent(viewId, + FastImageViewManager.REACT_ON_LOAD_START_EVENT, + new WritableNativeMap()); + } + + if (requestManager != null) { + RequestBuilder 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(mDefaultSource) // show until loaded + .fallback(mDefaultSource)); // null will not be treated as error + + if (key != null) + builder.listener(new FastImageRequestListener(key)); + + builder.into(this); + } + } + + public void clearView(@Nullable RequestManager requestManager) { + if (requestManager != null && getTag() != null && getTag() instanceof Request) { + requestManager.clear(this); + } + } } diff --git a/ios/FastImage/FFFastImageView.h b/ios/FastImage/FFFastImageView.h index fb557cf39..e52fca798 100644 --- a/ios/FastImage/FFFastImageView.h +++ b/ios/FastImage/FFFastImageView.h @@ -17,6 +17,7 @@ @property (nonatomic, copy) RCTDirectEventBlock onFastImageLoadEnd; @property (nonatomic, assign) RCTResizeMode resizeMode; @property (nonatomic, strong) FFFastImageSource *source; +@property (nonatomic, strong) UIImage *defaultSource; @property (nonatomic, strong) UIColor *imageColor; @end diff --git a/ios/FastImage/FFFastImageView.m b/ios/FastImage/FFFastImageView.m index 9c0f1d3c1..f7100815e 100644 --- a/ios/FastImage/FFFastImageView.m +++ b/ios/FastImage/FFFastImageView.m @@ -2,15 +2,15 @@ #import #import -@interface FFFastImageView() +@interface FFFastImageView () -@property (nonatomic, assign) BOOL hasSentOnLoadStart; -@property (nonatomic, assign) BOOL hasCompleted; -@property (nonatomic, assign) BOOL hasErrored; +@property(nonatomic, assign) BOOL hasSentOnLoadStart; +@property(nonatomic, assign) BOOL hasCompleted; +@property(nonatomic, assign) BOOL hasErrored; // Whether the latest change of props requires the image to be reloaded -@property (nonatomic, assign) BOOL needsReload; +@property(nonatomic, assign) BOOL needsReload; -@property (nonatomic, strong) NSDictionary* onLoadEvent; +@property(nonatomic, strong) NSDictionary* onLoadEvent; @end @@ -23,35 +23,35 @@ - (id) init { return self; } -- (void)setResizeMode:(RCTResizeMode)resizeMode { +- (void) setResizeMode: (RCTResizeMode)resizeMode { if (_resizeMode != resizeMode) { _resizeMode = resizeMode; - self.contentMode = (UIViewContentMode)resizeMode; + self.contentMode = (UIViewContentMode) resizeMode; } } -- (void)setOnFastImageLoadEnd:(RCTDirectEventBlock)onFastImageLoadEnd { +- (void) setOnFastImageLoadEnd: (RCTDirectEventBlock)onFastImageLoadEnd { _onFastImageLoadEnd = onFastImageLoadEnd; if (self.hasCompleted) { _onFastImageLoadEnd(@{}); } } -- (void)setOnFastImageLoad:(RCTDirectEventBlock)onFastImageLoad { +- (void) setOnFastImageLoad: (RCTDirectEventBlock)onFastImageLoad { _onFastImageLoad = onFastImageLoad; if (self.hasCompleted) { _onFastImageLoad(self.onLoadEvent); } } -- (void)setOnFastImageError:(RCTDirectEventBlock)onFastImageError { +- (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError { _onFastImageError = onFastImageError; if (self.hasErrored) { _onFastImageError(@{}); } } -- (void)setOnFastImageLoadStart:(RCTDirectEventBlock)onFastImageLoadStart { +- (void) setOnFastImageLoadStart: (RCTDirectEventBlock)onFastImageLoadStart { if (_source && !self.hasSentOnLoadStart) { _onFastImageLoadStart = onFastImageLoadStart; onFastImageLoadStart(@{}); @@ -62,100 +62,106 @@ - (void)setOnFastImageLoadStart:(RCTDirectEventBlock)onFastImageLoadStart { } } -- (void)setImageColor:(UIColor *)imageColor { +- (void) setImageColor: (UIColor*)imageColor { if (imageColor != nil) { _imageColor = imageColor; - super.image = [self makeImage:super.image withTint:self.imageColor]; + if (super.image) { + super.image = [self makeImage: super.image withTint: self.imageColor]; + } } } -- (UIImage*)makeImage:(UIImage *)image withTint:(UIColor *)color { - UIImage *newImage = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +- (UIImage*) makeImage: (UIImage*)image withTint: (UIColor*)color { + UIImage* newImage = [image imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate]; UIGraphicsBeginImageContextWithOptions(image.size, NO, newImage.scale); [color set]; - [newImage drawInRect:CGRectMake(0, 0, image.size.width, newImage.size.height)]; + [newImage drawInRect: CGRectMake(0, 0, image.size.width, newImage.size.height)]; newImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return newImage; } -- (void)setImage:(UIImage *)image { +- (void) setImage: (UIImage*)image { if (self.imageColor != nil) { - super.image = [self makeImage:image withTint:self.imageColor]; + super.image = [self makeImage: image withTint: self.imageColor]; } else { super.image = image; } } -- (void)sendOnLoad:(UIImage *)image { +- (void) sendOnLoad: (UIImage*)image { self.onLoadEvent = @{ - @"width":[NSNumber numberWithDouble:image.size.width], - @"height":[NSNumber numberWithDouble:image.size.height] - }; + @"width": [NSNumber numberWithDouble: image.size.width], + @"height": [NSNumber numberWithDouble: image.size.height] + }; if (self.onFastImageLoad) { self.onFastImageLoad(self.onLoadEvent); } } -- (void)setSource:(FFFastImageSource *)source { +- (void) setSource: (FFFastImageSource*)source { if (_source != source) { _source = source; _needsReload = YES; } } -- (void)didSetProps:(NSArray *)changedProps -{ +- (void) setDefaultSource: (UIImage*)defaultSource { + if (_defaultSource != defaultSource) { + _defaultSource = defaultSource; + _needsReload = YES; + } +} + +- (void) didSetProps: (NSArray*)changedProps { if (_needsReload) { [self reloadImage]; } } -- (void)reloadImage -{ +- (void) reloadImage { _needsReload = NO; if (_source) { - // Load base64 images. NSString* url = [_source.url absoluteString]; - if (url && [url hasPrefix:@"data:image"]) { + if (url && [url hasPrefix: @"data:image"]) { if (self.onFastImageLoadStart) { self.onFastImageLoadStart(@{}); self.hasSentOnLoadStart = YES; - } { + } else { self.hasSentOnLoadStart = NO; } // Use SDWebImage API to support external format like WebP images - UIImage *image = [UIImage sd_imageWithData:[NSData dataWithContentsOfURL:_source.url]]; - [self setImage:image]; + UIImage* image = [UIImage sd_imageWithData: [NSData dataWithContentsOfURL: _source.url]]; + [self setImage: image]; if (self.onFastImageProgress) { self.onFastImageProgress(@{ - @"loaded": @(1), - @"total": @(1) - }); + @"loaded": @(1), + @"total": @(1) + }); } self.hasCompleted = YES; - [self sendOnLoad:image]; - + [self sendOnLoad: image]; + if (self.onFastImageLoadEnd) { self.onFastImageLoadEnd(@{}); } return; } - + // Set headers. - NSDictionary *headers = _source.headers; - SDWebImageDownloaderRequestModifier *requestModifier = [SDWebImageDownloaderRequestModifier requestModifierWithBlock:^NSURLRequest * _Nullable(NSURLRequest * _Nonnull request) { - NSMutableURLRequest *mutableRequest = [request mutableCopy]; - for (NSString *header in headers) { - NSString *value = headers[header]; - [mutableRequest setValue:value forHTTPHeaderField:header]; + NSDictionary* headers = _source.headers; + SDWebImageDownloaderRequestModifier* requestModifier = [SDWebImageDownloaderRequestModifier requestModifierWithBlock: ^NSURLRequest* _Nullable (NSURLRequest* _Nonnull request) { + NSMutableURLRequest* mutableRequest = [request mutableCopy]; + for (NSString* header in headers) { + NSString* value = headers[header]; + [mutableRequest setValue: value forHTTPHeaderField: header]; } return [mutableRequest copy]; }]; - SDWebImageContext *context = @{SDWebImageContextDownloadRequestModifier : requestModifier}; - + SDWebImageContext* context = @{SDWebImageContextDownloadRequestModifier: requestModifier}; + // Set priority. SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies; switch (_source.priority) { @@ -169,7 +175,7 @@ - (void)reloadImage options |= SDWebImageHighPriority; break; } - + switch (_source.cacheControl) { case FFFCacheControlWeb: options |= SDWebImageRefreshCached; @@ -180,56 +186,58 @@ - (void)reloadImage case FFFCacheControlImmutable: break; } - + if (self.onFastImageLoadStart) { self.onFastImageLoadStart(@{}); self.hasSentOnLoadStart = YES; - } { + } else { self.hasSentOnLoadStart = NO; } self.hasCompleted = NO; self.hasErrored = NO; - - [self downloadImage:_source options:options context:context]; + + [self downloadImage: _source options: options context: context]; + } else if (_defaultSource) { + [self setImage: _defaultSource]; } } -- (void)downloadImage:(FFFastImageSource *) source options:(SDWebImageOptions) options context:(SDWebImageContext *)context { +- (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:nil - options:options - context:context - progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { + [self sd_setImageWithURL: _source.url + placeholderImage: _defaultSource + options: options + context: context + progress: ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) { if (weakSelf.onFastImageProgress) { weakSelf.onFastImageProgress(@{ - @"loaded": @(receivedSize), - @"total": @(expectedSize) - }); - } - } completed:^(UIImage * _Nullable image, - NSError * _Nullable error, - SDImageCacheType cacheType, - NSURL * _Nullable imageURL) { - if (error) { - weakSelf.hasErrored = YES; - if (weakSelf.onFastImageError) { - weakSelf.onFastImageError(@{}); - } - if (weakSelf.onFastImageLoadEnd) { - weakSelf.onFastImageLoadEnd(@{}); - } - } else { - weakSelf.hasCompleted = YES; - [weakSelf sendOnLoad:image]; - if (weakSelf.onFastImageLoadEnd) { - weakSelf.onFastImageLoadEnd(@{}); - } + @"loaded": @(receivedSize), + @"total": @(expectedSize) + }); } - }]; + } completed: ^(UIImage* _Nullable image, + NSError* _Nullable error, + SDImageCacheType cacheType, + NSURL* _Nullable imageURL) { + if (error) { + weakSelf.hasErrored = YES; + if (weakSelf.onFastImageError) { + weakSelf.onFastImageError(@{}); + } + if (weakSelf.onFastImageLoadEnd) { + weakSelf.onFastImageLoadEnd(@{}); + } + } else { + weakSelf.hasCompleted = YES; + [weakSelf sendOnLoad: image]; + if (weakSelf.onFastImageLoadEnd) { + weakSelf.onFastImageLoadEnd(@{}); + } + } + }]; } -- (void)dealloc { +- (void) dealloc { [self sd_cancelCurrentImageLoad]; } diff --git a/ios/FastImage/FFFastImageViewManager.m b/ios/FastImage/FFFastImageViewManager.m index a8059afb2..84ca94e26 100644 --- a/ios/FastImage/FFFastImageViewManager.m +++ b/ios/FastImage/FFFastImageViewManager.m @@ -13,6 +13,7 @@ - (FFFastImageView*)view { } RCT_EXPORT_VIEW_PROPERTY(source, FFFastImageSource) +RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage) RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode) RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock) diff --git a/package.json b/package.json index 51e77a999..83f58adf6 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@babel/runtime": "^7.14.6", "@types/jest": "^26.0.24", "@types/react": "^17.0.14", - "@types/react-native": "^0.64.12", + "@types/react-native": "^0.69.5", "@types/react-test-renderer": "^17.0.1", "dv-scripts": "^1.6.0", "eslint-config-dv-scripts": "^1.1.1", diff --git a/src/__snapshots__/index.test.tsx.snap b/src/__snapshots__/index.test.tsx.snap index 5bb674f47..5993b6700 100644 --- a/src/__snapshots__/index.test.tsx.snap +++ b/src/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FastImage renders correctly. 1`] = ` +exports[`FastImage (Android) renders a non-existing defaultSource 1`] = ` + +`; + +exports[`FastImage (Android) renders a normal defaultSource 1`] = ` + + + +`; + +exports[`FastImage (Android) renders a normal defaultSource when fails to load source 1`] = ` + + + +`; + +exports[`FastImage (iOS) renders 1`] = ` + + `; -exports[`Renders Image with fallback prop. 1`] = ` +exports[`FastImage (iOS) renders Image with fallback prop 1`] = ` `; -exports[`Renders a normal Image when not passed a uri. 1`] = ` +exports[`FastImage (iOS) renders a normal Image when not passed a uri 1`] = ` `; + +exports[`FastImage (iOS) renders defaultSource 1`] = ` + + + +`; diff --git a/src/index.js.flow b/src/index.js.flow index 7e17f3731..ed96938c0 100644 --- a/src/index.js.flow +++ b/src/index.js.flow @@ -56,7 +56,8 @@ export type FastImageProps = $ReadOnly<{| onLoadStart?: ?() => void, onProgress?: ?(event: OnProgressEvent) => void, - source: FastImageSource | number, + source?: ?(FastImageSource | number), + defaultSource?: ?number resizeMode?: ?ResizeModes, fallback?: ?boolean, diff --git a/src/index.test.tsx b/src/index.test.tsx index 1b43c37c6..c53c10886 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -1,52 +1,133 @@ -import { StyleSheet } from 'react-native' +import { StyleSheet, Platform, NativeModules } from 'react-native' import React from 'react' import renderer from 'react-test-renderer' import FastImage from './index' const style = StyleSheet.create({ image: { width: 44, height: 44 } }) -test('FastImage renders correctly.', () => { - const tree = renderer - .create( - , - ) - .toJSON() - - expect(tree).toMatchSnapshot() -}) +describe('FastImage (iOS)', () => { + beforeAll(() => { + Platform.OS = 'ios' + NativeModules.FastImageView = { + preload: Function.prototype, + clearMemoryCache: Function.prototype, + clearDiskCache: Function.prototype, + } + }) + + it('renders', () => { + const tree = renderer + .create( + , + ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + it('renders a normal Image when not passed a uri', () => { + const tree = renderer + .create( + , + ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + it('renders Image with fallback prop', () => { + const tree = renderer + .create( + , + ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + it('renders defaultSource', () => { + const tree = renderer + .create( + , + ) + .toJSON() -test('Renders a normal Image when not passed a uri.', () => { - const tree = renderer - .create( - , - ) - .toJSON() - - expect(tree).toMatchSnapshot() + expect(tree).toMatchSnapshot() + }) + + it('runs static functions', () => { + FastImage.preload([ + { + uri: 'https://facebook.github.io/react/img/logo_og.png', + headers: { + token: 'someToken', + }, + priority: FastImage.priority.high, + }, + ]) + FastImage.clearMemoryCache() + FastImage.clearDiskCache() + }) }) -test('Renders Image with fallback prop.', () => { - const tree = renderer - .create( - , - ) - .toJSON() - - expect(tree).toMatchSnapshot() +describe('FastImage (Android)', () => { + beforeAll(() => { + Platform.OS = 'android' + }) + + it('renders a normal defaultSource', () => { + const tree = renderer + .create( + , + ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + it('renders a normal defaultSource when fails to load source', () => { + const tree = renderer + .create( + , + ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + it('renders a non-existing defaultSource', () => { + const tree = renderer + .create() + .toJSON() + + expect(tree).toMatchSnapshot() + }) }) diff --git a/src/index.tsx b/src/index.tsx index 921422cd7..540e64b1a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,12 +10,12 @@ import { ShadowStyleIOS, StyleProp, TransformsStyle, + ImageRequireSource, + Platform, AccessibilityProps, ViewProps, } from 'react-native' -const FastImageViewNativeModule = NativeModules.FastImageView - export type ResizeMode = 'contain' | 'cover' | 'stretch' | 'center' const resizeMode = { @@ -81,7 +81,8 @@ export interface ImageStyle extends FlexStyle, TransformsStyle, ShadowStyleIOS { } export interface FastImageProps extends AccessibilityProps, ViewProps { - source: Source | number + source?: Source | ImageRequireSource + defaultSource?: ImageRequireSource resizeMode?: ResizeMode fallback?: boolean @@ -129,8 +130,32 @@ export interface FastImageProps extends AccessibilityProps, ViewProps { children?: React.ReactNode } +const resolveDefaultSource = ( + defaultSource?: ImageRequireSource, +): string | number | null => { + if (!defaultSource) { + return null + } + if (Platform.OS === 'android') { + // Android receives a URI string, and resolves into a Drawable using RN's methods. + const resolved = Image.resolveAssetSource( + defaultSource as ImageRequireSource, + ) + + if (resolved) { + return resolved.uri + } + + return null + } + // iOS or other number mapped assets + // In iOS the number is passed, and bridged automatically into a UIImage + return defaultSource +} + function FastImageBase({ source, + defaultSource, tintColor, onLoadStart, onProgress, @@ -156,6 +181,7 @@ function FastImageBase({ {...props} style={StyleSheet.absoluteFill} source={resolvedSource} + defaultSource={defaultSource} onLoadStart={onLoadStart} onProgress={onProgress} onLoad={onLoad as any} @@ -169,6 +195,7 @@ function FastImageBase({ } const resolvedSource = Image.resolveAssetSource(source as any) + const resolvedDefaultSource = resolveDefaultSource(defaultSource) return ( @@ -177,6 +204,7 @@ function FastImageBase({ tintColor={tintColor} style={StyleSheet.absoluteFill} source={resolvedSource} + defaultSource={resolvedDefaultSource} onFastImageLoadStart={onLoadStart} onFastImageProgress={onProgress} onFastImageLoad={onLoad} @@ -218,11 +246,12 @@ FastImage.cacheControl = cacheControl FastImage.priority = priority FastImage.preload = (sources: Source[]) => - FastImageViewNativeModule.preload(sources) + NativeModules.FastImageView.preload(sources) -FastImage.clearMemoryCache = () => FastImageViewNativeModule.clearMemoryCache() +FastImage.clearMemoryCache = () => + NativeModules.FastImageView.clearMemoryCache() -FastImage.clearDiskCache = () => FastImageViewNativeModule.clearDiskCache() +FastImage.clearDiskCache = () => NativeModules.FastImageView.clearDiskCache() const styles = StyleSheet.create({ imageContainer: { diff --git a/yarn.lock b/yarn.lock index 1f7b6eebf..4b969efdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2078,10 +2078,10 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== -"@types/react-native@^0.64.12": - version "0.64.12" - resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.64.12.tgz#1c6a3226c26d7a5949cdf8878e6cfe95fe0951d6" - integrity sha512-sw6WGSaL219zqrgdb4kQUtFB9iGXC/LmecLZ+UUWEgwYvD0YH81FqWYmONa2HuTkOFAsxu2bK4DspkWRUHIABQ== +"@types/react-native@^0.69.5": + version "0.69.5" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.69.5.tgz#7709fdbff031a5ecf1956705e6c4a07cdfe6867c" + integrity sha512-mSUCuGUsW2kJlZiu4GmdYVDKZX/52iyC9rm6dxAmflJj1b7kSO/CMSDy5WbcfS8QerxTqbYGTrIwHD0GnXHzbQ== dependencies: "@types/react" "*" @@ -7318,7 +7318,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0"