Back out "Convert com.facebook.react.views.image to Kotlin"

Summary:
bypass-github-export-checks

Changelog:
[Internal] [Changed] - Back out "[RN][Android] Convert com.facebook.react.views.image to Kotlin"

Original commit changeset: 72450d37969b

Original Phabricator Diff: D60170084

Reviewed By: sammy-SC, rshest

Differential Revision: D60578451

fbshipit-source-id: b6c57eaf1e3cceff43b10680f6b9abf53cbf8605
This commit is contained in:
Nicola Corti
2024-08-01 13:31:06 -07:00
committed by Facebook GitHub Bot
parent 5f14963da0
commit 6393a3cd78
3 changed files with 774 additions and 751 deletions
@@ -6640,35 +6640,34 @@ public class com/facebook/react/views/image/ReactImageManager$$PropsSetter : com
public final class com/facebook/react/views/image/ReactImageManager$Companion {
}
public final class com/facebook/react/views/image/ReactImageView : com/facebook/drawee/view/GenericDraweeView {
public static final field Companion Lcom/facebook/react/views/image/ReactImageView$Companion;
public class com/facebook/react/views/image/ReactImageView : com/facebook/drawee/view/GenericDraweeView {
public static final field REMOTE_IMAGE_FADE_DURATION_MS I
public fun <init> (Landroid/content/Context;Lcom/facebook/drawee/controller/AbstractDraweeControllerBuilder;Lcom/facebook/react/views/image/GlobalImageLoadListener;Ljava/lang/Object;)V
public fun getImageSource ()Lcom/facebook/react/views/imagehelper/ImageSource;
public fun hasOverlappingRendering ()Z
public final fun maybeUpdateView ()V
public fun maybeUpdateView ()V
public fun onDraw (Landroid/graphics/Canvas;)V
protected fun onSizeChanged (IIII)V
public fun setBackgroundColor (I)V
public final fun setBlurRadius (F)V
public final fun setBorderColor (I)V
public final fun setBorderRadius (F)V
public final fun setBorderRadius (FI)V
public final fun setBorderWidth (F)V
public final fun setDefaultSource (Ljava/lang/String;)V
public final fun setFadeDuration (I)V
public final fun setHeaders (Lcom/facebook/react/bridge/ReadableMap;)V
public final fun setLoadingIndicatorSource (Ljava/lang/String;)V
public final fun setOverlayColor (I)V
public final fun setProgressiveRenderingEnabled (Z)V
public final fun setResizeMethod (Lcom/facebook/react/views/image/ImageResizeMethod;)V
public final fun setResizeMultiplier (F)V
public final fun setScaleType (Lcom/facebook/drawee/drawable/ScalingUtils$ScaleType;)V
public final fun setShouldNotifyLoadEvents (Z)V
public final fun setSource (Lcom/facebook/react/bridge/ReadableArray;)V
public final fun setTileMode (Landroid/graphics/Shader$TileMode;)V
public final fun updateCallerContext (Ljava/lang/Object;)V
}
public final class com/facebook/react/views/image/ReactImageView$Companion {
public fun setBlurRadius (F)V
public fun setBorderColor (I)V
public fun setBorderRadius (F)V
public fun setBorderRadius (FI)V
public fun setBorderWidth (F)V
public fun setControllerListener (Lcom/facebook/drawee/controller/ControllerListener;)V
public fun setDefaultSource (Ljava/lang/String;)V
public fun setFadeDuration (I)V
public fun setHeaders (Lcom/facebook/react/bridge/ReadableMap;)V
public fun setLoadingIndicatorSource (Ljava/lang/String;)V
public fun setOverlayColor (I)V
public fun setProgressiveRenderingEnabled (Z)V
public fun setResizeMethod (Lcom/facebook/react/views/image/ImageResizeMethod;)V
public fun setResizeMultiplier (F)V
public fun setScaleType (Lcom/facebook/drawee/drawable/ScalingUtils$ScaleType;)V
public fun setShouldNotifyLoadEvents (Z)V
public fun setSource (Lcom/facebook/react/bridge/ReadableArray;)V
public fun setTileMode (Landroid/graphics/Shader$TileMode;)V
public fun updateCallerContext (Ljava/lang/Object;)V
}
public final class com/facebook/react/views/imagehelper/ImageSource {
@@ -0,0 +1,751 @@
/*
* Copyright (c) Meta Platforms, Inc. and 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.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.facebook.common.internal.Objects;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.util.UriUtil;
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
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.infer.annotation.Nullsafe;
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.config.ReactFeatureFlags;
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
import com.facebook.react.modules.fresco.ReactNetworkImageRequest;
import com.facebook.react.uimanager.BackgroundStyleApplicator;
import com.facebook.react.uimanager.FloatUtil;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.util.RNLog;
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.react.views.view.ReactViewBackgroundManager;
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.
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
public class ReactImageView extends GenericDraweeView {
public static final int REMOTE_IMAGE_FADE_DURATION_MS = 300;
private static float[] sComputedCornerRadii = new float[4];
private ImageResizeMethod mResizeMethod = ImageResizeMethod.AUTO;
public void updateCallerContext(@Nullable Object callerContext) {
if (!Objects.equal(mCallerContext, callerContext)) {
mCallerContext = callerContext;
mIsDirty = true;
}
}
// 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 = new LinkedList<>();
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 = ImageResizeMode.defaultValue();
private Shader.TileMode mTileMode = ImageResizeMode.defaultTileMode();
private boolean mIsDirty;
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
private @Nullable TilePostprocessor mTilePostprocessor;
private @Nullable IterativeBoxBlurPostProcessor mIterativeBoxBlurPostProcessor;
private @Nullable ReactImageDownloadListener mDownloadListener;
private @Nullable ControllerListener mControllerForTesting;
private @Nullable GlobalImageLoadListener mGlobalImageLoadListener;
private @Nullable Object mCallerContext;
private int mFadeDurationMs = -1;
private boolean mProgressiveRenderingEnabled;
private @Nullable ReadableMap mHeaders = null;
private float mResizeMultiplier = 1.0f;
private ReactViewBackgroundManager mReactBackgroundManager;
// We can't specify rounding in XML, so have to do so here
private static GenericDraweeHierarchy buildHierarchy(Context context) {
RoundingParams roundingParams = RoundingParams.fromCornersRadius(0);
roundingParams.setPaintFilterBitmap(true);
return new GenericDraweeHierarchyBuilder(context.getResources())
.setRoundingParams(roundingParams)
.build();
}
public ReactImageView(
Context context,
AbstractDraweeControllerBuilder draweeControllerBuilder,
@Nullable GlobalImageLoadListener globalImageLoadListener,
@Nullable Object callerContext) {
super(context, buildHierarchy(context));
mDraweeControllerBuilder = draweeControllerBuilder;
mGlobalImageLoadListener = globalImageLoadListener;
mCallerContext = callerContext;
mReactBackgroundManager = new ReactViewBackgroundManager(this);
mReactBackgroundManager.setOverflow("hidden");
// Workaround Android bug where ImageView visibility is not propagated to the Drawable, so you
// have to manually update visibility. Will be resolved once we move to VitoView.
setLegacyVisibilityHandlingEnabled(true);
}
public void setShouldNotifyLoadEvents(boolean shouldNotify) {
// Skip update if shouldNotify is already in sync with the download listener
if (shouldNotify == (mDownloadListener != null)) {
return;
}
if (!shouldNotify) {
mDownloadListener = null;
} else {
final EventDispatcher mEventDispatcher =
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) getContext(), getId());
mDownloadListener =
new ReactImageDownloadListener<ImageInfo>() {
@Override
public void onProgressChange(int loaded, int total) {
// TODO: Somehow get image size and convert `loaded` and `total` to image bytes.
if (mEventDispatcher == null || mImageSource == null) {
return;
}
mEventDispatcher.dispatchEvent(
ImageLoadEvent.createProgressEvent(
UIManagerHelper.getSurfaceId(ReactImageView.this),
getId(),
mImageSource.getSource(),
loaded,
total));
}
@Override
public void onSubmit(String id, Object callerContext) {
if (mEventDispatcher == null) {
return;
}
mEventDispatcher.dispatchEvent(
ImageLoadEvent.createLoadStartEvent(
UIManagerHelper.getSurfaceId(ReactImageView.this), getId()));
}
@Override
public void onFinalImageSet(
String id, @Nullable final ImageInfo imageInfo, @Nullable Animatable animatable) {
if (imageInfo != null && mEventDispatcher != null && mImageSource != null) {
mEventDispatcher.dispatchEvent(
ImageLoadEvent.createLoadEvent(
UIManagerHelper.getSurfaceId(ReactImageView.this),
getId(),
mImageSource.getSource(),
imageInfo.getWidth(),
imageInfo.getHeight()));
mEventDispatcher.dispatchEvent(
ImageLoadEvent.createLoadEndEvent(
UIManagerHelper.getSurfaceId(ReactImageView.this), getId()));
}
}
@Override
public void onFailure(String id, Throwable throwable) {
if (mEventDispatcher == null) {
return;
}
mEventDispatcher.dispatchEvent(
ImageLoadEvent.createErrorEvent(
UIManagerHelper.getSurfaceId(ReactImageView.this), getId(), throwable));
}
};
}
mIsDirty = true;
}
public void setBlurRadius(float blurRadius) {
// Divide `blurRadius` by 2 to more closely match other platforms.
int pixelBlurRadius = (int) PixelUtil.toPixelFromDIP(blurRadius) / 2;
if (pixelBlurRadius == 0) {
mIterativeBoxBlurPostProcessor = null;
} else {
mIterativeBoxBlurPostProcessor = new IterativeBoxBlurPostProcessor(2, pixelBlurRadius);
}
mIsDirty = true;
}
@Override
public void setBackgroundColor(int backgroundColor) {
if (ReactNativeFeatureFlags.useNewReactImageViewBackgroundDrawing()) {
mReactBackgroundManager.setBackgroundColor(backgroundColor);
} else if (mBackgroundColor != backgroundColor) {
mBackgroundColor = backgroundColor;
mBackgroundImageDrawable = new RoundedColorDrawable(backgroundColor);
mIsDirty = true;
}
}
public void setBorderColor(int borderColor) {
if (ReactNativeFeatureFlags.useNewReactImageViewBackgroundDrawing()) {
mReactBackgroundManager.setBorderColor(Spacing.ALL, borderColor);
} else if (mBorderColor != borderColor) {
mBorderColor = borderColor;
mIsDirty = true;
}
}
public void setOverlayColor(int overlayColor) {
if (mOverlayColor != overlayColor) {
mOverlayColor = overlayColor;
mIsDirty = true;
}
}
public void setBorderWidth(float borderWidth) {
float newBorderWidth = PixelUtil.toPixelFromDIP(borderWidth);
if (ReactNativeFeatureFlags.useNewReactImageViewBackgroundDrawing()) {
mReactBackgroundManager.setBorderWidth(Spacing.ALL, newBorderWidth);
} else if (!FloatUtil.floatsEqual(mBorderWidth, newBorderWidth)) {
mBorderWidth = newBorderWidth;
mIsDirty = true;
}
}
public void setBorderRadius(float borderRadius) {
if (ReactNativeFeatureFlags.useNewReactImageViewBackgroundDrawing()) {
mReactBackgroundManager.setBorderRadius(borderRadius);
} else if (!FloatUtil.floatsEqual(mBorderRadius, borderRadius)) {
mBorderRadius = borderRadius;
mIsDirty = true;
}
}
public void setBorderRadius(float borderRadius, int position) {
if (ReactNativeFeatureFlags.useNewReactImageViewBackgroundDrawing()) {
mReactBackgroundManager.setBorderRadius(borderRadius, position + 1);
} else {
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) {
if (mScaleType != scaleType) {
mScaleType = scaleType;
mIsDirty = true;
}
}
public void setTileMode(Shader.TileMode tileMode) {
if (mTileMode != tileMode) {
mTileMode = tileMode;
if (isTiled()) {
mTilePostprocessor = new TilePostprocessor();
} else {
mTilePostprocessor = null;
}
mIsDirty = true;
}
}
public void setResizeMethod(ImageResizeMethod resizeMethod) {
if (mResizeMethod != resizeMethod) {
mResizeMethod = resizeMethod;
mIsDirty = true;
}
}
public void setResizeMultiplier(float multiplier) {
boolean isNewMultiplier = Math.abs(mResizeMultiplier - multiplier) > 0.0001f;
if (isNewMultiplier) {
mResizeMultiplier = multiplier;
mIsDirty = true;
}
}
public void setSource(@Nullable ReadableArray sources) {
List<ImageSource> tmpSources = new LinkedList<>();
if (sources == null || sources.size() == 0) {
tmpSources.add(ImageSource.getTransparentBitmapImageSource(getContext()));
} else if (sources.size() == 1) {
// Optimize for the case where we have just one uri, case in which we don't need the sizes
ReadableMap source = sources.getMap(0);
ImageSource imageSource = new ImageSource(getContext(), source.getString("uri"));
if (Uri.EMPTY.equals(imageSource.getUri())) {
warnImageSource(source.getString("uri"));
imageSource = ImageSource.getTransparentBitmapImageSource(getContext());
}
tmpSources.add(imageSource);
} else {
for (int idx = 0; idx < sources.size(); idx++) {
ReadableMap source = sources.getMap(idx);
ImageSource imageSource =
new ImageSource(
getContext(),
source.getString("uri"),
source.getDouble("width"),
source.getDouble("height"));
if (Uri.EMPTY.equals(imageSource.getUri())) {
warnImageSource(source.getString("uri"));
imageSource = ImageSource.getTransparentBitmapImageSource(getContext());
}
tmpSources.add(imageSource);
}
}
// Don't reset sources and dirty node if sources haven't changed
if (mSources.equals(tmpSources)) {
return;
}
mSources.clear();
for (ImageSource src : tmpSources) {
mSources.add(src);
}
mIsDirty = true;
}
public void setDefaultSource(@Nullable String name) {
Drawable newDefaultDrawable =
ResourceDrawableIdHelper.getInstance().getResourceDrawable(getContext(), name);
if (!Objects.equal(mDefaultImageDrawable, newDefaultDrawable)) {
mDefaultImageDrawable = newDefaultDrawable;
mIsDirty = true;
}
}
public void setLoadingIndicatorSource(@Nullable String name) {
Drawable drawable =
ResourceDrawableIdHelper.getInstance().getResourceDrawable(getContext(), name);
Drawable newLoadingIndicatorSource =
drawable != null ? (Drawable) new AutoRotateDrawable(drawable, 1000) : null;
if (!Objects.equal(mLoadingImageDrawable, newLoadingIndicatorSource)) {
mLoadingImageDrawable = newLoadingIndicatorSource;
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 getCornerRadii(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);
}
getCornerRadii(sComputedCornerRadii);
RoundingParams roundingParams = hierarchy.getRoundingParams();
if (roundingParams != null) {
roundingParams.setCornersRadii(
sComputedCornerRadii[0],
sComputedCornerRadii[1],
sComputedCornerRadii[2],
sComputedCornerRadii[3]);
if (mBackgroundImageDrawable != null) {
mBackgroundImageDrawable.setBorder(mBorderColor, mBorderWidth);
mBackgroundImageDrawable.setRadii(roundingParams.getCornersRadii());
hierarchy.setBackgroundImage(mBackgroundImageDrawable);
}
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);
Drawable drawable = getDrawableIfUnsupported(mImageSource);
if (drawable != null) {
maybeUpdateViewFromDrawable(drawable);
} else {
maybeUpdateViewFromRequest(doResize);
}
mIsDirty = false;
}
private void maybeUpdateViewFromRequest(boolean doResize) {
if (mImageSource == null) {
return;
}
List<Postprocessor> postprocessors = new LinkedList<>();
if (mIterativeBoxBlurPostProcessor != null) {
postprocessors.add(mIterativeBoxBlurPostProcessor);
}
if (mTilePostprocessor != null) {
postprocessors.add(mTilePostprocessor);
}
Postprocessor postprocessor = MultiPostprocessor.from(postprocessors);
ResizeOptions resizeOptions = doResize ? getResizeOptions() : 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)
.setOldController(getController())
.setImageRequest(imageRequest);
if (mCallerContext != null) {
mDraweeControllerBuilder.setCallerContext(mCallerContext);
}
if (mCachedImageSource != null) {
ImageRequest cachedImageRequest =
ImageRequestBuilder.newBuilderWithSource(mCachedImageSource.getUri())
.setPostprocessor(postprocessor)
.setResizeOptions(resizeOptions)
.setAutoRotateEnabled(true)
.setProgressiveRenderingEnabled(mProgressiveRenderingEnabled)
.build();
mDraweeControllerBuilder.setLowResImageRequest(cachedImageRequest);
}
if (mDownloadListener != null && mControllerForTesting != null) {
ForwardingControllerListener combinedListener = new ForwardingControllerListener();
combinedListener.addListener(mDownloadListener);
combinedListener.addListener(mControllerForTesting);
mDraweeControllerBuilder.setControllerListener(combinedListener);
} else if (mControllerForTesting != null) {
mDraweeControllerBuilder.setControllerListener(mControllerForTesting);
} else if (mDownloadListener != null) {
mDraweeControllerBuilder.setControllerListener(mDownloadListener);
}
if (mDownloadListener != null) {
getHierarchy().setProgressBarImage(mDownloadListener);
}
setController(mDraweeControllerBuilder.build());
// Reset again so the DraweeControllerBuilder clears all it's references. Otherwise, this causes
// a memory leak.
mDraweeControllerBuilder.reset();
}
private void maybeUpdateViewFromDrawable(Drawable drawable) {
final boolean shouldNotify = mDownloadListener != null;
final EventDispatcher mEventDispatcher =
shouldNotify
? UIManagerHelper.getEventDispatcherForReactTag((ReactContext) getContext(), getId())
: null;
if (mEventDispatcher != null) {
mEventDispatcher.dispatchEvent(
ImageLoadEvent.createLoadStartEvent(
UIManagerHelper.getSurfaceId(ReactImageView.this), getId()));
}
getHierarchy().setImage(drawable, 1, false);
if (mEventDispatcher != null && mImageSource != null) {
mEventDispatcher.dispatchEvent(
ImageLoadEvent.createLoadEvent(
UIManagerHelper.getSurfaceId(ReactImageView.this),
getId(),
mImageSource.getSource(),
getWidth(),
getHeight()));
mEventDispatcher.dispatchEvent(
ImageLoadEvent.createLoadEndEvent(
UIManagerHelper.getSurfaceId(ReactImageView.this), getId()));
}
}
// VisibleForTesting
public void setControllerListener(ControllerListener controllerListener) {
mControllerForTesting = controllerListener;
mIsDirty = true;
maybeUpdateView();
}
// VisibleForTesting
public @Nullable ImageSource getImageSource() {
return mImageSource;
}
@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();
}
}
@Override
public boolean hasOverlappingRendering() {
return mBackgroundImageDrawable != null || super.hasOverlappingRendering();
}
@Override
public void onDraw(Canvas canvas) {
if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) {
BackgroundStyleApplicator.clipToPaddingBox(this, canvas);
} else if (ReactNativeFeatureFlags.useNewReactImageViewBackgroundDrawing()) {
mReactBackgroundManager.maybeClipToPaddingBox(canvas);
}
super.onDraw(canvas);
}
private boolean hasMultipleSources() {
return mSources.size() > 1;
}
private boolean isTiled() {
return mTileMode != Shader.TileMode.CLAMP;
}
private void setSourceImage() {
mImageSource = null;
if (mSources.isEmpty()) {
mSources.add(ImageSource.getTransparentBitmapImageSource(getContext()));
} else if (hasMultipleSources()) {
MultiSourceResult multiSource =
MultiSourceHelper.getBestSourceForSize(getWidth(), getHeight(), mSources);
mImageSource = multiSource.bestResult;
mCachedImageSource = multiSource.bestResultInCache;
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;
}
}
/**
* Checks if the provided ImageSource should not be requested through Fresco and instead loaded
* directly from the resources table. Fresco explicitly does not support a number of drawable
* types like VectorDrawable but they can still be mounted in the image hierarchy.
*
* @param imageSource
* @return drawable resource if Fresco cannot load the image, null otherwise
*/
private @Nullable Drawable getDrawableIfUnsupported(ImageSource imageSource) {
if (!ReactNativeFeatureFlags.loadVectorDrawablesOnImages()) {
return null;
}
String resourceName = imageSource.getSource();
if (!imageSource.isResource() || resourceName == null) {
return null;
}
ResourceDrawableIdHelper drawableHelper = ResourceDrawableIdHelper.getInstance();
boolean isVectorDrawable = drawableHelper.isVectorDrawable(getContext(), resourceName);
if (!isVectorDrawable) {
return null;
}
return drawableHelper.getResourceDrawable(getContext(), resourceName);
}
@Nullable
private ResizeOptions getResizeOptions() {
int width = Math.round((float) getWidth() * mResizeMultiplier);
int height = Math.round((float) getHeight() * mResizeMultiplier);
if (width <= 0 || height <= 0) {
return null;
}
return new ResizeOptions(width, height);
}
private void warnImageSource(@Nullable String uri) {
// TODO(T189014077): This code-path produces an infinite loop of js calls with logbox.
// This is an issue with Fabric view preallocation, react, and LogBox. Fix.
// The bug:
// 1. An app renders an <Image/>
// 2. Fabric preallocates <Image/>; sets a null src to ReactImageView (potential problem?).
// 3. ReactImageView detects the null src; displays a warning in LogBox (via this code).
// 3. LogBox renders an <Image/>, which fabric preallocates.
// 4. Rinse and repeat.
if (ReactBuildConfig.DEBUG && !ReactFeatureFlags.enableBridgelessArchitecture) {
RNLog.w(
(ReactContext) getContext(),
"ReactImageView: Image source \"" + uri + "\" doesn't exist");
}
}
}
@@ -1,727 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION") // As we depend on ReactFeatureFlags still
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.Rect
import android.graphics.Shader
import android.graphics.Shader.TileMode
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.net.Uri
import com.facebook.common.references.CloseableReference
import com.facebook.common.util.UriUtil
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder
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.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.CloseableImage
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.annotations.UnstableReactNativeAPI
import com.facebook.react.common.annotations.VisibleForTesting
import com.facebook.react.common.build.ReactBuildConfig
import com.facebook.react.config.ReactFeatureFlags
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags.enableBackgroundStyleApplicator
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags.loadVectorDrawablesOnImages
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags.useNewReactImageViewBackgroundDrawing
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.FloatUtil.floatsEqual
import com.facebook.react.uimanager.LengthPercentage
import com.facebook.react.uimanager.LengthPercentageType
import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP
import com.facebook.react.uimanager.Spacing
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.style.BorderRadiusProp
import com.facebook.react.uimanager.style.LogicalEdge
import com.facebook.react.util.RNLog
import com.facebook.react.views.image.ImageLoadEvent.Companion.createErrorEvent
import com.facebook.react.views.image.ImageLoadEvent.Companion.createLoadEndEvent
import com.facebook.react.views.image.ImageLoadEvent.Companion.createLoadEvent
import com.facebook.react.views.image.ImageLoadEvent.Companion.createLoadStartEvent
import com.facebook.react.views.image.ImageLoadEvent.Companion.createProgressEvent
import com.facebook.react.views.image.ImageResizeMode.defaultTileMode
import com.facebook.react.views.image.ImageResizeMode.defaultValue
import com.facebook.react.views.image.MultiPostprocessor.Companion.from
import com.facebook.react.views.imagehelper.ImageSource
import com.facebook.react.views.imagehelper.ImageSource.Companion.getTransparentBitmapImageSource
import com.facebook.react.views.imagehelper.MultiSourceHelper.getBestSourceForSize
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper.Companion.instance
import com.facebook.react.views.view.ReactViewBackgroundManager
import com.facebook.yoga.YogaConstants
import kotlin.math.abs
/**
* Wrapper class around Fresco's GenericDraweeView, enabling persisting props across multiple view
* update and consistent processing of both static and network images.
*/
@OptIn(UnstableReactNativeAPI::class)
public class ReactImageView(
context: Context,
private val draweeControllerBuilder: AbstractDraweeControllerBuilder<*, *, *, *>,
private val globalImageLoadListener: GlobalImageLoadListener?,
private var callerContext: Any?
) : GenericDraweeView(context, buildHierarchy(context)) {
private val sources: MutableList<ImageSource> = mutableListOf()
internal var imageSource: ImageSource? = null
private var cachedImageSource: ImageSource? = null
private var defaultImageDrawable: Drawable? = null
private var loadingImageDrawable: Drawable? = null
private var backgroundImageDrawable: RoundedColorDrawable? = null
private var backgroundColor = 0x00000000
private var borderColor = 0
private var overlayColor = 0
private var borderWidth = 0f
private var borderRadius = YogaConstants.UNDEFINED
private var borderCornerRadii: FloatArray? = null
private var scaleType = defaultValue()
private var tileMode = defaultTileMode()
private var isDirty = false
private var tilePostprocessor: TilePostprocessor? = null
private var iterativeBoxBlurPostProcessor: IterativeBoxBlurPostProcessor? = null
private var downloadListener: ReactImageDownloadListener<ImageInfo>? = null
private var controllerForTesting: ControllerListener<ImageInfo>? = null
private var fadeDurationMs = -1
private var progressiveRenderingEnabled = false
private var headers: ReadableMap? = null
private var resizeMultiplier = 1.0f
private val reactBackgroundManager = ReactViewBackgroundManager(this)
private var resizeMethod = ImageResizeMethod.AUTO
init {
reactBackgroundManager.setOverflow("hidden")
// Workaround Android bug where ImageView visibility is not propagated to the Drawable, so you
// have to manually update visibility. Will be resolved once we move to VitoView.
setLegacyVisibilityHandlingEnabled(true)
}
public fun updateCallerContext(callerContext: Any?) {
if (this.callerContext != callerContext) {
this.callerContext = callerContext
isDirty = true
}
}
public fun setShouldNotifyLoadEvents(shouldNotify: Boolean) {
// Skip update if shouldNotify is already in sync with the download listener
if (shouldNotify == (downloadListener != null)) {
return
}
if (!shouldNotify) {
downloadListener = null
} else {
val eventDispatcher =
UIManagerHelper.getEventDispatcherForReactTag((context as ReactContext), id)
downloadListener =
object : ReactImageDownloadListener<ImageInfo>() {
override fun onProgressChange(loaded: Int, total: Int) {
// TODO: Somehow get image size and convert `loaded` and `total` to image bytes.
if (eventDispatcher == null || imageSource == null) {
return
}
// TODO: Somehow get image size and convert `loaded` and `total` to image bytes.
eventDispatcher.dispatchEvent(
createProgressEvent(
UIManagerHelper.getSurfaceId(this@ReactImageView),
id,
imageSource?.source,
loaded,
total))
}
override fun onSubmit(id: String, callerContext: Any) {
if (eventDispatcher == null) {
return
}
eventDispatcher.dispatchEvent(
createLoadStartEvent(UIManagerHelper.getSurfaceId(this@ReactImageView), getId()))
}
override fun onFinalImageSet(
id: String,
imageInfo: ImageInfo?,
animatable: Animatable?
) {
if (imageInfo != null && imageSource != null && eventDispatcher != null) {
eventDispatcher.dispatchEvent(
createLoadEvent(
UIManagerHelper.getSurfaceId(this@ReactImageView),
getId(),
imageSource?.source,
imageInfo.width,
imageInfo.height))
eventDispatcher.dispatchEvent(
createLoadEndEvent(UIManagerHelper.getSurfaceId(this@ReactImageView), getId()))
}
}
override fun onFailure(id: String, throwable: Throwable) {
if (eventDispatcher == null) {
return
}
eventDispatcher.dispatchEvent(
createErrorEvent(
UIManagerHelper.getSurfaceId(this@ReactImageView), getId(), throwable))
}
}
}
isDirty = true
}
public fun setBlurRadius(blurRadius: Float) {
// Divide `blurRadius` by 2 to more closely match other platforms.
val pixelBlurRadius = toPixelFromDIP(blurRadius).toInt() / 2
iterativeBoxBlurPostProcessor =
if (pixelBlurRadius == 0) {
null
} else {
IterativeBoxBlurPostProcessor(2, pixelBlurRadius)
}
isDirty = true
}
public override fun setBackgroundColor(backgroundColor: Int) {
if (enableBackgroundStyleApplicator()) {
BackgroundStyleApplicator.setBackgroundColor(this, backgroundColor)
} else if (useNewReactImageViewBackgroundDrawing()) {
reactBackgroundManager.backgroundColor = backgroundColor
} else if (this.backgroundColor != backgroundColor) {
this.backgroundColor = backgroundColor
backgroundImageDrawable = RoundedColorDrawable(backgroundColor)
isDirty = true
}
}
public fun setBorderColor(borderColor: Int) {
if (enableBackgroundStyleApplicator()) {
BackgroundStyleApplicator.setBorderColor(this, LogicalEdge.ALL, borderColor)
} else if (useNewReactImageViewBackgroundDrawing()) {
reactBackgroundManager.setBorderColor(Spacing.ALL, borderColor)
} else if (this.borderColor != borderColor) {
this.borderColor = borderColor
isDirty = true
}
}
public fun setOverlayColor(overlayColor: Int) {
if (this.overlayColor != overlayColor) {
this.overlayColor = overlayColor
isDirty = true
}
}
public fun setBorderWidth(borderWidth: Float) {
val newBorderWidth = toPixelFromDIP(borderWidth)
if (enableBackgroundStyleApplicator()) {
BackgroundStyleApplicator.setBorderWidth(this, LogicalEdge.ALL, borderWidth)
} else if (useNewReactImageViewBackgroundDrawing()) {
reactBackgroundManager.setBorderWidth(Spacing.ALL, newBorderWidth)
} else if (!floatsEqual(this.borderWidth, newBorderWidth)) {
this.borderWidth = newBorderWidth
isDirty = true
}
}
public fun setBorderRadius(borderRadius: Float) {
if (enableBackgroundStyleApplicator()) {
val radius =
if (borderRadius.isNaN()) null
else LengthPercentage(borderRadius, LengthPercentageType.POINT)
BackgroundStyleApplicator.setBorderRadius(this, BorderRadiusProp.BORDER_RADIUS, radius)
} else if (useNewReactImageViewBackgroundDrawing()) {
reactBackgroundManager.setBorderRadius(borderRadius)
} else if (!floatsEqual(this.borderRadius, borderRadius)) {
this.borderRadius = borderRadius
isDirty = true
}
}
public fun setBorderRadius(borderRadius: Float, position: Int) {
if (enableBackgroundStyleApplicator()) {
val radius =
if (borderRadius.isNaN()) null
else LengthPercentage(borderRadius, LengthPercentageType.POINT)
BackgroundStyleApplicator.setBorderRadius(this, BorderRadiusProp.values()[position], radius)
} else if (useNewReactImageViewBackgroundDrawing()) {
reactBackgroundManager.setBorderRadius(borderRadius, position + 1)
} else {
if (borderCornerRadii == null) {
borderCornerRadii = FloatArray(4) { YogaConstants.UNDEFINED }
}
if (!floatsEqual(borderCornerRadii?.get(position), borderRadius)) {
borderCornerRadii?.set(position, borderRadius)
isDirty = true
}
}
}
public fun setScaleType(scaleType: ScalingUtils.ScaleType) {
if (this.scaleType !== scaleType) {
this.scaleType = scaleType
isDirty = true
}
}
public fun setTileMode(tileMode: TileMode) {
if (this.tileMode != tileMode) {
this.tileMode = tileMode
tilePostprocessor = if (isTiled) TilePostprocessor() else null
isDirty = true
}
}
public fun setResizeMethod(resizeMethod: ImageResizeMethod) {
if (this.resizeMethod != resizeMethod) {
this.resizeMethod = resizeMethod
isDirty = true
}
}
public fun setResizeMultiplier(multiplier: Float) {
val isNewMultiplier = abs((resizeMultiplier - multiplier).toDouble()) > 0.0001f
if (isNewMultiplier) {
resizeMultiplier = multiplier
isDirty = true
}
}
public fun setSource(sources: ReadableArray?) {
val tmpSources = mutableListOf<ImageSource>()
if (sources == null || sources.size() == 0) {
tmpSources.add(getTransparentBitmapImageSource(context))
} else if (sources.size() == 1) {
// Optimize for the case where we have just one uri, case in which we don't need the sizes
val source = sources.getMap(0)
var imageSource = ImageSource(context, source.getString("uri"))
if (Uri.EMPTY == imageSource.uri) {
warnImageSource(source.getString("uri"))
imageSource = getTransparentBitmapImageSource(context)
}
tmpSources.add(imageSource)
} else {
for (idx in 0 until sources.size()) {
val source = sources.getMap(idx)
var imageSource =
ImageSource(
context,
source.getString("uri"),
source.getDouble("width"),
source.getDouble("height"))
if (Uri.EMPTY == imageSource.uri) {
warnImageSource(source.getString("uri"))
imageSource = getTransparentBitmapImageSource(context)
}
tmpSources.add(imageSource)
}
}
// Don't reset sources and dirty node if sources haven't changed
if (this.sources == tmpSources) {
return
}
this.sources.clear()
this.sources.addAll(tmpSources)
isDirty = true
}
public fun setDefaultSource(name: String?) {
val newDefaultDrawable = instance.getResourceDrawable(context, name)
if (defaultImageDrawable != newDefaultDrawable) {
defaultImageDrawable = newDefaultDrawable
isDirty = true
}
}
public fun setLoadingIndicatorSource(name: String?) {
val drawable = instance.getResourceDrawable(context, name)
val newLoadingIndicatorSource = drawable?.let { AutoRotateDrawable(it, 1000) }
if (loadingImageDrawable != newLoadingIndicatorSource) {
loadingImageDrawable = newLoadingIndicatorSource
isDirty = true
}
}
public fun setProgressiveRenderingEnabled(enabled: Boolean) {
progressiveRenderingEnabled = enabled
// no worth marking as dirty if it already rendered.
}
public fun setFadeDuration(durationMs: Int) {
fadeDurationMs = durationMs
// no worth marking as dirty if it already rendered.
}
public fun setHeaders(headers: ReadableMap?) {
this.headers = headers
}
public override fun hasOverlappingRendering(): Boolean =
backgroundImageDrawable != null || super.hasOverlappingRendering()
public override fun onDraw(canvas: Canvas) {
if (enableBackgroundStyleApplicator()) {
BackgroundStyleApplicator.clipToPaddingBox(this, canvas)
} else if (useNewReactImageViewBackgroundDrawing()) {
reactBackgroundManager.maybeClipToPaddingBox(canvas)
}
super.onDraw(canvas)
}
public fun maybeUpdateView() {
if (!isDirty) {
return
}
if (hasMultipleSources() && (width <= 0 || height <= 0)) {
// If we need to choose from multiple uris but the size is not yet set, wait for layout pass
return
}
setSourceImage()
val imageSourceSafe = this.imageSource ?: return
val doResize = shouldResize(imageSourceSafe)
if (doResize && (width <= 0 || height <= 0)) {
// If need a resize and the size is not yet set, wait until the layout pass provides one
return
}
if (isTiled && (width <= 0 || height <= 0)) {
// If need to tile and the size is not yet set, wait until the layout pass provides one
return
}
// We store this in a local variable as it's coming from super.getHierarchy()
val hierarchy = this.hierarchy
hierarchy.actualImageScaleType = scaleType
if (defaultImageDrawable != null) {
hierarchy.setPlaceholderImage(defaultImageDrawable, scaleType)
}
if (loadingImageDrawable != null) {
hierarchy.setPlaceholderImage(loadingImageDrawable, ScalingUtils.ScaleType.CENTER)
}
getCornerRadii(computedCornerRadii)
val roundingParams = hierarchy.roundingParams
if (roundingParams != null) {
roundingParams.setCornersRadii(
computedCornerRadii[0],
computedCornerRadii[1],
computedCornerRadii[2],
computedCornerRadii[3])
backgroundImageDrawable?.let { background ->
background.setBorder(borderColor, borderWidth)
roundingParams.cornersRadii?.let { background.radii = it }
hierarchy.setBackgroundImage(background)
}
roundingParams.setBorder(borderColor, borderWidth)
if (overlayColor != Color.TRANSPARENT) {
roundingParams.setOverlayColor(overlayColor)
} else {
// make sure the default rounding method is used.
roundingParams.setRoundingMethod(RoundingParams.RoundingMethod.BITMAP_ONLY)
}
hierarchy.roundingParams = roundingParams
}
hierarchy.fadeDuration =
when {
fadeDurationMs >= 0 -> fadeDurationMs
imageSourceSafe.isResource -> 0
else -> REMOTE_IMAGE_FADE_DURATION_MS
}
val drawable = getDrawableIfUnsupported(imageSourceSafe)
if (drawable != null) {
maybeUpdateViewFromDrawable(drawable)
} else {
maybeUpdateViewFromRequest(doResize)
}
isDirty = false
}
private fun maybeUpdateViewFromRequest(doResize: Boolean) {
if (imageSource == null) {
return
}
val postprocessorList = mutableListOf<Postprocessor>()
iterativeBoxBlurPostProcessor?.let { postprocessorList.add(it) }
tilePostprocessor?.let { postprocessorList.add(it) }
val postprocessor = from(postprocessorList)
val resizeOptions = if (doResize) resizeOptions else null
val imageSourceSafe = this.imageSource ?: return
val imageRequestBuilder =
ImageRequestBuilder.newBuilderWithSource(imageSourceSafe.uri)
.setPostprocessor(postprocessor)
.setResizeOptions(resizeOptions)
.setAutoRotateEnabled(true)
.setProgressiveRenderingEnabled(progressiveRenderingEnabled)
val imageRequest: ImageRequest =
ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers)
globalImageLoadListener?.onLoadAttempt(imageSourceSafe.uri)
@Suppress("UNCHECKED_CAST") // Unsafe cast necessary as this java class used raw generics
val builder =
draweeControllerBuilder
as
AbstractDraweeControllerBuilder<
*, ImageRequest, CloseableReference<CloseableImage>, ImageInfo>
// This builder is reused
builder.reset()
builder.setImageRequest(imageRequest).setAutoPlayAnimations(true).setOldController(controller)
callerContext?.let { builder.setCallerContext(it) }
cachedImageSource?.let { cachedSource ->
val cachedImageRequest =
ImageRequestBuilder.newBuilderWithSource(cachedSource.uri)
.setPostprocessor(postprocessor)
.setResizeOptions(resizeOptions)
.setAutoRotateEnabled(true)
.setProgressiveRenderingEnabled(progressiveRenderingEnabled)
.build()
builder.setLowResImageRequest(cachedImageRequest)
}
if (downloadListener != null && controllerForTesting != null) {
val combinedListener: ForwardingControllerListener<ImageInfo> =
ForwardingControllerListener<ImageInfo>()
combinedListener.addListener(downloadListener)
combinedListener.addListener(controllerForTesting)
builder.setControllerListener(combinedListener)
} else if (controllerForTesting != null) {
builder.setControllerListener(controllerForTesting)
} else if (downloadListener != null) {
builder.setControllerListener(downloadListener)
}
if (downloadListener != null) {
hierarchy.setProgressBarImage(downloadListener)
}
controller = builder.build()
// Reset again so the DraweeControllerBuilder clears all it's references. Otherwise, this causes
// a memory leak.
builder.reset()
}
private fun maybeUpdateViewFromDrawable(drawable: Drawable) {
val shouldNotify = downloadListener != null
val eventDispatcher =
if (shouldNotify) {
UIManagerHelper.getEventDispatcherForReactTag((context as ReactContext), id)
} else {
null
}
eventDispatcher?.dispatchEvent(
createLoadStartEvent(UIManagerHelper.getSurfaceId(this@ReactImageView), id))
hierarchy.setImage(drawable, 1f, false)
if (eventDispatcher != null && imageSource != null) {
eventDispatcher.dispatchEvent(
createLoadEvent(
UIManagerHelper.getSurfaceId(this@ReactImageView),
id,
imageSource?.source,
width,
height))
eventDispatcher.dispatchEvent(
createLoadEndEvent(UIManagerHelper.getSurfaceId(this@ReactImageView), id))
}
}
private fun getCornerRadii(computedCorners: FloatArray) {
val defaultBorderRadius = if (!YogaConstants.isUndefined(borderRadius)) borderRadius else 0f
val radii = borderCornerRadii ?: FloatArray(4) { Float.NaN }
computedCorners[0] = if (!YogaConstants.isUndefined(radii[0])) radii[0] else defaultBorderRadius
computedCorners[1] = if (!YogaConstants.isUndefined(radii[1])) radii[1] else defaultBorderRadius
computedCorners[2] = if (!YogaConstants.isUndefined(radii[2])) radii[2] else defaultBorderRadius
computedCorners[3] = if (!YogaConstants.isUndefined(radii[3])) radii[3] else defaultBorderRadius
}
@VisibleForTesting
public fun setControllerListener(controllerListener: ControllerListener<ImageInfo>?) {
controllerForTesting = controllerListener
isDirty = true
maybeUpdateView()
}
protected override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w > 0 && h > 0) {
isDirty = isDirty || hasMultipleSources() || isTiled
maybeUpdateView()
}
}
private fun hasMultipleSources(): Boolean = sources.size > 1
private val isTiled: Boolean
get() = tileMode != TileMode.CLAMP
private fun setSourceImage() {
imageSource = null
if (sources.isEmpty()) {
sources.add(getTransparentBitmapImageSource(context))
} else if (hasMultipleSources()) {
val multiSource = getBestSourceForSize(width, height, sources)
imageSource = multiSource.bestResult
cachedImageSource = multiSource.bestResultInCache
return
}
imageSource = sources[0]
}
private fun shouldResize(imageSource: ImageSource): Boolean =
// 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
when (resizeMethod) {
ImageResizeMethod.AUTO ->
(UriUtil.isLocalContentUri(imageSource.uri) || UriUtil.isLocalFileUri(imageSource.uri))
ImageResizeMethod.RESIZE -> true
else -> false
}
/**
* Checks if the provided ImageSource should not be requested through Fresco and instead loaded
* directly from the resources table. Fresco explicitly does not support a number of drawable
* types like VectorDrawable but they can still be mounted in the image hierarchy.
*
* @param imageSource
* @return drawable resource if Fresco cannot load the image, null otherwise
*/
private fun getDrawableIfUnsupported(imageSource: ImageSource): Drawable? {
if (!loadVectorDrawablesOnImages()) {
return null
}
val resourceName = imageSource.source
if (!imageSource.isResource || resourceName == null) {
return null
}
val drawableHelper = instance
val isVectorDrawable = drawableHelper.isVectorDrawable(context, resourceName)
if (!isVectorDrawable) {
return null
}
return drawableHelper.getResourceDrawable(context, resourceName)
}
private val resizeOptions: ResizeOptions?
get() {
val width = Math.round(width.toFloat() * resizeMultiplier)
val height = Math.round(height.toFloat() * resizeMultiplier)
if (width <= 0 || height <= 0) {
return null
}
return ResizeOptions(width, height)
}
private fun warnImageSource(uri: String?) {
// TODO(T189014077): This code-path produces an infinite loop of js calls with logbox.
// This is an issue with Fabric view preallocation, react, and LogBox. Fix.
// The bug:
// 1. An app renders an <Image/>
// 2. Fabric preallocates <Image/>; sets a null src to ReactImageView (potential problem?).
// 3. ReactImageView detects the null src; displays a warning in LogBox (via this code).
// 3. LogBox renders an <Image/>, which fabric preallocates.
// 4. Rinse and repeat.
if (ReactBuildConfig.DEBUG && !ReactFeatureFlags.enableBridgelessArchitecture) {
RNLog.w(context as ReactContext, "ReactImageView: Image source \"$uri\" doesn't exist")
}
}
private inner class TilePostprocessor : BasePostprocessor() {
override fun process(
source: Bitmap,
bitmapFactory: PlatformBitmapFactory
): CloseableReference<Bitmap> {
val destRect = Rect(0, 0, width, height)
scaleType.getTransform(tileMatrix, destRect, source.width, source.height, 0.0f, 0.0f)
val paint = Paint()
paint.isAntiAlias = true
val shader: Shader = BitmapShader(source, tileMode, tileMode)
shader.setLocalMatrix(tileMatrix)
paint.setShader(shader)
val output = bitmapFactory.createBitmap(width, height)
try {
val canvas = Canvas(output.get())
canvas.drawRect(destRect, paint)
return output.clone()
} finally {
CloseableReference.closeSafely(output)
}
}
}
public companion object {
public const val REMOTE_IMAGE_FADE_DURATION_MS: Int = 300
private val computedCornerRadii = FloatArray(4)
// Fresco lacks support for repeating images, see https://github.com/facebook/fresco/issues/1575
// We implement it here as a postprocessing step.
private val tileMatrix = Matrix()
// We can't specify rounding in XML, so have to do so here
private fun buildHierarchy(context: Context) =
GenericDraweeHierarchyBuilder(context.resources)
.setRoundingParams(
RoundingParams.fromCornersRadius(0f).apply { setPaintFilterBitmap(true) })
.build()
}
}