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

5

13

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;
    }
}
Conventioner answered 18/2, 2016 at 12:42 Comment(2)
Do you just need the ImageView to display the cropped image? You don't need to actually crop the Bitmap itself?Illampu
@MikeM. I don't want to change the ImageView or the bitmap. The input is already available and only needs to be drawn in a circle shape. This should all be done inside a drawable. cropping the image will create a new bitmap, which means extra memory usage.Conventioner
C
4

try this minimalist custom Drawable and modify it to meet your needs:

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;}
}
Concinnity answered 29/2, 2016 at 8:49 Comment(34)
I've tested it, and I see that if the bitmap isn't square, the output looks like rounded-rectangle, instead of a circle.Conventioner
sure, it uses addRoundRect inside onBoundsChange, change it to addCircle if you want a circle instead of a rounded rectangleConcinnity
ok, now after adding this: maskPath.addCircle(bounds.width() / 2, bounds.height() / 2, Math.min(bounds.width(), bounds.height()) / 2, Path.Direction.CW); , I got a circle, but it doesn't scale to the size of the imageView, like on the original solution.Conventioner
see the second DrawableConcinnity
What should I do? both you and another person wrote a working code... :( What did you do that the other didn't, and the opposite? Maybe I can decide by quality of code.Conventioner
the other solution is not only 2.5 x longer but also scales the Bitmap way too much (try it with a white rectamgular image that has for example 2 px red border: the border will be cut with no reason, now updated Drawable draws the bitmap more accurately (the previous solution with Canvas#concat had worse scaling quality)Concinnity
hmmm... I guess so, but about the longer code, that's also because you didn't implement some functions, and made them take a single line instead. He is the person behind the library I've mentioned. Anyway, you say yours perform better and works more correctly?Conventioner
oh only two methods are oneliners, but actually their real implementation has 3-4 lines, so when implemented my code would have not 40 but ~50 lines, compared to >100 from the other solutionConcinnity
In order to add their implementation, I need to do the same, but with "maskPaint" , right? meaning call their same-name function and call invalidateSelf() ?Conventioner
Ok, then. Thank you. This will now be marked as the correct answer.Conventioner
Say, do you know what should be changed to make only some of the corners being rounded?Conventioner
use Path#addRoundRectConcinnity
And also change the "draw" function, so that the "canvas" will call "drawRoundedRect" or something?Conventioner
what dont you understand? how to draw a Path?Concinnity
I don't understand what you wrote. The code doesn't have a Path instance. It has Canvas, Paint, Matrix, Shader... Why should a Path be added here? Can't the other variables be used, as before?Conventioner
no. only a Path has a feature for different radii for round rectConcinnity
Can't this be useful: developer.android.com/reference/android/graphics/…, float, float, float, float, float, android.graphics.Paint) ?Conventioner
and where you have radii for every corner?Concinnity
Right, but as I've read, using the Path method is less efficient, as written here: https://mcmap.net/q/50224/-how-to-have-a-circular-center-cropped-imageview-without-creating-a-new-bitmap . Is it true here too? Will the other answer just need to be modified to use addRoundRect instead?Conventioner
it is less efficient when using clipPath, not drawPath, but seriously: did you check how often Drawable#draw would be called (10 times per second or one time per minute)? so does it really matter?Concinnity
I see. Do you know of a nice tutorial/sample for this?Conventioner
For using what you wrote, in order to draw the bitmap in "semi"-rounded rect, while also being able to draw an outline/stroke around it.Conventioner
I've just found out this: developer.android.com/reference/android/support/v4/graphics/… . What's your opinion about it? Does it work as efficient as yours, without bitmap creation and with shaders? Can it also have an outline and also have semi-rounded corners (I don't see those)?Conventioner
see this ^F draw(Canvas canvas)Concinnity
Seems they use BitmapShader, and that all features we wrote about are missing. Too bad.Conventioner
Thank you ! What is the note part? What is there to match for "radii" ? the size of the bitmap is already known, so I can set whatever radius-per-corner I wish, no?Conventioner
did you run the code? can you see that the border doesnt perfectly match the bitmap? this is because both bitmap and border use the same radiiConcinnity
I can't run it right now (I'm at work and handle something else). I don't understand, but maybe after checking it out, I will. Thank you.Conventioner
ok, i don't know why it happens. I will need to read what you wrote better to understand. sorry and thank you.Conventioner
I've tried it now without a border, to see how it works, but it has an issue: showing a landscape bitmap on a squared imageView will still show the content as landscape (having empty space), even though I set android:scaleType="centerCrop" . I think I should create a new question for this.Conventioner
the rounded rect version using the Path shows the whole bitmap, so it doesn't crop the content like the previous one: you need to combine those two versions - it shouldn't take more than quarter of an hourConcinnity
I wonder: is it maybe possible using the normal drawables for this? Like LayerDrawable, ShapeDrawable,... ?Conventioner
i dont think so, ShapeDrawable is made for really trivial purposes, just merge two versions i posted to get the desired resultConcinnity
I made a new post about this, here: https://mcmap.net/q/50144/-how-to-create-a-partially-rounded-corners-rectangular-drawable-with-center-crop-and-without-creating-new-bitmap/878126 . Please continue there.Conventioner
I
6

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

Illampu answered 18/2, 2016 at 13:50 Comment(13)
Can you please explain this? what do you use "rewind" for? Where is the part that takes a square part of the image? How did you find this solution? How, for example would you change it to support border (like other solutions have) ?Conventioner
Sure, but FYI, I'm still kinda unclear as to your end goal; this was merely a suggestion. "what do you use "rewind" for?" - It essentially resets the Path, so we're not adding another circle to it should your layout change in situ; e.g., if you add a Button to it, or something. "Where is the part that takes a square part of the image?" - It's the figuring of the least dimension - the Math.min() function - and making it the radius of our circle. "How did you find this solution?" - Um, just thinking? I dunno. I'm just familiar with thinking about such things.Illampu
"How, for example would you change it to support border (like other solutions have) ?" - I'm not sure what you mean by border, but if it is as I'm as thinking, we'd just decrease the radius of the Path's circle - i.e., the third parameter in the addCircle() method. Please lemme know where I'm lacking in my description.Illampu
@androiddeveloper I feel I've not adequately explained and/or provided the appropriate solution. Please let me know what further I can do.Illampu
About "border", I meant a colored circle around the circular-content of the image.Conventioner
Also, thank you for trying to help. I will check it on Monday and see how well it works.Conventioner
Thank you again. Also, I don't understand the last sentence. Please forgive me. English isn't my main language, but it seems like you don't understand that I appreciate you trying to help me. I will now add +1 for the answer.Conventioner
ok, I've tested the code. It works, but I had an issue with the border (AKA outline AKA stroke). I've updated the question to show the current code, and when changing the third parameter as you wrote caused it to have a bad padding and truncating of the border. Can you please check it out and tell me what's wrong? Also, btw, it's important to set scaleType="centerCrop" to make it take the whole space that the imageView can provide, otherwise, even if the bitmap is large, it can get shrunk a lot.Conventioner
I've found out that this solution doesn't work at all on Android 4.2.2 (tested on LG G2) . Instead of being rounded, it's shown as a square image. How come? How can I fix it?Conventioner
ok, I think it's because clipPath isn't supported on 4.3 and below using hardware acceleration (reported here: code.google.com/p/android/issues/detail?id=58737) . Using this setLayerType(View.LAYER_TYPE_SOFTWARE, null); will fix it on such Android versions. If you know a better solution, please let me know.Conventioner
As I've found out, this method is not efficient (here: curious-creature.com/2012/12/11/… ), and BitmapShader is better. If you know how to use it and post a code, I will appreciate itConventioner
Oh, hey, sorry. I've not been around for a while. Well, that's a bummer about the clipPath() method. I wasn't aware of those issues. Thanks, though, for the info and link. Sorry you had to spend a 100 rep points, but I'm glad you got an answer. Cheers!Illampu
It is a very nice solution though, and I think it can be customized to any content. It also avoids creation of bitmaps, and work on any drawable. I think that if a bitmap isn't available, and you wish to save on memory, this can still be a nice solution.Conventioner
C
4

try this minimalist custom Drawable and modify it to meet your needs:

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;}
}
Concinnity answered 29/2, 2016 at 8:49 Comment(34)
I've tested it, and I see that if the bitmap isn't square, the output looks like rounded-rectangle, instead of a circle.Conventioner
sure, it uses addRoundRect inside onBoundsChange, change it to addCircle if you want a circle instead of a rounded rectangleConcinnity
ok, now after adding this: maskPath.addCircle(bounds.width() / 2, bounds.height() / 2, Math.min(bounds.width(), bounds.height()) / 2, Path.Direction.CW); , I got a circle, but it doesn't scale to the size of the imageView, like on the original solution.Conventioner
see the second DrawableConcinnity
What should I do? both you and another person wrote a working code... :( What did you do that the other didn't, and the opposite? Maybe I can decide by quality of code.Conventioner
the other solution is not only 2.5 x longer but also scales the Bitmap way too much (try it with a white rectamgular image that has for example 2 px red border: the border will be cut with no reason, now updated Drawable draws the bitmap more accurately (the previous solution with Canvas#concat had worse scaling quality)Concinnity
hmmm... I guess so, but about the longer code, that's also because you didn't implement some functions, and made them take a single line instead. He is the person behind the library I've mentioned. Anyway, you say yours perform better and works more correctly?Conventioner
oh only two methods are oneliners, but actually their real implementation has 3-4 lines, so when implemented my code would have not 40 but ~50 lines, compared to >100 from the other solutionConcinnity
In order to add their implementation, I need to do the same, but with "maskPaint" , right? meaning call their same-name function and call invalidateSelf() ?Conventioner
Ok, then. Thank you. This will now be marked as the correct answer.Conventioner
Say, do you know what should be changed to make only some of the corners being rounded?Conventioner
use Path#addRoundRectConcinnity
And also change the "draw" function, so that the "canvas" will call "drawRoundedRect" or something?Conventioner
what dont you understand? how to draw a Path?Concinnity
I don't understand what you wrote. The code doesn't have a Path instance. It has Canvas, Paint, Matrix, Shader... Why should a Path be added here? Can't the other variables be used, as before?Conventioner
no. only a Path has a feature for different radii for round rectConcinnity
Can't this be useful: developer.android.com/reference/android/graphics/…, float, float, float, float, float, android.graphics.Paint) ?Conventioner
and where you have radii for every corner?Concinnity
Right, but as I've read, using the Path method is less efficient, as written here: https://mcmap.net/q/50224/-how-to-have-a-circular-center-cropped-imageview-without-creating-a-new-bitmap . Is it true here too? Will the other answer just need to be modified to use addRoundRect instead?Conventioner
it is less efficient when using clipPath, not drawPath, but seriously: did you check how often Drawable#draw would be called (10 times per second or one time per minute)? so does it really matter?Concinnity
I see. Do you know of a nice tutorial/sample for this?Conventioner
For using what you wrote, in order to draw the bitmap in "semi"-rounded rect, while also being able to draw an outline/stroke around it.Conventioner
I've just found out this: developer.android.com/reference/android/support/v4/graphics/… . What's your opinion about it? Does it work as efficient as yours, without bitmap creation and with shaders? Can it also have an outline and also have semi-rounded corners (I don't see those)?Conventioner
see this ^F draw(Canvas canvas)Concinnity
Seems they use BitmapShader, and that all features we wrote about are missing. Too bad.Conventioner
Thank you ! What is the note part? What is there to match for "radii" ? the size of the bitmap is already known, so I can set whatever radius-per-corner I wish, no?Conventioner
did you run the code? can you see that the border doesnt perfectly match the bitmap? this is because both bitmap and border use the same radiiConcinnity
I can't run it right now (I'm at work and handle something else). I don't understand, but maybe after checking it out, I will. Thank you.Conventioner
ok, i don't know why it happens. I will need to read what you wrote better to understand. sorry and thank you.Conventioner
I've tried it now without a border, to see how it works, but it has an issue: showing a landscape bitmap on a squared imageView will still show the content as landscape (having empty space), even though I set android:scaleType="centerCrop" . I think I should create a new question for this.Conventioner
the rounded rect version using the Path shows the whole bitmap, so it doesn't crop the content like the previous one: you need to combine those two versions - it shouldn't take more than quarter of an hourConcinnity
I wonder: is it maybe possible using the normal drawables for this? Like LayerDrawable, ShapeDrawable,... ?Conventioner
i dont think so, ShapeDrawable is made for really trivial purposes, just merge two versions i posted to get the desired resultConcinnity
I made a new post about this, here: https://mcmap.net/q/50144/-how-to-create-a-partially-rounded-corners-rectangular-drawable-with-center-crop-and-without-creating-new-bitmap/878126 . Please continue there.Conventioner
C
2

As it seems, using "clipPath" isn't efficient and needs to have hardware-acceleration disabled on 4.3 and below.

A better solution is to use something like on this library, using BitmapShader :

https://github.com/hdodenhof/CircleImageView

which is based on :

http://www.curious-creature.com/2012/12/11/android-recipe-1-image-with-rounded-corners/

The relevant code is:

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

I still wish to know how to do it all in a drawable, if the input is a bitmap.

Conventioner answered 28/2, 2016 at 7:33 Comment(22)
@Concinnity Well, there are some disadvantages: it requires a bitmap, so it can't work on any kind of content, and the solutions I've found work inside a customized imageView, instead of a drawable that just uses the bitmap, as I wrote on SimpleRoundedDrawable . If you know how to make a drawable that uses BitmapShader, including border (AKA stroke AKA outline), please post it. If it works, I will tick it (even though it won't work on other types of drawable, because that's what I need anyway).Conventioner
so if you want to avoid a Bitmap, use Canvas#clipPath or porter-duff modesConcinnity
@Concinnity as I wrote, it seems that other solutions are less efficient. I prefer to use the best solution out there. About porter-duff, can you please show an example (or link) about this being a possible solution? I don't think I saw a solution with this. Is it as efficient as BitmapShader? Will it work on all Android versions, without the need to disable hardware-acceleration ?Conventioner
so decide either you want to avoid the Bitmap or you want the fastest solution, you cannot have bothConcinnity
@Concinnity I wrote I want to avoid creating a new bitmap, in addition to the one that you get as an input. BitmapShader is a way to do it (again, assuming you have a bitmap as an input) while still being more efficient than the other methods of doing it.Conventioner
when using BitmapShader you dont have to create any additional BitmapConcinnity
@Concinnity Yes. That's why it works. It doesn't need an additional bitmap. It can work with what you give it.Conventioner
so just use a BitmapShader solution as it is the fastest oneConcinnity
@Concinnity Exactly. What I wrote is that if you know how to make it work from within a Drawable class (instead of a customized view) and also have a border, I would really appreciate it. Currently I use the library.Conventioner
see RoundedBitnapDrawable.java sourcesConcinnity
android/support/v4/graphics/drawable/RoundedBitmapDrawable.javaConcinnity
@Concinnity I'm pretty sure I've tried it. Are you sure it works? Have you tried it? If you've succeeded, please post a sample code.Conventioner
it is just a Drawable, it works as any other Drawable, like BitmapDrawable, GradientDrawable etcConcinnity
@Concinnity Well, I tried now to use it, and it says it's not public. How did you manage to use it?Conventioner
android.support.v4.graphics.drawable.RoundedBitmapDrawableFactoryConcinnity
@Concinnity Interesting. I also had to call "setCircular(true)" , but now the output image is stretched. I tried calling "setGravity(Gravity.CENTER)", but it didn't help. I'm also not sure if it has the ability to add borders.Conventioner
no, it has no "borders" concept, i mentioned about RoundedBitmapDrawable so that you can see how it works and add your own "border" stuffConcinnity
@Concinnity Well, even if I ignore the border or add my own (because I can even add it as an extra layer in LayerDrawable), thing is, I can't find how to make the content be center-cropped in this class you've provided.Conventioner
so write your own Drawable based on RoundedBitmapDrawable, something like this for example: pastebin.com/fzUtPGzkConcinnity
@Concinnity If you have an answer that works, please publish it here and I will check it out and tick it.Conventioner
check out my previous commentConcinnity
@Concinnity I meant really here, in stackOverflow. I can tick your answer and give you the bounty.Conventioner
O
2

A quick draft of a CircleImageDrawable based on my CircleImageView library. This does not create a new Bitmap, uses a BitmapShader to achieve the desired effect and center-crops the image.

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

}
Obrien answered 29/2, 2016 at 14:7 Comment(1)
What should I do? both you and another person wrote a working code... :( What did you do that the other didn't, and the opposite? Maybe I can decide by quality of code.Conventioner
S
1

There is already a built-in way to accomplish this and it's 1 line of code (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();
    }  
} 

If you want the bitmap object to be recycled, you can pass options that make it so:

bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);

Below is the link for the documentation: ThumbnailUtils Documentation

Sleekit answered 2/3, 2016 at 6:40 Comment(1)
Title says "without creating a new bitmap", and this solution do create a new bitmap. Plus, it doesn't make the bitmap into a circle shape at allConventioner

© 2022 - 2024 — McMap. All rights reserved.