com.facebook.react.views.image.ReactImageView.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.react.views.image.ReactImageView.java

Source

/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.views.image;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.util.UriUtil;
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.controller.ForwardingControllerListener;
import com.facebook.drawee.drawable.AutoRotateDrawable;
import com.facebook.drawee.drawable.RoundedColorDrawable;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
import com.facebook.drawee.view.GenericDraweeView;
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory;
import com.facebook.imagepipeline.common.ResizeOptions;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.postprocessors.IterativeBoxBlurPostProcessor;
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.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.modules.fresco.ReactNetworkImageRequest;
import com.facebook.react.uimanager.FloatUtil;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.imagehelper.ImageSource;
import com.facebook.react.views.imagehelper.MultiSourceHelper;
import com.facebook.react.views.imagehelper.MultiSourceHelper.MultiSourceResult;
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
import com.facebook.yoga.YogaConstants;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * Wrapper class around Fresco's GenericDraweeView, enabling persisting props across multiple view
 * update and consistent processing of both static and network images.
 */
public class ReactImageView extends GenericDraweeView {

    public static final int REMOTE_IMAGE_FADE_DURATION_MS = 300;

    public static final String REMOTE_TRANSPARENT_BITMAP_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";

    private static float[] sComputedCornerRadii = new float[4];

    /*
     * Implementation note re rounded corners:
     *
     * Fresco's built-in rounded corners only work for 'cover' resize mode -
     * this is a limitation in Android itself. Fresco has a workaround for this, but
     * it requires knowing the background color.
     *
     * So for the other modes, we use a postprocessor.
     * Because the postprocessor uses a modified bitmap, that would just get cropped in
     * 'cover' mode, so we fall back to Fresco's normal implementation.
     */
    private static final Matrix sMatrix = new Matrix();
    private static final Matrix sInverse = new Matrix();
    private ImageResizeMethod mResizeMethod = ImageResizeMethod.AUTO;

    private class RoundedCornerPostprocessor extends BasePostprocessor {

        void getRadii(Bitmap source, float[] computedCornerRadii, float[] mappedRadii) {
            mScaleType.getTransform(sMatrix, new Rect(0, 0, source.getWidth(), source.getHeight()),
                    source.getWidth(), source.getHeight(), 0.0f, 0.0f);
            sMatrix.invert(sInverse);

            mappedRadii[0] = sInverse.mapRadius(computedCornerRadii[0]);
            mappedRadii[1] = mappedRadii[0];

            mappedRadii[2] = sInverse.mapRadius(computedCornerRadii[1]);
            mappedRadii[3] = mappedRadii[2];

            mappedRadii[4] = sInverse.mapRadius(computedCornerRadii[2]);
            mappedRadii[5] = mappedRadii[4];

            mappedRadii[6] = sInverse.mapRadius(computedCornerRadii[3]);
            mappedRadii[7] = mappedRadii[6];
        }

        @Override
        public void process(Bitmap output, Bitmap source) {
            cornerRadii(sComputedCornerRadii);

            output.setHasAlpha(true);
            if (FloatUtil.floatsEqual(sComputedCornerRadii[0], 0f)
                    && FloatUtil.floatsEqual(sComputedCornerRadii[1], 0f)
                    && FloatUtil.floatsEqual(sComputedCornerRadii[2], 0f)
                    && FloatUtil.floatsEqual(sComputedCornerRadii[3], 0f)) {
                super.process(output, source);
                return;
            }
            Paint paint = new Paint();
            paint.setAntiAlias(true);
            paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
            Canvas canvas = new Canvas(output);

            float[] radii = new float[8];

            getRadii(source, sComputedCornerRadii, radii);

            Path pathForBorderRadius = new Path();

            pathForBorderRadius.addRoundRect(new RectF(0, 0, source.getWidth(), source.getHeight()), radii,
                    Path.Direction.CW);

            canvas.drawPath(pathForBorderRadius, paint);
        }
    }

    // Fresco lacks support for repeating images, see https://github.com/facebook/fresco/issues/1575
    // We implement it here as a postprocessing step.
    private static final Matrix sTileMatrix = new Matrix();

