Custom view from scratch. Part III. Performance and optimisation.
Published: 2023-06-13 15:21:58

Previously on “Custom view”…

We’ve [added](https://dev.to/ezubkov/custom-view-from-scratch-part-ii-canvas-3jk5) all the constituent parts of the chart, and started the app for the first time. But the chart disappeared. It’s as frustrating as creating a lovely painting, but not being able to show anyone to get that “WOW” effect!

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

So what now? First, we need to discover the reasons for this mysterious disappearance. Let’s go to the logcat and see what it says:

> Shape too large to be rendered into a texture (18104x787, max=16384x16384)

This error message tells us that one of the shapes exceeds the maximum size of a texture that can possibly be rendered by a phone. This must be our chart. The size of a texture may vary depending on the device.

In our case, the chart was growing as it was getting new values. The larger the _Path_ became, the more resource-intensive it was to render it. This resulted in it exceeding the texture size limit.

How is the _Path_ linked with the texture?

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

This is the approximate path from calling **canvas.drawPath**, to drawing it. Most of it goes through Skia, a graphics library used inside Android.

You may notice that _SkBitmapDevice_ is used at the end, demonstrating that the _Path_ is drawn in _Bitmap_. This _Bitmap_ is then loaded as a texture to the GPU and rendered there.

This leads to an obvious problem with the Path:

> The larger the Path region, the larger the texture.

Now where can we go from here? There are a few options, but you should always figure out the most optimal solution for your needs. The most obvious solution is to use the minimum amount of data required for display. Alternatively, we could pay attention to the size of the occupied area rather than the complexity (this might be cheating a little bit…!). Another option still would be to re-use the _Path_, and if possible use **rewind** (however, the number of points has to remain constant). Finally, we could pre-fetch the chart in _Bitmap_. This should be done in a separate thread, so as not to block the current one.

### Fixed-size chart

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9wdzrcbkc6pr36yl3zjb.gif)

The chart continuously shifts to the left, but when a new value is added, we delete the last one and shift the chart to the right. Thus, we create the effect of continuous movement.

The main drawback of the Path is that the actual rendering to the texture is performed on the processor. Therefore, it’s reasonable to try rendering with primitives. However, you might encounter the problem of limiting hardware acceleration and the Canvas API. As I [mentioned](https://dev.to/ezubkov/custom-view-from-scratch-part-ii-canvas-3jk5) earlier, the **drawVertices** method used to fill the chart doesn’t support hardware acceleration and can only be used without it. It doesn’t work for us.

Let’s compare the two approaches — using the Path and the primitives. For visual clarity, I removed the fill and remove rounding effect.

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

This is what we get. On the left, the chart is drawn by calling the **drawLines** method from _Canvas_. On the right, the _Path_ is used. As you can see, drawing with primitives is much lighter than _Path_. Therefore, I recommend drawing with primitives whenever possible.

There is still one way to improve performance, but also use _Canvas_. You need to implement rendering on _Canvas_ in a separate thread. Two _Views_ from the Android SDK come to the rescue — _SurfaceView_ and _TextureView_. Let’s take a look at their differences and choose the one that would serve us better.

### SurfaceView
The main drawback of _SurfaceView_ is that a separate _Window_ is created for It. This is good from the point of view of performance, but also puts some restrictions on building the user interface. For example, we cannot animate the _View_ (as far it’s not a part of a view hierarchy).

_SurfaceView_ works well when you need to stream a video or use OpenGL, but at the same time _SurfaceView_ does not interact with other _Views_.

### TextureView
_TextureView_ doesn’t have the drawbacks of _SurfaceView_ that I’ve mentioned, but its performance is lower. Usually, this isn’t a problem.

Since we need to use the chart component together with other UI elements, _TextureView_ is the best choice for us.

Let’s take a look at the basic steps for configuring _TextureView_:
* Create a _View_ that is inherited from TextureView.
* Implement the _SurfaceTextureListener_, which describes the _TextureView_ lifecycle. Subscribe in **onAttachedToWindow** and unsubscribe in **onDetachedFromWindow**
* Implement the dedicated rendering thread, which is the main advantage of _TextureView_ over the regular View

### SurfaceTextureListener

```kotlin
fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) 

fun onSurfaceTextureUpdated(surface: SurfaceTexture)

fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) 

fun onSurfaceTextureDestroyed(surface: SurfaceTexture) 
```

This interface contains four methods. The name of each clearly indicates its purpose. In most cases, we only need the last two:
* **onSurfaceTextureAvailable**, which tells us that SurfaceTexture is ready for use.
* **onSurfaceTextureDestroyed**, which is called before terminating the SurfaceTexture.

In the simplest implementation, we need to save the reference to _SurfaceTexture_ and start the rendering thread in the first method, reset the reference and stop the thread in the second method.

```kotlin
val surface = Surface(surfaceTexture)
while (true) {
    val canvas = surface.lockCanvas(null) ?: break
    
    delegate.onDraw(canvas) //delegate is an object which draws our chart
    
    try {
        surface.unlockCanvasAndPost(canvas)
    } catch (e: IllegalArgumentException) {
        break
    }
}
```

Let’s move on to creating the rendering thread. We’ll choose the simplest implementation. The code of the run method is shown in the snippet above. This is a step-by-step description of what happens there:
1. The surface is created based on SurfaceTexture.
2. An unending cycle. Here’s what happens inside this cycle:
  * Get the canvas object from Surface.
  * Draw on this canvas.
  * Post the modified canvas to surface.
  * The cycle may be interrupted if we couldn’t get the canvas or post it to Surface.
3. When finished, we release the created surface.

---

### Things to keep in mind:
* It takes a lot of effort to implement a thread-safe render, because the data for rendering will come from other threads.
* As described earlier, you’ll need to control the lifecycle using _SurfaceTextureListener_.
* It’s worth highlighting the issue of animation. Since rendering is performed in a separate thread, we don’t know the frame time. To find it, you need to use the SDK’s [_Choreographer_](https://developer.android.com/reference/android/view/Choreographer) feature. It coordinates all the work in the system related to animation, user input, and rendering. You can use the _Choreographer_ either explicitly, or implicitly. You may apply higher-level abstractions of the [_ValueAnimator_](https://developer.android.com/reference/android/animation/ValueAnimator) type, which is the preferred option.

For more information about _TextureView_, see the [Grafika](https://github.com/google/grafika) demo app that is stored in the Google repository on GitHub.

### Launch our app with TextureView

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

We can enjoy this result primarily because the Path is rendered to the texture in a separate thread. As the Profiler shows, our work can be considered completed.

## Take-away
It’s been a long ride through this Custom View tutorial! Check out [Part 1](https://dev.to/ezubkov/custom-view-from-scratch-part-i-15la) and [Part 2](https://dev.to/ezubkov/custom-view-from-scratch-part-ii-canvas-3jk5). Now that we’re finished, let’s summarise the recommendations. What should you take into account when you work with canvas and the _Path_ to create your own custom views?

* Obviously, the less you do during rendering, the higher is the frame rate.
* Don’t rush to complicate the rendering logic. In most cases, the Path of a fixed size is enough.
* If you don’t use the Path, then give a preference to line, point, or rectangle primitives, since they don’t use the Path at the Skia level. For example, **drawCircle** uses the Path.
* If you draw several primitives, group them by type and draw them together. This way, rendering will be faster.
* Watch this video:
{% embed https://www.youtube.com/watch?v=zdQRIYOST64 %}

There you go! Now you’re ready to go and create your own custom views. Good luck!