4

I'm writing a custom Circular ImageView in Android. I need to set a Drawable overlay on top of it, so I chose to write a custom CircularImageView that holds the picture itself + the drawable.

Actually I have 2 problems:

  1. The image is drawn top-left, I need it to be drawn on the center of the View
  2. I need my crown to be bigger (drawable) but I don't know how to resize it.

Some imgs to clarify:
What I'd like to achieve:
enter image description here
What I have now:(please, disconsider the black frame border, it's just to clarify the wrong image "gravity")
enter image description here

My view code:

public class CrownCircularImageView extends ImageView {

    private Drawable crown;
    private int canvasSize;
    private int crownWidth;
    private int crownHeight;

    // Object used to draw
    private Bitmap image;
    private Drawable drawable;
    private Paint paint;
    private Paint crownPaint;

    public CrownCircularImageView(Context context) {
        this(context, null, 0);
    }

    public CrownCircularImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CrownCircularImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        crownPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        this.crown = ContextCompat.getDrawable(context, R.drawable.ic_crown);
    }

    private void loadBitmap() {
        if (this.drawable == getDrawable())
            return;

        this.drawable = getDrawable();
        this.image = drawableToBitmap(this.drawable);
        updateShader();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        canvasSize = w - crownWidth;
        if (h < canvasSize)
            canvasSize = h - crownHeight;
        if (image != null)
            updateShader();
    }

    private void updateShader() {
        if (image == null)
            return;

        // Crop Center Image
        image = cropBitmap(image);

        // Create Shader
        BitmapShader shader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        // Center Image in Shader
        Matrix matrix = new Matrix();
        matrix.setScale((float) canvasSize / (float) image.getWidth(), (float) canvasSize / (float) image.getHeight());
        shader.setLocalMatrix(matrix);

        // Set Shader in Paint
        paint.setShader(shader);
    }

    private Bitmap cropBitmap(Bitmap bitmap) {
        Bitmap bmp;
        if (bitmap.getWidth() >= bitmap.getHeight()) {
            bmp = Bitmap.createBitmap(
                    bitmap,
                    bitmap.getWidth() / 2 - bitmap.getHeight() / 2,
                    0,
                    bitmap.getHeight(),
                    bitmap.getHeight());
        } else {
            bmp = Bitmap.createBitmap(
                    bitmap,
                    0,
                    bitmap.getHeight() / 2 - bitmap.getWidth() / 2,
                    bitmap.getWidth(),
                    bitmap.getWidth());
        }
        return bmp;
    }

    private Bitmap drawableToBitmap(Drawable drawable) {
        if (drawable == null) {
            return null;
        } else if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        int intrinsicWidth = drawable.getIntrinsicWidth();
        int intrinsicHeight = drawable.getIntrinsicHeight();

        if (!(intrinsicWidth > 0 && intrinsicHeight > 0))
            return null;

        try {
            // Create Bitmap object out of the drawable
            Bitmap bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        } catch (OutOfMemoryError e) {
            // Simply return null of failed bitmap creations
            Log.e(getClass().toString(), "Encountered OutOfMemoryError while generating bitmap!");
            return null;
        }
    }

    @Override
    public void onDraw(Canvas canvas) {
        // Load the bitmap
        loadBitmap();

        // Check if image isn't null
        if (image == null)
            return;

        if (!isInEditMode()) {
            canvasSize = canvas.getWidth();
            if (canvas.getHeight() < canvasSize) {
                canvasSize = canvas.getHeight();
            }
        }

        int circleCenter = (canvasSize - crownHeight) / 2;
        int cx = (canvasSize - crownWidth) / 2;
        int cy = (canvasSize - crownHeight) / 2;

        Bitmap crownBmp = drawableToBitmap(crown);

        int crownX = cx;
        int crownY = cy;

        canvas.drawCircle(cx, cy, circleCenter, paint);
        canvas.drawBitmap(crownBmp, crownX, crownY, crownPaint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);

        crownWidth = crown.getIntrinsicWidth();
        crownHeight = crown.getIntrinsicHeight();

        setMeasuredDimension(width, height);
    }

    @Override
    public ScaleType getScaleType() {
        return ScaleType.CENTER_CROP;
    }

    private int measureWidth(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // The parent has determined an exact size for the child.
            result = specSize;
        } else if (specMode == MeasureSpec.AT_MOST) {
            // The child can be as large as it wants up to the specified size.
            result = specSize;
        } else {
            // The parent has not imposed any constraint on the child.
            result = canvasSize;
        }

        return result + crown.getIntrinsicWidth();
    }

    private int measureHeight(int measureSpecHeight) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpecHeight);
        int specSize = MeasureSpec.getSize(measureSpecHeight);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else if (specMode == MeasureSpec.AT_MOST) {
            // The child can be as large as it wants up to the specified size.
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = canvasSize;
        }

        return (result + 2 + crown.getIntrinsicHeight());
    }

}
Leonardo
  • 3,141
  • 3
  • 31
  • 60

2 Answers2

1

You can create a new bitmap to draw the old one on a transparent background with a circle mask, and then draw the crown on it. This example also allows to add a padding around the image.

