Ever wonder where to place activity indicators in your mobile apps? Quite often, I find myself placing the indicator right on the button itself. One of the benefits of such a design, is that you don’t have to free up any screen real estate to show the indicator. Here is an example of what I am talking about:
Personally, I think this is a clean and simple design. So let’s see how we can achieve this using just Xamarin.Forms.
Create a custom ContentView
- In the
.xaml
file, add the following:
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ActivityButton.Controls.ActivityButton">
<ContentView.Content>
<Grid
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<Button
x:Name="InnerButton"
Text="{Binding Text}"
Command="{Binding Command}"
CommandParameter="{Binding CommandParameter}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
BackgroundColor="{Binding BackgroundColor}"
TextColor="{Binding TextColor}"
BorderColor="Transparent"
Margin="0" />
<ActivityIndicator
x:Name="InnerActivityIndicator"
VerticalOptions="Center"
HorizontalOptions="Center"
Color="White"
Scale="{OnPlatform Default=1, Android=0.7}"
Opacity="0"
IsRunning="{Binding IsBusy}"
IsVisible="{Binding IsBusy}"
IsEnabled="{Binding IsBusy}" />
</Grid>
</ContentView.Content>
</ContentView>
There are a couple of things worth pointing out about this layout.
- As you might expect, there are two components that make up the
ActivityButton
. We are able to use anActivityIndicator
and aButton
in combination to achieve this look. - We use a
Grid
to display these elements on top of each other.Grid
s can sometimes be a good replacement forAbsoluteLayouts
when you want to place certain things in front of one another. To do so, we just omit assiging a row or column to each element.
- We are able to use bindings to set
BindableProperties
on theActivityButton
. For instance, we will have a property in the code behind that allows us to tell theActivityIndicator
when to start running.
- In the code behind add the following
BindableProperty
s:
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(ActivityButton), propertyChanged: TextUpdated);
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(ActivityButton), propertyChanged: CommandUpdated);
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(ActivityButton), propertyChanged: CommandParameterUpdated);
public static readonly BindableProperty IsBusyProperty = BindableProperty.Create(nameof(IsBusy), typeof(bool), typeof(ActivityButton), propertyChanged: IsBusyUpdated);
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(int), typeof(ActivityButton), propertyChanged: CornerRadiusUpdated);
public static readonly BindableProperty TextColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(ActivityButton));
public static new readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(ActivityButton));
Since we are using a ContentView
to combine our two elements, we will have to add a little bit of boiler plate code to make our ActivityButton
reusable. For example, we add a TextProperty
, CommandProperty
, and so on. Also take note, that we are using the new
keyword when declaring the BackgroundColorProperty
. This is to override the background color of the ContentView
. For example, we don’t ever want to set the ContentView
’s background color, but instead the Button
’s.
- Add the following
propertyChanged
method for theTextProperty
:
private static void TextUpdated(object sender, object oldValue, object newValue)
{
if (sender is ActivityButton activityButton && newValue is string newString)
{
activityButton.buttonText = newString;
activityButton.InnerButton.Text = newString;
}
}
In the above code, we are setting the Button
’s text to the new value, but also setting a local variable with the new value.
- Use the following code to respond to the bindable value for the
IsBusyProperty
update.
private static async void IsBusyUpdated(object sender, object oldValue, object newValue)
{
if (sender is ActivityButton activityButton && newValue is bool newBool)
{
activityButton.InnerButton.IsEnabled = !newBool;
activityButton.InnerActivityIndicator.IsRunning = newBool;
activityButton.InnerActivityIndicator.IsEnabled = newBool;
activityButton.InnerActivityIndicator.IsVisible = newBool;
var opacity = newBool ? 1 : 0;
await activityButton.InnerActivityIndicator.FadeTo(opacity, 200);
activityButton.InnerButton.Text = newBool ? string.Empty : activityButton.buttonText;
}
}
In the above method, we will enable or disable the button, fade the activity indicator in or out, and also set the button text to a value or null. This is really the meat and potatos of what makes this a custom control.
All said and done, your code behind should look something like this:
using System;
using System.Windows.Input;
using Xamarin.Forms;
namespace ActivityButton.Controls
{
public partial class ActivityButton : ContentView
{
private string buttonText;
public event EventHandler Clicked = delegate { };
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(ActivityButton), propertyChanged: TextUpdated);
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(ActivityButton), propertyChanged: CommandUpdated);
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(ActivityButton), propertyChanged: CommandParameterUpdated);
public static readonly BindableProperty IsBusyProperty = BindableProperty.Create(nameof(IsBusy), typeof(bool), typeof(ActivityButton), propertyChanged: IsBusyUpdated);
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(int), typeof(ActivityButton), propertyChanged: CornerRadiusUpdated);
public static readonly BindableProperty TextColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(ActivityButton));
public static new readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(ActivityButton));
public ActivityButton()
{
this.InitializeComponent();
this.InnerButton.BindingContext = this;
this.InnerButton.Clicked += this.OnClicked;
}
public string Text
{
get => (string)this.GetValue(TextProperty);
set => this.SetValue(TextProperty, value);
}
public ICommand Command
{
get => (ICommand)this.GetValue(CommandProperty);
set => this.SetValue(CommandProperty, value);
}
public object CommandParameter
{
get => this.GetValue(CommandParameterProperty);
set => this.SetValue(CommandParameterProperty, value);
}
public bool IsBusy
{
get => (bool)this.GetValue(IsBusyProperty);
set => this.SetValue(IsBusyProperty, value);
}
public int CornerRadius
{
get => (int)this.GetValue(CornerRadiusProperty);
set => this.SetValue(CornerRadiusProperty, value);
}
public new Color BackgroundColor
{
get => (Color)this.GetValue(BackgroundColorProperty);
set => this.SetValue(BackgroundColorProperty, value);
}
public Color TextColor
{
get => (Color)this.GetValue(TextColorProperty);
set => this.SetValue(TextColorProperty, value);
}
private void OnClicked(object sender, EventArgs args)
{
this.Clicked?.Invoke(sender, args);
}
private static void TextUpdated(object sender, object oldValue, object newValue)
{
if (sender is ActivityButton activityButton && newValue is string newString)
{
activityButton.buttonText = newString;
activityButton.InnerButton.Text = newString;
}
}
private static void CommandUpdated(object sender, object oldValue, object newValue)
{
if (sender is ActivityButton activityButton && newValue is ICommand newCommand)
{
activityButton.InnerButton.Command = newCommand;
}
}
private static void CommandParameterUpdated(object sender, object oldValue, object newValue)
{
if (sender is ActivityButton activityButton && newValue != null)
{
activityButton.InnerButton.CommandParameter = newValue;
}
}
private static async void IsBusyUpdated(object sender, object oldValue, object newValue)
{
if (sender is ActivityButton activityButton && newValue is bool newBool)
{
activityButton.InnerButton.IsEnabled = !newBool;
activityButton.InnerActivityIndicator.IsRunning = newBool;
activityButton.InnerActivityIndicator.IsEnabled = newBool;
activityButton.InnerActivityIndicator.IsVisible = newBool;
var opacity = newBool ? 1 : 0;
await activityButton.InnerActivityIndicator.FadeTo(opacity, 200);
activityButton.InnerButton.Text = newBool ? string.Empty : activityButton.buttonText;
}
}
private static void CornerRadiusUpdated(object sender, object oldValue, object newValue)
{
if (sender is ActivityButton activityButton && newValue is int newInt)
{
activityButton.InnerButton.CornerRadius = newInt;
}
}
}
}
Ok, so that should be it. For a working sample, check out my GitHub repo.
In order to keep things short and sweet, I omitted some of the boiler plate code. In case you want me to go through that in more detail, just let me know.