

Ultimate Guide To Android Custom View
source link: https://vladsonkin.com/ultimate-guide-to-android-custom-view/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Ultimate Guide To Android Custom View
Android has lots of standard views to cover all our needs in the app. But sometimes, the designers come up with some new UI elements, and the only way to implement it is by creating an Android Custom View.
If this new UI element looks like some improved standard view or contains multiple standard views, we can create the Custom View by extending the existing View. This is the simplest way to create a Custom View – we use the existing ones and modify them.
However, we might not be so lucky, so we need to create this new Custom View from scratch. In this article, we will build a custom animated loader and cover such topics as a View Lifecycle, Constructors, Attributes, and Animations.

Android View Lifecycle
Android Views have their lifecycle, and you’ll not find it in the official documentation. The important part of this lifecycle looks like this:

When the activity comes into the foreground, Android asks for the root view, and then it draws the views from the top to bottom. The drawing is happening in 3 stages:
onMeasure()
, where we need to understand the size of the ViewonLayout()
, where we find the right position for this ViewonDraw()
, in which we draw the View with a known size and position.
Note: because of this process, it’s recommended to keep your layout as flat as possible, so the system can save resources on calculating the size and position of the inner ViewGroups.
Let’s implement the custom view and see this lifecycle at work. The creation of the custom view starts with the constructors.
Android Custom View Constructors
Create the LoadingView class and extend it from the general View:
class LoadingView : View {
}
Android Studio will highlight this and say that we need to provide the constructors for the View. The View has lots of constructors:
constructor(context: Context)
This constructor is used when we create the View from our code programmatically.constructor(context: Context, @Nullable attrs: AttributeSet?)
This constructor is used when we create the View from an XML layoutconstructor(context: Context, @Nullable attrs: AttributeSet?, defStyleAttr: Int)
This constructor is used when we create the View from the XML layout and use the theme attribute style.constructor(context: Context, @Nullable attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int)
This constructor is similar to the previous one, but it additionally can take the style resource.
The advice is to always start with using the first 2 constructors. And add the 3rd or 4th only if you will use the theme or style.
class LoadingView : View {
constructor(context: Context) : super(context)
constructor(context: Context, @Nullable attrs: AttributeSet?) : super(context, attrs)
}
Now let’s see this lifecycle in practice.
onMeasure()
At this stage, the Android system is calculating the size of the view. There is a default implementation, where the view is measured based on the provided width and heights, and in most cases, it’s enough. So if you create the custom view like this:
<com.vladsonkin.customview.LoadingView
android:id="@+id/loading"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
The default onMeasure()
implementation will calculate the correct size 100×100, and you don’t need to implement this function.
But if it’s not your case, then you can override the onMeasure()
where you basically do 2 things:
- Calculate the size of the View
- Call the
setMeasuredDimension(int, int)
with the size from step 1
onLayout()
At this stage, Android takes the measurement from the previous step and assigns the size and position for each View. Same as onMeasure()
, the default implementation will do fine in most cases, and it’ll take the position from the XML.
onDraw()
This is the primary function in any custom view, and here we draw it. To do this, we have 2 objects:
- Canvas. This object is provided as an argument in onDraw(), and it is responsible for drawing the View on the screen.
- Paint. We create this object, and it describes the style of the View.
Back to our example, we need to create a circular loading view, so we need to draw a circle with a stroke:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val circleRadius = 100F
val paint = Paint().apply {
color = ContextCompat.getColor(context, R.color.teal_700)
style = Paint.Style.STROKE
strokeWidth = 20F
}
canvas.drawCircle(width / 2F, height / 2F, circleRadius, paint)
}
Here the Canvas is responsible for drawing the circle with a drawCircle()
and Paint is responsible for the circle styling. Add this custom view to some layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<com.vladsonkin.customview.LoadingView
android:id="@+id/loading"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
And check the results:

