如何在不创建新位图的情况下拥有圆形、中心裁剪的 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?的主要内容,如果未能解决你的问题,请参考以下文章

在不改变原始纵横比的情况下将位图大小调整为 512x512

在不裁剪的情况下填充 CGPath 的倒数

如何在不裁剪的情况下调整新图像对象的大小?

以矩形裁剪位图图像

如何像imageview一样裁剪位图中心? [复制]

如何在不更改 C#.Net 中的分辨率的情况下裁剪图像?