You will want to tweak the values of CIRCLE_PADDING and RESIZE_CROWN_FACTOR to fulfill your needs.

public class CrownImageView extends ImageView {
    private static final int CIRCLE_PADDING = 25;
    private static final float RESIZE_CROWN_FACTOR = 1.5f;

    private Bitmap rounded;
    private Bitmap resizedCrown;

    public CrownImageView(final Context context) {
        super(context);
    }

    public CrownImageView(final Context context, final AttributeSet attrs) {
        super(context, attrs);
    }

    public CrownImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Drawable drawable = getDrawable();

        if (drawable == null || getWidth() == 0 || getHeight() == 0) {
            return;
        }

        if (resizedCrown == null) {
            loadCrown();
        }

        loadImage(drawable);

        canvas.drawBitmap(rounded, 0, 0, null);
        canvas.drawBitmap(resizedCrown, canvas.getWidth() - resizedCrown.getWidth(), 0, null);
    }

    private void loadImage(Drawable drawable) {
        Bitmap bmp = bitmapFromDrawable(drawable);

        final Rect rect = new Rect(0, 0, bmp.getWidth(), bmp.getHeight());

        final Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);
        paint.setDither(true);

        rounded = Bitmap.createBitmap(bmp.getWidth(),
                bmp.getHeight(), Bitmap.Config.ARGB_8888);

        Canvas newCanvas = new Canvas(rounded);
        newCanvas.drawARGB(0, 0, 0, 0);

        float centerX = getWidth() / 2;
        float centerY = getHeight() / 2;
        float radius = Math.min(getWidth(), getHeight()) / 2 - CIRCLE_PADDING;
        newCanvas.drawCircle(centerX, centerY, radius, paint);

        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        newCanvas.drawBitmap(bmp, rect, rect, paint);
    }

    private void loadCrown() {
        Bitmap crown = BitmapFactory.decodeResource(getResources(), R.drawable.crown);
        resizedCrown = Bitmap.createScaledBitmap(crown,
                (int) (crown.getWidth() * RESIZE_CROWN_FACTOR),
                (int) (crown.getHeight() * RESIZE_CROWN_FACTOR),
                true);
    }

    private Bitmap bitmapFromDrawable(Drawable drawable) {
        Bitmap bmp;

        if (drawable instanceof BitmapDrawable) {
            bmp = ((BitmapDrawable) drawable).getBitmap();
        } else {
            bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas bmpCanvas = new Canvas(bmp);
            drawable.setBounds(0, 0, bmpCanvas.getWidth(), bmpCanvas.getHeight());
            drawable.draw(bmpCanvas);
        }

        return bmp;
    }
}

Update: You can use it with Glide like this

final CrownImageView imageView = (CrownImageView) findViewById(R.id.fragment_kids_row_img_kids);
Glide.with(this).load(yourimageurl).into(imageView);
antonio
  • 18,044
  • 4
  • 45
  • 61
  • First of all thanks for the help, I'll try this code in a minute. Just a point, isn't it bad to create instances on onDraw ? I think it should be done on the constructor, right ? – Leonardo Jun 28 '16 at 16:24
  • Absolutely right, creating instances on the `onDraw` is not a good practice. I'm doing it only if they are `null`, but you can move the creation to the constructor. – antonio Jun 28 '16 at 16:29
  • I forgot to mention that I'm using Glide, so I got an exception on the BitmapDrawable casting. – Leonardo Jun 28 '16 at 21:27
  • Have you tried using the `drawableToBitmap` method that you had in your code? – antonio Jun 28 '16 at 21:38
  • I have changed the code using http://stackoverflow.com/questions/18459618/java-lang-classcastexception-in-android-when-try-to-get-application-icon to avoid the `ClassCastException` – antonio Jun 29 '16 at 13:36
  • Using your code, that's what I got : http://i.imgur.com/9BYLVVR.png; As you can see, some imgs are getting cropped. – Leonardo Jun 29 '16 at 14:07
  • It seems that the image has not loaded completely. Is Glide returning the image correctly? – antonio Jun 29 '16 at 14:10
  • We can solve it creating the image on the ondraw as you were doing on your code. I'm out of my computer now, I will take a look at it ASAP – antonio Jun 29 '16 at 16:30
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/115995/discussion-between-antonio-and-leonardo-ferrari). – antonio Jun 29 '16 at 17:44
0

The image is drawn top-left, I need it to be drawn on the center of the View

Yes, that is because of parameters to .drawCircle

canvas.drawCircle(cx, cy, circleCenter, paint);

You are not calculating them for your image. This should be something like

int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
float bitmapRadius = Math.min(image.getWidth(), image.getHeight()) / 2f;
canvas.drawCircle(centerX, centerY, bitmapRadius, paint);

But I think it is better to save this values and recalculate them when size changes.

I need my crown to be bigger (drawable) but I don't know how to resize it.

You specify size of bitmap yourself - make it a bit bigger by specifying another size.

Bitmap bitmap = Bitmap.createBitmap(
      intrinsicWidth + scale, 
      intrinsicHeight + scale, 
      Bitmap.Config.ARGB_8888);

I've modified your code: http://pastebin.com/0h02Tqh1

Yaroslav
  • 4,750
  • 3
  • 22
  • 33