AvatarView - Custom Implementation of ImageView

Let’s deal with it. In almost every app, we need this kind of circular image as a profile image for people. We should show the image with the user’s initial while we load the original image.

Simply, we need to accommodate a placeholder and the user image. So, how do we do it?

First we need to define placeholder. We can use a TextView and set a drawable shape with some color for the placeholder. For the original image, we can use an ImageView. But we got a problem here. The default implementation of ImageView is rectangle shaped. But we need circular image. With a simple google search, we can get so many custom implementations of ImageView which provides circular image. We can use any of them.

Now we need some logic to show the initials while loading original image. Let’s put a Framelayout with the TextView and ImageView as children.

avatar_view.xml
Shape circle drawable for background of textview

Now, we first set the initials for the TextView, then set the visibility of the imageView as INVISIBLE or GONE. I suggest use some image loading library like Glide or Picasso to load images from URL as the library handles bitmap recycling, offline cache mechanism etc. I use Glide here.

As soon as the bitmap is ready, we set it to the ImageView, make it visible and make the placeholder invisible.

Logic to load avatar

Great. Everything is working fine.

Now your designer is giving some cool designs for multiple themes. In that theme, all the images are square instead of circle. So now we need to support circle images for one theme and rounded square images for another theme.

Okay, we can add another normal ImageView inside FrameLayout which would hold the rectangle image. For the placeholder, we need to define another shape drawable with square shape and set it as background dynamically for the TextView based on the theme.

Shape square drawable for background of textview
avatar_view.xml after adding square image support
Logic to load avatar

It does the job right? But can we do it better somehow?

The problem here is, to show a simple avatar, we are using three views although either one of them will be used at a time. And also, it is not maintainable. What if the next day your designer asks you to make the rectangle images rounded rectangle with slight bit of curve on all edges?

So let’s take a step back and think of an another approach.

Custom Views. Using custom views, we can accommodate all these various designs in a single view. If you don’t know about custom views, I suggest read some articles or watch some videos about it. It’s good to know about it since in every project, you’ll have to use some custom views someday to make life a lot easier.

So what are we gonna do? We are gonna extend ImageView and put all our logic (circle, rounded rectangle, placeholder with initials) in there.

Member variables needed in AvatarView

These are the variables we are going to use in the custom view. We need to initialise each of these variables.

Initialising member variables

Here, I’m going to use a POJO class named User which would contain the name of the user, avatar url, background color of the placeholder image etc. What is gonna be in the POJO class depends on you application architecture. Or you can also pass the URL, name through setters in the custom view.

We are storing the initials of the user in text field using a helper method.

We are returning first two letter if the name contains single word, else return first letters of first two words.

In the setDrawable method, we are drawing the text and background color on a canvas and store it in the drawable member variable.

If the avatar url is not null,

  • we get the image and set it in the same view (Note that we are extending ImageView, so this will make the imageView to draw the image when it is fetched from Glide) and put the placeholder as the drawable we set before.
  • else, we set the drawable as the source to be drawn using setImageDrawable method.
Initializing drawable

We create a new Drawable, fill the canvas with the background color and then draw the text in center of the drawable canvas.

  1. First we measure the width and height required for the text to be drawn using measureText method and getFontMetrics().ascent field.
  2. Then we draw rounded rectangle or circle depends upon the theme in the canvas with paint contains the background color (The shape can be initialized using separate setter or using custom atrributes).
  3. Then we draw the text in the canvas. (Drawing text after drawing background color ensures that the text will be in top and will not be overlapped by the background color. Consider drawing operations like a stack).

That’s all we have to do to initialize the placeholder drawable. Now we need to initialize rectF which would contain the bounds of the view. The best place to get bounds of the view would be onMeasure().

Now comes the main part of the custom view. onDraw() method.

onDraw part

This is where the actual drawing happens. We already draw something (Initials and background color) in the canvas before right? That was a drawable. That drawable itself will be drawn on the screen through this onDraw() method only.

In onDraw() method, we will be provided a canvas where we draw what needs to be drawn.

We can just use drawRoundedRect and drawCircle to draw paints into the canvas. But to draw and image, we need to convert it into bitmap and draw it. Even if we did like that, we are gonna end up with white background on undrawn areas in the canvas. This is because, in Android all views are rectangular (or square).

So what’s the solution for that? ClipPath.

We can clip the path of the canvas itself into any shape we want, which will stop the framework from draw unclipped areas into white. Cool, isn’t it?

All we need to do is draw the path we just need and pass that path to clipPath method in the canvas and the framework will do the rest.

There is one problem with clipPath method in canvas. From Honeycomb to API level 17, clipPath method is no longer supported in devices with hardware acceleration turned on. From API level 18, it is supported.

So, you will still see white background on undrawn areas from Honeycomb to API level 17. The solution for this problem is discussed here and as per the solution,

/*
* Below Jelly Bean, clipPath on canvas would not work because lack of hardware acceleration
* support. Hence, we should explicitly say to use software acceleration.
* */
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(LAYER_TYPE_SOFTWARE, null);
}

we should force it to use software acceleration. You can place this snippet anywhere in the custom view which uses clipPath.

So, our final view would be look like this.

AvatarView.java

Note that I extended AppCompatImageView instead of ImageView to utilize support library features. In xml, if you use just specify ImageView, TextView, EditText like that, on layout inflation, if you use support library, the framework inflates AppCompatImageView, AppCompatTextView, AppCompatEditText respectively (which contains custom implementations to backport lollipop features to pre-lollipop). So, if you need to use lollipop and above features to pre-lollipop like vector drawables, background tinting, then extend AppCompatImageView instead of ImageView.

I declared the shapes as custom attributes and set them on xml based on theme. This is defined in attr.xml.

<declare-styleable name="AvatarView">
<attr name="avatar_shape" format="string" />
</declare-styleable>

And in strings.xml,

<string name="circle">Circle</string>
<string name="rectangle">Rectangle</string>

And in styles.xml,

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
....
<item name="avatarShape">@string/circle</item>
</style>
<style name="AnotherTheme" parent="AppTheme">
....
<item name="avatarShape">@string/circle</item>
</style>

And in layout xmls,

<com.example.customviews.AvatarView
android:id="@+id/avatar_view"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
app:avatar_shape="?avatarShape" />

And in the activity class,

AvatarView avatarView = (AvatarView) findViewById(R.id.avatar_view);
avatarView.setUser(user);

That’s all. Of course, customize it based on your needs.

Happy coding :)

Android Developer at Zoho Corp