Post

The Quest To Create Drawn .NET MAUI Apps

.NET MAUI app completely drawn on a Skia canvas and its rendering engine.

Drawing Context

Today cross-platform apps can successfully be drawn on a canvas.
.NET MAUI, previously Xamarin, team had put efforts into creating drawn controls with Material Design library and offer tools for creating such controls with Maui.Graphics.
Recall what Adam Pedley and other enthusiasts have done in an attempt to make Xamarin drawn. A lot of developers and companies have created their MAUI drawn controls ever since.

Recently a .NET 8 MAUI app totally drawn on a single hardware-accelerated Skia canvas went to GooglePlay and we will explore some details behind its creation.

SkiaSharp

SkiaSharp 😍 is a fully cross-platform, rich 2D graphics drawing API powered by Google’s Skia library, the one used by Web browsers and Android OS. SkiaSharp was created by Matthew Leibowitz who is still working on it every day, and a new version 3 is expected by the end of the year.

Years ago this awesome library solved a case when I was working on a Xamarin app: different heavy cell templates were switching inside a CollectionView, the designer asked for shadows around them, and the scrolling ended up being very laggy. After each cell content was replaced with a SkiaSharp canvas scrolling went much smoother, with shadows and complex layouts. The need of different templates was removed, SkiaSharp was drawing different layouts inside same canvases.

That led to a logical thought: what if we could replace the whole CollectionView with a drawn control? And what if the whole app was drawn?..

A Completely Drawn .NET MAUI App

One day after evaluating a new Figma design I started a challenge to make this app totally drawn with SkiaSharp.
The root view would be a SkiaSharp canvas and all the navigation would happen on it: modals, popups, camera preview and all the rest would be drawn on same canvas.

This is still an experiment for a commercial app but worked well enough for this one. The final result is somewhat interesting.

Bug ID: Insect Identifier AI .NET MAUI Android app in GooglePlay

This is a .NET 8 MAUI Android app for a real-time AI identification of insects.

  • Custom camera control displays and sends frames to a TensorFlow ML model
  • TensorFlow analyzes images on the device in real-time
  • Custom bindings for TensorFlow Light to be able to use GPU
  • Custom bindings and native wrapper for Adapty in-app purchases and library
  • Custom bindings and native wrapper for AppsFlyer analytics library
  • Custom Android bindings for Amplitude tracking library
  • Firebase Analytics and Crashlitycs
  • Local SQLite database with Entity-Framework code-first migrations
  • Available in different languages

Some native views were placed seamlessly on top of the canvas: web pages for “Privacy” and “Terms Of Use” and a customized MAUI Entry for input. Those are wrapped inside SkiaMauiElement which can take a snapshot of a native view to draw and animate it over the canvas.

A lot of interesting problems were solved while making the app. For example, the page with insect details first renders a short info then loads a full one from API and has to appear to re-render while scrolling. Moreover, there is an unknown number of wiki articles defined by a DataTemplate in XAML, that must then be instantiated without the UI freezing while constructing. Then they load optional banner images from the Web, all of this happening while scrolling.

An ImageDoubleBuffered cache type was then born: to render the last prepared cache while constructing a new one in the background. Instead of the usual ListView or CollectionView we have this:

1
2
3
4
5
6
7
<draw:SkiaScroll HorizontalOptions="Fill">
    <draw:SkiaLayout Type="Column" HorizontalOptions="Fill">
    <draw:SkiaLayout.DataTemplate>
        whatever, cache is `ImageDoubleBuffered` type
    </draw:SkiaLayout.DataTemplate>
    </draw:SkiaLayout>
</draw:SkiaScroll>

SkiaLayout implements ILayoutInsideViewport to be notified by the parent scroll of the rendering area. This way it can virtualize the rendering and recycle its cells defined by the data template. SkiaShell navigates inside the Canvas view like MAUI Shell does for native root view; we have total control over rendering and releasing memory for views. This drawn shell can bring new views, modals, and popups, freeze the background behind, and apply customizable effects to it (blur and dim in this case).

An important note, this specific app had an app owner requirement to connect to a third-party API and several analytics providers before showing the app content, so on slower devices the splash screen might take some time to go off.

Around 90% of the app UI was defined in XAML 😅.

The Rendering Engine

The tools used to make this app formed a stand-alone open-source library, DrawnUI for .NET MAUI, consumable as a nuget or a source code dependency.

Features

  • Draw UI using SkiaSharp with hardware acceleration
  • Easily create your controls and animations
  • Design in XAML or C#
  • Create for Android, iOS, MacCatalyst, Windows
  • Use MAUI Hotreload
  • 2D and 3D Transforms
  • Animations, easy to customize
  • Visual Effects, filters, shaders etc
  • Gestures support for down, up, taps, longpressing, panning, scrolling, zooming, rotating
  • Caching system for operations and images
  • Optimized for performance, rendering only visible elements, recycling templates etc
  • Navigate on canvas using MAUI familiar Shell techniques
  • Prebuilt UI elements for layouts, scrolling, rich text rendering etc.
  • Can reuse your SkiaSharp and Maui.Graphics existing code
  • Extendable with additional packages, already includes MauiGraphics, MapsUi etc.

