Xamarin.Control – Xamarin.Forms MaterialEntry

Back by popular demand, bringing more Material Design controls to you Xamarin.Forms app! This time we will look at implementing the standards in Material Design’s text fields by building a MaterialEntry control. You can find the source code and example app here: https://github.com/SuavePirate/MaterialEntry but you can build your own by following this post.

Let’s build our Xamarin.Forms control to work the same on not only Android where Material Design is baked in, but also to run on iOS AND UWP. In the end, we should be able to use our floating label, set an accent color that expands on the label and underline when focused while being able to bind these properties through MVVM.

Simulator Screen Shot Jul 14, 2017, 3.42.03 PM

The first thing we need to do is create a BorderlessEntry that removes the border from our entry on all 3 platforms. I’ve done this in a previous blog post here: Xamarin.Forms Borderless Entry, so we won’t be implementing it here. This code is also in the GitHub link above.

With our BorderlessEntry we can now create our custom control WITHOUT ANY MORE CUSTOM RENDERERS!

Let’s set up the layout structure in our XAML file, and then wire up the animation logic in our code behind.

MaterialEntry.xaml

<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"               xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"              xmlns:local="clr-namespace:SuaveControls.MaterialEntry"              x:Class="SuaveControls.MaterialEntry.MaterialEntry">
  <ContentView.Content>
        <Grid ColumnSpacing="16" Margin="0,8">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="1"/>
            </Grid.RowDefinitions>
            <Label x:Name="HiddenLabel" FontSize="10" IsVisible="False" Margin="0"/>
            <local:BorderlessEntry x:Name="EntryField" Text="{Binding Text, Mode=TwoWay}" Margin="0,12,0,0"/>
            <BoxView x:Name="BottomBorder" BackgroundColor="Gray"  Grid.Row="1" HeightRequest="1" Margin="0" HorizontalOptions="FillAndExpand"/>
            <BoxView x:Name="HiddenBottomBorder" BackgroundColor="Gray" Grid.Row="1" HeightRequest="1" Margin="0" WidthRequest="0" HorizontalOptions="Center"/>
        </Grid>
    </ContentView.Content>
</ContentView>

We set up our BorderlessEntry that will act as our formal point for entering text. We also add a label that is initially hidden and laid out on top of the BorderlessEntry. This is the label we will be using to animate the floating action that Material Design uses based while we fade out the placeholder text. The last bit is two BoxViews that act as the bottom line below the Entry. One is the unfocused which has a standard gray color, while the other has a width of 0 and will have a background color of our selected AccentColor. This will have an animated width expansion when the BorderlessEntry is focused.

Now let’s look at the animation and bindings in the code behind:

MaterialEntry.xaml.cs

public partial class MaterialEntry : ContentView
    {
        public static void Init() { }
        public static BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(MaterialEntry), defaultBindingMode: BindingMode.TwoWay);
        public static BindableProperty PlaceholderProperty = BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(MaterialEntry), defaultBindingMode: BindingMode.TwoWay, propertyChanged: (bindable, oldVal, newval) =>
        {
            var matEntry = (MaterialEntry)bindable;
            matEntry.EntryField.Placeholder = (string)newval;
            matEntry.HiddenLabel.Text = (string)newval;
        });

        public static BindableProperty IsPasswordProperty = BindableProperty.Create(nameof(IsPassword), typeof(bool), typeof(MaterialEntry), defaultValue: false, propertyChanged: (bindable, oldVal, newVal) =>
        {
            var matEntry = (MaterialEntry)bindable;
            matEntry.EntryField.IsPassword = (bool)newVal;
        });
        public static BindableProperty KeyboardProperty = BindableProperty.Create(nameof(Keyboard), typeof(Keyboard), typeof(MaterialEntry), defaultValue: Keyboard.Default, propertyChanged: (bindable, oldVal, newVal) =>
        {
            var matEntry = (MaterialEntry)bindable;
            matEntry.EntryField.Keyboard = (Keyboard)newVal;
        });
        public static BindableProperty AccentColorProperty = BindableProperty.Create(nameof(AccentColor), typeof(Color), typeof(MaterialEntry), defaultValue: Color.Accent);
        public Color AccentColor
        {
            get
            {
                return (Color)GetValue(AccentColorProperty);
            }
            set
            {
                SetValue(AccentColorProperty, value);
            }
        }
        public Keyboard Keyboard
        {
            get
            {
                return (Keyboard)GetValue(KeyboardProperty);
            }
            set
            {
                SetValue(KeyboardProperty, value);
            }
        }

        public bool IsPassword
        {
            get
            {
                return (bool)GetValue(IsPasswordProperty);
            }
            set
            {
                SetValue(IsPasswordProperty, value);
            }
        }

        public string Text
        {
            get
            {
                return (string)GetValue(TextProperty);
            }
            set
            {
                SetValue(TextProperty, value);
            }
        }
        public string Placeholder
        {
            get
            {
                return (string)GetValue(PlaceholderProperty);
            }
            set
            {
                SetValue(PlaceholderProperty, value);
            }
        }
        public MaterialEntry()
        {
            InitializeComponent();
            EntryField.BindingContext = this;
            EntryField.Focused += async (s, a) =>
            {
                HiddenBottomBorder.BackgroundColor = AccentColor;
                HiddenLabel.TextColor = AccentColor;
                HiddenLabel.IsVisible = true;
                if (string.IsNullOrEmpty(EntryField.Text))
                {
                    // animate both at the same time
                    await Task.WhenAll(
                        HiddenBottomBorder.LayoutTo(new Rectangle(BottomBorder.X, BottomBorder.Y, BottomBorder.Width, BottomBorder.Height), 200),
                        HiddenLabel.FadeTo(1, 60),
                        HiddenLabel.TranslateTo(HiddenLabel.TranslationX, EntryField.Y - EntryField.Height + 4, 200, Easing.BounceIn)
                     );
                    EntryField.Placeholder = null;
                }
                else
                {
                    await HiddenBottomBorder.LayoutTo(new Rectangle(BottomBorder.X, BottomBorder.Y, BottomBorder.Width, BottomBorder.Height), 200);
                }
            };
            EntryField.Unfocused += async (s, a) =>
            {
                HiddenLabel.TextColor = Color.Gray;
                if (string.IsNullOrEmpty(EntryField.Text))
                {
                    // animate both at the same time
                    await Task.WhenAll(
                        HiddenBottomBorder.LayoutTo(new Rectangle(BottomBorder.X, BottomBorder.Y, 0, BottomBorder.Height), 200),
                        HiddenLabel.FadeTo(0, 180),
                        HiddenLabel.TranslateTo(HiddenLabel.TranslationX, EntryField.Y, 200, Easing.BounceIn)
                     );
                    EntryField.Placeholder = Placeholder;
                }
                else
                {
                    await HiddenBottomBorder.LayoutTo(new Rectangle(BottomBorder.X, BottomBorder.Y, 0, BottomBorder.Height), 200);
                }
            };
        }
    }

