/* * 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.drawee.drawable; import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.Arrays; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.VisibleForTesting; /** * A drawable that can have rounded corners. */ public class RoundedBitmapDrawable extends BitmapDrawable implements TransformAwareDrawable { @VisibleForTesting boolean mIsCircle = false; @VisibleForTesting float[] mCornerRadii = new float[8]; @VisibleForTesting RectF mRootBounds = new RectF(); @VisibleForTesting final RectF mLastRootBounds = new RectF(); @VisibleForTesting final Matrix mTransform = new Matrix(); @VisibleForTesting final Matrix mInverseTransform = new Matrix(); @VisibleForTesting final Matrix mLastTransform = new Matrix(); @VisibleForTesting float mBorderWidth = 0; @VisibleForTesting int mBorderColor = Color.TRANSPARENT; private final Path mPath = new Path(); private boolean mIsPathDirty = true; private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private boolean mIsShaderTransformDirty = true; private WeakReference<Bitmap> mLastBitmap; private @Nullable TransformCallback mTransformCallback; public RoundedBitmapDrawable(Resources res, Bitmap bitmap) { super(res, bitmap); mBorderPaint.setStyle(Paint.Style.STROKE); } /** * Creates a new RoundedBitmapDrawable from the given BitmapDrawable. * @param res resources to use for this drawable * @param bitmapDrawable bitmap drawable containing the bitmap to be used for this drawable * @return the RoundedBitmapDrawable that is created */ public static RoundedBitmapDrawable fromBitmapDrawable( Resources res, BitmapDrawable bitmapDrawable) { return new RoundedBitmapDrawable(res, bitmapDrawable.getBitmap()); } /** * Sets whether to round as circle. * * @param isCircle whether or not to round as circle */ public void setCircle(boolean isCircle) { mIsCircle = isCircle; mIsPathDirty = true; invalidateSelf(); } /** * Specify radius for the corners of the rectangle. If this is > 0, then the * drawable is drawn in a round-rectangle, rather than a rectangle. * @param radius the radius for the corners of the rectangle */ public void setCornerRadius(float radius) { Preconditions.checkState(radius >= 0); Arrays.fill(mCornerRadii, radius); mIsPathDirty = true; invalidateSelf(); } /** * Specify radii for each of the 4 corners. For each corner, the array * contains 2 values, [X_radius, Y_radius]. The corners are ordered * top-left, top-right, bottom-right, bottom-left * @param radii the x and y radii of the corners */ public void setCornerRadii(float[] radii) { if (radii == null) { Arrays.fill(mCornerRadii, 0); } else { Preconditions.checkArgument(radii.length == 8, "radii should have exactly 8 values"); System.arraycopy(radii, 0, mCornerRadii, 0, 8); } mIsPathDirty = true; invalidateSelf(); } /** * Sets the border * @param color of the border * @param width of the border */ public void setBorder(int color, float width) { if (mBorderColor != color || mBorderWidth != width) { mBorderColor = color; mBorderWidth = width; mIsPathDirty = true; invalidateSelf(); } } /** * TransformAwareDrawable method */ @Override public void setTransformCallback(@Nullable TransformCallback transformCallback) { mTransformCallback = transformCallback; } @Override public void setAlpha(int alpha) { if (alpha != mPaint.getAlpha()) { mPaint.setAlpha(alpha); invalidateSelf(); } } @Override public void setColorFilter(ColorFilter colorFilter) { mPaint.setColorFilter(colorFilter); invalidateSelf(); } @Override public void draw(Canvas canvas) { updateTransform(); updatePath(); updatePaint(); int saveCount = canvas.save(); canvas.concat(mInverseTransform); canvas.drawPath(mPath, mPaint); if (mBorderWidth != 0) { mBorderPaint.setStrokeWidth(mBorderWidth); mBorderPaint.setColor(DrawableUtils.multiplyColorAlpha(mBorderColor, mPaint.getAlpha())); canvas.drawPath(mPath, mBorderPaint); } canvas.restoreToCount(saveCount); } private void updateTransform() { if (mTransformCallback != null) { mTransformCallback.getTransform(mTransform); mTransformCallback.getRootBounds(mRootBounds); } else { mTransform.reset(); mRootBounds.set(getBounds()); } if (!mTransform.equals(mLastTransform)) { mIsShaderTransformDirty = true; if (!mTransform.invert(mInverseTransform)) { mInverseTransform.reset(); mTransform.reset(); } mLastTransform.set(mTransform); } if (!mRootBounds.equals(mLastRootBounds)) { mIsPathDirty = true; mLastRootBounds.set(mRootBounds); } } private void updatePath() { if (mIsPathDirty) { mPath.reset(); mRootBounds.inset(mBorderWidth/2, mBorderWidth/2); if (mIsCircle) { mPath.addCircle( mRootBounds.centerX(), mRootBounds.centerY(), Math.min(mRootBounds.width(), mRootBounds.height())/2, Path.Direction.CW); } else { mPath.addRoundRect(mRootBounds, mCornerRadii, Path.Direction.CW); } mRootBounds.inset(-(mBorderWidth/2), -(mBorderWidth/2)); mPath.setFillType(Path.FillType.WINDING); mIsPathDirty = false; } } private void updatePaint() { Bitmap bitmap = getBitmap(); if (mLastBitmap == null || mLastBitmap.get() != bitmap) { mLastBitmap = new WeakReference<Bitmap>(bitmap); mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); mIsShaderTransformDirty = true; } if (mIsShaderTransformDirty) { mPaint.getShader().setLocalMatrix(mTransform); mIsShaderTransformDirty = false; } } }