Chet at C2IT
Chet at C2IT

Reputation: 547

Custom Font in Xamarin.Forms Label with FormattedString

I have created a custom LabelRenderer in my Android app to apply a custom font in a Xamarin Android app (https://developer.xamarin.com/guides/xamarin-forms/user-interface/text/fonts/).

Everything works great for a normal label with the content added to the .Text property. However, if I create a label using .FormattedText property, the custom font is not applied.

Anyone have success doing this? An option, since I'm just stacking lines of different sized text, is to use separate label controls for each, but I'd prefer to use a formatted string if possible.

Here's the guts of my custom renderer:

[assembly: ExportRenderer (typeof (gbrLabel), typeof (gbrLabelRenderer))]

public class gbrLabelRenderer: LabelRenderer
{
    protected override void OnElementChanged (ElementChangedEventArgs<Label> e)
    {
        base.OnElementChanged (e);
        var label = (TextView)Control;
        Typeface font = Typeface.CreateFromAsset (Forms.Context.Assets, "Lobster-Regular.ttf");
        label.Typeface = font;
    }
}

And here's my simple label control... all it does is apply the font to iOS, and leaves applying the font for Android up to the custom renderer.

public class gbrLabel: Label
{
    public gbrLabel ()
    {
        Device.OnPlatform (
            iOS: () => {
                FontFamily = "Lobster-Regular";
                FontSize = Device.GetNamedSize(NamedSize.Medium,this);
            }
    }
}

Works fine for labels with just the .Text property... but not for labels with the .FormattedText property.

Should I keep digging, or just stack my labels since that's an option in this case?

Here's an example of the various ways I've tried this in the Formatted text, since that was requested:

var fs = new FormattedString ();
fs.Spans.Add (new Span { 
    Text = string.Format("LINE 1\n",Title), 
    FontSize = Device.GetNamedSize(NamedSize.Large,typeof(Label))
});
fs.Spans.Add (new Span { 
    Text = string.Format ("LINE 2\n"), 
    FontSize = Device.GetNamedSize(NamedSize.Large,typeof(Label)) * 2,
    FontAttributes = FontAttributes.Bold,
    FontFamily = "Lobster-Regular"
});
fs.Spans.Add (new Span {
    Text = string.Format ("LINE 3\n"),
    FontSize = Device.GetNamedSize(NamedSize.Medium,typeof(Label)),
    FontFamily = "Lobster-Regular.ttf"
});

gbrLabel lblContent = new gbrLabel {
    FormattedText = fs
}

None of these (the first should be set by the default class / renderer, and the second 2 are variations of including the font in a span definition itself) work on Android.

Upvotes: 4

Views: 4835

Answers (1)

Sven-Michael St&#252;be
Sven-Michael St&#252;be

Reputation: 14750

Note: Android and iOS issues have been summarized on a blog post: smstuebe.de/2016/04/03/formattedtext.xamrin.forms/


The font is set as long as you do not set FontSize or FontAttributes. So I had the look at the implementation and found that the FormattedText is trying to load the font like the default renderer which doesn't work on Android.

The android formatting system works very similar to that one of Xamarin.Forms. It's using spans to define text attributes. The renderer is adding a FontSpan for every Span with a custom font, size or attribute. Unfortunately, the FontSpanclass is a private inner class of FormattedStringExtensions so we have to deal with reflections.

Our Renderer is updating the Control.TextFormatted on initialization and when the FormattedText property changes. In the update method, we get all FontSpans and replace them with our CustomTypefaceSpan.

Renderer

public class FormattedLabelRenderer : LabelRenderer
{
    private static readonly Typeface Font = Typeface.CreateFromAsset(Forms.Context.Assets, "LobsterTwo-Regular.ttf");
    protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
    {
        base.OnElementChanged(e);
        Control.Typeface = Font;
        UpdateFormattedText();
    }

    private void UpdateFormattedText()
    {
        if (Element.FormattedText != null)
        {
            var extensionType = typeof(FormattedStringExtensions);
            var type = extensionType.GetNestedType("FontSpan", BindingFlags.NonPublic);
            var ss = new SpannableString(Control.TextFormatted);
            var spans = ss.GetSpans(0, ss.ToString().Length, Class.FromType(type));
            foreach (var span in spans)
            {
                var start = ss.GetSpanStart(span);
                var end = ss.GetSpanEnd(span);
                var flags = ss.GetSpanFlags(span);
                var font = (Font)type.GetProperty("Font").GetValue(span, null);
                ss.RemoveSpan(span);
                var newSpan = new CustomTypefaceSpan(Control, font);
                ss.SetSpan(newSpan, start, end, flags);
            }
            Control.TextFormatted = ss;
        }
    }

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

        if (e.PropertyName == Label.FormattedTextProperty.PropertyName)
        {
            UpdateFormattedText();
        }
    }
}

I'm not sure, why you introduced a new element type gbrLabel, but as long as you only wan't to change the renderer, you don't have to create a custom element. You can replace the renderer of the default element:

[assembly: ExportRenderer(typeof(Label), typeof(FormattedLabelRenderer))]

CustomTypefaceSpan

public class CustomTypefaceSpan : MetricAffectingSpan
{
    private readonly Typeface _typeFace;
    private readonly Typeface _typeFaceBold;
    private readonly Typeface _typeFaceItalic;
    private readonly Typeface _typeFaceBoldItalic;
    private readonly TextView _textView;
    private Font _font;

    public CustomTypefaceSpan(TextView textView, Font font)
    {
        _textView = textView;
        _font = font;
        // Note: we are ignoring _font.FontFamily (but thats easy to change)
        _typeFace = Typeface.CreateFromAsset(Forms.Context.Assets, "LobsterTwo-Regular.ttf");
        _typeFaceBold = Typeface.CreateFromAsset(Forms.Context.Assets, "LobsterTwo-Bold.ttf");
        _typeFaceItalic = Typeface.CreateFromAsset(Forms.Context.Assets, "LobsterTwo-Italic.ttf");
        _typeFaceBoldItalic = Typeface.CreateFromAsset(Forms.Context.Assets, "LobsterTwo-BoldItalic.ttf");
    }

    public override void UpdateDrawState(TextPaint paint)
    {
        ApplyCustomTypeFace(paint);
    }

    public override void UpdateMeasureState(TextPaint paint)
    {
        ApplyCustomTypeFace(paint);
    }

    private void ApplyCustomTypeFace(Paint paint)
    {
        var tf = _typeFace;

        if (_font.FontAttributes.HasFlag(FontAttributes.Bold) && _font.FontAttributes.HasFlag(FontAttributes.Italic))
        {
            tf = _typeFaceBoldItalic;
        }
        else if (_font.FontAttributes.HasFlag(FontAttributes.Bold))
        {
            tf = _typeFaceBold;
        }
        else if (_font.FontAttributes.HasFlag(FontAttributes.Italic))
        {
            tf = _typeFaceItalic;
        }

        paint.SetTypeface(tf);
        paint.TextSize = TypedValue.ApplyDimension(ComplexUnitType.Sp, _font.ToScaledPixel(), _textView.Resources.DisplayMetrics);
    }
}

Our Custom CustomTypefaceSpanis similar to the FontSpan of Xamarin.Forms, but is loading the custom fonts and can load different fonts for different FontAttributes.

The result is a nice colorful Text :) enter image description here

Upvotes: 24

Related Questions