We first set up the BindableProperties and public properties to enable the binding of the AccentColor, Text, Placeholder, and Keyboard. These BindableProperties also handle their own PropertyChanged events to update the view elements dynamically.

After that, we handle our constructor and wire up our Focused and Unfocused events on our BorderlessEntry. In the Focused event, we set the colors of the hidden bar, and the floating label to the accent color. We then start the animations of expanding the hidden bar, and the fade in and float up of the floating label.

On the Unfocused event, we do the inverse of setting the floating label color back to the unfocused color, check if there is text, if there is not – float the label back down, and then animate the collapse of the colored bar.

With all these things together, we get a nicely animated text field that has a floating label and expanding bottom bar with a given accent color!

ios_Material_Entry2

If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile developer tips and tricks!

Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.

Advertisement

Xamarin.Controls – Xamarin.Forms FloatingActionButton (including iOS!)

You did actually read that title correctly – we have a FloatingActionButton to use in Xamarin.Forms that works in both Android and iOS!

I’ve put the source code up for this here: https://github.com/SuavePirate/Xamarin.Forms.Controls.FloatingActionButton

It’s rudimentary and has room for some more fun properties, but it is fully functional! If you would like to contribute to the repository, see the TODO: list at the bottom of the README and start forking and making pull requests!

To breakdown the steps to create your own Floating Action Button in Xamarin.Forms, you’ll need:

  1. A custom Xamarin.Forms `Element`
  2. An Android Custom renderer to use the native `Android.Compat.Design.Widgets.FloatingActionButton`
  3. An iOS Custom renderer to create a button that looks like a FAB.

So let’s go in that order.

In Xamarin.Forms PCL

FloatingActionButton.xaml.cs

  public partial class FloatingActionButton : Button
    {
        public static BindableProperty ButtonColorProperty = BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(FloatingActionButton), Color.Accent);
        public Color ButtonColor
        {
            get
            {
                return (Color)GetValue(ButtonColorProperty);
            }
            set
            {
                SetValue(ButtonColorProperty, value);
            }
        }
        public FloatingActionButton()
        {
            InitializeComponent();
        }
    }

We added a new BindableProperty for the ButtonColor. This is done because setting the BackgroundColor will mess up the Android renderer and apply the background behind the FAB. We want to inherit from Button so that we can utilize some of the already useful properties that come with it – namely the Image property that consumes a FileImageSource. We can use this to set the icon for our FAB.

In Android

FloatingActionButtonRenderer.cs

using FAB = Android.Support.Design.Widget.FloatingActionButton;

[assembly: ExportRenderer(typeof(SuaveControls.Views.FloatingActionButton), typeof(FloatingActionButtonRenderer))]
namespace SuaveControls.FloatingActionButton.Droid.Renderers
{
    public class FloatingActionButtonRenderer : Xamarin.Forms.Platform.Android.AppCompat.ViewRenderer<SuaveControls.Views.FloatingActionButton, FAB>
    {
        protected override void OnElementChanged(ElementChangedEventArgs<SuaveControls.Views.FloatingActionButton> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement == null)
                return;

            var fab = new FAB(Context);
            // set the bg
            fab.BackgroundTintList = ColorStateList.ValueOf(Element.ButtonColor.ToAndroid());

            // set the icon
            var elementImage = Element.Image;
            var imageFile = elementImage?.File;

