Hidden Gem of Android: Custom Views

Anand Mishra
12 min readAug 12, 2019

--

Source: Google

It had been a couple of years into Android Development for me and I wasn’t fully aware of something called Custom Views in Android. A couple of people around me kept telling things about it and how beautiful they are if you can master it. Didn’t buy it at that time. Maybe, I wasn’t ready to move out of my comfort zone, for all the streaming platforms had me so hooked that I actually forgot to learn something which will help me in the longer run. You see? Not really my fault at all. It’s them.

But it wasn’t too late when I got my hands on it and when I got to know about the things that you could do with it, surprised me and also the number of ways you can make the most of Custom Views in order to optimize most of the things related to your app in terms of Views and their drawing.

When do you need Custom Views?

Anyone new to Android Development, most of us always start with a TextView, ButtonView, ImageView and so on. These are subclass of Views and Android has already exposed it for us. We just need to add some attributes to them via the xml file or programmatically and we are done. But what if someday you need to create a view which is not exposed by Android for you. What if you want to create a TextView which will always have an Image beside the text, let’s call it, TextImageView for that matter. By the power of ViewGroups we can achieve that but what if it is more complex than a Text and an Image side by side. What if it is a different kind of Circular Progress Bar?

Source: Google

Android simply cannot assume that you need something like this and have it exposed for you just like the ButtonViews and TextViews and whatnot!

If need be, you have to create this on your own and for that you will have to make use of the fundamentals provided by android and create your own view for that matter. This is where CustomViews come into picture.

Simply put: A friend in need is a friend indeed!

Alright, the intention of this post is to not go all gaga about CustomViews. A whole lot of people have already done that before. I am sure if the basics are clear you will do wonders with them. And that is why my intention is to take you onboard with a very simple example in this Part 1 of a series and that’s it! In the next part, we will be building something beautiful based on the knowledge we gain over here.

We are going to create a CustomView which will give us an Uber Like Animation while you have waited multiple times for your cab to be booked!

First things first, CustomViews are just a class that extend the View class in order to use the power of inheritance to set their base straight and then take the idea forward to define some traits of their own. If you see carefully, all the Button and Text Views extend View and then on top of that they add their own property. So what is it that they need View for? Can they not write everything from the scratch? Good question. This is where the power of inheritance come into picture. A subclass extends everything from the super class and then if need be, overrides something that defines them. And the one thing we need to override in order to make the most of View class is:

Method inside a View class: onDraw(Canvas canvas)

The canvas that you get is the Canvas where you will use to draw something on, same as you would if it were a painting. And then you just need a Paint object to take it forward. Can’t just imagine all the drawings. Can you? You need to draw them with the help of a paint brush to make sense.

The land of make believe!

The Canvas and the Paint object is all you need to create a view of your own.

Let’s get to the coding part, shall we?

For setting up the base we will create a basic CustomView which will draw a Circle for us and that is it.

Once you use the View class as your super class to your sub class, CircleView in our case, you will get an error saying it needs a default constructor. You do an Option + Enter and it asks you to select 4 constructors if you can or want to? But why?

Simply put, it depends on the way you want to use your CustomView.

  • Programmatically: using LayoutInflater you will need the first one.
  • XMLs: You will need the second one as you will be passing a set of attributes, right? Hence, AttributeSet.
  • Rest of them are needed when you want to add a default style or keep an option to extend the same for other developers, let’s say in cases of Widgets. Don’t worry, we won’t need them for our example or if you are curious go here.

You can just import first two of them and be done with this window. Ah! That was scary!

To think of it, since we are dealing with multiple constructors you will need a common method which will initialize things for you, be it any constructor. For this reason we will add a common init() method where all the initialization will be done and we will call the same method from all our constructors.

Yes, this makes sense!

And init is exactly where we will create our Paint object as we will already get the Canvas object from onDraw. You may ask why aren’t we using the onDraw method to create an instance of the Paint object so that there is no need to create the ugly init method. And, yes, that will be a valid point.

Let’s do it then. We will create our Paint object inside onDraw and see what happens.

The moment I create any object inside onDraw, the IDE starts complaining. Reason? This onDraw method will be called multiple(a lot) times and it is tightly coupled with many UI operations that are internally present in Android. This is the reason it is highly advisable to create new instances of any object at some place other than onDraw. This piece of information will be available in every piece of article written on introduction to CVs and I am also mentioning it up front as this is very important for the optimization of GC.

Now that we know about the overhead that will get introduced if used onDraw for object creation we will move back to our ugly init method.

Stop it. It is not that UGLY!

Our early version of init method is going to be pretty straightforward. We will define a global Paint object and will create an instance over here.

Now in the onDraw method you will keep using this paint object as a brush to draw whatever you need.

Before that, it is important to know that the Canvas class has predefined methods which helps you easily create geometrical forms if you have passed the right parameters. Here we only need to draw a circle, so we will use drawCircle with the right parameters. You can go through the whole list of methods that are exposed for you. This is your BIBLE.

The drawCircle method needs 4 parameters:

In our case we are going to draw the circle at the center of the screen so we will use getWidth() and getHeight() methods which is basically the width and the height of the parent view. If we divide both of them by 2 we will be at the center of the screen, right? Yes!

Now, we are all set up with our view. We can go ahead and use the same in our layouts same as we would if it were a Text or Button View. Start typing and the IntelliSense will guide you.

Run your app and you will see something like this:

Pretty, ain’t it?

The circle is black and the style type is FILL_COLOR as we have not overridden them anywhere. If you want the style type to be stroke you can ask the Paint object to do it for you.

