Commit d03d3e45 authored by David Trainor's avatar David Trainor Committed by Commit Bot

Fix RoundedCornerImageView issues

RoundedCornerImageView ran into a few problems exposed by omnibox usage:
- ImageView#setImageResource() doesn't end up calling setImageDrawable()
on some Android versions.
- BitmapDrawable can apply a scale so getIntrinsicWidth/Height won't be
the same as get Bitmap#getWidth/Height.  This wasn't handled in this
code.
- Also some general code clean-up to make it easier to parse and update
in the future.

Bug: 1001209
Change-Id: Ib861686ce51eb019cc068e24e042aca1ae42cd23
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1788299
Commit-Queue: Ender <ender@google.com>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Reviewed-by: default avatarShakti Sahu <shaktisahu@chromium.org>
Auto-Submit: David Trainor <dtrainor@chromium.org>
Cr-Commit-Position: refs/heads/master@{#706645}
parent 9d83031b
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
package org.chromium.chrome.browser.download.home.list.holder; package org.chromium.chrome.browser.download.home.list.holder;
import android.graphics.Bitmap;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
...@@ -193,25 +192,33 @@ class OfflineItemViewHolder extends ListItemViewHolder implements ListMenuButton ...@@ -193,25 +192,33 @@ class OfflineItemViewHolder extends ListItemViewHolder implements ListMenuButton
@Override @Override
public void maybeResizeImage(Drawable drawable) { public void maybeResizeImage(Drawable drawable) {
if (!(drawable instanceof BitmapDrawable)) return; Matrix matrix = null;
if (drawable instanceof BitmapDrawable) {
matrix = upscaleBitmapIfNecessary((BitmapDrawable) drawable);
}
Matrix matrix = upscaleBitmapIfNecessary(((BitmapDrawable) drawable).getBitmap());
mImageView.setImageMatrix(matrix); mImageView.setImageMatrix(matrix);
mImageView.setScaleType( mImageView.setScaleType(
matrix == null ? ImageView.ScaleType.CENTER_CROP : ImageView.ScaleType.MATRIX); matrix == null ? ImageView.ScaleType.CENTER_CROP : ImageView.ScaleType.MATRIX);
} }
private Matrix upscaleBitmapIfNecessary(Bitmap bitmap) { private Matrix upscaleBitmapIfNecessary(BitmapDrawable drawable) {
if (bitmap == null) return null; if (drawable == null) return null;
float scale = computeScaleFactor(bitmap); int width = drawable.getBitmap().getWidth();
int height = drawable.getBitmap().getHeight();
float scale = computeScaleFactor(width, height);
if (scale <= 1.f) return null; if (scale <= 1.f) return null;
// Compute the required matrix to scale and center the bitmap. // Compute the required matrix to scale and center the bitmap.
float dx = (mImageView.getWidth() - width * scale) / 2.f;
float dy = (mImageView.getHeight() - height * scale) / 2.f;
Matrix matrix = new Matrix(); Matrix matrix = new Matrix();
matrix.setScale(scale, scale); matrix.postScale(scale, scale);
matrix.postTranslate((mImageView.getWidth() - scale * bitmap.getWidth()) * 0.5f, matrix.postTranslate(dx, dy);
(mImageView.getHeight() - scale * bitmap.getHeight()) * 0.5f);
return matrix; return matrix;
} }
...@@ -220,9 +227,9 @@ class OfflineItemViewHolder extends ListItemViewHolder implements ListMenuButton ...@@ -220,9 +227,9 @@ class OfflineItemViewHolder extends ListItemViewHolder implements ListMenuButton
* dimensions. The scaled bitmap will be centered inside the view. No scaling if the * dimensions. The scaled bitmap will be centered inside the view. No scaling if the
* dimensions are comparable. * dimensions are comparable.
*/ */
private float computeScaleFactor(Bitmap bitmap) { private float computeScaleFactor(int width, int height) {
float widthRatio = (float) mImageView.getWidth() / bitmap.getWidth(); float widthRatio = (float) mImageView.getWidth() / width;
float heightRatio = (float) mImageView.getHeight() / bitmap.getHeight(); float heightRatio = (float) mImageView.getHeight() / height;
UmaUtils.recordImageViewRequiredStretch(widthRatio, heightRatio, mFilter); UmaUtils.recordImageViewRequiredStretch(widthRatio, heightRatio, mFilter);
if (Math.max(widthRatio, heightRatio) < IMAGE_VIEW_MAX_SCALE_FACTOR) return 1.f; if (Math.max(widthRatio, heightRatio) < IMAGE_VIEW_MAX_SCALE_FACTOR) return 1.f;
......
...@@ -19,11 +19,11 @@ import android.graphics.drawable.ColorDrawable; ...@@ -19,11 +19,11 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.shapes.RoundRectShape; import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.shapes.Shape; import android.graphics.drawable.shapes.Shape;
import android.support.annotation.ColorInt;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.ColorRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
/** /**
...@@ -41,20 +41,20 @@ import androidx.annotation.Nullable; ...@@ -41,20 +41,20 @@ import androidx.annotation.Nullable;
* corners are used. * corners are used.
*/ */
public class RoundedCornerImageView extends ImageView { public class RoundedCornerImageView extends ImageView {
private Shape mRoundedRectangle; private final RectF mTmpRect = new RectF();
private BitmapShader mShader; private final Matrix mTmpMatrix = new Matrix();
private Paint mPaint;
private Paint mFillPaint; private final Paint mRoundedBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint mRoundedContentPaint;
private final Matrix mScaleMatrix = new Matrix(); private final Matrix mScaleMatrix = new Matrix();
private boolean mRoundCorners; private boolean mRoundCorners;
// True, if constructor had a chance to run.
// This is needed, because ImageView's constructor may trigger updates on our end
// if certain attributes (eg. Drawable) are supplied via layout attributes.
private final boolean mIsInitialized;
// Object to avoid allocations during draw calls. private Shape mRoundedRectangle;
private final RectF mTmpRect = new RectF(); private @ColorInt int mFillColor = Color.TRANSPARENT;
// Whether or not to apply the shader, if we have one. This might be set to false if the image
// is smaller than the view and does not need to have the corners rounded.
private boolean mApplyShader;
public RoundedCornerImageView(Context context) { public RoundedCornerImageView(Context context) {
this(context, null, 0); this(context, null, 0);
...@@ -66,32 +66,69 @@ public class RoundedCornerImageView extends ImageView { ...@@ -66,32 +66,69 @@ public class RoundedCornerImageView extends ImageView {
public RoundedCornerImageView(Context context, AttributeSet attrs, int defStyle) { public RoundedCornerImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
// Set attribute indicating that all required objects are created.
mIsInitialized = true;
int radiusTopStart = 0;
int radiusTopEnd = 0;
int radiusBottomStart = 0;
int radiusBottomEnd = 0;
int color = Color.TRANSPARENT;
if (attrs != null) { if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes( TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.RoundedCornerImageView, 0, 0); attrs, R.styleable.RoundedCornerImageView, 0, 0);
int cornerRadiusTopStart = a.getDimensionPixelSize( radiusTopStart = a.getDimensionPixelSize(
R.styleable.RoundedCornerImageView_cornerRadiusTopStart, 0); R.styleable.RoundedCornerImageView_cornerRadiusTopStart, 0);
int cornerRadiusTopEnd = a.getDimensionPixelSize( radiusTopEnd = a.getDimensionPixelSize(
R.styleable.RoundedCornerImageView_cornerRadiusTopEnd, 0); R.styleable.RoundedCornerImageView_cornerRadiusTopEnd, 0);
int cornerRadiusBottomStart = a.getDimensionPixelSize( radiusBottomStart = a.getDimensionPixelSize(
R.styleable.RoundedCornerImageView_cornerRadiusBottomStart, 0); R.styleable.RoundedCornerImageView_cornerRadiusBottomStart, 0);
int cornerRadiusBottomEnd = a.getDimensionPixelSize( radiusBottomEnd = a.getDimensionPixelSize(
R.styleable.RoundedCornerImageView_cornerRadiusBottomEnd, 0); R.styleable.RoundedCornerImageView_cornerRadiusBottomEnd, 0);
if (a.hasValue(R.styleable.RoundedCornerImageView_roundedfillColor)) {
mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFillPaint.setColor(a.getColor(
R.styleable.RoundedCornerImageView_roundedfillColor, Color.WHITE));
}
a.recycle();
setRoundedCorners(cornerRadiusTopStart, cornerRadiusTopEnd, cornerRadiusBottomStart, color = a.getColor(
cornerRadiusBottomEnd); R.styleable.RoundedCornerImageView_roundedfillColor, Color.TRANSPARENT);
a.recycle();
} }
setRoundedCorners(radiusTopStart, radiusTopEnd, radiusBottomStart, radiusBottomEnd);
setRoundedFillColor(color);
refreshState();
} }
/** /**
* Updates the rounded corners, using the radius set in the layout. * Sets the rounded corner fill color to {@code color}. This can be used to make sure the
* rounded shape shows even if the actual content isn't full-bleed (e.g. icon with transparency
* or too small to reach the edges).
* @param color The color to use. Setting to {@link Color#TRANSPARENT} will remove the color.
*/ */
public void setRoundedFillColor(@ColorInt int color) {
mFillColor = color;
mRoundedBackgroundPaint.setColor(color);
invalidate();
}
// ImageView implementation.
@Override
public void setImageDrawable(@Nullable Drawable drawable) {
super.setImageDrawable(drawable);
refreshState();
}
@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
refreshState();
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
refreshState();
}
public void setRoundedCorners(int cornerRadiusTopStart, int cornerRadiusTopEnd, public void setRoundedCorners(int cornerRadiusTopStart, int cornerRadiusTopEnd,
int cornerRadiusBottomStart, int cornerRadiusBottomEnd) { int cornerRadiusBottomStart, int cornerRadiusBottomEnd) {
mRoundCorners = (cornerRadiusTopStart != 0 || cornerRadiusTopEnd != 0 mRoundCorners = (cornerRadiusTopStart != 0 || cornerRadiusTopEnd != 0
...@@ -110,154 +147,84 @@ public class RoundedCornerImageView extends ImageView { ...@@ -110,154 +147,84 @@ public class RoundedCornerImageView extends ImageView {
} }
mRoundedRectangle = new RoundRectShape(radii, null, null); mRoundedRectangle = new RoundRectShape(radii, null, null);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
} }
@Override private void refreshState() {
public void setImageDrawable(@Nullable Drawable drawable) { Drawable drawable = getDrawable();
super.setImageDrawable(drawable);
reset();
}
@Override
public void setImageResource(int res) {
super.setImageResource(res);
reset();
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
reset();
}
private void reset() {
// Reset shaders. We will need to recalculate them.
mShader = null;
mApplyShader = false;
// Reset shader in Paint to avoid retaining the old Bitmap. // Do not update state if we were invoked from the ImageView's constructor
if (mPaint != null) mPaint.setShader(null); // (before we had the chance to initialize our own private data).
if (!mIsInitialized) {
return;
}
maybeCreateShader(); if (drawable instanceof ColorDrawable) {
updateApplyShader(); mRoundedBackgroundPaint.setColor(((ColorDrawable) getDrawable()).getColor());
} mRoundedContentPaint = null;
} else if (drawable instanceof BitmapDrawable) {
mRoundedBackgroundPaint.setColor(mFillColor);
mRoundedContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/** Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
* Set the fill color resource.
* @param id The color resource id.
*/
public void setRoundedFillColor(@ColorRes int id) {
mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFillPaint.setColor(getContext().getResources().getColor(id));
}
protected void maybeCreateShader() { mRoundedContentPaint.setShader(
// Only create the shader if we have a rectangle to use as a mask. new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
Drawable drawable = getDrawable(); } else {
Bitmap bitmap = (drawable instanceof BitmapDrawable) mRoundedBackgroundPaint.setColor(mFillColor);
? ((BitmapDrawable) drawable).getBitmap() mRoundedContentPaint = null;
: null;
if (mRoundedRectangle != null && bitmap != null) {
mShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
} }
} }
@Override @Override
protected boolean setFrame(int l, int t, int r, int b) { protected void onDraw(Canvas canvas) {
boolean changed = super.setFrame(l, t, r, b); if (!mRoundCorners) {
updateApplyShader(); super.onDraw(canvas);
return changed;
}
@Override
public void setScaleType(ScaleType scaleType) {
super.setScaleType(scaleType);
updateApplyShader();
}
/**
* Updates the flag to tell whether or not to apply the shader that produces the rounded
* corners. We should not apply the shader if the final image is smaller than the view, because
* it will try to tile the image, which is not desirable. This should be called when the image
* is changed, or the view bounds change.
*/
private void updateApplyShader() {
Drawable drawable = getDrawable();
if (!(drawable instanceof BitmapDrawable) || (mShader == null) || (mPaint == null)) {
// In this state we wouldn't use the shader anyway.
mApplyShader = false;
return; return;
} }
// Default to using the shader.
mApplyShader = true;
}
@Override
protected void onDraw(Canvas canvas) {
final int width = getWidth() - getPaddingLeft() - getPaddingRight(); final int width = getWidth() - getPaddingLeft() - getPaddingRight();
final int height = getHeight() - getPaddingTop() - getPaddingBottom(); final int height = getHeight() - getPaddingTop() - getPaddingBottom();
if (width <= 0 || height <= 0) return;
Drawable drawable = getDrawable(); mRoundedRectangle.resize(width, height);
Shape localRoundedRect = mRoundedRectangle;
Paint localPaint = mPaint;
boolean drawFill = mFillPaint != null && localRoundedRect != null
&& !(drawable instanceof ColorDrawable);
boolean drawContent = localPaint != null && localRoundedRect != null
&& isSupportedDrawable(drawable) && mRoundCorners;
if (drawFill || drawContent) localRoundedRect.resize(width, height);
final int saveCount = canvas.save(); final int saveCount = canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop()); try {
canvas.translate(getPaddingLeft(), getPaddingTop());
// First, fill the drawing area with the given fill paint.
if (drawFill) localRoundedRect.draw(canvas, mFillPaint); if (mRoundedBackgroundPaint.getColor() != Color.TRANSPARENT) {
mRoundedRectangle.draw(canvas, mRoundedBackgroundPaint);
if (!drawContent) { // Note: RoundedBackgroundPaint is also used as ColorDrawable.
// We probably have an unsupported drawable or we don't want rounded corners. Draw if (getDrawable() instanceof ColorDrawable) {
// normally and return. return;
// Undo our modifications to canvas first. ImageView will re-apply these. }
canvas.restoreToCount(saveCount); }
super.onDraw(canvas);
return;
}
// We have a drawable to draw with rounded corners. Let's first set up the paint.
if (drawable instanceof ColorDrawable) {
ColorDrawable colorDrawable = (ColorDrawable) drawable;
localPaint.setColor(colorDrawable.getColor());
}
if (mApplyShader) { if (mRoundedContentPaint == null) {
assert mShader != null; canvas.restoreToCount(saveCount);
super.onDraw(canvas);
return;
}
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); Shader shader = mRoundedContentPaint.getShader();
mScaleMatrix.set(getImageMatrix()); if (shader != null) {
mScaleMatrix.preScale(1.f * drawable.getIntrinsicWidth() / bitmap.getWidth(), Drawable drawable = getDrawable();
1.f * drawable.getIntrinsicHeight() / bitmap.getHeight());
mShader.setLocalMatrix(mScaleMatrix);
localPaint.setShader(mShader);
// Find the desired bounding box where the bitmap is to be shown.
mTmpRect.set(getDrawable().getBounds());
getImageMatrix().mapRect(mTmpRect);
}
// Clip the canvas to the desired bounding box so that the shader isn't applied anywhere Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
// outside the desired area. mTmpMatrix.set(getImageMatrix());
if (mApplyShader) canvas.clipRect(mTmpRect); mTmpMatrix.preScale((float) drawable.getIntrinsicWidth() / bitmap.getWidth(),
(float) drawable.getIntrinsicHeight() / bitmap.getHeight());
// Draw the rounded rectangle. shader.setLocalMatrix(mTmpMatrix);
localRoundedRect.draw(canvas, localPaint);
// Remove the clip. mTmpRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
canvas.restoreToCount(saveCount); mTmpMatrix.mapRect(mTmpRect);
} canvas.clipRect(mTmpRect);
}
private boolean isSupportedDrawable(Drawable drawable) { mRoundedRectangle.draw(canvas, mRoundedContentPaint);
return (drawable instanceof ColorDrawable) || (drawable instanceof BitmapDrawable); } finally {
canvas.restoreToCount(saveCount);
}
} }
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment