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> |
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;
LikeLike
Is it possible to give letter spacing for Entry?
LikeLike