diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 0bfcf09a632..5c99afe0589 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6640,34 +6640,35 @@ public class com/facebook/react/views/image/ReactImageManager$$PropsSetter : com public final class com/facebook/react/views/image/ReactImageManager$Companion { } -public class com/facebook/react/views/image/ReactImageView : com/facebook/drawee/view/GenericDraweeView { +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 static final field REMOTE_IMAGE_FADE_DURATION_MS I public fun (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 fun maybeUpdateView ()V + public final fun maybeUpdateView ()V public fun onDraw (Landroid/graphics/Canvas;)V - protected fun onSizeChanged (IIII)V public fun setBackgroundColor (I)V - 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 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 final class com/facebook/react/views/imagehelper/ImageSource { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java deleted file mode 100644 index 4d37f6c3a33..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ /dev/null @@ -1,751 +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. - */ - -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 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 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 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() { - @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 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 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 - // 2. Fabric preallocates ; 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 , which fabric preallocates. - // 4. Rinse and repeat. - if (ReactBuildConfig.DEBUG && !ReactFeatureFlags.enableBridgelessArchitecture) { - RNLog.w( - (ReactContext) getContext(), - "ReactImageView: Image source \"" + uri + "\" doesn't exist"); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt new file mode 100644 index 00000000000..bebfdb37baf --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -0,0 +1,707 @@ +/* + * 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.PixelUtil.toPixelFromDIP +import com.facebook.react.uimanager.Spacing +import com.facebook.react.uimanager.UIManagerHelper +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 = 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? = null + private var controllerForTesting: ControllerListener? = 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() { + 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 (useNewReactImageViewBackgroundDrawing()) { + reactBackgroundManager.backgroundColor = backgroundColor + } else if (this.backgroundColor != backgroundColor) { + this.backgroundColor = backgroundColor + backgroundImageDrawable = RoundedColorDrawable(backgroundColor) + isDirty = true + } + } + + public fun setBorderColor(borderColor: Int) { + 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 (useNewReactImageViewBackgroundDrawing()) { + reactBackgroundManager.setBorderWidth(Spacing.ALL, newBorderWidth) + } else if (!floatsEqual(this.borderWidth, newBorderWidth)) { + this.borderWidth = newBorderWidth + isDirty = true + } + } + + public fun setBorderRadius(borderRadius: Float) { + if (useNewReactImageViewBackgroundDrawing()) { + reactBackgroundManager.setBorderRadius(borderRadius) + } else if (!floatsEqual(this.borderRadius, borderRadius)) { + this.borderRadius = borderRadius + isDirty = true + } + } + + public fun setBorderRadius(borderRadius: Float, position: Int) { + 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() + + 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() + 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, 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 = + ForwardingControllerListener() + 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?) { + 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 + // 2. Fabric preallocates ; 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 , 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 { + 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() + } +}