Xamarin.Forms Recipe: Label with Letter Spacing

Setting a letter spacing for a Label in Android and iOS turned into an interesting research for me. I would expect that such a common task would be easily done with a help of a Renderer or an Effect. However, I was very surprised to discover that some platforms do not have a built-in support for setting letter spacing.

Android

It turned out that setting letter spacing it is not possible on pre Lollipop (< 21) without hacks when on Lollipop+ there is a dedicated method: setLetterSpacing(). Luckily this SO post offered a creative solution:

.. method adds a space between each letter of the String and with SpannedString changes the TextScaleX of the spaces, allowing positive and negative spacing ..

The math in this solution is a bit weird and should be adjusted according to your specific needs. Otherwise it is doing it’s job nicely.

Checking the Distribution dashboard, it seems that 15.3% of users are using KitKat or older and 84.7% are using Lollipop or newer Android version. So hopefully very soon this recipe could be simplified🤞.

iOS

On iOS it is (as expected) a one-liner using attributedText property.

The recipe

I combined the details described above into Effect:

using System.Linq;
using System.Text;
using Android.Text;
using Android.Text.Style;
using Android.Widget;
using MyNamespace.Effects;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using static Android.Widget.TextView;
[assembly: ResolutionGroupName("MyNamespace.Effects")]
[assembly: ExportEffect(typeof(MyNamespace.Droid.Effects.LabelTextKerningEffect), nameof(LabelTextKerningEffect))]
namespace MyNamespace.Droid.Effects
{
public class LabelTextKerningEffect : PlatformEffect
{
protected override void OnAttached()
{
var effect = (MyNamespace.Effects.LabelTextKerningEffect)Element.Effects.FirstOrDefault(e => e is MyNamespace.Effects.LabelTextKerningEffect);
ApplySpacing(effect.Kerning);
}
protected override void OnDetached() { }
void ApplySpacing(float letterSpacing)
{
var lbl = Control as TextView;
if (lbl == null || string.IsNullOrEmpty(lbl.Text)) return;
if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
{
lbl.LetterSpacing = letterSpacing;
}
else
{
var originalText = lbl.Text;
var builder = new StringBuilder();
for (int i = 0; i < originalText.Length; i++)
{
builder.Append(originalText[i]);
if (i + 1 < originalText.Length)
builder.Append("\u00A0"); // 'NO-BREAK SPACE'
}
var finalText = new SpannableString(builder.ToString());
if (builder.ToString().Length > 1)
{
for (int i = 1; i < builder.ToString().Length; i += 2)
finalText.SetSpan(new ScaleXSpan(letterSpacing * 4), i, i + 1, SpanTypes.ExclusiveExclusive);
}
lbl.SetText(finalText, BufferType.Spannable);
}
}
}
}
using System.Linq;
using MyNamespace.Effects;
using Foundation;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ResolutionGroupName("MyNamespace.Effects")]
[assembly: ExportEffect(typeof(MyNamespace.iOS.Effects.LabelTextKerningEffect), nameof(LabelTextKerningEffect))]
namespace MyNamespace.iOS.Effects
{
public class LabelTextKerningEffect : PlatformEffect
{
protected override void OnAttached()
{
var effect = (MyNamespace.Effects.LabelTextKerningEffect)Element.Effects.FirstOrDefault(e => e is MyNamespace.Effects.LabelTextKerningEffect);
ApplySpacing(effect.Kerning);
}
protected override void OnDetached() { }
void ApplySpacing(float letterSpacing)
{
var lbl = Control as UILabel;
if (lbl == null || string.IsNullOrEmpty(lbl.Text)) return;
lbl.AttributedText = new NSAttributedString(lbl.Text, kerning: letterSpacing);
}
}
}
<?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:XFLetterSpacing"
xmlns:effects="clr-namespace:XFLetterSpacing.Effects"
x:Class="XFLetterSpacing.MainPage">
<StackLayout>
<!-- Place new controls here -->
<Label Text="Welcome!" HorizontalOptions="Center" VerticalOptions="CenterAndExpand">
<Label.Effects>
<effects:LabelTextLetterSpacingEffect Kerning="5" />
</Label.Effects>
</Label>
</StackLayout>
</ContentPage>
view raw MainPage.xaml hosted with ❤ by GitHub
public class LabelTextKerningEffect : RoutingEffect
{
public float Kerning { get; set; }
public LabelTextKerningEffect() : base($"MyNamespace.Effects.{nameof(LabelTextKerningEffect)}") { }
}

3 thoughts on “Xamarin.Forms Recipe: Label with Letter Spacing

  1. Just to add some information: You can also add line spacing or underline your label.
    Just change your code inside your effects to the following…

    Android effect:
    var formsLabel = Element as Label;

    // Letter spacing
    lbl.LetterSpacing = letterSpacing / (float) formsLabel.FontSize;
    // By the way: You need to divide through the original font size of your forms label because Android is using em here instead of points.

    // Line spacing
    lbl.SetLineSpacing(0, lineSpacing); // available since API level 1

    // Underline
    lbl.PaintFlags = textView.PaintFlags | PaintFlags.UnderlineText; // not sure if works since API level 1

    iOS effect:
    var text = new NSMutableAttributedString(lbl.Text);

    // Letter spacing
    text.AddAttribute(UIStringAttributeKey.KerningAdjustment, new NSNumber(letterSpacing), new NSRange(0, text.Length));

    // Line spacing
    var paragraphStyle = new NSMutableParagraphStyle { LineSpacing = lineSpacing };
    text.AddAttribute(UIStringAttributeKey.ParagraphStyle, paragraphStyle, new NSRange(0, text.Length));

    // Underline
    text.AddAttribute(UIStringAttributeKey.UnderlineStyle, new NSNumber((int) NSUnderlineStyle.Single), new NSRange(0, text.Length));

    lbl.AttributedText = text;

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s