    private class TilePostprocessor extends BasePostprocessor {
        @Override
        public CloseableReference<Bitmap> process(Bitmap source, PlatformBitmapFactory bitmapFactory) {
            final Rect destRect = new Rect(0, 0, getWidth(), getHeight());

            mScaleType.getTransform(sTileMatrix, destRect, source.getWidth(), source.getHeight(), 0.0f, 0.0f);

            Paint paint = new Paint();
            paint.setAntiAlias(true);
            Shader shader = new BitmapShader(source, mTileMode, mTileMode);
            shader.setLocalMatrix(sTileMatrix);
            paint.setShader(shader);

            CloseableReference<Bitmap> output = bitmapFactory.createBitmap(getWidth(), getHeight());
            try {
                Canvas canvas = new Canvas(output.get());
                canvas.drawRect(destRect, paint);
                return output.clone();
            } finally {
                CloseableReference.closeSafely(output);
            }
        }
    }

    private final List<ImageSource> mSources;

    private @Nullable ImageSource mImageSource;
    private @Nullable ImageSource mCachedImageSource;
    private @Nullable Drawable mDefaultImageDrawable;
    private @Nullable Drawable mLoadingImageDrawable;
    private @Nullable RoundedColorDrawable mBackgroundImageDrawable;
    private int mBackgroundColor = 0x00000000;
    private int mBorderColor;
    private int mOverlayColor;
    private float mBorderWidth;
    private float mBorderRadius = YogaConstants.UNDEFINED;
    private @Nullable float[] mBorderCornerRadii;
    private ScalingUtils.ScaleType mScaleType;
    private Shader.TileMode mTileMode = ImageResizeMode.defaultTileMode();
    private boolean mIsDirty;
    private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
    private final RoundedCornerPostprocessor mRoundedCornerPostprocessor;
    private final TilePostprocessor mTilePostprocessor;
    private @Nullable IterativeBoxBlurPostProcessor mIterativeBoxBlurPostProcessor;
    private @Nullable ControllerListener mControllerListener;
    private @Nullable ControllerListener mControllerForTesting;
    private @Nullable GlobalImageLoadListener mGlobalImageLoadListener;
    private final @Nullable Object mCallerContext;
    private int mFadeDurationMs = -1;
    private boolean mProgressiveRenderingEnabled;
    private ReadableMap mHeaders;

    // We can't specify rounding in XML, so have to do so here
    private static GenericDraweeHierarchy buildHierarchy(Context context) {
        return new GenericDraweeHierarchyBuilder(context.getResources())
                .setRoundingParams(RoundingParams.fromCornersRadius(0)).build();
    }

    public ReactImageView(Context context, AbstractDraweeControllerBuilder draweeControllerBuilder,
            @Nullable GlobalImageLoadListener globalImageLoadListener, @Nullable Object callerContext) {
        super(context, buildHierarchy(context));
        mScaleType = ImageResizeMode.defaultValue();
        mDraweeControllerBuilder = draweeControllerBuilder;
        mRoundedCornerPostprocessor = new RoundedCornerPostprocessor();
        mTilePostprocessor = new TilePostprocessor();
        mGlobalImageLoadListener = globalImageLoadListener;
        mCallerContext = callerContext;
        mSources = new LinkedList<>();
    }

    public void setShouldNotifyLoadEvents(boolean shouldNotify) {
        if (!shouldNotify) {
            mControllerListener = null;
        } else {
            final EventDispatcher mEventDispatcher = ((ReactContext) getContext())
                    .getNativeModule(UIManagerModule.class).getEventDispatcher();

            mControllerListener = new BaseControllerListener<ImageInfo>() {
                @Override
                public void onSubmit(String id, Object callerContext) {
                    mEventDispatcher.dispatchEvent(new ImageLoadEvent(getId(), ImageLoadEvent.ON_LOAD_START));
                }

                @Override
                public void onFinalImageSet(String id, @Nullable final ImageInfo imageInfo,
                        @Nullable Animatable animatable) {
                    if (imageInfo != null) {
                        mEventDispatcher.dispatchEvent(new ImageLoadEvent(getId(), ImageLoadEvent.ON_LOAD,
                                mImageSource.getSource(), imageInfo.getWidth(), imageInfo.getHeight()));
                        mEventDispatcher.dispatchEvent(new ImageLoadEvent(getId(), ImageLoadEvent.ON_LOAD_END));
                    }
                }

                @Override
                public void onFailure(String id, Throwable throwable) {
                    mEventDispatcher.dispatchEvent(
                            new ImageLoadEvent(getId(), ImageLoadEvent.ON_ERROR, true, throwable.getMessage()));
                }
            };
        }

        mIsDirty = true;
    }

