Every day we use various applications and despite of their diverse intentions, most of them are very similar or even resemble each other in terms of design. That's why a lot of customers are requesting specific, customized layouts and appearances that no other application has embodied yet, in order to make the Android application unique and contrast from others.
If a specific feature requires a very customized functionality that could not be created by android build-in views - then here comes custom view drawing. What that means in most cases, is that it will take quite a while to complete it. But it does mean that we should not do this, moreover it’s very exciting and interesting to implement.
I recently got into a similar situation: my task was to create a page indicator for Android ViewPager. Unlike iOS, Android does not provide such view, so I had to implement it as a custom one.
I spent a decent amount of time trying to implement it. Fortunately enough that view is quite reusable in projects nowadays , so to save personal time and the time of other developers, I decided to make a public library based on that view. If you have similar functionality and lack the time to implement it by yourself, then find it on github repo.
Well, since most of custom views are more time consuming than the regular ones, you should only make it, if there is no easier way to implement a specific feature or you have the following issues that custom view could have resolved:
- Performance. If you have a lots of views in your layout and you want to optimize it by drawing a single custom view to make it lighter.
- A big view hierarchy that is complex to operate and support.
- A complete custom view that requires manually drawing.
If you have not tried working out a custom view, then this article is a great opportunity to stay closer to drawing your own flat custom view. It will show you the overall view structure, how to implement specific things, how to avoid common mistakes and even how to animate your view!
The very first thing we need to do, is jump into View lifecycle. For some reason Google does not provide an official diagram of view lifecycle, it is quite a widespread misunderstanding between developers that leads to unexpected bugs and issues, so lets keep our eye on it!
Every view starts it's life from a Constructor. And what it gives us, is a great opportunity to prepare it for an initial drawing, make various calculation, set default values or whatever we need.
But to make our view easy to use and setup, there is useful AttributeSet interface. It's easy to implement and is definitely worth the time to spend on it, because it will help you (and your team) to setup your view with some static parameters on further screens. First, create a new file and call it `attrs.xml`. That file can have all the attributes for different custom views. As you can see in this example, we have a view called PageIndicatorView and single attribute piv_count.
Secondly, in your View constructor, you need to obtain attributes and use it as shown below.
- While creating custom attributes, make a simple prefix to avoid name conflicts between other views with similar attribute names. Most of the time, it is just an abbreviation of view name, just like we have `piv_`.
- If you are using Android Studio, Lint will advise you to utilize the recycle() method as long as you are done with your attributes. The reason is, so that you can get rid of inefficiently bound data that's not going to be used again.
After parent view calls addView(View) that view will be attached to a window. At this stage our view will know the other views that it is surrounded by. If your view is working with user's other views located in same `layout.xml` it is good place to find them by id (which you can set by attributes) and save as a global reference (if needed).
Means that our custom view is on stage to find out it’s own size. It's a very important method, as for most cases you will need your view to have specific size to fit in your layout.
While overriding this method, what you need to do, is to set setMeasuredDimension(int width, int height).
While setting the size of a custom view you should handle case, that view could have specific size that user will set in layout.xml or programmatically. To calculate it properly, a few steps need to be done
- Calculate your view content desired size (width and height).
- Get your view MeasureSpec (width and height) for size and mode.
Take a look at MeasureSpec values:
- MeasureSpec.EXACTLY means that user hardcoded the size value, regardless of your view size, you should set a specific width or height.
- MeasureSpec.AT_MOST used for making your view match parent size, so it can be as big as possible.
- MeasureSpec.UNSPECIFIED is actually a wrap size of view. So with this parameter you can use the desired size that you calculated above.
Before setting final values to setMeasuredDimension just in case, check if those values are not negative. That will bypass any issues in the layout preview.
This method, incorporates assigning a size and position to each of its children. Because of that, we are looking into a flat custom view (that extends a simple View) that does not have any children so there is no reason to override this method.
That’s where the magic happens. Having both Canvas and Paint objects will allow you to draw anything that you need.
A Canvas instance comes as onDraw parameter, is basicly respond to drawing different shapes, while Paint object defines the color that the shape will get. Simply, canvas respond for drawing an object, while Paint is for styling it. And it’s used mostly everywhere whether it is going to be a line, circle or rectangle.
While making a custom view, always keep in mind that onDraw calls lots of time, like really alot. While having some changes, scrolling, swiping your will be redrawn. So that’s why even Android Studio recommend to avoid object allocation during onDraw operation, instead to create it once and reuse further on.
- While performing drawing, always keep in mind to reuse objects instead of creating new ones. Don’t rely on your IDE that will highlight a potential issue, but do it yourself because IDE could not see it if you create objects inside the methods called from onDraw.
- Don’t hard code your view size while drawing. Handle case that other developers could have the same view but in different size, so draw your view depending on what size it has.
From the view lifecycle diagram you may notice that there are two methods that leads view to redraw itself. invalidate() and requestLayout() methods will help you to make an interactive custom view, that may change its look on runtime. But why are there two of them?
invalidate() method is used to simply redraw view. For example, while your view updates its text, color or touch interactivity. It means that the view will only call onDraw() method once more to update its state.
requestLayout() method, as you can see will produce view update through its lifecycle just from onMeasure() method. And what it means is that you will need it right after your view updates, is changed in it’s size and you need to measure it once again to draw it depending on the new size.
Animations in custom view is a frame by frame process. It means that if you for example want to make a circle radius animated from smaller to bigger, you will need to increase it sequentially and after each step call invalidate to draw it.
Your best friend in custom view animations is ValueAnimator. This class will help you to animate any value from start to the end with even Interpolator support (if you need).
Don’t forget to call invalidate every time a new animated value comes out.
Hopefully this article will help you to draw your first custom view and if you want to learn more about it, watch a decent presentation video. You can find this article on Medium too.
If you have some questions be sure to leave comments below.