I have also set the stroke width to 15.

That’s it. You are all set up with your first Custom View!

The Pursuit of Happyness

If you see clearly you’d have seen that using the paint object we have set the color in the class file and not in the layout as we generally do. If you want to add it in the layout, that is where you’d need help of the second constructor. You’d want to do this in case you want this view of yours to be reusable.

We will also have to add a couple of signatures to our init method so that it is reusable for as many constructors we’d like to add.

I have added a AttributeSet parameter as we will be passing an attribute in the xml_layout file and we’d be expecting it to be present over here. To set those things in motion we’d need to add a couple of things. We will update the attrs.xml inside res => values and over there we will add our custom set of attributes we’d like to add to our CircleView. Right now, we only need color and the type of the attribute is ‘color’. It could be ‘boolean’ , ‘integer’, ‘float’ or ‘string’ and so on if it is needed. With that our attrs.xml file is going to look something like this:

And we can add the below line in the layout file where we are using our CircleView:

app:widthStroke="4"
app:color="@color/circle_color"

After all these changes our init method will be something like this:

We are reading two values from the xml attributes: color and widthStroke. There are couple of more lines that I have added related to TypedArray. The reason we did it is that whenever we are manipulating resources(resource attributes in this case) on our own, it is important for them to be reused or GC’d as much as possible. In this case the line where we have called the recycle() method on TypedArray we clearly mention it to be reusable and even if we don’t the IDE would want us to.

Now, our CircleView looks exactly how it was earlier but the two things that we have added is to customize widthStroke and color at the layout level and it all makes sense because we’d want to use our custom view in as many places as possible with some tweaks. In simple words, NO HARDCODING!

Let’s just move up to the animation part. We will be using ValueAnimator and will add an INFINITE Animation with a start and stop action. Ready?

We will use ValueAnimator to animate the value of the radius property of the circle as we want our infinite animation to be based on that.

To give you a brief of how a ValueAnimator works, at the very lowest level it changes a value, we can call it a property of a view. It wants a range from where you want it to move as in to and fro motion. It then changes its value from lowest to highest in between the amount of duration you have set the animation to.

For example, let’s say you want a value to start from 10 and end at 100 at a regular interval of 2seconds. ValueAnimator is responsible to change the value from 10–100 in 2 seconds. You don’t have to worry about any of that. It’s a built in functionality. You just have to pass the parameters based on your requirements. Also, you can pass multiple properties to the VA to operate on. It is just not limited to one. Here we will be animating on radius, you can also change the color of the circle at regular intervals. Your call.

Let’s define a ValueAnimator object as a class level object because we’d use it to start and stop the animation inside any method defined at the class level. Before that let’s also add a radius attribute in our layout and values file same as we have added for widthStroke and color. And then rather than hardcoding it in our onDraw method where we call drawCircle, we will use this as the 3rd parameter.

canvas.drawCircle(viewWidth, viewHeight, circleRadius, paintObject);

We will define two methods inside CricleView: startAnimation and stopAnimation. We will control it from our Activity with the help of a Button.

The list of things that is being done in the above method:

  • We have set the range from 20(minimum radius) to whatever you have defined in the layout.
  • Duration of the animation: 2000ms
  • A predefined interpolator : AccelerateDecelerateInterpolator. Link
  • AnimationUpdateListener: Callback which will get called every time the value gets changed. We will save the same in circleRadius.
  • setRepeatCount: INFINITE => Run Indefinitely.
  • setRepeatMode: REVERSE => Reverse from extremes. Don’t restart.
  • valueAnimator.start() => In the end just start the animation.
  • stopAnimation() => Stop the ValueAnimator.

To get this up and running you can create an instance of the CircleView view in your activity’s onCreate using findViewById or ButterKnife in case your preferences are important. Not judging you!

And call startAnimation and stopAnimation whenever you want to.

There’s a catch!

The value animator is changing the value from start to end and in onAnimationUpdate we are saving that value inside circleRadius. We are also using the same field, circleRadius when we are calling drawCircle on the canvas object and passing the 3rd parameter as the radius of the circle inside onDraw. But once the view gets drawn and even when the value of circleRadius is getting changed- you can put a log to verify this- the view just doesn’t animate.

Reason?

Think about it, once the view is drawn, who is telling the view to re-draw itself with the updated value? No one!

This is why you need to call invalidate every time you get the updated value. In our case it is the last line inside: onAnimationUpdate

Finally we have our Circle animation up and running:

GitHub: https://github.com/mistarA/customviewhandson

If you want, you can add a Fade-In-Fade-Out animation to your liking to make it look exactly similar to what is there in Uber. Also, if interested, for the sake of examples, there are a couple of more classes inside this repo which can be helpful. If you look back, we were able to create an animated view to our liking in just around 10 lines of code and that is the beauty of CustomViews. Obviously when you have complex use cases you will have complex logic involved.

For more exploration you can visit Part 2 :)

Confession: After procrastinating for more than a year, this is my first article. Have always wanted this to be something special and very unique and it has been two years since I started that journey. Never found something that fit the bill. To be honest, I am still struggling to find that article but there is one thing that I learnt from this:

There is no such thing as perfect.

If this helps at least one reader, this will be perfect for me. Donn Felker taught me this!
Also, if not writing about tech you will find me writing about movies over here. Hit me up on the below links, if you want to have a chat about anything related to Android, Movies and Cricket!

Twitter, Facebook, GitHub, LinkedIn

📝 Read this story later in Journal.

👩‍💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.

--

--