            if (imageFile != null)
            {
                fab.SetImageDrawable(Context.Resources.GetDrawable(imageFile));
            }
            fab.Click += Fab_Click;
            SetNativeControl(fab);

        }
        protected override void OnLayout(bool changed, int l, int t, int r, int b)
        {
            base.OnLayout(changed, l, t, r, b);
            Control.BringToFront();
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var fab = (FAB)Control;
            if (e.PropertyName == nameof(Element.ButtonColor))
            {
                fab.BackgroundTintList = ColorStateList.ValueOf(Element.ButtonColor.ToAndroid());
            }
            if (e.PropertyName == nameof(Element.Image))
            {
                var elementImage = Element.Image;
                var imageFile = elementImage?.File;

                if (imageFile != null)
                {
                    fab.SetImageDrawable(Context.Resources.GetDrawable(imageFile));
                }
            }
            base.OnElementPropertyChanged(sender, e);

        }

        private void Fab_Click(object sender, EventArgs e)
        {
            // proxy the click to the element
            ((IButtonController)Element).SendClicked();
        }
    }
}

A few important things to point out:

  • We add the additional using statement `using FAB = Android.Support.Design.Widget.FloatingActionButton;` to help us distinguish between our Xamarin.Forms element and the built in Android control.
  • We are NOT using a `ButtonRenderer` as our base class, but instead using a basic `ViewRenderer`. This is because the underlying control will not be a native Android `Button`, but the native Android `FloatingActionButton`.
  • Because we replace the `ButtonRenderer`, we need to make sure we still propagate click events up to the Xamarin.Forms element.

Now let’s look at iOS, which can utilize more of the built in pieces from Xamarin.Forms since it supports the BorderRadius property on Buttons.

In iOS

FloatingActionButtonRenderer.cs

[assembly: ExportRenderer(typeof(SuaveControls.Views.FloatingActionButton), typeof(FloatingActionButtonRenderer))]
namespace SuaveControls.FloatingActionButton.iOS.Renderers
{
    [Preserve]
    public class FloatingActionButtonRenderer : ButtonRenderer
    {
        public static void InitRenderer()
        {
        }
        public FloatingActionButtonRenderer()
        {
        }
        protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement == null)
                return;

            // remove text from button and set the width/height/radius
            Element.WidthRequest = 50;
            Element.HeightRequest = 50;
            Element.BorderRadius = 25;
            Element.BorderWidth = 0;
            Element.Text = null;

            // set background
            Control.BackgroundColor = ((SuaveControls.Views.FloatingActionButton)Element).ButtonColor.ToUIColor();
        }
        public override void Draw(CGRect rect)
        {
            base.Draw(rect);
            // add shadow
            Layer.ShadowRadius = 2.0f;
            Layer.ShadowColor = UIColor.Black.CGColor;
            Layer.ShadowOffset = new CGSize(1, 1);
            Layer.ShadowOpacity = 0.80f;
            Layer.ShadowPath = UIBezierPath.FromOval(Layer.Bounds).CGPath;
            Layer.MasksToBounds = false;

        }
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == "ButtonColor")
            {
                Control.BackgroundColor = ((SuaveControls.Views.FloatingActionButton)Element).ButtonColor.ToUIColor();
            }
        }
    }
}

We set an explicit WidthRequest, HeightRequest, and BorderRadius to get ourselves a circle. I’m not a big fan of doing it here, since it’s better suited as a calculation, but for now it works.

Lastly in our Draw override, we set up the drop shadow behind out button, and make sure that our ShadowPath is actually built from an oval so that it rounds off with the Button.
Also note that we take the ButtonColor property and apply it as the BackgroundColor of the UIButton to override the color from Xamarin.Forms. Don’t forget to set Text to null so that we can’t add text to the button and mess it up.

As a side note, iOS might try to link our your custom renderer if you are using it in an iOS Class Library. In order to avoid this, make sure to call a static InitRenderer method in your AppDelegate.cs as it will prevent it from being linked out.

Using the FloatingActionButton

Now that we have our renderers registered for our new Element, we can use it in our XAML or C# of our PCL or Shared Project:

MainPage.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:SuaveControls.FabExample"              xmlns:controls="clr-namespace:SuaveControls.Views;assembly=SuaveControls.FloatingActionButton"              x:Class="SuaveControls.FabExample.MainPage">
    <StackLayout Margin="32">
        <Label Text="This is a Floating Action Button!"             VerticalOptions="Center"             HorizontalOptions="Center"/>

        <controls:FloatingActionButton x:Name="FAB" HorizontalOptions="CenterAndExpand" WidthRequest="50" HeightRequest="50"  VerticalOptions="CenterAndExpand" Image="ic_add_white.png" ButtonColor="#03A9F4" Clicked="Button_Clicked"/>
    </StackLayout>
</ContentPage>

and our code behind:

MainPage.xaml.cs

namespace SuaveControls.FabExample
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

        }

        private async void Button_Clicked(object sender, EventArgs e)
        {
            await DisplayAlert("FAB Clicked!", "Congrats on creating your FAB!", "Thanks!");
        }
    }
}

Then we get these results in our Android and iOS apps:

Android

Screenshot_1493173400

iOS

2017-04-25_10-38-38-PM

