如何在不创建新位图的情况下拥有圆形、中心裁剪的 imageView?
Posted
技术标签:
【中文标题】如何在不创建新位图的情况下拥有圆形、中心裁剪的 imageView?【英文标题】:How to have a circular, center-cropped imageView, without creating a new bitmap? 【发布时间】:2016-02-18 12:42:43 【问题描述】:注意:我知道有很多关于此的问题和存储库,但似乎没有一个适合我尝试实现的目标。
背景
给定任何纵横比的位图,我希望将其设置为 ImageView 的内容(仅使用可绘制对象,不扩展 ImageView),以便内容将被中心裁剪,但形状一个圆圈。
所有这些都使用最少的内存,因为有时图像可能非常大。我不想为此创建一个全新的位图。内容已经存在...
问题
我发现的所有解决方案都缺少我所写的内容之一:有些没有居中裁剪,有些假设图像是方形的,有些从给定的位图创建一个新的位图...
我尝试过的
除了尝试各种存储库之外,我还尝试了this tutorial,并尝试针对非正方形纵横比的情况进行修复,但失败了。
这是它的代码,以防网站关闭:
public class RoundImage extends Drawable
private final Bitmap mBitmap;
private final Paint mPaint;
private final RectF mRectF;
private final int mBitmapWidth;
private final int mBitmapHeight;
public RoundImage(Bitmap bitmap)
mBitmap = bitmap;
mRectF = new RectF();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
final BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(shader);
mBitmapWidth = mBitmap.getWidth();
mBitmapHeight = mBitmap.getHeight();
@Override
public void draw(Canvas canvas)
canvas.drawOval(mRectF, mPaint);
@Override
protected void onBoundsChange(Rect bounds)
super.onBoundsChange(bounds);
mRectF.set(bounds);
@Override
public void setAlpha(int alpha)
if (mPaint.getAlpha() != alpha)
mPaint.setAlpha(alpha);
invalidateSelf();
@Override
public void setColorFilter(ColorFilter cf)
mPaint.setColorFilter(cf);
@Override
public int getOpacity()
return PixelFormat.TRANSLUCENT;
@Override
public int getIntrinsicWidth()
return mBitmapWidth;
@Override
public int getIntrinsicHeight()
return mBitmapHeight;
public void setAntiAlias(boolean aa)
mPaint.setAntiAlias(aa);
invalidateSelf();
@Override
public void setFilterBitmap(boolean filter)
mPaint.setFilterBitmap(filter);
invalidateSelf();
@Override
public void setDither(boolean dither)
mPaint.setDither(dither);
invalidateSelf();
public Bitmap getBitmap()
return mBitmap;
我发现的一个非常好的解决方案 (here) 完全符合我的需要,除了它在 ImageView 本身中使用它,而不是创建一个可绘制对象。这意味着我不能将它设置为例如视图的背景。
问题
我怎样才能做到这一点?
编辑:这是当前代码,因为我想添加边框,它也有这个代码:
public class SimpleRoundedDrawable extends BitmapDrawable
private final Path p = new Path();
private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public SimpleRoundedDrawable(final Resources res, final Bitmap bitmap)
super(res, bitmap);
mBorderPaint.setStyle(Paint.Style.STROKE);
public SimpleRoundedDrawable setBorder(float borderWidth, @ColorInt int borderColor)
mBorderPaint.setStrokeWidth(borderWidth);
mBorderPaint.setColor(borderColor);
invalidateSelf();
return this;
@Override
protected void onBoundsChange(Rect bounds)
super.onBoundsChange(bounds);
p.rewind();
p.addCircle(bounds.width() / 2,
bounds.height() / 2,
Math.min(bounds.width(), bounds.height()) / 2,
Path.Direction.CW);
@Override
public void draw(Canvas canvas)
canvas.clipPath(p);
super.draw(canvas);
final float width = getBounds().width(), height = getBounds().height();
canvas.drawCircle(width / 2, height / 2, Math.min(width, height) / 2, mBorderPaint);
我希望事情应该是这样运作的。
编辑:似乎该解决方案仅适用于特定的 android 版本,因为它不适用于 Android 4.2.2。相反,它显示了一个正方形的图像。
编辑:似乎上述解决方案也比使用 BitmapShader 效率低得多(链接here)。知道如何在可绘制对象中而不是在自定义的 ImageView 中使用它真的很棒
-- 这是以下解决方案的当前修改版本。我希望它对某些人有用:
public class SimpleRoundedDrawable extends Drawable
final Paint mMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG), mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Bitmap mBitmap;
int mSide;
float mRadius;
public SimpleRoundedDrawable()
this(null);
public SimpleRoundedDrawable(Bitmap bitmap)
this(bitmap, 0, 0);
public SimpleRoundedDrawable(Bitmap bitmap, float width, @ColorInt int color)
mBorderPaint.setStyle(Paint.Style.STROKE);
mBitmap = bitmap;
mSide = mBitmap == null ? 0 : Math.min(bitmap.getWidth(), bitmap.getHeight());
mBorderPaint.setStrokeWidth(width);
mBorderPaint.setColor(color);
public SimpleRoundedDrawable setBitmap(final Bitmap bitmap)
mBitmap = bitmap;
mSide = Math.min(bitmap.getWidth(), bitmap.getHeight());
invalidateSelf();
return this;
public SimpleRoundedDrawable setBorder(float width, @ColorInt int color)
mBorderPaint.setStrokeWidth(width);
mBorderPaint.setColor(color);
invalidateSelf();
return this;
@Override
protected void onBoundsChange(Rect bounds)
if (mBitmap == null)
return;
Matrix matrix = new Matrix();
RectF src = new RectF(0, 0, mSide, mSide);
src.offset((mBitmap.getWidth() - mSide) / 2f, (mBitmap.getHeight() - mSide) / 2f);
RectF dst = new RectF(bounds);
final float strokeWidth = mBorderPaint.getStrokeWidth();
if (strokeWidth > 0)
dst.inset(strokeWidth, strokeWidth);
matrix.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
Shader shader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
shader.setLocalMatrix(matrix);
mMaskPaint.setShader(shader);
matrix.mapRect(src);
mRadius = src.width() / 2f;
@Override
public void draw(Canvas canvas)
Rect b = getBounds();
if (mBitmap != null)
canvas.drawCircle(b.exactCenterX(), b.exactCenterY(), mRadius, mMaskPaint);
final float strokeWidth = mBorderPaint.getStrokeWidth();
if (strokeWidth > 0)
canvas.drawCircle(b.exactCenterX(), b.exactCenterY(), mRadius + strokeWidth / 2, mBorderPaint);
@Override
public void setAlpha(int alpha)
mMaskPaint.setAlpha(alpha);
invalidateSelf();
@Override
public void setColorFilter(ColorFilter cf)
mMaskPaint.setColorFilter(cf);
invalidateSelf();
@Override
public int getOpacity()
return PixelFormat.TRANSLUCENT;
【问题讨论】:
您只需要ImageView
来显示裁剪后的图像吗?您不需要实际裁剪 Bitmap
本身吗?
@MikeM。我不想更改 ImageView 或位图。输入已经可用,只需要绘制成圆形即可。这一切都应该在drawable中完成。裁剪图像将创建一个新的位图,这意味着额外的内存使用。
【参考方案1】:
如果我没听错的话,你的 Drawable
班级会是这样的:
public class CroppedDrawable extends BitmapDrawable
private Path p = new Path();
public CroppedDrawable(Bitmap b)
super(b);
@Override
protected void onBoundsChange(Rect bounds)
super.onBoundsChange(bounds);
p.rewind();
p.addCircle(bounds.width() / 2,
bounds.height() / 2,
Math.min(bounds.width(), bounds.height()) / 2,
Path.Direction.CW);
@Override
public void draw(Canvas canvas)
canvas.clipPath(p);
super.draw(canvas);
一个示例用法是:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mila);
CroppedDrawable cd = new CroppedDrawable(bitmap);
imageView.setImageDrawable(cd);
根据您之前的示例图像,会给出如下内容:
【讨论】:
你能解释一下吗?你用“倒带”做什么?占据图像正方形部分的部分在哪里?您是如何找到此解决方案的?例如,您将如何更改它以支持边框(就像其他解决方案一样)? 当然,但仅供参考,我对你的最终目标还是有点不清楚;这只是一个建议。 “你用‘倒带’做什么?” - 它实质上重置了Path
,因此如果您的布局发生原位更改,我们不会向其添加另一个圆圈;例如,如果您向其添加 Button
或其他内容。 “占据图像正方形部分的部分在哪里?” - 这是最小维度的计算 - Math.min()
函数 - 并使其成为我们圆的半径。 “你是怎么找到这个解决方案的?” - 嗯,只是在想?我不知道。我只是熟悉思考这些事情。
"例如,您将如何更改它以支持边框(就像其他解决方案一样)?" - 我不确定你所说的边界是什么意思,但如果是我想的那样,我们只会减小Path
圆的半径 - 即addCircle()
方法中的第三个参数.请让我知道我的描述中缺少哪里。
@androiddeveloper 我觉得我没有充分解释和/或提供适当的解决方案。请让我知道我还能做些什么。
关于“边框”,我指的是图像的圆形内容周围的彩色圆圈。【参考方案2】:
试试这个极简的自定义Drawable
并修改它以满足您的需求:
class D extends Drawable
Bitmap bitmap;
Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
int side;
float radius;
public D(Bitmap wrappedBitmap)
bitmap = wrappedBitmap;
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(16);
borderPaint.setColor(0xcc220088);
side = Math.min(bitmap.getWidth(), bitmap.getHeight());
@Override
protected void onBoundsChange(Rect bounds)
Matrix matrix = new Matrix();
RectF src = new RectF(0, 0, side, side);
src.offset((bitmap.getWidth() - side) / 2f, (bitmap.getHeight() - side) / 2f);
RectF dst = new RectF(bounds);
dst.inset(borderPaint.getStrokeWidth(), borderPaint.getStrokeWidth());
matrix.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
Shader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
shader.setLocalMatrix(matrix);
maskPaint.setShader(shader);
matrix.mapRect(src);
radius = src.width() / 2f;
@Override
public void draw(Canvas canvas)
Rect b = getBounds();
canvas.drawCircle(b.exactCenterX(), b.exactCenterY(), radius, maskPaint);
canvas.drawCircle(b.exactCenterX(), b.exactCenterY(), radius + borderPaint.getStrokeWidth() / 2, borderPaint);
@Override public void setAlpha(int alpha)
@Override public void setColorFilter(ColorFilter cf)
@Override public int getOpacity() return PixelFormat.TRANSLUCENT;
【讨论】:
我已经测试过了,我发现如果位图不是正方形,输出看起来像圆角矩形,而不是圆形。 当然,它在onBoundsChange
中使用addRoundRect
,如果你想要一个圆而不是圆角矩形,请将其更改为addCircle
好的,现在添加这个之后: maskPath.addCircle(bounds.width() / 2, bounds.height() / 2, Math.min(bounds.width(), bounds.height() ) / 2、路径.方向.CW); ,我得到了一个圆圈,但它不像原始解决方案那样缩放到 imageView 的大小。
见第二个Drawable
使用Path#addRoundRect
【参考方案3】:
看起来,使用“clipPath”效率不高,需要在 4.3 及更低版本上禁用硬件加速。
更好的解决方案是在这个库上使用类似的东西,使用 BitmapShader :
https://github.com/hdodenhof/CircleImageView
基于:
http://www.curious-creature.com/2012/12/11/android-recipe-1-image-with-rounded-corners/
相关代码为:
BitmapShader shader;
shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
RectF rect = new RectF(0.0f, 0.0f, width, height);
// rect contains the bounds of the shape
// radius is the radius in pixels of the rounded corners
// paint contains the shader that will texture the shape
canvas.drawRoundRect(rect, radius, radius, paint);
如果输入是位图,我仍然想知道如何在可绘制对象中完成所有操作。
【讨论】:
@pskink 嗯,有一些缺点:它需要一个位图,所以它不能处理任何类型的内容,我发现的解决方案在自定义的 imageView 中工作,而不是正如我在 SimpleRoundedDrawable 上所写的那样,只使用位图的可绘制对象。如果您知道如何使用 BitmapShader 制作可绘制对象,包括边框(AKA stroke AKA outline),请发布。如果它有效,我会勾选它(即使它不适用于其他类型的可绘制对象,因为这就是我需要的)。 所以如果你想避免使用位图,请使用 Canvas#clipPath 或 porter-duff 模式 @pskink 正如我所写,其他解决方案似乎效率较低。我更喜欢使用最好的解决方案。关于 porter-duff,您能否举一个例子(或链接)来说明这是一个可能的解决方案?我认为我没有看到解决方案。它和BitmapShader一样高效吗?它是否适用于所有 Android 版本,而无需禁用硬件加速? 所以决定要么避免使用位图,要么想要最快的解决方案,你不能同时拥有两者 @pskink 我写了我想避免创建一个新的位图,除了作为输入的那个。 BitmapShader 是一种方法(同样,假设您有一个位图作为输入),同时仍然比其他方法更有效。【参考方案4】:基于我的 CircleImageView 库的 CircleImageDrawable 的快速草稿。这不会创建新的 Bitmap,而是使用 BitmapShader 来实现所需的效果并居中裁剪图像。
public class CircleImageDrawable extends Drawable
private final RectF mBounds = new RectF();
private final RectF mDrawableRect = new RectF();
private final RectF mBorderRect = new RectF();
private final Matrix mShaderMatrix = new Matrix();
private final Paint mBitmapPaint = new Paint();
private final Paint mBorderPaint = new Paint();
private int mBorderColor = Color.BLACK;
private int mBorderWidth = 0;
private Bitmap mBitmap;
private BitmapShader mBitmapShader;
private int mBitmapWidth;
private int mBitmapHeight;
private float mDrawableRadius;
private float mBorderRadius;
public CircleImageDrawable(Bitmap bitmap)
mBitmap = bitmap;
mBitmapHeight = mBitmap.getHeight();
mBitmapWidth = mBitmap.getWidth();
@Override
public void draw(Canvas canvas)
canvas.drawCircle(mBounds.width() / 2.0f, mBounds.height() / 2.0f, mDrawableRadius, mBitmapPaint);
if (mBorderWidth != 0)
canvas.drawCircle(mBounds.width() / 2.0f, mBounds.height() / 2.0f, mBorderRadius, mBorderPaint);
@Override
protected void onBoundsChange(Rect bounds)
super.onBoundsChange(bounds);
mBounds.set(bounds);
setup();
private void setup()
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mBitmapPaint.setAntiAlias(true);
mBitmapPaint.setShader(mBitmapShader);
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
mBorderRect.set(mBounds);
mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);
mDrawableRect.set(mBorderRect);
mDrawableRect.inset(mBorderWidth, mBorderWidth);
mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
updateShaderMatrix();
invalidateSelf();
private void updateShaderMatrix()
float scale;
float dx = 0;
float dy = 0;
mShaderMatrix.set(null);
if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight)
scale = mDrawableRect.height() / (float) mBitmapHeight;
dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
else
scale = mDrawableRect.width() / (float) mBitmapWidth;
dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
mShaderMatrix.setScale(scale, scale);
mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
mBitmapShader.setLocalMatrix(mShaderMatrix);
@Override
public void setAlpha(int alpha)
mBitmapPaint.setAlpha(alpha);
invalidateSelf();
@Override
public void setColorFilter(ColorFilter colorFilter)
mBitmapPaint.setColorFilter(colorFilter);
invalidateSelf();
@Override
public int getOpacity()
return 0;
【讨论】:
我该怎么办?你和另一个人都写了一个工作代码...... :(你做了什么别人没有做的,反之亦然?也许我可以根据代码的质量来决定。【参考方案5】:已经有一个内置的方法来完成这个,它是 1 行代码 (ThumbnailUtils.extractThumbnail())
int dimension = getSquareCropDimensionForBitmap(bitmap);
bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension);
...
//I added this method because people keep asking how
//to calculate the dimensions of the bitmap...see comments below
public int getSquareCropDimensionForBitmap(Bitmap bitmap)
//If the bitmap is wider than it is tall
//use the height as the square crop dimension
if (bitmap.getWidth() >= bitmap.getHeight())
dimension = bitmap.getHeight();
//If the bitmap is taller than it is wide
//use the width as the square crop dimension
else
dimension = bitmap.getWidth();
如果您希望位图对象被回收,您可以传递使其如此的选项:
bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
以下是文档的链接: ThumbnailUtils Documentation
【讨论】:
标题说“不创建新位图”,这个解决方案确实创建了一个新位图。另外,它根本不会使位图变成圆形以上是关于如何在不创建新位图的情况下拥有圆形、中心裁剪的 imageView?的主要内容,如果未能解决你的问题,请参考以下文章