3
votes

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.

1
True, it's very similar... no one has given a good answer to that question, though. I've never asked a dupe before - can I move my question over there to freshen things up?Chet at C2IT
What are you missing from that answer? You can set the font for each span. Or is your goal, to set the font in the background for all Labels,so that you don't have to set it manually?Sven-Michael Stübe
That works fine for iOS, but you can't set the font in the spans (or anywhere in Xamarin.Forms for that matter) for Android. You need the custom renderer, but it only seems to work when you you use the .Text property, not the .FormattedText property, even if you want ALL of the text to be the same font, just different sizes. As a side note, I DO want a base font for all labels, but that's easy enough to accomplish. The issue here is entirely focused on the .FormattedText property and setting fonts there for the Android platform.Chet at C2IT
Can you please show how you set the FormattedText and what the formatted text is?Sven-Michael Stübe

1 Answers

22
votes

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