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;
    }
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • Do you just need the `ImageView` to display the cropped image? You don't need to actually crop the `Bitmap` itself? – Mike M. Feb 18 '16 at 13:13
  • @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. – android developer Feb 18 '16 at 13:16

5 Answers5

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

Mike M.
  • 38,532
  • 8
  • 99
  • 95
  • 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) ? – android developer Feb 19 '16 at 00:07
  • 1
    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. – Mike M. Feb 19 '16 at 01:05
  • "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. – Mike M. Feb 19 '16 at 01:05
  • @androiddeveloper I feel I've not adequately explained and/or provided the appropriate solution. Please let me know what further I can do. – Mike M. Feb 19 '16 at 22:57
  • About "border", I meant a colored circle around the circular-content of the image. – android developer Feb 20 '16 at 00:14
  • Also, thank you for trying to help. I will check it on Monday and see how well it works. – android developer Feb 20 '16 at 00:15
  • 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. – android developer Feb 20 '16 at 09:48
  • 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. – android developer Feb 22 '16 at 08:27
  • 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? – android developer Feb 24 '16 at 09:51
  • ok, I think it's because clipPath isn't supported on 4.3 and below using hardware acceleration (reported here: https://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. – android developer Feb 24 '16 at 11:13
  • As I've found out, this method is not efficient (here: http://www.curious-creature.com/2012/12/11/android-recipe-1-image-with-rounded-corners/ ), and BitmapShader is better. If you know how to use it and post a code, I will appreciate it – android developer Feb 25 '16 at 13:32
  • 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! – Mike M. Mar 02 '16 at 18:28
  • 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. – android developer Mar 02 '16 at 22:56
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;}
}
pskink
  • 23,874
  • 6
  • 66
  • 77
  • I've tested it, and I see that if the bitmap isn't square, the output looks like rounded-rectangle, instead of a circle. – android developer Feb 29 '16 at 11:53
  • sure, it uses `addRoundRect` inside `onBoundsChange`, change it to `addCircle` if you want a circle instead of a rounded rectangle – pskink Feb 29 '16 at 11:58
  • 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. – android developer Feb 29 '16 at 12:28
  • see the second `Drawable` – pskink Feb 29 '16 at 12:33
  • 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. – android developer Feb 29 '16 at 14:15
  • 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) – pskink Feb 29 '16 at 17:17
  • 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? – android developer Feb 29 '16 at 18:15
  • 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 solution – pskink Feb 29 '16 at 18:19
  • 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() ? – android developer Feb 29 '16 at 18:57
  • Ok, then. Thank you. This will now be marked as the correct answer. – android developer Feb 29 '16 at 20:33
  • Say, do you know what should be changed to make only some of the corners being rounded? – android developer Mar 28 '16 at 13:36
  • 1
    use `Path#addRoundRect` – pskink Mar 28 '16 at 14:24
  • And also change the "draw" function, so that the "canvas" will call "drawRoundedRect" or something? – android developer Mar 29 '16 at 05:16
  • what dont you understand? how to draw a `Path`? – pskink Mar 29 '16 at 06:40
  • 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? – android developer Mar 29 '16 at 06:45
  • no. only a `Path` has a feature for different radii for round rect – pskink Mar 29 '16 at 06:47
  • Can't this be useful: http://developer.android.com/reference/android/graphics/Canvas.html#drawRoundRect(float, float, float, float, float, float, android.graphics.Paint) ? – android developer Mar 29 '16 at 06:49
  • and where you have radii for every corner? – pskink Mar 29 '16 at 06:50
  • Right, but as I've read, using the Path method is less efficient, as written here: http://stackoverflow.com/a/35483275/878126 . Is it true here too? Will the other answer just need to be modified to use addRoundRect instead? – android developer Mar 29 '16 at 06:51
  • 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? – pskink Mar 29 '16 at 06:57
  • I see. Do you know of a nice tutorial/sample for this? – android developer Mar 29 '16 at 06:59
  • 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. – android developer Mar 29 '16 at 07:10
  • I've just found out this: http://developer.android.com/reference/android/support/v4/graphics/drawable/RoundedBitmapDrawable.html . 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)? – android developer Mar 29 '16 at 07:55
  • see [this](https://github.com/android/platform_frameworks_support/blob/master/v4/donut/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java) ^F `draw(Canvas canvas)` – pskink Mar 29 '16 at 07:59
  • Seems they use BitmapShader, and that all features we wrote about are missing. Too bad. – android developer Mar 29 '16 at 08:15
  • 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? – android developer Mar 29 '16 at 08:59
  • 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 radii – pskink Mar 29 '16 at 09:01
  • 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. – android developer Mar 29 '16 at 09:03
  • ok, i don't know why it happens. I will need to read what you wrote better to understand. sorry and thank you. – android developer Mar 29 '16 at 09:21
  • 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. – android developer Mar 31 '16 at 13:14
  • 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 hour – pskink Mar 31 '16 at 13:20
  • I wonder: is it maybe possible using the normal drawables for this? Like LayerDrawable, ShapeDrawable,... ? – android developer Mar 31 '16 at 15:11
  • i dont think so, ShapeDrawable is made for really trivial purposes, just merge two versions i posted to get the desired result – pskink Mar 31 '16 at 15:51
  • I made a new post about this, here: http://stackoverflow.com/q/36370964/878126 . Please continue there. – android developer Apr 02 '16 at 10:37
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.

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • @pskink 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). – android developer Feb 28 '16 at 09:31
  • so if you want to avoid a Bitmap, use Canvas#clipPath or porter-duff modes – pskink Feb 28 '16 at 09:36
  • @pskink 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 ? – android developer Feb 28 '16 at 09:42
  • so decide either you want to avoid the Bitmap or you want the fastest solution, you cannot have both – pskink Feb 28 '16 at 09:45
  • @pskink 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. – android developer Feb 28 '16 at 14:44
  • when using `BitmapShader` you dont have to create any additional `Bitmap` – pskink Feb 28 '16 at 14:48
  • @pskink Yes. That's why it works. It doesn't need an additional bitmap. It can work with what you give it. – android developer Feb 28 '16 at 14:50
  • so just use a BitmapShader solution as it is the fastest one – pskink Feb 28 '16 at 14:52
  • @pskink 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. – android developer Feb 28 '16 at 14:56
  • see RoundedBitnapDrawable.java sources – pskink Feb 28 '16 at 14:57
  • android/support/v4/graphics/drawable/RoundedBitmapDrawable.java – pskink Feb 28 '16 at 15:03
  • @pskink 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. – android developer Feb 28 '16 at 15:05
  • it is just a `Drawable`, it works as any other `Drawable`, like `BitmapDrawable`, `GradientDrawable` etc – pskink Feb 28 '16 at 15:07
  • @pskink Well, I tried now to use it, and it says it's not public. How did you manage to use it? – android developer Feb 28 '16 at 15:10
  • android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory – pskink Feb 28 '16 at 15:15
  • @pskink 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. – android developer Feb 28 '16 at 15:23
  • no, it has no "borders" concept, i mentioned about `RoundedBitmapDrawable` so that you can see how it works and add your own "border" stuff – pskink Feb 28 '16 at 15:26
  • @pskink 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. – android developer Feb 28 '16 at 18:47
  • so write your own `Drawable` based on `RoundedBitmapDrawable`, something like this for example: http://pastebin.com/fzUtPGzk – pskink Feb 28 '16 at 19:14
  • @pskink If you have an answer that works, please publish it here and I will check it out and tick it. – android developer Feb 29 '16 at 00:23
  • check out my previous comment – pskink Feb 29 '16 at 08:03
  • @pskink I meant really here, in stackOverflow. I can tick your answer and give you the bounty. – android developer Feb 29 '16 at 08:40
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;
    }

}
  • 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. – android developer Feb 29 '16 at 14:15
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

Mayank Bhatnagar
  • 2,120
  • 1
  • 12
  • 20
  • 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 all – android developer Mar 02 '16 at 07:32