If you want to just pull down the control I built on GitHub, the steps are straight forward:

  1. Clone the repository
  2. Reference the PCL in your PCL/Shared Lib
  3. Reference the PCL and native projects in your respective native project
  4. Pull the namespace into your XAML (or C#)
  5. Start using it!

The repository also contains an example app that references the source libraries.

If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile developer tips and tricks!

Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.

Xamarin.Controls – BadgeView

Who knew it could be so difficult to just put a number on a circle in Xamarin.Forms? Here’s a freebie to make your life easier in creating your own badges.

Before we dive in, I want to note that with the Xamarin.Forms.Themes, this can be easier. Check out the docs here: https://developer.xamarin.com/guides/xamarin-forms/user-interface/themes/. Basically, they added a StyleClass for BoxView that allows you to render it as a circle (although I’ve had problems with it in the past). This example is going to be avoiding the Themes package with a custom rolled implementation.

To start, we are going to create a custom CircleView. That CircleView is going to inherit from BoxView and use a custom renderer to give us the rounded edges we want. After that, we are going to make a reusable view called BadgeView that will essentially just slap a Label on top of our new CircleView.

Start here, with your CircleView in your PCL or Shared Library:

CircleView.cs

    public partial class CircleView : BoxView
    {
        public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(double), typeof(CircleView), 0.0);

        public double CornerRadius
        {
            get { return (double)GetValue(CornerRadiusProperty); }
            set { SetValue(CornerRadiusProperty, value); }
        }

        public CircleView()
        {
            InitializeComponent();
        }
    }

Now let’s create our custom renderers. I want to note, that for iOS it is much simpler, and could also be done as an Effect rather than a BoxRenderer, however, in order to be consistent with the more complicated Android implementation, we are doing both as renderers.

First, and easiest – iOS:

CircleViewRenderer.cs


[assembly: ExportRenderer(typeof(CircleView), typeof(CircleViewRenderer))]
namespace YourNamespace.iOS
{
    public class CircleViewRenderer : BoxRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e)
        {
            base.OnElementChanged(e);

            if (Element == null)
                return;

            Layer.MasksToBounds = true;
            Layer.CornerRadius = (float)((CircleView)Element).CornerRadius / 2.0f;
        }

    }
}

and of course Android:

CircleViewRenderer.cs


[assembly: ExportRenderer(typeof(CircleView), typeof(CircleViewRenderer))]
namespace YouNamespace.Droid
{
    public class CircleViewRenderer : BoxRenderer
    {
        private float _cornerRadius;
        private RectF _bounds;
        private Path _path;
        protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e)
        {
            base.OnElementChanged(e);

            if (Element == null)
            {
                return;
            }
            var element = (CircleView)Element;

            _cornerRadius = TypedValue.ApplyDimension(ComplexUnitType.Dip, (float)element.CornerRadius, Context.Resources.DisplayMetrics);

        }

        protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
        {
            base.OnSizeChanged(w, h, oldw, oldh);
            if (w != oldw && h != oldh)
            {
                _bounds = new RectF(0, 0, w, h);
            }

            _path = new Path();
            _path.Reset();
            _path.AddRoundRect(_bounds, _cornerRadius, _cornerRadius, Path.Direction.Cw);
            _path.Close();
        }

        public override void Draw(Canvas canvas)
        {
            canvas.Save();
            canvas.ClipPath(_path);
            base.Draw(canvas);
            canvas.Restore();
        }
    }
}

Cool. Now we can draw pretty circles in our Xamarin.Forms views:

<views:CircleView CornerRadius="16" WidthRequest="16" HeightRequest="16"/>

Now let’s apply that to a reusable BadgeView.

BadgeView.xaml

<Grid      xmlns="http://xamarin.com/schemas/2014/forms"      xmlns:local="clr-namespace:your_local_namespace"     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"      x:Class="your_local_namespace.BadgeView"     HeightRequest="16"     WidthRequest="16">
    <local:CircleView x:Name="BadgeCircle" HeightRequest="16" WidthRequest="16" CornerRadius="16" VerticalOptions="Center" HorizontalOptions="Center" />
    <Label x:Name="BadgeLabel" TextColor="White" VerticalOptions="Center" HorizontalOptions="Center" VerticalTextAlignment="Center" HorizontalTextAlignment="Center" FontSize="10"/>
</Grid>

BadgeView.xaml.cs

 public partial class BadgeView : Grid
    {
        public static BindableProperty TextProperty = BindableProperty.Create("Text", typeof(string), typeof(BadgeView), "0", propertyChanged: (bindable, oldVal, newVal) =>
        {
            var view = (BadgeView)bindable;
            view.BadgeLabel.Text = (string)newVal;
        });

        public static BindableProperty BadgeColorProperty = BindableProperty.Create("BadgeColor", typeof(Color), typeof(BadgeView), Color.Blue, propertyChanged: (bindable, oldVal, newVal) =>
        {
            var view = (BadgeView)bindable;
            view.BadgeCircle.BackgroundColor = (Color)newVal;
        });

        public string Text
        {
            get
            {
                return (string)GetValue(TextProperty);
            }
            set
            {
                SetValue(TextProperty, value);
            }
        }
        public Color BadgeColor
        {
            get
            {
                return (Color)GetValue(BadgeColorProperty);
            }
            set
            {
                SetValue(BadgeColorProperty, value);
            }
        }
        public BadgeView()
        {
            InitializeComponent();
            BadgeLabel.Text = Text;
            BadgeCircle.BackgroundColor = BadgeColor;
        }
    }

This is obviously a super simple example, but you can always add any other properties you want such as handling changing sizes, corners, shapes, colors, etc.

