Drawing your own views in Android

Take full control of your view and optimize its performance

On the eve of the start of the course “Android Developer. Professional” we invite everyone to take part in an open webinar on the topic “Writing a gradle plugin”

In the meantime, we are sharing the translation of useful material.


Introduction

Developers are constantly designing different kinds of user interfaces using XML, but in addition to that, it is fairly easy to learn how to create your own views, which open up new benefits and avoid boilerplate code reuse.

Android has a wide range of pre-built widgets and layouts for creating a user interface, but they cannot meet all the requirements of our applications. And here the ability to create your own views comes to the rescue. By creating your own view subclass, you can get the most complete control over the appearance and functionality of a display element.

Before you start working with your own views, it’s a good idea to learn view lifecycle

Why create your own views?

Implementing your own view will take longer in most cases than using regular views. You should only create your own views if there is no other, simpler way to implement the functionality you need, or if you have any of the following problems that you can fix by creating your own view.

  1. Performance: There are many views in your layout and you want to optimize them by drawing one, lighter view of your own.

  2. There is a complex hierarchy of views that are difficult to use and maintain.

  3. You need to create a custom view that requires manual drawing.

General approach

To start creating components to implement your own views, you must complete the following basic steps.

  1. Create a class that extends the base class or subclass of the view.

  2. Implement constructors that use attributes from an XML file.

  3. Override some methods of the parent class (onDraw (), onMeasure (), etc.) according to our requirements.

  4. After completing these steps, the generated extension class can be used instead of the view it was based on.

Example

In one of my projects, I needed to create a round TextView widget to display the number of notifications. To achieve this goal, you need to subclass TextView.

Step 1. Let’s create a class named CircularTextView

Step 2. Let’s extend the widget class TextView. Here under TextView an error is thrown in the IDE stating that this type has a constructor and must be initialized.

Step 3. Add constructors to the class.

This can be done in two ways.

The first way to add constructors to a class is shown below.

Another way is to add annotation @JvmOverloads to the constructor call as shown below.

We are often confused by the fact that a view has several different types of constructors.

View (Context context)

A simple constructor to dynamically create a view from program code. Here parameter context Is the context in which the view operates and through which the current theme, resources, etc. can be accessed.

View (Context context, @Nullable AttributeSet attrs)

A constructor that is called when rendering a view from an XML file. It is called when the view is created from an XML file that contains the view’s attributes. This constructor option uses the default style (0), so only those attribute values ​​that are in the context theme and the given set are applied AttributeSet

Step 4. The most important step in rendering your own view is overriding the method onDraw() and implementing the necessary rendering logic inside this method.

Method OnDraw(canvas: Canvas?) has a parameter Canvas (canvas) with which the view component can render itself. To paint on the canvas, you need to create a Paint object.

Typically, the drawing process is determined by two aspects:

  • what to draw (determined by the Canvas object);

  • how to paint (defined by the Paint object).

For example, Canvas provides a method for drawing a line, and Paint provides methods for specifying the color of that line. In our case, the Canvas object in the class CircularTextView provides a method for drawing a circle, and the Paint object fills it with color. In simple terms, Canvas defines what shapes can be drawn on the screen, while Paint defines the properties of the drawn shapes – color, style, font, etc.

Let’s get into the code. We create a Paint object and assign some properties to it, and then draw the shape onto the canvas (Canvas object) using our Paint object. Method onDraw() would look like this:

The IDE displays a warning to avoid selecting objects during drawing or layout operations. This warning occurs because the method onDraw() Called many times when rendering a view, which creates unnecessary objects each time. Therefore, to avoid unnecessary object creation, we will move the corresponding part of the code outside the method onDraw()as shown below.

When doing rendering, always remember to reuse objects instead of creating new ones. Your IDE can indicate potential problems, but you shouldn’t rely on it. For example, it will not be able to trace the case when objects are created inside methods called from a method onDraw()… Therefore, it is better to check everything yourself.

Step 5. We are done with drawing. Now let’s bring this presentation class into XML.

Add this XML layout to your Activity and start the application. This is what will be on the screen.

Looks pretty good, right? Now let’s make the value of the dynamic color property in circlePaint assigned from the activity, and also add an outline to the circle. For this in the class CircularTextView you need to create multiple setter methods so that you can call these methods and set properties dynamically.

First, let’s implement a render color setting. To do this, let’s create a setter as shown below.

fun setSolidColor(color: String) {
    solidColor = Color.parseColor(color)
    circlePaint?.color = solidColor
}

Now we can set the color from our activity dynamically by calling this method.

circularTextView.setSolidColor("#FF0000")

Not bad, right? Now let’s add an outline to the circle. The contour will be specified by two input parameters: the contour line width and its color. To set the color of the outline line, we need to create a Paint object in the same way as we did for the circle. To set the width of the path line, we will create a variable, set the desired value for it and use it in the method onDraw()… The complete code will look like this:

Now in the activity, you can dynamically configure these attributes as needed.

Next, let’s launch the application by setting different colors for our widget.

So now it became clear how to dynamically set properties from an activity, but the question is how to set attributes from XML. Let’s continue our research.

First, let’s create a file named attrs.xml in folder values… This file will contain all the attributes for the various views that we create ourselves. In the example below, our view called CircularTextView there is an attribute ct_circle_fill_colorthat takes on a color value. We can add other attributes in the same way.

Then we will need to read these properties in the class we created to implement our own view. In the initialization block, we read a set of attributes as shown below.

val typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircularTextView)
circlePaint?.color = typedArray.getColor(R.styleable.CircularTextView_ct_circle_fill_color,Color.BLUE)
typedArray.recycle()

Now we just go to the XML layout and set the property to the color we want, and then launch the application. At the output, we will see the desired result.

app:ct_circle_fill_color="@color/green"

In my case, the result was like this:

Note. When drawing, do not hard-code the size of your view, as other developers can use it at different sizes. Draw the view according to its current size.

Updating the view

So, we have set our own view. If we want to update the view when a property changes or for some other reason, there are two main ways to do this.

invalidate ()

invalidate() Is a method that initiates a forced redrawing of a specific view. Simply put, the method invalidate() should be called when a change in the appearance of the view is required.

requestLayout ()

If at some point there is a change in the state of the view, then the method requestLayout() tells the view system to recalculate the Measure and Layout phases for this view (dimension → layout → drawing). Simply put, the method requestLayout() should be called when a change to the view boundaries is required.

Now, I hope you know in general terms how to create your own views. For them to demonstrate excellent performance, you must master all the methods described here.

Thank you for attention!


Learn more about the course “Android Developer. Professional”.

Sign up for an open webinar “Writing a gradle plugin”.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *