Custom view from scratch. Part I.
Tags: android, kotlin
Published: 2023-06-13 14:12:37

_In this series of articles I’ll tell you how to create a Custom View like an artist. I’ve prepared a creative task for you and we’ll do it together. In Part I, I’ll give you a succinct account of your options when you can’t use UI components from the standard Android SDK. Sorry, but I won’t let you touch the brushes during the first lesson. This time, we will consider the requirements, choose the approach, build the basic logic, and prepare our canvas. In the next part, we’ll get straight to the View drawing and optimisation. So get comfortable and focus on creating something unique._

Imagine you need to create a complex UI that contains multiple visual components. Something like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vbfe0vu4lizjgzswqk8u.jpg)

What do we have here?
* **Chart**: A dynamic element, continuously receiving new values and changing accordingly. Charts can be used to show currency change online or data collected by some sensor.
* **Current state marker**: This smoothly follows the changes in the chart.
* **Text**: Showing current values.

The main feature of our Custom View is that the chart updates live as new values are added. Markers must change smoothly, and that means that View implementation won’t be easy.

In the Revolut app, we use these charts for things like screens showing currencies, trading, and analytics.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/phr8xq3vhwmpk8rqxlp7.jpeg)

### Ways of creating a non-standard component
This chart comprises visual components that you won’t find in the Android SDK. What options do you have when the standard components don’t behave the way you need them to, or they’re not optimised for your tasks?

_Option 1_. Customize the standard components. This is the most obvious and preferable way of solving the task unless you need to add some new logic.

_Option 2_. Use a Github library. This is a great option if you’ve found a good library.

_Option 3_. Inherit and extend. This allows you to add new logic by writing the code yourself. However, this is not the easiest way to solve our problem because many things that you may want to adjust have **private** or **package-private** access in the Android SDK API. If the **package-private** access method is used, you may place a new component within the same package as its parent — but you can hardly call this a beautiful solution.

_Option 4_. Inherit a View from a ViewGroup and inflate the layout you already use. This method works when you need to create a component with several simpler constituents. For example, a widget where you can enter the sum of money.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wwqp0z7lnwale2s127r.png)

_Option 5_. Inherit from a View and create a widget from scratch. This is the method we’re going to use. It’s the most flexible, but the most difficult — it requires that you know the ins and outs.

## What is the View and how does it work?
Every visual component of the UI is associated with a base parent View or with its child―so they all follow the same rules.

A View exists only in connection with an Activity/Fragment, and has its own lifecycle.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eoipfcfaqq0uj46q1os5.png)

1. Inflating a View from an .xml file
2. Attaching the View to a ViewGroup
3. Defining the layout
4. Drawing
5. User interaction
6. Detaching from the screen

Now let’s take a look at each of the steps in detail.

## Inflation
A View can be created from code or inflated from a layout XML file. Depending on your case, you should call the corresponding type of Constructor. We are inflating the View from XML, so our Constructor will look as follows:

```java
public View(Context context,
            AttributeSet attrs,
            int defStyleAttr,
            int defStyleRes)
```

Here you specify the context through which your View can access the resources. Then add **AttributeSet** to use attributes specified in the XML file. You can use **defStyleAttr** to add a reference to a style resource that supplies default values for the View. If **defStyleAttr** is 0, or cannot be found in the theme, then the style is applied according to the resource ID set in **defStyleRes**.

This is how all this looks in Java

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vdyk89zjlyjsz23aouii.png)

And in Kotlin

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b9mr5xkp2ugodujhxd13.png)

Pay attention that some Android views (e.g. TextView) may have different behaviour on different constructors and this simplifacation may break this.

E.g. this constructor of TextView passes default style but will never be called with **@JvmOverloads** trick:

```java
public TextView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
```

### Specify custom attributes
First of all, **add the declarable-style tag to your resources**, and define each attribute by specifying its name and format. Usually the file with these values is called attrs.xml.

```xml
<declare-styleable name="CustomView">
    <attr name="circleRadius" format="dimension" />
    <attr name="circleLabel" format="string"/>
    <attr name="showLabel" format="boolean"/>
    <attr name="circleColor" format="color"/>
    <attr name="circleDrawable" format="reference"/>
    <attr name="circleColorType">
        <flag name="none" value="0" />
        <flag name="fill" value="1" />
        <flag name="stroke" value="2" />
        <flag name="fillStroke" value="3" />
    </attr>
</declare-styleable>
```

In the XML layout, these attributes will look as follows:

```xml
app:circleRadius="5dp"
app:circleDrawable="@drawable/ic_bell"
app:circleColorType="fillStroke"
…
```

Then you should use the obtainStyledAttributes(AttributeSet, int[], int, int) API to **retrieve XML attributes at the construction of the View**. Retrieved attributes will be used for drawing on canvas. You also have to remember to add recycle() to clear all the data associated with this TypedArray:

```java
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
            position = a.getInt(R.styleable.CustomLayoutLP_layout_position, position);
            a.recycle();
```

If you use the android-ktx set of Kotlin extensions, you don’t have to worry about recycling.

```kotlin
context.withStyledAttributes(attrs, R.styleable.YourCustomViewClass) {
    radius = getDimension(R.styleable.CustomView_circleRadius, fallback)
    showLabel= getDimension(R.styleable.CustomView_showName, fallback)
    colorType= getInteger(R.styleable.CustomView_colorType, colorType)
}
```

### Finalizing inflation
After all the attributes have been inflated from XML, the onFinishInflate() method is called. It won’t be called if you create View programmatically.

## Attaching the View to a ViewGroup
A ViewGroup is a special view that contains other views called children. The view group is the base class for layouts and views containers.

The **onAttachedToWindow** callback means that the view will be shown on the screen. Calling of **onAttachedToWindow** guarantees calling of **onDetachedFromWindow** at view termination. When you may need this callback? If you subscribe to events within the view, **onDetachedFromWindow** stops subscriptions when the view is not displayed anymore and thus frees up memory.

There’s a case when **onAttachedToWindow** is not called. Let’s take a step-by-step look at this case:

1. You defined the view within the xml layout referred to in the Activity’s **setContentView**.
2. You start the app. The Activity is created and the layout is inflated. Upon inflation, our view is created.
3. The **finish()** method is called for some reason in **onCreate()**.
4. The Activity shuts down with **onDestroy()**. The view lifecycle never starts.

Why might this be frustrating? Imagine that the subscription to some events inside your view happened not in **onAttachedToWindow**, but in the constructor’s code or the **onFinishInflate** callback. The view lifecycle didn’t start, and the **onDetachedFromWindow** callback didn’t happen. Without **onDetachedFromWindow**, the subscription to changes won’t be terminated, leading to memory overload.

## Preparing a canvas
Your next step is getting a Canvas object on which you can draw your view. To do this, you’ll need to override the **onDraw()** method of the View class. Here is what you need to know about onDraw.

1. The screen refreshes at the standard rate of _60 Hz_, so we have about 16 ms per frame.
2. The **onDraw** callback is just one of the steps of the frame rendering, so we actually have less than 16 ms for **onDraw**.
3. The **onDraw()** method is invoked only when **invalidate()** is being called which does not happen every 16 ms.
4. A call to **onDraw()** is made on the main thread.

All this leads us to the following conclusions:

1. You cannot create objects within **onDraw()**- otherwise the _Garbage Collector_ (GC) may get stuck. Further, you can’t draw until the GC method has executed, and its execution may take a long time in this case.
2. Calculation processes must be minimized to ensure the method runtime doesn’t take a long time.

While preparing our Canvas, we need to define _what we’re going to draw_. We have the following:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/775bxk1m4k3xlamonpe2.png)*Dynamic background containing dozens of circles*

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9zs2nwq7s41a2ejzs6n.png)*Marker and text reflecting the current state*

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/da85a6jo0397bczssva7.png)*And the chart*

The chart occupies the whole width of the screen. Our task requires that the chart has the right indent. We can do this by using Canvas.clipRect(left, top, right, bottom).

```java
canvas.clipRect(0f, 0f, clipWidth, height)
```

returns the following result:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzx744cmnyvohs1uv6cn.png)

The red frame selects the area that will be sent to the function.

Now that you’ve learned what options are available when you can’t use standard Android SDK components, you’re ready to learn how to draw the chart. But that’s for another time!

_In the next part, we’ll get straight to drawing the chart. We’ll learn how to specify the logic of display and animation, delve into hardware acceleration, learn to draw smooth lines, use profiling, and more. After the first start of the app with the new chart, we’ll single out a few performance problems — then I’ll show you the solutions._