But now we can see our final results when using our control:

 <Grid>
    <Label HorizontalTextAlignment="Center" Text="Look at me!"/>
    <views:BadgeView Text="3" BadgeColor="Green" VerticalOptions="Start" HorizontalOptions="End"/>
</Grid>

BadgeExample

If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile developer tips and tricks!

Xamarin.Controls – Creating Your Own iOS Markdown UILabel

In a previous post, I talked about a Xamarin.Forms control I put on GitHub to allow you to display Markdown strings properly. It worked by parsing the markdown into html, then using the custom renderer to display the html string in a TextView for Android and a UILabel for iOS.

However, we are not always using Xamarin.Forms, so let’s take a look at achieving the same functionality with just Xamarin.iOS.

If you’re looking for the same type of solution, check out my previous post: Xamarin.Controls – Creating Your Own Android Markdown TextView

We’ll break it down into a few parts:

  1. Parse a markdown string into an html string
  2. Parse the html string into an NSAttributedString
  3. Set the AttributedText of the UILabel

Parsing Markdown

This is traditionally the most difficult part. However, our community is awesome and open sourced a Markdown processor with an MIT license (so use it freely!).

I won’t put the actual code in here because it is overwhelmingly long, but here is a link to it:

https://github.com/SuavePirate/MarkdownTextView/blob/master/src/Forms/SPControls.MarkdownTextView/SPControls.MarkdownTextView/Markdown.cs

Note that this is portable, so you can use it in a PCL without a problem and share it between your platforms.

Now that we have our means of processing the Markdown, let’s create some extension methods to make it easier to parse and do some extra processing like cleaning up our tags, line breaks, etc.

#region MARKDOWN STYLES
private const string ORIGINAL_PATTERN_BEGIN = "<code>";
private const string ORIGINAL_PATTERN_END = "</code>";
private const string PARSED_PATTERN_BEGIN = "<font color=\"#888888\" face=\"monospace\"><tt>";
private const string PARSED_PATTERN_END = "</tt></font>";
 
#endregion
 
public static string ToHtml(this string markdownText)
{
    var markdownOptions = new MarkdownOptions
    {
        AutoHyperlink = true,
        AutoNewlines = false,
        EncodeProblemUrlCharacters = false,
        LinkEmails = true,
        StrictBoldItalic = true
    };
    var markdown = new Markdown(markdownOptions);
    var htmlContent = markdown.Transform(markdownText);
    var regex = new Regex("\n");
    htmlContent = regex.Replace(htmlContent, "<br/>");
 
    var html = htmlContent.HtmlWrapped();
    var regex2 = new Regex("\r");
    html = regex.Replace(html, string.Empty);
    html = regex2.Replace(html, string.Empty);
    return html;
}
 
///
<summary>
/// Wrap html with a full html tag
/// </summary>
/// <param name="html"></param>
/// <returns></returns>
public static string HtmlWrapped(this string html)
{
    if (!html.StartsWith("<html>") || !html.EndsWith("</html>"))
    {
        html = $"<html><body>{html}</body></html>";
    }
    return html;
}
 
///<summary>
/// Parses html with code or pre tags and gives them proper
/// styled spans so that Android can parse it properly
/// </summary>
/// <param name="htmlText">The html string</param>
/// <returns>The html string with parsed code tags</returns>
public static string ParseCodeTags(this string htmlText)
{
    if (htmlText.IndexOf(ORIGINAL_PATTERN_BEGIN) < 0) return htmlText;
    var regex = new Regex(ORIGINAL_PATTERN_BEGIN);
    var regex2 = new Regex(ORIGINAL_PATTERN_END);
 
    htmlText = regex.Replace(htmlText, PARSED_PATTERN_BEGIN);
    htmlText = regex2.Replace(htmlText, PARSED_PATTERN_END);
    htmlText = htmlText.TrimLines();
    return htmlText;
}
 
public static bool EqualsIgnoreCase(this string text, string text2)
{
    return text.Equals(text2, StringComparison.CurrentCultureIgnoreCase);
}
 
public static string ReplaceBreaks(this string html)
{
    var regex = new Regex("<br/>");
    html = regex.Replace(html, "\n");
    return html;
}
 
public static string ReplaceBreaksWithSpace(this string html)
{
    var regex = new Regex("<br/>");
    html = regex.Replace(html, " ");
    return html;
}
 
public static string TrimLines(this string originalString)
{
    originalString = originalString.Trim('\n');
    return originalString;
}

Now we can properly parse markdown to html:

var markdown = "# Hello *World*";
var html = markdown.ToHtml();
// html = "<h1>Hello <strong>World</strong></h1>"

Parsing Html to NSAttributedString

The next step is to take our processed html string and turn it into something that an iOS UILabel can use.

How about another extension method?

public static NSAttributedString ToAttributedText(this string html)
{
    NSError error = new NSError();
     try
     {
         var htmlData = NSData.FromString(html);
         if (htmlData != null && htmlData.Length > 0)
         {
             NSAttributedString attributedString = null;

             attributedString = new NSAttributedString(htmlData, new DocumentType = NSDocumentType.HTML, StringEncoding = NSStringEncoding.UTF8    }, ref error);
             return attributedString;
         }
         return null;
     }
     catch (Exception ex)
     {
         Console.WriteLine(ex);
         return null;
     }
}

We’ll add one more extension method just to make the full conversion from markdown to html to formatted html:

