Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to have a circular, center-cropped imageView, without creating a new bitmap?

Note: I know there are a lot of questions and repositories about this, but none seems to fit what I try to achieve.

Background

Given a bitmap of any aspect-ratio, I wish to set it as the content of an ImageView (using a drawable only, without extending the ImageView), so that the content will be center-cropped, and yet in the shape of a circle.

All of this, with minimal memory usage, because the images could be quite large sometimes. I do not want to create a whole new Bitmap just for this. The content is already there...

The problem

All solutions I've found lack one of the things I've written: some do not center-crop, some assume the image is square-shaped, some create a new bitmap from the given bitmap...

What I've tried

Other than trying various repositories, I've tried this tutorial, and I tried to fix it for the case of non-square aspect ratios, but I've failed.

Here's its code, in case the website will get closed:

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;
      }

}

A very good solution I've found (here) does exactly what I need, except it uses it all in the ImageView itself, instead of creating a drawable. This means that I can't set it, for example, as the background of a view.

The question

How can I achieve this?


EDIT: this is the current code, and as I wanted to add border, it also has this code for it:

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);
    }
}

I hope this is how things should really work.


EDIT: It seems that the solution works only from specific Android version, as it doesn't work on Android 4.2.2. Instead, it shows a squared image.

EDIT: it seems that the above solution is also much less efficient than using BitmapShader (Link here). It would be really great to know how to use it within a drawable instead of within a customized ImageView

-- Here's the current modified version of the below solutions. I hope it will be handy for some people:

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;
    }
}
like image 937
android developer Avatar asked Feb 18 '16 12:02

android developer


People also ask

How do you center a crop bitmap in android programmatically?

getWidth()) { value = bitmap. getHeight(); } else { value = bitmap. getWidth(); } Bitmap finalBitmap = null; finalBitmap = Bitmap. createBitmap(bitmap, 0, 0, value, value);


1 Answers

If I'm following you correctly, your Drawable class would be like so:

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);
    }
}

An example usage would be:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mila);
CroppedDrawable cd = new CroppedDrawable(bitmap);
imageView.setImageDrawable(cd);

Which, with your previous sample image, would give something like this:

screenshot

like image 111
Mike M. Avatar answered Oct 09 '22 09:10

Mike M.