onDraw()
is called many times, so the general advice is to move the Paint object to another place and just reuse it in onDraw()
. For example, you can do something like this:
class LoadingView : View {
private lateinit var paint: Paint
private var circleRadius = 100F
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, @Nullable attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
private fun init(context: Context, attributeSet: AttributeSet?) {
paint = Paint().apply {
color = ContextCompat.getColor(context, R.color.teal_700)
style = Paint.Style.STROKE
strokeWidth = 20F
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2F, height / 2F, circleRadius, paint)
}
}
Here we extracted the init()
function that takes all the constructor parameters and creates the Paint object reused in onDraw()
.
Custom View Update
You may have noticed that we also have invalidate()
and requestLayout()
functions on the lifecycle diagram. They have the same goal – to redraw the View if something is changed. The only difference is that when you call requestLayout()
, the Android will recalculate the View’s size and position.
If an update doesn’t affect the size of the View, call invalidate()
and otherwise, call the requestLayout()
.
Now we have covered all the view lifecycle and drew our simple circle custom view. At this point, we can achieve the same result by just using the ImageView with a circle image. Fair enough, it’s way easier. But the custom view can do much more, and one of the examples is the animations.
Custom View Animations
We should handle the animations for the custom view step by step for each change. For each change, we call the invalidate()
, and View draws again. Do this repeatedly, and you achieved an animation.
We want to animate our circle, so it shrinks and grows again infinitely until we cancel the animation. What we need to do is to change the radius value gradually, redraw the View and repeat it. Sounds tricky, but luckily we have a ValueAnimator:
class LoadingView : View {
private var valueAnimator: ValueAnimator? = null
...
fun showLoading() {
isVisible = true
valueAnimator = ValueAnimator.ofFloat(10F, circleRadius).apply {
duration = 1000
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animation ->
circleRadius = animation.animatedValue as Float
animation.repeatCount = ValueAnimator.INFINITE
animation.repeatMode = ValueAnimator.REVERSE
invalidate()
}
start()
}
}
fun hideLoading() {
isVisible = false
valueAnimator?.end()
}
}
Here we change the circleRadius value from 10 to 100 gradually at a 1-second interval. ValueAnimator is responsible for changing this value, and for each change, we call the invalidate()
in addUpdateListener.
Let’s add a couple of buttons that will trigger the showLoading()
and hideLoading()
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<Button
android:id="@+id/showLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Show Loading"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/hideLoading"/>
<Button
android:id="@+id/hideLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hide Loading"
app:layout_constraintStart_toEndOf="@id/showLoading"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.vladsonkin.customview.LoadingView
android:id="@+id/loading"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="24dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/showLoading"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
And call the functions in Activity:
class MainActivity : AppCompatActivity() {
private lateinit var ui: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ui = ActivityMainBinding.inflate(layoutInflater).apply { setContentView(root) }
ui.showLoading.setOnClickListener { ui.loading.showLoading() }
ui.hideLoading.setOnClickListener { ui.loading.hideLoading() }
}
}
Run it, and you should see the animated loader:

Android Custom View Custom Attributes
Right now the stroke color, stroke width, and radius are hardcoded in the Custom View, but we can make it dynamic with custom attributes. For this we need to create the add the desired attributes in the attrs.xml resource file:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LoadingView">
<attr name="lv_color" format="color"/>
</declare-styleable>
</resources>
Here we declare the lv_color attribute, which is responsible for the circle color. A good tone is to add a unique prefix for the attribute to avoid naming conflicts, and usually, it’s an abbreviation of the custom view.
These attributes are passed through our constructor and we can use them in our init()
function:
private fun init(context: Context, attributeSet: AttributeSet?) {
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.LoadingView)
val loadingColor = typedArray.getColor(
R.styleable.LoadingView_lv_color,
ContextCompat.getColor(context, R.color.teal_700)
)
paint = Paint().apply {
color = loadingColor
style = Paint.Style.STROKE
strokeWidth = 20F
}
typedArray.recycle()
}
Don’t forget to call recycle()
when you’re done with attributes because this data is not needed anymore.
Now you don’t need to touch the Custom View when the designer wants to change the color of it. Simply use this new attribute in the XML:
<com.vladsonkin.customview.LoadingView
android:id="@+id/loading"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="24dp"
app:lv_color="@color/black"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/showLoading"
app:layout_constraintEnd_toEndOf="parent"/>
Android Custom View Summary
In this article, we saw how powerful the Android custom view could be. You can create everything you want, and the only limit is your imagination (or imagination of designers).
Draw everything you want, give it some life with animations, and style it with custom attributes. Do you have any custom views in your project, or you want to share some of your favorites? I would love to see your examples in the comments below. Stay safe out there, and Fijne Feestdagen!
Recommend
-
42
The Collection View provides a flexible way to present content to the user. Similar to a table view, a collection view gets data from custom data source objects and displays it using a combination of cell, layout, and sup...
-
6
A custom C++20 range view Posted on June 6, 2020 by Marius Bancila Some time ago, I wrote
-
11
In this part, I will demostrate how to publish the custom view so that other developers can use it by just simply adding dependency like this: dependencies { ... compile 'com.dualcores.swagpoints:swagpoints:1.0.2' }
-
19
SwiftUI Custom View Modifiers Jan 18, 2021 · 4 minute read Create your own custom SwiftUI view modifier when you want to reuse a set of modifiers on multiple views. Removing duplication also cleans up and improv...
-
9
The Ultimate Guide to Android Enterprise Migration with Workspace ONEThis is your one stop shop for everything you need to know about migrating from Device Administrator (referred to as Android (Legacy) in the Workspace ONE UEM Console) to An...
-
23
Custom navigation bar title view in SwiftUI 05 Jul 2020 ⋅ 1 min read ⋅ SwiftUI
-
8
Add custom SwiftUI view to View Library with LibraryContentProvider Table of Contents This article covers beta technology (iOS 14 and Xcode 12) which might subject to change in the future. ...
-
5
How to make a custom scroll container view controller? advertisements There are two view controllers in my app, e.g, vc1 and vc2. The two view...
-
4
Where is the RCI indicator? — Google Maps “immersive view” is the ultimate graphics mode for Google Maps Simulated cars drive down the roads. Birds fly in the sky. It rains...
-
6
The ultimate guide to Android security: Everything you need to know By Jerry Hildenbrand published abo...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK