Using a Native Custom Control with Xamarin.Forms via a Custom Renderer
We've come to the close of our custom control series: this week we're looking at how you can use your custom native control in Xamarin.Forms via a custom renderer. We've recently covered how this process works in our custom renderer blog . Over the past few weeks we've also been exploring creating your own custom control, and bringing it to the Xamarin Platform.
Xamarin.Forms Checkbox
Xamarin.Forms has no available checkbox control at the moment (partially, I suspect, because there isn't a checkbox control available on iOS), but since we created one at the beginning of this series, we can use that custom iOS checkbox here as well. Google already has a checkbox control available for Android. This means that much of the groundwork is already completed at this point, and the focal point will be getting our custom renderer to work. An important note before we get too much further: We're working with two very different controls through the custom renderer. Though both controls are both just simple checkboxes, their object models will conform to each platform's conventions. Xamarin follows this process for every control available in Xamarin.Forms (here's a helpful list for reference). We'll define a basic checkbox class in Xamarin.Forms, and a custom renderer for each platform will act as a mechanism for abstracting these differences.
Getting Started in Xamarin.Forms: Create a Checkbox Class
The first step is to create a Checkbox class in Xamarin.Forms. This class defines the API for the Xamarin.Forms control. Our Checkbox needs to subclass the View class, and we can declare a couple of bindable properties for our control. We'll keep things fairly basic here and only expose a couple of common properties, such as the color and whether the control is checked. We'll also set up an EventHandler to capture when the Checkbox becomes checked or unchecked.
namespace CheckboxCustomRenderer
{
public class Checkbox : View
{
public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create<Checkbox, bool>(p => p.IsChecked, true, propertyChanged: (s, o, n) => { (s as Checkbox).OnChecked(new EventArgs()); });
public static readonly BindableProperty ColorProperty = BindableProperty.Create<Checkbox, Color>(p => p.Color, Color.Default);
public bool IsChecked{
get{
return (bool)GetValue(IsCheckedProperty);
}
set{
SetValue(IsCheckedProperty, value);
}
}
public Color Color{
get{
return (Color)GetValue(ColorProperty);
}
set{
SetValue(ColorProperty, value);
}
}
public event EventHandler Checked;
protected virtual void OnChecked(EventArgs e){
if (Checked != null)
Checked(this, e);
}
}
Android Custom Renderer
Now we'll create a custom renderer for each of the platforms. Each implementation will be slightly different since they're each working with a different underlying control (the Android CheckBox vs. our custom iOS implementation). We'll start off with the Android Custom Renderer by creating a new class (CheckboxCustomRenderer) in the Android portion of our solution. A few important details to note:
- We need to mark the top of our class with the ExportRenderer attribute so that the renderer is registered with Xamarin.Forms. This way, Xamarin.Forms will use this renderer when it's trying to create our Checkbox object on Android.
- We're doing most of our work in the OnElementChanged method, where we instantiate and set up our native control.
- Setting the color of the Android CheckBox requires that we actually create a ColorStateList with a number of different color values for each possible CheckBox state.
- Note the model.Color.ToAndroid() where we convert from a Xamarin Color object into the equivalent Android Color object.
[assembly: ExportRenderer(typeof(CheckboxCustomRenderer.Checkbox), typeof(CheckboxRenderer))]
namespace CheckboxCustomRenderer.Droid
{
public class CheckboxRenderer : ViewRenderer<Checkbox, CheckBox>
{
private CheckBox checkBox;
protected override void OnElementChanged(ElementChangedEventArgs<Checkbox> e){
base.OnElementChanged(e);
var model = e.NewElement;
checkBox = new CheckBox(Context);
checkBox.Tag = this;
CheckboxPropertyChanged(model, null);
checkBox.SetOnClickListener(new ClickListener(model));
SetNativeControl(checkBox);
}
private void CheckboxPropertyChanged(Checkbox model, String propertyName){
if(propertyName == null || Checkbox.IsCheckedProperty.PropertyName == propertyName){
checkBox.Checked = model.IsChecked;
}
if (propertyName == null || Checkbox.ColorProperty.PropertyName == propertyName){
int[][] states = {
new int[] { Android.Resource.Attribute.StateEnabled}, // enabled
new int[] {Android.Resource.Attribute.StateEnabled}, // disabled
new int[] {Android.Resource.Attribute.StateChecked}, // unchecked
new int[] { Android.Resource.Attribute.StatePressed} // pressed
};
var checkBoxColor = (int)model.Color.ToAndroid ();
int[] colors = {
checkBoxColor,
checkBoxColor,
checkBoxColor,
checkBoxColor
};
var myList = new Android.Content.Res.ColorStateList(states, colors);
checkBox.ButtonTintList = myList;
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e){
if (checkBox != null){
base.OnElementPropertyChanged(sender, e);
CheckboxPropertyChanged((Checkbox)sender, e.PropertyName);
}
}
public class ClickListener : Java.Lang.Object, IOnClickListener{
private Checkbox _myCheckbox;
public ClickListener(Checkbox myCheckbox){
this._myCheckbox = myCheckbox;
}
public void OnClick(global::Android.Views.View v){
\_myCheckbox.IsChecked = !\_myCheckbox.IsChecked;
}
}
}
}
iOS Custom Renderer
Since iOS does not have a native checkbox control, we created one of our own previously. This means we'll need to add our Xamarin.iOS project into this solution and add it as a reference to the iOS portion of our CustomRenderer Project. In terms of class construction, the code for the iOS version of the CheckboxCustomRenderer is similar to what we saw on Android. Once again, the class needs to be registered with Xamarin.Forms, and most of the work will be done in OnElementChanged. One notable difference involves how color is handled in the native iOS control. First, color is represented by UIColor objects on iOS, so note the calls to model.Color.ToUIColor(). We also have two color properties that we'll set to correspond to the Android properties, so we get similar behavior. Because the iOS and Android checkbox controls are inherently different, this isn't exactly a one-to-one correspondence, but it's similar enough to suit our purposes here.
[assembly: ExportRenderer (typeof(CheckboxCustomRenderer.Checkbox), typeof(CheckboxRenderer))]
namespace CheckboxCustomRenderer.iOS
{
public class CheckboxRenderer : ViewRenderer<Checkbox, GCCheckbox.Checkbox>
{
private GCCheckbox.Checkbox nativeCheckbox;
protected override void OnElementChanged (ElementChangedEventArgs<CheckboxCustomRenderer.Checkbox> e)
{
base.OnElementChanged (e);
var model = e.NewElement;
if (model == null) {
return;
}
nativeCheckbox = new GCCheckbox.Checkbox ();
CheckboxPropertyChanged (model, null);
model.PropertyChanged += OnElementPropertyChanged;
nativeCheckbox.ValueChanged += (object sender, EventArgs eargs) => {
model.IsChecked = nativeCheckbox.IsChecked;
};
SetNativeControl (nativeCheckbox);
}
private void CheckboxPropertyChanged(Checkbox model, String propertyName){
if (propertyName == null || propertyName == Checkbox.IsCheckedProperty.PropertyName) {
nativeCheckbox.IsChecked = model.IsChecked;
}
if (propertyName == null || propertyName == Checkbox.ColorProperty.PropertyName) {
nativeCheckbox.BoxFillColor = model.Color.ToUIColor ();
nativeCheckbox.BoxBorderColor = model.Color.ToUIColor ();
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (nativeCheckbox != null)
{
base.OnElementPropertyChanged(sender, e);
CheckboxPropertyChanged((Checkbox)sender, e.PropertyName);
}
}
}
}
I had to implement a few small changes in the native iOS checkbox to bring more parity between the controls:
- The size of the checkbox was fixed and designed to appear comparable to the Android control.
- I added the call [self sendActionsForControlEvents:UIControlEventsValueChanged] to the method where the isChecked property is set. This was necessary for the iOS control to maintain similar behavior to the Android control, and allow Xamarin.Forms to behave the same in either circumstance.
Using the Custom Xamarin Control
The control should now be usable in Xamarin.Forms and display a checkbox on either iOS or Android. The control can either be created in code behind or XAML, and configured similarly to any other Xamarin.Forms control. Code behind
MainPage = new ContentPage {
Content = new StackLayout {
VerticalOptions = LayoutOptions.Center,
Children = {
new CheckboxCustomRenderer.Checkbox(){
WidthRequest = 24,
HeightRequest = 24,
Color = Xamarin.Forms.Color.Red
}
}
}
};
XAML
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:CheckboxCustomRenderer;assembly=CheckboxCustomRenderer"
x:Class="CheckboxCustomRenderer.TestPage">
<ContentPage.Content>
<StackLayout VerticalOptions="Center">
<local:Checkbox Color="Green" WidthRequest="24" HeightRequest="24"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
Wrap Up
Custom renderers are a powerful mechanism for adding new UI features to a Xamarin.Forms application, though the implementation can be complex and definitely requires some thought. There are many pieces to connect together in Xamarin.Forms to use your custom controls and the more similar controls are the easier this will be. Having reached the final stage of our custom control, it becomes apparent how much work goes into every stage of development to ensure that even a simple checkbox control works correctly in Xamarin.Forms.
In this article series, we also discuss Customizing Your Xamarin.Forms Apps with Animations, How to Create a Custom Color Picker for Xamarin.Forms, How to Create a Custom DropDown Control in Xamarin.Forms, and How to Customize Your Xamarin.Forms Apps with Animations.