/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.image;
import javax.annotation.Nullable;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.net.Uri;
import com.facebook.common.util.UriUtil;
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
import com.facebook.drawee.controller.ControllerListener;
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.interfaces.DraweeController;
import com.facebook.drawee.view.GenericDraweeView;
import com.facebook.imagepipeline.common.ResizeOptions;
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.uimanager.PixelUtil;
/**
* Wrapper class around Fresco's GenericDraweeView, enabling persisting props across multiple view
* update and consistent processing of both static and network images.
*/
public class ReactImageView extends GenericDraweeView {
private static final int REMOTE_IMAGE_FADE_DURATION_MS = 300;
public static final String TAG = ReactImageView.class.getSimpleName();
/*
* Implementation note re rounded corners:
*
* Fresco's built-in rounded corners only work for 'cover' resize mode -
* this is a limitation in Android itself. Fresco has a workaround for this, but
* it requires knowing the background color.
*
* So for the other modes, we use a postprocessor.
* Because the postprocessor uses a modified bitmap, that would just get cropped in
* 'cover' mode, so we fall back to Fresco's normal implementation.
*/
private static final Matrix sMatrix = new Matrix();
private static final Matrix sInverse = new Matrix();
private class RoundedCornerPostprocessor extends BasePostprocessor {
float getRadius(Bitmap source) {
ScalingUtils.getTransform(
sMatrix,
new Rect(0, 0, source.getWidth(), source.getHeight()),
source.getWidth(),
source.getHeight(),
0.0f,
0.0f,
mScaleType);
sMatrix.invert(sInverse);
return sInverse.mapRadius(mBorderRadius);
}
@Override
public void process(Bitmap output, Bitmap source) {
output.setHasAlpha(true);
if (mBorderRadius < 0.01f) {
super.process(output, source);
return;
}
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
Canvas canvas = new Canvas(output);
float radius = getRadius(source);
canvas.drawRoundRect(
new RectF(0, 0, source.getWidth(), source.getHeight()),
radius,
radius,
paint);
}
}
private @Nullable Uri mUri;
private int mBorderColor;
private float mBorderWidth;
private float mBorderRadius;
private ScalingUtils.ScaleType mScaleType;
private boolean mIsDirty;
private boolean mIsLocalImage;
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
private final RoundedCornerPostprocessor mRoundedCornerPostprocessor;
private final @Nullable Object mCallerContext;
private @Nullable ControllerListener mControllerListener;
private int mImageFadeDuration = -1;
// We can't specify rounding in XML, so have to do so here
private static GenericDraweeHierarchy buildHierarchy(Context context) {
return new GenericDraweeHierarchyBuilder(context.getResources())
.setRoundingParams(RoundingParams.fromCornersRadius(0))
.build();
}
public ReactImageView(
Context context,
AbstractDraweeControllerBuilder draweeControllerBuilder,
@Nullable Object callerContext) {
super(context, buildHierarchy(context));
mScaleType = ImageResizeMode.defaultValue();
mDraweeControllerBuilder = draweeControllerBuilder;
mRoundedCornerPostprocessor = new RoundedCornerPostprocessor();
mCallerContext = callerContext;
}
public void setBorderColor(int borderColor) {
mBorderColor = borderColor;
mIsDirty = true;
}
public void setBorderWidth(float borderWidth) {
mBorderWidth = PixelUtil.toPixelFromDIP(borderWidth);
mIsDirty = true;
}
public void setBorderRadius(float borderRadius) {
mBorderRadius = PixelUtil.toPixelFromDIP(borderRadius);
mIsDirty = true;
}
public void setScaleType(ScalingUtils.ScaleType scaleType) {
mScaleType = scaleType;
mIsDirty = true;
}
public void setSource(@Nullable String source) {
mUri = null;
if (source != null) {
try {
mUri = Uri.parse(source);
// Verify scheme is set, so that relative uri (used by static resources) are not handled.
if (mUri.getScheme() == null) {
mUri = null;
}
} catch (Exception e) {
// ignore malformed uri, then attempt to extract resource ID.
}
if (mUri == null) {
mUri = getResourceDrawableUri(getContext(), source);
mIsLocalImage = true;
} else {
mIsLocalImage = false;
}
}
mIsDirty = true;
}
public void maybeUpdateView() {
if (!mIsDirty) {
return;
}
boolean doResize = shouldResize(mUri);
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;
}
GenericDraweeHierarchy hierarchy = getHierarchy();
hierarchy.setActualImageScaleType(mScaleType);
boolean usePostprocessorScaling =
mScaleType != ScalingUtils.ScaleType.CENTER_CROP &&
mScaleType != ScalingUtils.ScaleType.FOCUS_CROP;
float hierarchyRadius = usePostprocessorScaling ? 0 : mBorderRadius;
RoundingParams roundingParams = hierarchy.getRoundingParams();
roundingParams.setCornersRadius(hierarchyRadius);
roundingParams.setBorder(mBorderColor, mBorderWidth);
hierarchy.setRoundingParams(roundingParams);
hierarchy.setFadeDuration(mImageFadeDuration >= 0
? mImageFadeDuration
: mIsLocalImage ? 0 : REMOTE_IMAGE_FADE_DURATION_MS);
Postprocessor postprocessor = usePostprocessorScaling ? mRoundedCornerPostprocessor : null;
ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null;
ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mUri)
.setPostprocessor(postprocessor)
.setResizeOptions(resizeOptions)
.build();
DraweeController draweeController = mDraweeControllerBuilder
.reset()
.setAutoPlayAnimations(true)
.setCallerContext(mCallerContext)
.setOldController(getController())
.setImageRequest(imageRequest)
.setControllerListener(mControllerListener)
.build();
setController(draweeController);
mIsDirty = false;
}
// VisibleForTesting
public void setControllerListener(ControllerListener controllerListener) {
mControllerListener = controllerListener;
mIsDirty = true;
maybeUpdateView();
}
// VisibleForTesting
public void setImageFadeDuration(int imageFadeDuration) {
mImageFadeDuration = imageFadeDuration;
mIsDirty = true;
maybeUpdateView();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0 && h > 0) {
maybeUpdateView();
}
}
/**
* ReactImageViews only render a single image.
*/
@Override
public boolean hasOverlappingRendering() {
return false;
}
private static boolean shouldResize(@Nullable Uri uri) {
// Resizing is inferior to scaling. See http://frescolib.org/docs/resizing-rotating.html#_
// We resize here only for images likely to be from the device's camera, where the app developer
// has no control over the original size
return uri != null && (UriUtil.isLocalContentUri(uri) || UriUtil.isLocalFileUri(uri));
}
private static @Nullable Uri getResourceDrawableUri(Context context, @Nullable String name) {
if (name == null || name.isEmpty()) {
return null;
}
name = name.toLowerCase().replace("-", "_");
int resId = context.getResources().getIdentifier(
name,
"drawable",
context.getPackageName());
return new Uri.Builder()
.scheme(UriUtil.LOCAL_RESOURCE_SCHEME)
.path(String.valueOf(resId))
.build();
}
}