public static NSAttributedString MarkdownToHtml(this string markdown) 
{
    return markdown.ToHtml().ToAttributedString();
}

Now for the very last bit: Show it!

Assiging To the UILabel

Pretty simple now that we have our useful extension methods!

var markdown = "# Hello *World*";
myLabel.AttributedText = markdown.MarkdownToHtml();

That’s it! Now you can see some cool stylized text in your labels.

Xamarin.Controls – Creating Your Own Android Markdown TextView

In a previous post, I talked about a Xamarin.Forms control I put on GitHub to allow you to display Markdown strings properly. It worked by parsing the markdown into html, then using the custom renderer to display the html string in a TextView for Android and a UILabel for iOS.

However, we are not always using Xamarin.Forms, so let’s take a look at achieving the same functionality with just Xamarin.Android.

We’ll break it down into a few parts:

  1. Parse a markdown string into an html string
  2. Parse the html string into an ICharSequence
  3. Create an extra tag handler to help show html elements that are not traditionally supported in a TextView
  4. Set the TextFormatted of the TextView

Parsing Markdown

This is traditionally the most difficult part. However, our community is awesome and open sourced a Markdown processor with an MIT license (so use it freely!).
I won’t put the actual code in here because it is overwhelmingly long, but here is a link to it:

https://github.com/SuavePirate/MarkdownTextView/blob/master/src/Forms/SPControls.MarkdownTextView/SPControls.MarkdownTextView/Markdown.cs

Note that this is portable, so you can use it in a PCL without a problem and share it between your platforms.

Now that we have our means of processing the Markdown, let’s create some extension methods to make it easier to parse and do some extra processing like cleaning up our tags, line breaks, etc.


 #region MARKDOWN STYLES
private const string ORIGINAL_PATTERN_BEGIN = "<code>";
private const string ORIGINAL_PATTERN_END = "</code>";
private const string PARSED_PATTERN_BEGIN = "<font color=\"#888888\" face=\"monospace\"><tt>";
private const string PARSED_PATTERN_END = "</tt></font>";

#endregion

public static string ToHtml(this string markdownText)
{
    var markdownOptions = new MarkdownOptions
    {
        AutoHyperlink = true,
        AutoNewlines = false,
        EncodeProblemUrlCharacters = false,
        LinkEmails = true,
        StrictBoldItalic = true
    };
    var markdown = new Markdown(markdownOptions);
    var htmlContent = markdown.Transform(markdownText);
    var regex = new Regex("\n");
    htmlContent = regex.Replace(htmlContent, "<br/>");

    var html = htmlContent.HtmlWrapped();
    var regex2 = new Regex("\r");
    html = regex.Replace(html, string.Empty);
    html = regex2.Replace(html, string.Empty);
    return html;
}

///
<summary>
/// Wrap html with a full html tag
/// </summary>

/// <param name="html"></param>
/// <returns></returns>
public static string HtmlWrapped(this string html)
{
    if (!html.StartsWith("<html>") || !html.EndsWith("</html>"))
    {
        html = $"<html><body>{html}</body></html>";
    }
    return html;
}

///<summary>
/// Parses html with code or pre tags and gives them proper
/// styled spans so that Android can parse it properly
/// </summary>

/// <param name="htmlText">The html string</param>
/// <returns>The html string with parsed code tags</returns>
public static string ParseCodeTags(this string htmlText)
{
    if (htmlText.IndexOf(ORIGINAL_PATTERN_BEGIN) < 0) return htmlText;
    var regex = new Regex(ORIGINAL_PATTERN_BEGIN);
    var regex2 = new Regex(ORIGINAL_PATTERN_END);

    htmlText = regex.Replace(htmlText, PARSED_PATTERN_BEGIN);
    htmlText = regex2.Replace(htmlText, PARSED_PATTERN_END);
    htmlText = htmlText.TrimLines();
    return htmlText;
}

public static bool EqualsIgnoreCase(this string text, string text2)
{
    return text.Equals(text2, StringComparison.CurrentCultureIgnoreCase);
}

public static string ReplaceBreaks(this string html)
{
    var regex = new Regex("<br/>");
    html = regex.Replace(html, "\n");
    return html;
}

public static string ReplaceBreaksWithSpace(this string html)
{
    var regex = new Regex("<br/>");
    html = regex.Replace(html, " ");
    return html;
}

public static string TrimLines(this string originalString)
{
    originalString = originalString.Trim('\n');
    return originalString;
}


Now we can properly parse markdown to html:


var markdown = "# Hello *World*";
var html = markdown.ToHtml();
// html = "<h1>Hello <strong>World</strong></h1>"

Parsing Html to ICharSequence

The next step is to take our processed html string and turn it into something that an Android TextView can use.

How about another extension method?


public static ICharSequence ToFormattedHtml(this string htmlText)
{
    try
    {
        ICharSequence html;
        if(Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.N)
        {
            html = Html.FromHtml(htmlText.ParseCodeTags(), FromHtmlOptions.ModeLegacy, null, new HtmlTagHandler()) as ICharSequence;
        }
        else
        {
            // handle legacy builds
            html = Html.FromHtml(htmlText.ParseCodeTags(), null, new HtmlTagHandler()) as ICharSequence;
        }
        // this is required to get rid of the end two "\n" that android adds with Html.FromHtml
        // see: http://stackoverflow.com/questions/16585557/extra-padding-on-textview-with-html-contents for example
        while (html.CharAt(html.Length() - 1) == '\n')
        {
            html = html.SubSequenceFormatted(0, html.Length() - 1);
        }
        return html;
    }
    catch
    {
        return null;
    }
}

