diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 5c99afe0589..0bfcf09a632 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -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 (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 { 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 new file mode 100644 index 00000000000..4d37f6c3a33 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -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 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 deleted file mode 100644 index 4af4776d593..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ /dev/null @@ -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 = 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 (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() - - 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() - } -}