diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK index 4ab1fc6f5de..84e25e32a98 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK @@ -15,6 +15,7 @@ android_library( react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'), react_native_dep('third-party/android/support/v4:lib-support-v4'), react_native_dep('third-party/android/support-annotations:android-support-annotations'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), ], visibility = [ diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java index 7d029f0e4c4..616042cee9b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -12,8 +12,8 @@ package com.facebook.react.views.image; import javax.annotation.Nullable; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import java.util.LinkedList; +import java.util.List; import android.content.Context; import android.graphics.Bitmap; @@ -44,11 +44,14 @@ import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.RoundingParams; import com.facebook.drawee.view.GenericDraweeView; import com.facebook.imagepipeline.common.ResizeOptions; +import com.facebook.imagepipeline.core.ImagePipeline; +import com.facebook.imagepipeline.core.ImagePipelineFactory; import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.imagepipeline.request.BasePostprocessor; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; import com.facebook.imagepipeline.request.Postprocessor; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -139,10 +142,67 @@ public class ReactImageView extends GenericDraweeView { } } - private final ResourceDrawableIdHelper mResourceDrawableIdHelper; - private final Map mSources; + private class ImageSource { + private @Nullable Uri mUri; + private String mSource; + private double mSize; + private boolean mIsLocalImage; - private @Nullable Uri mUri; + public ImageSource(String source, double width, double height) { + mSource = source; + mSize = width * height; + } + + public ImageSource(String source) { + this(source, 0.0d, 0.0d); + } + + public String getSource() { + return mSource; + } + + public Uri getUri() { + if (mUri == null) { + computeUri(); + } + return Assertions.assertNotNull(mUri); + } + + public double getSize() { + return mSize; + } + + public boolean isLocalImage() { + if (mUri == null) { + computeUri(); + } + return mIsLocalImage; + } + + private void computeUri() { + try { + mUri = Uri.parse(mSource); + // Verify scheme is set, so that relative uri (used by static resources) are not handled. + if (mUri.getScheme() == null) { + mUri = null; + } + } catch (Exception e) { + // ignore malformed uri, then attempt to extract resource ID. + } + if (mUri == null) { + mUri = mResourceDrawableIdHelper.getResourceDrawableUri(getContext(), mSource); + mIsLocalImage = true; + } else { + mIsLocalImage = false; + } + } + } + + private final ResourceDrawableIdHelper mResourceDrawableIdHelper; + private final List mSources; + + private @Nullable ImageSource mImageSource; + private @Nullable ImageSource mCachedImageSource; private @Nullable Drawable mLoadingImageDrawable; private int mBorderColor; private int mOverlayColor; @@ -151,7 +211,6 @@ public class ReactImageView extends GenericDraweeView { private @Nullable float[] mBorderCornerRadii; private ScalingUtils.ScaleType mScaleType; private boolean mIsDirty; - private boolean mIsLocalImage; private final AbstractDraweeControllerBuilder mDraweeControllerBuilder; private final RoundedCornerPostprocessor mRoundedCornerPostprocessor; private @Nullable ControllerListener mControllerListener; @@ -178,7 +237,7 @@ public class ReactImageView extends GenericDraweeView { mRoundedCornerPostprocessor = new RoundedCornerPostprocessor(); mCallerContext = callerContext; mResourceDrawableIdHelper = resourceDrawableIdHelper; - mSources = new HashMap<>(); + mSources = new LinkedList<>(); } public void setShouldNotifyLoadEvents(boolean shouldNotify) { @@ -270,13 +329,14 @@ public class ReactImageView extends GenericDraweeView { if (sources != null && sources.size() != 0) { // Optimize for the case where we have just one uri, case in which we don't need the sizes if (sources.size() == 1) { - mSources.put(sources.getMap(0).getString("uri"), 0.0); + mSources.add(new ImageSource(sources.getMap(0).getString("uri"))); } else { for (int idx = 0; idx < sources.size(); idx++) { ReadableMap source = sources.getMap(idx); - mSources.put( + mSources.add(new ImageSource( source.getString("uri"), - source.getDouble("width") * source.getDouble("height")); + source.getDouble("width"), + source.getDouble("height"))); } } } @@ -319,12 +379,12 @@ public class ReactImageView extends GenericDraweeView { return; } - computeSourceUri(); - if (mUri == null) { + setSourceImage(); + if (mImageSource == null) { return; } - boolean doResize = shouldResize(mUri); + boolean doResize = shouldResize(mImageSource); if (doResize && (getWidth() <= 0 || getHeight() <= 0)) { // If need a resize and the size is not yet set, wait until the layout pass provides one return; @@ -362,13 +422,13 @@ public class ReactImageView extends GenericDraweeView { hierarchy.setFadeDuration( mFadeDurationMs >= 0 ? mFadeDurationMs - : mIsLocalImage ? 0 : REMOTE_IMAGE_FADE_DURATION_MS); + : mImageSource.isLocalImage() ? 0 : REMOTE_IMAGE_FADE_DURATION_MS); Postprocessor postprocessor = usePostprocessorScaling ? mRoundedCornerPostprocessor : null; ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null; - ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mUri) + ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri()) .setPostprocessor(postprocessor) .setResizeOptions(resizeOptions) .setAutoRotateEnabled(true) @@ -384,6 +444,17 @@ public class ReactImageView extends GenericDraweeView { .setOldController(getController()) .setImageRequest(imageRequest); + if (mCachedImageSource != null) { + ImageRequest cachedImageRequest = + ImageRequestBuilder.newBuilderWithSource(mCachedImageSource.getUri()) + .setPostprocessor(postprocessor) + .setResizeOptions(resizeOptions) + .setAutoRotateEnabled(true) + .setProgressiveRenderingEnabled(mProgressiveRenderingEnabled) + .build(); + mDraweeControllerBuilder.setLowResImageRequest(cachedImageRequest); + } + if (mControllerListener != null && mControllerForTesting != null) { ForwardingControllerListener combinedListener = new ForwardingControllerListener(); combinedListener.addListener(mControllerListener); @@ -427,61 +498,57 @@ public class ReactImageView extends GenericDraweeView { return mSources.size() > 1; } - private void computeSourceUri() { - mUri = null; + private void setSourceImage() { + mImageSource = null; if (mSources.isEmpty()) { return; } if (hasMultipleSources()) { - setUriFromMultipleSources(); + setImageSourceFromMultipleSources(); return; } - final String singleSource = mSources.keySet().iterator().next(); - setUriFromSingleSource(singleSource); - } - - private void setUriFromSingleSource(String source) { - try { - mUri = Uri.parse(source); - // Verify scheme is set, so that relative uri (used by static resources) are not handled. - if (mUri.getScheme() == null) { - mUri = null; - } - } catch (Exception e) { - // ignore malformed uri, then attempt to extract resource ID. - } - if (mUri == null) { - mUri = mResourceDrawableIdHelper.getResourceDrawableUri(getContext(), source); - mIsLocalImage = true; - } else { - mIsLocalImage = false; - } + mImageSource = mSources.get(0); } /** - * Chooses the uri with the size closest to the target image size. Must be called only after the - * layout pass when the sizes of the target image have been computed, and when there are at least - * two sources to choose from. + * Chooses the image source with the size closest to the target image size. Must be called only + * after the layout pass when the sizes of the target image have been computed, and when there + * are at least two sources to choose from. */ - private void setUriFromMultipleSources() { + private void setImageSourceFromMultipleSources() { + ImagePipeline imagePipeline = ImagePipelineFactory.getInstance().getImagePipeline(); final double targetImageSize = getWidth() * getHeight(); double bestPrecision = Double.MAX_VALUE; - String bestUri = null; - for (Map.Entry source : mSources.entrySet()) { - final double precision = Math.abs(1.0 - (source.getValue()) / targetImageSize); + double bestCachePrecision = Double.MAX_VALUE; + for (ImageSource source : mSources) { + final double precision = Math.abs(1.0 - (source.getSize()) / targetImageSize); if (precision < bestPrecision) { bestPrecision = precision; - bestUri = source.getKey(); + mImageSource = source; + } + + if (precision < bestCachePrecision && + (imagePipeline.isInBitmapMemoryCache(source.getUri()) || + imagePipeline.isInDiskCacheSync(source.getUri()))) { + bestCachePrecision = precision; + mCachedImageSource = source; } } - setUriFromSingleSource(bestUri); + + // don't use cached image source if it's the same as the image source + if (mCachedImageSource != null && + mImageSource.getSource().equals(mCachedImageSource.getSource())) { + mCachedImageSource = null; + } } - private static boolean shouldResize(Uri uri) { + private static boolean shouldResize(ImageSource imageSource) { // Resizing is inferior to scaling. See http://frescolib.org/docs/resizing-rotating.html#_ // We resize here only for images likely to be from the device's camera, where the app developer // has no control over the original size - return UriUtil.isLocalContentUri(uri) || UriUtil.isLocalFileUri(uri); + return + UriUtil.isLocalContentUri(imageSource.getUri()) || + UriUtil.isLocalFileUri(imageSource.getUri()); } }