To breakdown what this is doing: We check against the sdk version we are running against to ensure we don’t use an obsolete api, then call to get the Html as an ICharSequence. Then, we clean up the output and return it.

We’ll add one more extension method just to make the full conversion from markdown to html to formatted html:

public static ICharSequence MarkdownToHtml(this string markdown)
{
    return markdown.ToHtml().ToFormattedHtml();
}

There’s one piece here that is unique, and that is the HtmlTagHandler() used in the call to Html.FromHtml.

Extending the Tag Handler

Android, unfortunately, doesn’t support many html elements in its TextFormatted. Daniel Lew has a great post about the supported types.

In order to get around this, we need to use tags that are supported and combine those to simulate the missing tags; especially for things like ul and ol.

I’ve translated a popular processor from Java to C# so we can use it in Xamarin: HtmlTagHandler.cs

/// <summary>
/// Custom tag handler for parsing more html tags for android textviews
/// This is a port/translation of https://github.com/sufficientlysecure/html-textview/blob/master/HtmlTextView/src/main/java/org/sufficientlysecure/htmltextview/HtmlTagHandler.java
/// Which is considered the most robust handler
/// </summary>
public class HtmlTagHandler : Java.Lang.Object, Android.Text.Html.ITagHandler
{
    /**
     * Keeps track of lists (ol, ul). On bottom of Stack is the outermost list
     * and on top of Stack is the most nested list
     */
    Stack<String> lists = new Stack<String>();
    /**
     * Tracks indexes of ordered lists so that after a nested list ends
     * we can continue with correct index of outer list
     */
    Stack<Int32> olNextIndex = new Stack<Int32>();
    /**
     * List indentation in pixels. Nested lists use multiple of this.
     */
    private static int indent = 10;
    private static int listItemIndent = indent * 2;
    private static BulletSpan bullet = new BulletSpan(indent);
    private class Ul : Java.Lang.Object
    {
    }
    private class Ol : Java.Lang.Object
    {
    }

    private class Code : Java.Lang.Object
    {
    }
    private class Center : Java.Lang.Object
    {
    }

    private class Strike : Java.Lang.Object
    {
    }


    public void HandleTag(Boolean opening, String tag, Android.Text.IEditable output, IXMLReader xmlReader)
    {
        if (opening)
        {
            if (tag.ToLower() == "ul")
            {
                lists.Push(tag);
            }
            else if (tag.EqualsIgnoreCase("ol"))
            {
                lists.Push(tag);
                olNextIndex.Push(1);
            }
            else if (tag.EqualsIgnoreCase("li"))
            {
                if (output.Length() > 0 && output.CharAt(output.Length() - 1) != '\n')
                {
                    output.Append("\n");
                }
                String parentList = lists.Peek();
                if (parentList.EqualsIgnoreCase("ol"))
                {
                    Start(output, new Ol());
                     output.Append(olNextIndex.Peek().ToString()).Append('.').Append(' ');
                    olNextIndex.Push(olNextIndex.Pop() + 1);
                }
                else if (parentList.EqualsIgnoreCase("ul"))
                {
                    Start(output, new Ul());
                }
            }
            else if (tag.EqualsIgnoreCase("code"))
            {
                Start(output, new Code());
            }
            else if (tag.EqualsIgnoreCase("center"))
            {
                Start(output, new Center());
            }
            else if (tag.EqualsIgnoreCase("s") || tag.EqualsIgnoreCase("strike"))
            {
                Start(output, new Strike());
            }
        }
        else
        {
            if (tag.EqualsIgnoreCase("ul"))
            {
                lists.Pop();
            }
            else if (tag.EqualsIgnoreCase("ol"))
            {
                lists.Pop();
                olNextIndex.Pop();
            }
            else if (tag.EqualsIgnoreCase("li"))
            {
                if (lists.Peek().EqualsIgnoreCase("ul"))
                {
                    if (output.Length() > 0 && output.CharAt(output.Length() - 1) != '\n')
                    {
                        output.Append("\n");
                    }
                    // Nested BulletSpans increases distance between bullet and Text, so we must prevent it.
                    int bulletMargin = indent;
                    if (lists.Count > 1)
                    {
                        bulletMargin = indent - bullet.GetLeadingMargin(true);
                        if (lists.Count > 2)
                        {
                            // This get's more complicated when we add a LeadingMarginSpan into the same line:
                            // we have also counter it's effect to BulletSpan
                            bulletMargin -= (lists.Count - 2) * listItemIndent;
                        }
                    }
                    BulletSpan newBullet = new BulletSpan(bulletMargin);
                    End(output, typeof(Ul), false,
                            new LeadingMarginSpanStandard(listItemIndent * (lists.Count - 1)),
                            newBullet);
                }
                else if (lists.Peek().EqualsIgnoreCase("ol"))
                {
                    if (output.Length() > 0 && output.CharAt(output.Length() - 1) != '\n')
                    {
                        output.Append("\n");
                    }
                    int numberMargin = listItemIndent * (lists.Count - 1);
                    if (lists.Count > 2)
                    {
                        // Same as in ordered lists: counter the effect of nested Spans
                        numberMargin -= (lists.Count - 2) * listItemIndent;
                    }
                    End(output, typeof(Ol), false, new LeadingMarginSpanStandard(numberMargin));
               }
            }
            else if (tag.EqualsIgnoreCase("code"))
            {
                End(output, typeof(Code), false, new TypefaceSpan("monospace"));
            }
            else if (tag.EqualsIgnoreCase("center"))
            {
                End(output, typeof(Center), true, new AlignmentSpanStandard(Layout.Alignment.AlignCenter));
            }
            else if (tag.EqualsIgnoreCase("s") || tag.EqualsIgnoreCase("strike"))
            {
                End(output, typeof(Strike), false, new StrikethroughSpan());
            }
        }
    }