    public void setBlurRadius(float blurRadius) {
        int pixelBlurRadius = (int) PixelUtil.toPixelFromDIP(blurRadius);
        if (pixelBlurRadius == 0) {
            mIterativeBoxBlurPostProcessor = null;
        } else {
            mIterativeBoxBlurPostProcessor = new IterativeBoxBlurPostProcessor(pixelBlurRadius);
        }
        mIsDirty = true;
    }

    @Override
    public void setBackgroundColor(int backgroundColor) {
        if (mBackgroundColor != backgroundColor) {
            mBackgroundColor = backgroundColor;
            mBackgroundImageDrawable = new RoundedColorDrawable(backgroundColor);
            mIsDirty = true;
        }
    }

    public void setBorderColor(int borderColor) {
        mBorderColor = borderColor;
        mIsDirty = true;
    }

    public void setOverlayColor(int overlayColor) {
        mOverlayColor = overlayColor;
        mIsDirty = true;
    }

    public void setBorderWidth(float borderWidth) {
        mBorderWidth = PixelUtil.toPixelFromDIP(borderWidth);
        mIsDirty = true;
    }

    public void setBorderRadius(float borderRadius) {
        if (!FloatUtil.floatsEqual(mBorderRadius, borderRadius)) {
            mBorderRadius = borderRadius;
            mIsDirty = true;
        }
    }

    public void setBorderRadius(float borderRadius, int position) {
        if (mBorderCornerRadii == null) {
            mBorderCornerRadii = new float[4];
            Arrays.fill(mBorderCornerRadii, YogaConstants.UNDEFINED);
        }

        if (!FloatUtil.floatsEqual(mBorderCornerRadii[position], borderRadius)) {
            mBorderCornerRadii[position] = borderRadius;
            mIsDirty = true;
        }
    }

    public void setScaleType(ScalingUtils.ScaleType scaleType) {
        mScaleType = scaleType;
        mIsDirty = true;
    }

    public void setTileMode(Shader.TileMode tileMode) {
        mTileMode = tileMode;
        mIsDirty = true;
    }

    public void setResizeMethod(ImageResizeMethod resizeMethod) {
        mResizeMethod = resizeMethod;
        mIsDirty = true;
    }

    public void setSource(@Nullable ReadableArray sources) {
        mSources.clear();
        if (sources == null || sources.size() == 0) {
            ImageSource imageSource = new ImageSource(getContext(), REMOTE_TRANSPARENT_BITMAP_URI);
            mSources.add(imageSource);
        } else {
            // Optimize for the case where we have just one uri, case in which we don't need the sizes
            if (sources.size() == 1) {
                ReadableMap source = sources.getMap(0);
                String uri = source.getString("uri");
                ImageSource imageSource = new ImageSource(getContext(), uri);
                mSources.add(imageSource);
                if (Uri.EMPTY.equals(imageSource.getUri())) {
                    warnImageSource(uri);
                }
            } else {
                for (int idx = 0; idx < sources.size(); idx++) {
                    ReadableMap source = sources.getMap(idx);
                    String uri = source.getString("uri");
                    ImageSource imageSource = new ImageSource(getContext(), uri, source.getDouble("width"),
                            source.getDouble("height"));
                    mSources.add(imageSource);
                    if (Uri.EMPTY.equals(imageSource.getUri())) {
                        warnImageSource(uri);
                    }
                }
            }
        }
        mIsDirty = true;
    }