The library is best used for these cases:

  1. To create pixel-perfect custom-drawn controls
  2. To identify and replace app laggy UI parts to replace with faster-drawn alternatives
  3. To create totally drawn apps

Prebuilt elements are SkiaControl, SkiaLayout, SkiaShape, SkiaLabel, SkiaImage, SkiaSvg and many others, easily subclassable via virtual methods, with no private API. The main idea behind is to make the toolset reusable and customizable at will. Controls have usual MAUI VisualElement properties like BackgroundColor, HorizontalOptions and so on, along with many supplementary additions.

There are some key differences with MAUI though, one being that HorizontalOptions and VerticalOptions are not Fill by default; if you just write a <SkiaLayout/> it will not display, because it has Start layout options (not Fill), empty content, and no size requests.

Toolkit drawn controls built on top of the basic ones include SkiaDrawer, SkiaCarousel,SkiaGif, and many more. And SkiaShell makes possible the creation of totally drawn apps.

Accessibility can be totally compatible and is on the roadmap.

Documentation is still very much in the ToDo 😔 state; while many examples are already here, more about them below.

Quick Start

Install the nuget package AppoMobi.Maui.DrawnUi , using the latest stable version.

Initialize the library inside your MauiProgram.cs file:

1
builder.UseDrawnUi();

Anywhere in your existing MAUI app you can include a Canvas and start drawing your UI. The Canvas control is aware of its children’s size and will resize accordingly.

At the same time, you could set a fixed size for the Canvas and its children will adapt to it. When opting for max fps do not make it adapt to content size, use Fill or Height/WidthRequest.

Import the namespace:

1
  xmlns:draw="http://schemas.appomobi.com/drawnUi/2023/draw"

Consume:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<draw:Canvas>
    <draw:SkiaLayout HorizonatlOptions="Fill" Type="Column">

    <draw:SkiaShape 
        HorizontalOptions="Center"
        WidthRequest="90" LockRatio="1" Type="Circle">
    
        <SkiaImage Source="https://api.skia.org/logo.png"/>
    
    </draw:SkiaShape>

	<SkiaButton 
            Text="Hello" 
            HorizontalOptions="Center"
            TextColor="White"
            TintColor="Black"
            CornerRadius="8"/>

    </draw:SkiaLayout>
</draw:Canvas>

Creating Custom-Drawn Controls

With DrawnUI the process is much easier than creating controls directly using low-level SkiaSharp primitives. We already have Lego-like bricks you to assemble from, compositing controls like you would with MAUI Frame, Border, etc.

It’s obviously much easier to maintain and fix issues for a single drawn control than for all platform-dependent implementations.

The process of creating a custom control is out of the scope of this article; I could touch it later if this finds enough interest. It would be created like a usual MAUI control, the main difference being using DrawnUI elements instead of MAUI controls.

Many examples are inside the Sandbox project as well as in the demo apps.

Here is an example of “partly-drawn” .NET 8 MAUI created with DrawnUI published app full of drawn custom controls; canvases are everywhere inside standard pages. But all the scrolls, recycled cells collections, maps, buttons, labels, and other controls are drawn. The main difference with the “totally drawn insects app” is that here we have many canvases all around the app.

Racebox

Android + iOS + Windows app for measuring vehicle performance in real-time in pair with external hardware BLE measurement Racebox device.

iOS
GooglePlay

  • Tinted online maps for vehicle paths
  • Drawn controls everywhere including scrollable recycled cells
  • Custom SDK for Racebox BLE protocol communication
  • Using third-party API for Weather retrieval at local GPS coordinates
  • Local SQLite database with Entity-Framework code-first migrations

Examples With Source Code

There are a lot of drawn controls inside the Sandbox project inside the DrawnUI repository:

Replacing Native UI With Drawn

What’s better than another example:

App example with source code

Here we have recycled drawn cells with shadows scrolling smoothly, as compared to standard MAUI controls, which put more pressure on the device.

The replacement process is rather intuitive; you need a top container as Canvas, then basically you can replace standard MAUI controls with drawn, like replacing Grid with SkiaLayout Type="Grid", VerticalStackLayout with SkiaLayout Type="Column", ScrollView with SkiaScroll and so on.

This could very useful for existing MAUI libraries that use composited custom controls, for example, a month calendar view with tons of native views/handlers for days, arrows, grids etc, could be replaced with just one Canvas native view; everything else would become virtual.

Here is one of the examples of embedding drawn controls into a usual MAUI layout, drawn controls SkiaLabel and SkiaMarkdownLabel in action, with HotReaload:

Creating Completely Drawn Apps

Besides the already demonstrated published Insects app there are some other examples with source code of completely drawn apps with Canvas as root view:

SpaceShooter Game

DrawnUI Demo

Final Words

Creating totally drawn NET MAUI apps remains in an experimental state as of today, and it could benefit from a lot more work to make it a well supported way to develop MAUI apps. At the same time I believe this is a very interesting challenge for all of us.

I would greatly appreciate your feedback, especially on what do you think of this approach and what spots in your apps would be most useful to use drawn controls. There is a related thread on Reddit, where I invite you to post any of your thoughts or questions. Would you have any specific technical questions about using DrawnUI please feel free to use Discussions.

The DrawnUI for .NET MAUI repository contains most of the reference links used in this article.

This post is licensed under CC BY 4.0 by the author.