From 71ca522c68f831a06bbba194a9f7251f45c5fad4 Mon Sep 17 00:00:00 2001 From: Denis Koroskin Date: Sat, 12 Dec 2015 14:48:34 -0800 Subject: [PATCH] Support borders in RCTImageView Summary: @public Initial RCTImageView implementation only supported 'src', 'tintColor' and 'resizeMode'. This diff adds support for the rest of the properties: 'borderColor', 'borderWidth' and 'borderRadius'. `AbstractDrawBorders` class is reused in a follow up diff to draw borders for 'RCTView'. Reviewed By: sriramramani Differential Revision: D2693560 --- .../react/flat/AbstractDrawBorder.java | 132 ++++++++++++++++++ .../react/flat/AbstractDrawCommand.java | 5 + .../react/flat/BitmapRequestHelper.java | 4 + .../com/facebook/react/flat/DrawImage.java | 12 ++ .../react/flat/DrawImageWithPipeline.java | 67 ++++++++- .../com/facebook/react/flat/RCTImageView.java | 24 ++++ 6 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawBorder.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawBorder.java b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawBorder.java new file mode 100644 index 000000000..e8e19bf91 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawBorder.java @@ -0,0 +1,132 @@ +/** + * 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.flat; + +import javax.annotation.Nullable; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathEffect; +import android.graphics.RectF; + +/** + * Base class for border drawing operations (used by DrawImage and DrawBorder). Draws rectangular or + * rounded border along the rectangle, defined by the bounding box of the AbstractDrawCommand. + */ +/* package */ abstract class AbstractDrawBorder extends AbstractDrawCommand { + + private static final Paint PAINT = new Paint(Paint.ANTI_ALIAS_FLAG); + private static final RectF TMP_RECT = new RectF(); + private static final int BORDER_PATH_DIRTY = 1 << 0; + + static { + PAINT.setStyle(Paint.Style.STROKE); + } + + private int mSetPropertiesFlag; + private int mBorderColor = Color.BLACK; + private float mBorderWidth; + private float mBorderRadius; + private @Nullable Path mPathForBorderRadius; + + public final void setBorderWidth(float borderWidth) { + mBorderWidth = borderWidth; + setFlag(BORDER_PATH_DIRTY); + } + + public final float getBorderWidth() { + return mBorderWidth; + } + + public void setBorderRadius(float borderRadius) { + mBorderRadius = borderRadius; + setFlag(BORDER_PATH_DIRTY); + } + + public final float getBorderRadius() { + return mBorderRadius; + } + + public final void setBorderColor(int borderColor) { + mBorderColor = borderColor; + } + + public final int getBorderColor() { + return mBorderColor; + } + + @Override + protected void onBoundsChanged() { + setFlag(BORDER_PATH_DIRTY); + } + + protected final void drawBorders(Canvas canvas) { + if (mBorderWidth < 0.5f) { + return; + } + + if (mBorderColor == 0) { + return; + } + + PAINT.setColor(mBorderColor); + PAINT.setStrokeWidth(mBorderWidth); + PAINT.setPathEffect(getPathEffectForBorderStyle()); + canvas.drawPath(getPathForBorderRadius(), PAINT); + } + + protected final void updatePath(Path path, float correction) { + path.reset(); + + TMP_RECT.set( + getLeft() + correction, + getTop() + correction, + getRight() - correction, + getBottom() - correction); + + path.addRoundRect( + TMP_RECT, + mBorderRadius, + mBorderRadius, + Path.Direction.CW); + } + + protected @Nullable PathEffect getPathEffectForBorderStyle() { + return null; + } + + protected final boolean isFlagSet(int mask) { + return (mSetPropertiesFlag & mask) == mask; + } + + protected final void setFlag(int mask) { + mSetPropertiesFlag |= mask; + } + + protected final void resetFlag(int mask) { + mSetPropertiesFlag &= ~mask; + } + + protected final Path getPathForBorderRadius() { + if (isFlagSet(BORDER_PATH_DIRTY)) { + if (mPathForBorderRadius == null) { + mPathForBorderRadius = new Path(); + } + + updatePath(mPathForBorderRadius, mBorderWidth * 0.5f); + + resetFlag(BORDER_PATH_DIRTY); + } + + return mPathForBorderRadius; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java index 007145edd..f5375a98d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/AbstractDrawCommand.java @@ -103,6 +103,9 @@ package com.facebook.react.flat; return mBottom; } + protected void onBoundsChanged() { + } + /** * Updates boundaries of this DrawCommand. */ @@ -111,6 +114,8 @@ package com.facebook.react.flat; mTop = top; mRight = right; mBottom = bottom; + + onBoundsChanged(); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/BitmapRequestHelper.java b/ReactAndroid/src/main/java/com/facebook/react/flat/BitmapRequestHelper.java index 193b1535d..87711aac7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/BitmapRequestHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/BitmapRequestHelper.java @@ -107,6 +107,10 @@ import com.facebook.infer.annotation.Assertions; return ((CloseableBitmap) closeableImage).getUnderlyingBitmap(); } + /* package */ boolean isDetached() { + return mAttachCounter == 0; + } + @Override public void onNewResult(DataSource> dataSource) { if (!dataSource.isFinished()) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java index d8a7e7367..4ca727afe 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImage.java @@ -43,4 +43,16 @@ import com.facebook.imagepipeline.request.ImageRequest; * Returns a scale type to draw to the image with. */ public ScaleType getScaleType(); + + public void setBorderWidth(float borderWidth); + + public float getBorderWidth(); + + public void setBorderRadius(float borderRadius); + + public float getBorderRadius(); + + public void setBorderColor(int borderColor); + + public int getBorderColor(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java index f13a05a69..0b60c8d7b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/DrawImageWithPipeline.java @@ -12,11 +12,14 @@ package com.facebook.react.flat; import javax.annotation.Nullable; import android.graphics.Bitmap; +import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.Shader; import com.facebook.drawee.drawable.ScalingUtils.ScaleType; import com.facebook.imagepipeline.request.ImageRequest; @@ -27,15 +30,18 @@ import com.facebook.react.views.image.ImageResizeMode; * DrawImageWithPipeline is DrawCommand that can draw a local or remote image. * It uses BitmapRequestHelper internally to fetch and cache the images. */ -/* package */ final class DrawImageWithPipeline extends AbstractDrawCommand implements DrawImage { +/* package */ final class DrawImageWithPipeline extends AbstractDrawBorder implements DrawImage { private static final Paint PAINT = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + private static final int BORDER_BITMAP_PATH_DIRTY = 1 << 1; private final Matrix mTransform = new Matrix(); private ScaleType mScaleType = ImageResizeMode.defaultValue(); private @Nullable BitmapRequestHelper mBitmapRequestHelper; private @Nullable PorterDuffColorFilter mColorFilter; private @Nullable FlatViewGroup.InvalidateCallback mCallback; + private @Nullable Path mPathForRoundedBitmap; + private @Nullable BitmapShader mBitmapShader; private boolean mForceClip; @Override @@ -45,6 +51,8 @@ import com.facebook.react.views.image.ImageResizeMode; @Override public void setImageRequest(@Nullable ImageRequest imageRequest) { + mBitmapShader = null; + if (imageRequest == null) { mBitmapRequestHelper = null; } else { @@ -79,14 +87,34 @@ import com.facebook.react.views.image.ImageResizeMode; } PAINT.setColorFilter(mColorFilter); + if (mForceClip) { canvas.save(); canvas.clipRect(getLeft(), getTop(), getRight(), getBottom()); - canvas.drawBitmap(bitmap, mTransform, PAINT); - canvas.restore(); - } else { - canvas.drawBitmap(bitmap, mTransform, PAINT); } + + if (getBorderRadius() < 0.5f) { + canvas.drawBitmap(bitmap, mTransform, PAINT); + } else { + if (mBitmapShader == null) { + mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + mBitmapShader.setLocalMatrix(mTransform); + } + PAINT.setShader(mBitmapShader); + canvas.drawPath(getPathForRoundedBitmap(), PAINT); + } + + drawBorders(canvas); + + if (mForceClip) { + canvas.restore(); + } + } + + @Override + public void setBorderRadius(float borderRadius) { + super.setBorderRadius(borderRadius); + setFlag(BORDER_BITMAP_PATH_DIRTY); } @Override @@ -98,6 +126,17 @@ import com.facebook.react.views.image.ImageResizeMode; @Override public void onDetached() { Assertions.assumeNotNull(mBitmapRequestHelper).detach(); + + if (mBitmapRequestHelper.isDetached()) { + // Make sure we don't hold on to the Bitmap. + mBitmapShader = null; + } + } + + @Override + protected void onBoundsChanged() { + super.onBoundsChanged(); + setFlag(BORDER_BITMAP_PATH_DIRTY); } /* package */ void updateBounds(Bitmap bitmap) { @@ -140,5 +179,23 @@ import com.facebook.react.views.image.ImageResizeMode; mTransform.setScale(scale, scale); mTransform.postTranslate(left + paddingLeft, top + paddingTop); + + if (mBitmapShader != null) { + mBitmapShader.setLocalMatrix(mTransform); + } + } + + private Path getPathForRoundedBitmap() { + if (isFlagSet(BORDER_BITMAP_PATH_DIRTY)) { + if (mPathForRoundedBitmap == null) { + mPathForRoundedBitmap = new Path(); + } + + updatePath(mPathForRoundedBitmap, 1.0f); + + resetFlag(BORDER_BITMAP_PATH_DIRTY); + } + + return mPathForRoundedBitmap; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageView.java b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageView.java index 3a019e18b..a50d3032f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/RCTImageView.java @@ -17,6 +17,7 @@ import android.net.Uri; import com.facebook.drawee.drawable.ScalingUtils.ScaleType; import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactProp; import com.facebook.react.uimanager.ViewProps; import com.facebook.react.views.image.ImageResizeMode; @@ -102,6 +103,29 @@ import com.facebook.react.views.image.ImageResizeMode; } } + @ReactProp(name = "borderColor", customType = "Color") + public void setBorderColor(int borderColor) { + if (mDrawImage.getBorderColor() != borderColor) { + getMutableDrawImage().setBorderColor(borderColor); + } + } + + @ReactProp(name = "borderWidth") + public void setBorderWidth(float borderWidth) { + borderWidth = PixelUtil.toPixelFromDIP(borderWidth); + + if (mDrawImage.getBorderWidth() != borderWidth) { + getMutableDrawImage().setBorderWidth(borderWidth); + } + } + + @ReactProp(name = "borderRadius") + public void setBorderRadius(float borderRadius) { + if (mDrawImage.getBorderRadius() != borderRadius) { + getMutableDrawImage().setBorderRadius(PixelUtil.toPixelFromDIP(borderRadius)); + } + } + private T getMutableDrawImage() { if (mDrawImage.isFrozen()) { mDrawImage = (T) mDrawImage.mutableCopy();