Xamarin Forms: A Simple Circular Progress Control

FacebookTwitterGoogle+Share

Source code for this sample is available on GitHub here: https://github.com/billreiss/xamlnative/tree/master/XamarinForms/CircularProgress

Recently in a Xamarin Forms app I needed to show progress of an operation as a circle that would fill itself in as the progress completed. Something like this:

10uk8w_thumb.gif

 

The built in Xamarin Forms Progress control is a line, and can’t easily be changed to show a circle. I looked at some different options, including finding a native control for the different platforms and render them using custom renderers, or to use a component for the component store, or something on NuGet, but didn’t really find anything suitable. So I did my own thing, and I’m really happy with how it turned out, then I wanted to share what I came up with. Feel free to riff on this design and come up with your own variations.

First I thought about vector graphics. There doesn’t seem to be any built in support for this in Xamarin Forms, and I would have to do custom renderers for each platform. Then I started thinking about using images to do the animation. It certainly would be better with vector graphics, but maybe this would be “good enough”. Then I considered how many different images I would need to make it look smooth. A dozen? More? Something like this?

th_thumb.jpg

 

* this is a copy of a circle sprite sheet published here

I was able to come up with a solution that uses two images. Actually two instances each of two images, so four images total. Combining these images with rotation and changing which images show in front of each other, we can show any percentage of progress smoothly.

The two images we need are semicircles, one for completed progress, and one for incomplete progress (you can download them here https://github.com/billreiss/xamlnative/tree/master/XamarinForms/CircularProgress/CircularProgress/CircularProgress.Droid/Resources/drawable):

progress_pending_thumb.png progress_done_thumb.png

 

These are two semicircles saved as PNG format with transparent backgrounds. First let’s consider the case between 0 and 50% complete. Let’s call the first image the “completed” image, and the second is the “pending” image. We will need three images to handle the case from 0 to 50%, one “completed” and two “pending” images.

The completed image (in the code we’ll call it “progress1”) looks like this:

image.png

 

Then on top of that we display a “pending” image (in the code we’ll call it “background1” looks like this:

image.png

 

With these directly on top of each other, the darker blue circle is completely covered and not visible.

Then we add another copy of this image, but rotated 180 degrees:

image.png

 

This makes a solid light blue circle, which is what we expect to see at 0% complete.

image.png

Then if we rotate the instance of “background1”, we will expose part of the dark blue semicircle. Part of “background1” will overlap “background2”, but that’s ok, since they are the same color you won’t be able to tell.

image.png

 

Once we get to 50%, then things change a little. Now we need 2 instances of the dark blue image, one rotated 180 degrees, and one instance of the light blue image. This light blue image will reveal portions of the 180 degree rotated dark blue image similar to how the less than 50% scenario worked, and the non-rotated dark blue image needs to be in front of this pending image, and the rotated completed image needs to behind the pending image. So the light blue image is sandwiched between the other two, with the non-rotated image all the way to the front, otherwise as the pending image rotates it would obscure this “half-completed” part of the progress.

Are you totally confused yet? Thankfully it’s actually not a lot of code. We will have a dependency property called “Progress” that goes from 0 to 1 to match how the built-in progress control works. And then when that value changes, we do the right thing showing and rotating images to get the right effect.

IMPORTANT NOTE: If you create your Xamarin Forms project from scratch using the built in Visual Studio template, make sure you use NuGet to update Xamarin Forms to the latest stable version. There was a bug in Xamarin Forms on Windows platforms that would make this code not have the desired result.

Here is the code for the circular progress control:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace CircularProgress
{
    public class CircularProgressControl : Grid
    {
        View progress1;
        View progress2;
        View background1;
        View background2;
        public CircularProgressControl()
        {
            progress1 = CreateImage("progress_done");
            background1 = CreateImage("progress_pending");
            background2 = CreateImage("progress_pending");
            progress2 = CreateImage("progress_done");
            HandleProgressChanged(1, 0);
        }

        private View CreateImage(string v1)
        {
            var img = new Image();
            img.Source = ImageSource.FromFile(v1 + ".png");
            this.Children.Add(img);
            return img;
        }

        public static BindableProperty ProgressProperty =
    BindableProperty.Create("Progress", typeof(double), typeof(CircularProgressControl), 0d, propertyChanged: ProgressChanged);

        private static void ProgressChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var c = bindable as CircularProgressControl;
            c.HandleProgressChanged(Clamp((double)oldValue, 0, 1), Clamp((double)newValue, 0, 1));
        }

        static double Clamp(double value, double min, double max)
        {
            if (value <= max && value >= min) return value;
            else if (value > max) return max;
            else return min;
        }

        private void HandleProgressChanged(double oldValue, double p)
        {
            if (p < .5)
            {
                if (oldValue >= .5)
                {
                    // this code is CPU intensive so only do it if we go from >=50% to <50%
                    background1.IsVisible = true;
                    progress2.IsVisible = false;
                    background2.Rotation = 180;
                    progress1.Rotation = 0;
                }
                double rotation = 360 * p;
                background1.Rotation = rotation;
            }
            else
            {
                if (oldValue < .5)
                {
                    // this code is CPU intensive so only do it if we go from <50% to >=50%
                    background1.IsVisible = false;
                    progress2.IsVisible = true;
                    progress1.Rotation = 180;
                }
                double rotation = 360 * p;
                background2.Rotation = rotation;
            }
        }

        public double Progress
        {
            get { return (double)this.GetValue(ProgressProperty); }
            set { SetValue(ProgressProperty, value); }
        }
    }
}

We also need to put the images in the correct images folder for each platform. For iOS this is the Resources folder, and the Build Action should be set to BundleResource. For Android it’s the Resources/drawable folder and the type is AndroidResource. For the Windows platforms they go in the root folder.

Then to use the control. Assume we have a Xamarin Forms XAML page called MainPage.xaml, we can load the control like this:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="CircularProgress.MainPage" 
             xmlns:local="clr-namespace:CircularProgress" BackgroundColor="White">
  <Grid>
    <local:CircularProgressControl x:Name="progressControl" Progress="0" HorizontalOptions="Center" VerticalOptions="Center" WidthRequest="60" HeightRequest="60"/>
  </Grid>
</ContentPage>

And in the code behind we can have a timer that increments the Progress property a bit at a time from 0 to 1 and then starts over:

namespace CircularProgress
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            Xamarin.Forms.Device.StartTimer(TimeSpan.FromSeconds(.02), OnTimer);
        }

        private bool OnTimer()
        {
            var progress = (progressControl.Progress + .01) ;
            if (progress > 1) progress = 0;
            progressControl.Progress = progress;
            return true;
        }
    }
}

Now if you run the app you should see something like the animated image at the top of this post. No custom renderers and it runs on every Xamarin Forms platform!