    /**
     * Mark the opening tag by using private classes
     */
    private void Start(IEditable output, Java.Lang.Object mark)
    {
        int len = output.Length();
        output.SetSpan(mark, len, len, SpanTypes.MarkMark);
    }

    /**
     * Modified from {@link Android.Text.Html}
     */
    private void End(IEditable output, Type kind, Boolean paragraphStyle, params Java.Lang.Object[] replaces)
    {
        Java.Lang.Object obj = GetLast(output, kind);
        // start of the tag
        int where = output.GetSpanStart(obj);
        // end of the tag
        int len = output.Length();
        output.RemoveSpan(obj);

       if (where != len)
       {
            int thisLen = len;
            // paragraph styles like AlignmentSpan need to end with a new line!
            if (paragraphStyle)
            {
                output.Append("\n");
                thisLen++;
            }
            foreach (Java.Lang.Object replace in replaces)
            {
                output.SetSpan(replace, where, thisLen, SpanTypes.ExclusiveExclusive);
            }


       }
   }

    /**
     * Get last marked position of a specific tag kind (private class)
     */
    private static Java.Lang.Object GetLast(IEditable Text, Type kind)
    {
        Java.Lang.Object[] objs = Text.GetSpans(0, Text.Length(), Java.Lang.Class.FromType(kind)); // TODO: LOl will this work?
        if (objs.Length == 0)
        {
            return null;
        }
        else
        {
            for (int i = objs.Length; i > 0; i--)
            {
                if (Text.GetSpanFlags(objs[i - 1]) == SpanTypes.MarkMark)
                {
                    return objs[i - 1];
                }
            }
           return null;
        }
    }

}

So now we can properly display list elements as well as code elements!

Now for the very last bit: Show it!

Assiging To the TextView

Pretty simple now that we have our useful extension methods!

var markdown = "# Hello *World*";
var myTextView = FindViewById<TextView>(Resource.Id.MyTextView);
myTextView.TextFormatted = markdown.MarkdownToHtml();

That’s it! Now you can see some cool stylized text in your labels.

Xamarin.Controls – JsonTextView

I had a relatively unique requirement where we needed to list out items in a single list that were originated from Json. That’s obviously pretty straightforward. However, each of these items coming back as Json had DRASTICALLY different properties.

The go-to thought is to use different DataTemplates for each unique type that was coming back. However, there was no consistency on top of around 100 different types we were going to get. This sounds like a design flaw in what is coming down from the server, but we didn’t have control over that.

I put my solution up on GitHub as a reusable control: https://github.com/SuavePirate/JsonTextView

It’s simply a control for Xamarin.Forms that has a bindable property called Json that consumes a Json string. It then takes that Json and puts it into an organized Label using Span classes:

public void SetJson(string json)
{
    try
    {
        var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
        foreach (var pair in dict)
        {
            var boldSpan = new Span()
            {
                Text = UpperCase ? $"{pair.Key.SplitCamelCase().ToTitleCase()}: " : $"{pair.Key.SplitCamelCase()}: ",
                FontAttributes = FontAttributes.Bold
            };
            var detailSpan = new Span()
            {
                Text = pair.Value.SplitCamelCase().ToTitleCase()
            };
            var lineBreak = new Span()
            {
                Text = "\n"
            };
            FormattedText.Spans.Add(boldSpan);
            FormattedText.Spans.Add(detailSpan);
            FormattedText.Spans.Add(lineBreak);
        }
    }
    catch
    {
        WriteLine("Failed to parse json.");
    }
}

This also uses two extension methodsthat come in handy:

public static string SplitCamelCase(this string str)
{
    return Regex.Replace(
       Regex.Replace(
            str,
            @"(\P{Ll})(\P{Ll}\p{Ll})",
            "$1 $2"
        ),
        @"(\p{Ll})(\P{Ll})",
        "$1 $2"
     );
}

public static string ToTitleCase(this string str)
{
    return string.Join(" ", str.Split(' ').Select(i => $"{i.Substring(0, 1).ToUpper()}{i.Substring(1).ToLower()}").ToArray());
}

The usage is then pretty simple:

XAML


<ContentPage
  ...
  xmlns:sp="clr-namespace:SPControls;assembly=SPControls"
  ...>
    <Grid>
      <sp:JsonTextView Json="{Binding JsonString}" />
    </Grid>
</ContentPage>

Or in C#

...
var jsonTextView = new SPControls.JsonTextView();
jsonTextView.Json = jsonString;
...

Check out the repository for the full code, and help contribute! Fork the repository, add what features you’d like to see, then submit a pull request.