    public void setDefaultSource(@Nullable String name) {
        mDefaultImageDrawable = ResourceDrawableIdHelper.getInstance().getResourceDrawable(getContext(), name);
        mIsDirty = true;
    }

    public void setLoadingIndicatorSource(@Nullable String name) {
        Drawable drawable = ResourceDrawableIdHelper.getInstance().getResourceDrawable(getContext(), name);
        mLoadingImageDrawable = drawable != null ? (Drawable) new AutoRotateDrawable(drawable, 1000) : null;
        mIsDirty = true;
    }

    public void setProgressiveRenderingEnabled(boolean enabled) {
        mProgressiveRenderingEnabled = enabled;
        // no worth marking as dirty if it already rendered..
    }

    public void setFadeDuration(int durationMs) {
        mFadeDurationMs = durationMs;
        // no worth marking as dirty if it already rendered..
    }

    private void cornerRadii(float[] computedCorners) {
        float defaultBorderRadius = !YogaConstants.isUndefined(mBorderRadius) ? mBorderRadius : 0;

        computedCorners[0] = mBorderCornerRadii != null && !YogaConstants.isUndefined(mBorderCornerRadii[0])
                ? mBorderCornerRadii[0]
                : defaultBorderRadius;
        computedCorners[1] = mBorderCornerRadii != null && !YogaConstants.isUndefined(mBorderCornerRadii[1])
                ? mBorderCornerRadii[1]
                : defaultBorderRadius;
        computedCorners[2] = mBorderCornerRadii != null && !YogaConstants.isUndefined(mBorderCornerRadii[2])
                ? mBorderCornerRadii[2]
                : defaultBorderRadius;
        computedCorners[3] = mBorderCornerRadii != null && !YogaConstants.isUndefined(mBorderCornerRadii[3])
                ? mBorderCornerRadii[3]
                : defaultBorderRadius;
    }

    public void setHeaders(ReadableMap headers) {
        mHeaders = headers;
    }

    public void maybeUpdateView() {
        if (!mIsDirty) {
            return;
        }

        if (hasMultipleSources() && (getWidth() <= 0 || getHeight() <= 0)) {
            // If we need to choose from multiple uris but the size is not yet set, wait for layout pass
            return;
        }

        setSourceImage();
        if (mImageSource == null) {
            return;
        }

        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;
        }

        if (isTiled() && (getWidth() <= 0 || getHeight() <= 0)) {
            // If need to tile and the size is not yet set, wait until the layout pass provides one
            return;
        }

        GenericDraweeHierarchy hierarchy = getHierarchy();
        hierarchy.setActualImageScaleType(mScaleType);

        if (mDefaultImageDrawable != null) {
            hierarchy.setPlaceholderImage(mDefaultImageDrawable, mScaleType);
        }

        if (mLoadingImageDrawable != null) {
            hierarchy.setPlaceholderImage(mLoadingImageDrawable, ScalingUtils.ScaleType.CENTER);
        }

        boolean usePostprocessorScaling = mScaleType != ScalingUtils.ScaleType.CENTER_CROP
                && mScaleType != ScalingUtils.ScaleType.FOCUS_CROP;

        RoundingParams roundingParams = hierarchy.getRoundingParams();

        cornerRadii(sComputedCornerRadii);

        roundingParams.setCornersRadii(sComputedCornerRadii[0], sComputedCornerRadii[1], sComputedCornerRadii[2],
                sComputedCornerRadii[3]);

        if (mBackgroundImageDrawable != null) {
            mBackgroundImageDrawable.setBorder(mBorderColor, mBorderWidth);
            mBackgroundImageDrawable.setRadii(roundingParams.getCornersRadii());
            hierarchy.setBackgroundImage(mBackgroundImageDrawable);
        }

        if (usePostprocessorScaling) {
            roundingParams.setCornersRadius(0);
        }

