diff --git a/android/src/main/java/com/dylanvann/fastimage/BitmapSizeDecoder.java b/android/src/main/java/com/dylanvann/fastimage/BitmapSizeDecoder.java new file mode 100644 index 000000000..2bd58b861 --- /dev/null +++ b/android/src/main/java/com/dylanvann/fastimage/BitmapSizeDecoder.java @@ -0,0 +1,44 @@ +package com.dylanvann.fastimage; + +import android.graphics.BitmapFactory; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.exifinterface.media.ExifInterface; + +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 java.io.IOException; +import java.io.InputStream; + +public class BitmapSizeDecoder implements ResourceDecoder { + + @Override + public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException { + return true; + } + + @Nullable + @Override + public Resource decode(@NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException { + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inJustDecodeBounds = true; + BitmapFactory.decodeStream(source, null, bitmapOptions); + + // BitmapFactory#decodeStream leaves stream's position where ever it was after reading the encoded data + // https://developer.android.com/reference/android/graphics/BitmapFactory#decodeStream(java.io.InputStream) + // so we need to rewind the stream to be able to read image header with exif values + source.reset(); + + int orientation = new ExifInterface(source).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270) { + int tmpWidth = bitmapOptions.outWidth; + bitmapOptions.outWidth = bitmapOptions.outHeight; + bitmapOptions.outHeight = tmpWidth; + } + return new SimpleResource(bitmapOptions); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/dylanvann/fastimage/BitmapSizeTranscoder.java b/android/src/main/java/com/dylanvann/fastimage/BitmapSizeTranscoder.java new file mode 100644 index 000000000..7d208d155 --- /dev/null +++ b/android/src/main/java/com/dylanvann/fastimage/BitmapSizeTranscoder.java @@ -0,0 +1,23 @@ +package com.dylanvann.fastimage; + +import android.graphics.BitmapFactory; + +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.SimpleResource; +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; + +public class BitmapSizeTranscoder implements ResourceTranscoder { + @Nullable + @Override + public Resource transcode(@NonNull Resource toTranscode, @NonNull Options options) { + BitmapFactory.Options bitmap = toTranscode.get(); + Size size = new Size(); + size.width = bitmap.outWidth; + size.height = bitmap.outHeight; + return new SimpleResource(size); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java b/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java index 811292aa7..f60b87c43 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java @@ -2,6 +2,7 @@ import android.content.Context; import androidx.annotation.NonNull; +import android.graphics.BitmapFactory; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; @@ -47,6 +48,9 @@ public void registerComponents( .build(); OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client); registry.replace(GlideUrl.class, InputStream.class, factory); + // Decoder + Transcoder pair for InputStream -> Size + registry.prepend(InputStream.class, BitmapFactory.Options.class, new BitmapSizeDecoder()); + registry.register(BitmapFactory.Options.class, Size.class, new BitmapSizeTranscoder()); } private static Interceptor createInterceptor(final ResponseProgressListener listener) { diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java b/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java index dbeb81313..bf8f21cb8 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java @@ -22,13 +22,6 @@ public class FastImageRequestListener implements RequestListener { this.key = key; } - private static WritableMap mapFromResource(Drawable resource) { - WritableMap resourceData = new WritableNativeMap(); - resourceData.putInt("width", resource.getIntrinsicWidth()); - resourceData.putInt("height", resource.getIntrinsicHeight()); - return resourceData; - } - @Override public boolean onLoadFailed(@androidx.annotation.Nullable GlideException e, Object model, Target target, boolean isFirstResource) { FastImageOkHttpProgressGlideModule.forget(key); @@ -53,7 +46,6 @@ public boolean onResourceReady(Drawable resource, Object model, Target ThemedReactContext context = (ThemedReactContext) view.getContext(); RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); int viewId = view.getId(); - eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_EVENT, mapFromResource(resource)); eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_END_EVENT, new WritableNativeMap()); return false; } diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java index b098dcfee..34e9b4e32 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java @@ -2,6 +2,7 @@ import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT; +import androidx.annotation.NonNull; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; @@ -9,16 +10,24 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; +import com.bumptech.glide.Glide; import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.request.Request; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.request.transition.Transition; 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.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -130,9 +139,34 @@ public void onAfterUpdate( RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); int viewId = this.getId(); - eventEmitter.receiveEvent(viewId, - FastImageViewManager.REACT_ON_LOAD_START_EVENT, - new WritableNativeMap()); + // Request the URL from cache to see if it exists there and if so pass the cache + // path as an argument in the onLoadStart event + requestManager + .asFile() + .load(glideUrl) + .onlyRetrieveFromCache(true) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + WritableNativeMap result = new WritableNativeMap(); + result.putNull("cachePath"); + eventEmitter.receiveEvent(viewId, + FastImageViewManager.REACT_ON_LOAD_START_EVENT, + result); + return false; + } + + @Override + public boolean onResourceReady(File resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + WritableNativeMap result = new WritableNativeMap(); + result.putString("cachePath", resource.getAbsolutePath()); + eventEmitter.receiveEvent(viewId, + FastImageViewManager.REACT_ON_LOAD_START_EVENT, + result); + return false; + } + }) + .submit(); } if (requestManager != null) { @@ -158,6 +192,25 @@ public void onAfterUpdate( builder.listener(new FastImageRequestListener(key)); builder.into(this); + + // Used specifically to handle the `onLoad` event for the image + RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); + int viewId = this.getId(); + requestManager + .as(Size.class) + .load(imageSource == null ? null : imageSource.getSourceForLoad()) + .into(new SimpleTarget() { + @Override + public void onResourceReady(@NonNull Size resource, @Nullable Transition transition) { + WritableMap resourceData = new WritableNativeMap(); + resourceData.putInt("width", resource.width); + resourceData.putInt("height", resource.height); + eventEmitter.receiveEvent(viewId, + "onFastImageLoad", + resourceData + ); + } + }); } } diff --git a/android/src/main/java/com/dylanvann/fastimage/Size.java b/android/src/main/java/com/dylanvann/fastimage/Size.java new file mode 100644 index 000000000..2fe8a4779 --- /dev/null +++ b/android/src/main/java/com/dylanvann/fastimage/Size.java @@ -0,0 +1,6 @@ +package com.dylanvann.fastimage; + +public class Size { + int width; + int height; +} \ No newline at end of file diff --git a/ios/FastImage/FFFastImageView.m b/ios/FastImage/FFFastImageView.m index f7100815e..391ef92d1 100644 --- a/ios/FastImage/FFFastImageView.m +++ b/ios/FastImage/FFFastImageView.m @@ -54,7 +54,6 @@ - (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError { - (void) setOnFastImageLoadStart: (RCTDirectEventBlock)onFastImageLoadStart { if (_source && !self.hasSentOnLoadStart) { _onFastImageLoadStart = onFastImageLoadStart; - onFastImageLoadStart(@{}); self.hasSentOnLoadStart = YES; } else { _onFastImageLoadStart = onFastImageLoadStart; @@ -188,7 +187,18 @@ - (void) reloadImage { } if (self.onFastImageLoadStart) { - self.onFastImageLoadStart(@{}); + NSString* cachePath = [[SDImageCache sharedImageCache] cachePathForKey:url]; + BOOL isCached = [[SDImageCache sharedImageCache] diskImageDataExistsWithKey:url]; + if (isCached) { + self.onFastImageLoadStart(@{ + @"cachePath": cachePath + }); + } + else { + self.onFastImageLoadStart(@{ + @"cachePath": [NSNull null] + }); + } self.hasSentOnLoadStart = YES; } else { self.hasSentOnLoadStart = NO; diff --git a/src/index.tsx b/src/index.tsx index 018a7a65f..891c0975a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -52,6 +52,12 @@ export type Source = { cache?: Cache } +export interface OnLoadStartEvent { + nativeEvent: { + cachePath: string | null; + }; +} + export interface OnLoadEvent { nativeEvent: { width: number @@ -86,7 +92,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps { resizeMode?: ResizeMode fallback?: boolean - onLoadStart?(): void + onLoadStart?(event: OnLoadStartEvent): void onProgress?(event: OnProgressEvent): void