        roundingParams.setBorder(mBorderColor, mBorderWidth);
        if (mOverlayColor != Color.TRANSPARENT) {
            roundingParams.setOverlayColor(mOverlayColor);
        } else {
            // make sure the default rounding method is used.
            roundingParams.setRoundingMethod(RoundingParams.RoundingMethod.BITMAP_ONLY);
        }
        hierarchy.setRoundingParams(roundingParams);
        hierarchy.setFadeDuration(mFadeDurationMs >= 0 ? mFadeDurationMs
                : mImageSource.isResource() ? 0 : REMOTE_IMAGE_FADE_DURATION_MS);

        List<Postprocessor> postprocessors = new LinkedList<>();
        if (usePostprocessorScaling) {
            postprocessors.add(mRoundedCornerPostprocessor);
        }
        if (mIterativeBoxBlurPostProcessor != null) {
            postprocessors.add(mIterativeBoxBlurPostProcessor);
        }
        if (isTiled()) {
            postprocessors.add(mTilePostprocessor);
        }
        Postprocessor postprocessor = MultiPostprocessor.from(postprocessors);

        ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null;

        ImageRequestBuilder imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri())
                .setPostprocessor(postprocessor).setResizeOptions(resizeOptions).setAutoRotateEnabled(true)
                .setProgressiveRenderingEnabled(mProgressiveRenderingEnabled);

        ImageRequest imageRequest = ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, mHeaders);

        if (mGlobalImageLoadListener != null) {
            mGlobalImageLoadListener.onLoadAttempt(mImageSource.getUri());
        }

        // This builder is reused
        mDraweeControllerBuilder.reset();

        mDraweeControllerBuilder.setAutoPlayAnimations(true).setCallerContext(mCallerContext)
                .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);
            combinedListener.addListener(mControllerForTesting);
            mDraweeControllerBuilder.setControllerListener(combinedListener);
        } else if (mControllerForTesting != null) {
            mDraweeControllerBuilder.setControllerListener(mControllerForTesting);
        } else if (mControllerListener != null) {
            mDraweeControllerBuilder.setControllerListener(mControllerListener);
        }

        setController(mDraweeControllerBuilder.build());
        mIsDirty = false;

        // Reset again so the DraweeControllerBuilder clears all it's references. Otherwise, this causes
        // a memory leak.
        mDraweeControllerBuilder.reset();
    }

    // VisibleForTesting
    public void setControllerListener(ControllerListener controllerListener) {
        mControllerForTesting = controllerListener;
        mIsDirty = true;
        maybeUpdateView();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0 && h > 0) {
            mIsDirty = mIsDirty || hasMultipleSources() || isTiled();
            maybeUpdateView();
        }
    }

    /** ReactImageViews only render a single image. */
    @Override
    public boolean hasOverlappingRendering() {
        return false;
    }

    private boolean hasMultipleSources() {
        return mSources.size() > 1;
    }

    private boolean isTiled() {
        return mTileMode != Shader.TileMode.CLAMP;
    }

    private void setSourceImage() {
        mImageSource = null;
        if (mSources.isEmpty()) {
            ImageSource imageSource = new ImageSource(getContext(), REMOTE_TRANSPARENT_BITMAP_URI);
            mSources.add(imageSource);
        } else if (hasMultipleSources()) {
            MultiSourceResult multiSource = MultiSourceHelper.getBestSourceForSize(getWidth(), getHeight(),
                    mSources);
            mImageSource = multiSource.getBestResult();
            mCachedImageSource = multiSource.getBestResultInCache();
            return;
        }

        mImageSource = mSources.get(0);
    }

    private 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
        if (mResizeMethod == ImageResizeMethod.AUTO) {
            return UriUtil.isLocalContentUri(imageSource.getUri()) || UriUtil.isLocalFileUri(imageSource.getUri());
        } else if (mResizeMethod == ImageResizeMethod.RESIZE) {
            return true;
        } else {
            return false;
        }
    }

    private void warnImageSource(String uri) {
        if (ReactBuildConfig.DEBUG) {
            Toast.makeText(getContext(), "Warning: Image source \"" + uri + "\" doesn't exist", Toast.LENGTH_SHORT)
                    .show();
        }
    }
}