Need to put HTML into a Label

I am using Xamarin.Forms and the Label view. I use the FormattedText (FormattedString class) to put formatted text in the Label. My goal here is to be able to convert HTML to a FormattedString.

In Xamarin.Android I have acces to Html.FromHtml() wich returns a ISpanned. Is there a equivalent for Xamarin.Forms ?

I cannot really use a WebView since my goal is to have multiple labels populated from HTML. If I use a WebView, I would be forced to do the whole page in HTML and not use Xamarin.Forms views/controls.

Thanks,
Max.

Best Answer

Answers

  • BuildCalcBuildCalc USMember ✭✭✭

    FYI, I've just added the ability to the Forms9Patch library to create labels and buttons where you can format the text via HTML. For example:

    new Forms9Patch.Label { HtmlText =  "plain <b><i>Bold+Italic</i></b> plain"}
    
    

    ... would give you a label where the text has been formatted bold italic in the middle of the string.

    Also, as an aside, it allows you to use custom fonts that are embedded resources in your PCL project without any platform specific work. And, you can use these fonts via the HTLM <font> tag or and HTML font-family attribute.

    Here are some screen shots from the demo app:

    HtmlLabelDroid1
    HtmlLabelApple3

  • BuildCalcBuildCalc USMember ✭✭✭

    FYI, I've just added the ability to the Forms9Patch library to create labels and buttons where you can format the text via HTML. For example:

    new Forms9Patch.Label { HtmlText =  "plain <b><i>Bold+Italic</i></b> plain"}
    
    

    ... would give you a label where the text has been formatted bold italic in the middle of the string.

    Also, as an aside, it allows you to use custom fonts that are embedded resources in your PCL project without any platform specific work. And, you can use these fonts via the HTLM <font> tag or and HTML font-family attribute.

    Here are some screen shots from the demo app:

    HtmlLabelDroid1
    HtmlLabelApple3

  • HGiritzerHGiritzer ATMember ✭✭

    @MaximeLefebvre and @Bobisback
    Thanks for sharing, works great!

  • @Bobisback said:

    @MaximeLefebvre said:
    Thank you ! Here is my implementation if someone has the same problem

    For completeness here is the iOS renderer for the HtmlLabel posted by MaximeLefebvre

    using System.ComponentModel;
    using Foundation;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.iOS;
    
    [assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
    
    namespace YourNamespace.iOS
    {
        public class HtmlLabelRenderer : LabelRenderer
        {
            protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
            {
                base.OnElementChanged(e);
    
                if (Control != null && Element != null && !string.IsNullOrWhiteSpace(Element.Text))
                {
                    var attr = new NSAttributedStringDocumentAttributes();
                    var nsError = new NSError();
                    attr.DocumentType = NSDocumentType.HTML;
    
                    var myHtmlData = NSData.FromString(Element.Text, NSStringEncoding.Unicode);
                    Control.Lines = 0;
                    Control.AttributedText = new NSAttributedString(myHtmlData, attr, ref nsError);
                }
            }
    
            protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                base.OnElementPropertyChanged(sender, e);
    
                if (e.PropertyName == Label.TextProperty.PropertyName)
                {
                    if (Control != null && Element != null && !string.IsNullOrWhiteSpace(Element.Text))
                    {
                        var attr = new NSAttributedStringDocumentAttributes();
                        var nsError = new NSError();
                        attr.DocumentType = NSDocumentType.HTML;
    
                        var myHtmlData = NSData.FromString(Element.Text, NSStringEncoding.Unicode);
                        Control.Lines = 0;
                        Control.AttributedText = new NSAttributedString(myHtmlData, attr, ref nsError);
                    }
                }
            }
        }
    }
    

    Hope this helps someone.

    Thanks,
    Bob

    This helped me fix a bug with my version of the HtmlRendering! ty

  • JeremyBPJeremyBP FRMember ✭✭

    @HGiritzer said:
    I noticed an undesired font change on iOS 9 (might be the same on other iOS versions).
    The font seemed to be more like Times New Roman than the normal "sans serif" (.SFUI ...) font.
    So I replaced the line

    var myHtmlData = NSData.FromString(Element.Text, NSStringEncoding.Unicode);
    

    with this block to work around:

    UIKit.UIFont font = Control.Font;
    string fontName = font.Name;
    System.nfloat fontSize = font.PointSize;
    string htmlContents = "<span style=\"font-family: '" + fontName + "'; font-size: " + fontSize + "\">" + Element.Text + "</span>";
    var myHtmlData = NSData.FromString(htmlContents, NSStringEncoding.Unicode);
    

    And the same with color:

    nfloat r, g, b, a;
    Control.TextColor.GetRGBA(out r, out g, out b, out a);
    var textColor = string.Format("#{0:X2}{1:X2}{2:X2}", (int)(r * 255.0), (int)(g * 255.0), (int)(b * 255.0));
    
    var font = Control.Font;
    var fontName = font.Name;
    var fontSize = font.PointSize;
    var htmlContents = "<span style=\"font-family: '" + fontName + "'; color: " + textColor + "; font-size: " + fontSize + "\">" + Element.Text + "</span>";
    var myHtmlData = NSData.FromString(htmlContents, NSStringEncoding.Unicode);
    
  • neblazneblaz DEMember

    In my StackLayout I do the following:

    HtmlLabel htmlbl = new HtmlLabel();
    htmlbl.Text = .... my HTML Text ....
    

    It seems that the text on the resulting Label is allways in the upper left corner. How to position it (Center, Start, End).

    htmlbl.HorizontalTextAlignment = TextAlignment.Center;
    htmlbl.VerticalTextAlignment = TextAlignment.Center;
    

    doesn't seem to have effect.

  • JoseOtavioJoseOtavio USMember

    Does anyone know how to create a label renderer for html on WinPhone?

  • Ben.2646Ben.2646 USMember ✭✭

    Is there a way to do that with a UWP renderer ?

  • OyebodeOridotaOyebodeOridota ZAMember ✭✭

    @Ben.2646 said:
    Is there a way to do that with a UWP renderer ?

    Did you find a solution to this?

  • Chris_HChris_H NZMember ✭✭

    Does anyone know how to make the links clickable on iOS?

    On Android I did it just by adding this code to the HtmlLabelRenderer after the call to SetText:

     if(Control != null)
         Control.MovementMethod = new Android.Text.Method.LinkMovementMethod();
    
  • Chris_HChris_H NZMember ✭✭

    I've got clickable links working on iOS using the following code, adapted from Daniel Fredriksen's nuget package and the answers to this StackOverflow question:
    https://stackoverflow.com/questions/1256887/create-tap-able-links-in-the-nsattributedstring-of-a-uilabel

    This code is added to HtmlLabelRenderer for iOS:

        class LinkData
        {
            public LinkData(NSRange range, string URL) { this.range = range; this.URL = URL; }
            public NSRange range;
            public string URL;
        }
    
        private void CreateAttributedString(Label element, UILabel control)
        {
            var attr = new NSAttributedStringDocumentAttributes();
            var nsError = new NSError();
            attr.DocumentType = NSDocumentType.HTML;
    
            // create
            UIKit.UIFont font = control.Font;
            string fontName = font.Name;
            System.nfloat fontSize = font.PointSize;
            string htmlContents = "<span style=\"font-family: '" + fontName + "'; font-size: " + fontSize + "\">" + element.Text + "</span>";
            var myHtmlData = NSData.FromString(htmlContents, NSStringEncoding.Unicode);
            control.Lines = 0;
            NSMutableAttributedString mutable = new NSMutableAttributedString(new NSAttributedString(myHtmlData, attr, ref nsError));
            List<LinkData> links = new List<LinkData>();
            control.AttributedText = mutable;
    
            // make a list of all links:
            mutable.EnumerateAttributes(new NSRange(0, mutable.Length), NSAttributedStringEnumeration.LongestEffectiveRangeNotRequired, (NSDictionary attrs, NSRange range, ref Boolean stop) =>
            {
                foreach (var a in attrs) // should use attrs.ContainsKey(something) instead
                {
                    if (a.Key.ToString() == "NSLink")
                    {
                        links.Add(new LinkData(range, a.Value.ToString()));
                        return;
                    }
                }
            });
    
            // Set up a Gesture recognizer:
            if (links.Count > 0)
            {
                control.UserInteractionEnabled = true;
                UITapGestureRecognizer tapGesture = new UITapGestureRecognizer((tap) =>
                {
                    string URL = DetectTappedURL(tap, (UILabel)tap.View, links);
                    if (URL != null)
                    {
                        // open the link:
                        Device.OpenUri(new Uri(URL));
                    }
                });
                control.AddGestureRecognizer(tapGesture);
            }
        }               
    
        string DetectTappedURL(UITapGestureRecognizer tap, UILabel label, List<LinkData> linkList)
        {
            // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
            var layoutManager = new NSLayoutManager();
            var textContainer = new NSTextContainer();
            var textStorage = new NSTextStorage();
            textStorage.SetString(label.AttributedText);
    
            // Configure layoutManager and textStorage
            layoutManager.AddTextContainer(textContainer);
            textStorage.AddLayoutManager(layoutManager);
    
            // Configure textContainer
            textContainer.LineFragmentPadding = 0;
            textContainer.LineBreakMode = label.LineBreakMode;
            textContainer.MaximumNumberOfLines = (nuint)label.Lines;
            var labelSize = label.Bounds.Size;
            textContainer.Size = labelSize;
    
            // Find the tapped character location and compare it to the specified range
            var locationOfTouchInLabel = tap.LocationInView(label);
            var textBoundingBox = layoutManager.GetUsedRectForTextContainer(textContainer);
            var textContainerOffset = new CGPoint((labelSize.Width - textBoundingBox.Size.Width) * 0.0 - textBoundingBox.Location.X,
                (labelSize.Height - textBoundingBox.Size.Height) * 0.0 - textBoundingBox.Location.Y);
            var locationOfTouchInTextContainer = new CGPoint(locationOfTouchInLabel.X - textContainerOffset.X,
                locationOfTouchInLabel.Y - textContainerOffset.Y);
            nfloat partialFraction = 0;
            var indexOfCharacter = (nint)layoutManager.CharacterIndexForPoint(locationOfTouchInTextContainer, textContainer, ref partialFraction);
    
            foreach(var link in linkList)
            {
                // Xamarin version of NSLocationInRange?
                if((indexOfCharacter >= link.range.Location) && (indexOfCharacter < link.range.Location + link.range.Length))
                {
                    return link.URL;
                }
            }
            return null;
        }
    

    CreateAttributedString is called from onElementChanged and onElementPropertyChanged, replacing the code that sets Control.AttributedText, passing in Element and Control.

  • TimothyRisiXMTimothyRisiXM USXamarin Team Xamurai

    @Bobisback said:

    @MaximeLefebvre said:
    Thank you ! Here is my implementation if someone has the same problem

    For completeness here is the iOS renderer for the HtmlLabel posted by MaximeLefebvre

    using System.ComponentModel;
    using Foundation;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.iOS;
    
    [assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
    
    namespace YourNamespace.iOS
    {
        public class HtmlLabelRenderer : LabelRenderer
        {
            protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
            {
                base.OnElementChanged(e);
    
                if (Control != null && Element != null && !string.IsNullOrWhiteSpace(Element.Text))
                {
                    var attr = new NSAttributedStringDocumentAttributes();
                    var nsError = new NSError();
                    attr.DocumentType = NSDocumentType.HTML;
    
                    var myHtmlData = NSData.FromString(Element.Text, NSStringEncoding.Unicode);
                    Control.Lines = 0;
                    Control.AttributedText = new NSAttributedString(myHtmlData, attr, ref nsError);
                }
            }
    
            protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                base.OnElementPropertyChanged(sender, e);
    
                if (e.PropertyName == Label.TextProperty.PropertyName)
                {
                    if (Control != null && Element != null && !string.IsNullOrWhiteSpace(Element.Text))
                    {
                        var attr = new NSAttributedStringDocumentAttributes();
                        var nsError = new NSError();
                        attr.DocumentType = NSDocumentType.HTML;
    
                        var myHtmlData = NSData.FromString(Element.Text, NSStringEncoding.Unicode);
                        Control.Lines = 0;
                        Control.AttributedText = new NSAttributedString(myHtmlData, attr, ref nsError);
                    }
                }
            }
        }
    }
    

    Hope this helps someone.

    Thanks,
    Bob

    @JeremyBP said:

    @HGiritzer said:
    I noticed an undesired font change on iOS 9 (might be the same on other iOS versions).
    The font seemed to be more like Times New Roman than the normal "sans serif" (.SFUI ...) font.
    So I replaced the line

    var myHtmlData = NSData.FromString(Element.Text, NSStringEncoding.Unicode);
    

    with this block to work around:

    UIKit.UIFont font = Control.Font;
    string fontName = font.Name;
    System.nfloat fontSize = font.PointSize;
    string htmlContents = "<span style=\"font-family: '" + fontName + "'; font-size: " + fontSize + "\">" + Element.Text + "</span>";
    var myHtmlData = NSData.FromString(htmlContents, NSStringEncoding.Unicode);
    

    And the same with color:

    nfloat r, g, b, a;
    Control.TextColor.GetRGBA(out r, out g, out b, out a);
    var textColor = string.Format("#{0:X2}{1:X2}{2:X2}", (int)(r * 255.0), (int)(g * 255.0), (int)(b * 255.0));
    
    var font = Control.Font;
    var fontName = font.Name;
    var fontSize = font.PointSize;
    var htmlContents = "<span style=\"font-family: '" + fontName + "'; color: " + textColor + "; font-size: " + fontSize + "\">" + Element.Text + "</span>";
    var myHtmlData = NSData.FromString(htmlContents, NSStringEncoding.Unicode);
    

    I'm using these to show strings with custom fonts mixed in on iOS. A couple of the views where I'm using it have it included in ListViews. If I search a ListView using the SearchBar (with binding for the text of the searchbox so it updates the list every time the text changes), I start getting crashes if you type too quickly. Sometimes it gives me https://gist.github.com/timrisi/a3fe07381820890efeb74f6116836c6f in the application output while debugging, the rest of the time I just get:

    System.ArgumentOutOfRangeException has been thrown
    Specified argument was out of the range of valid values.
    Parameter name: index
    

    with a stack trace of https://gist.github.com/timrisi/c376d59fefb70d53fa7bef0056af6856.

    Has anyone else run into this or been able to fix it? If I remove everything in the if block and just do Control.AttributedText = new NSAttributedString (Element.Text); I can't get it to reproduce the crash, so it seems to be related to setting as an HTML string.

    I can't just invoke on main thread because then the formatting gets all kinds of thrown off

  • Joel.7378Joel.7378 USMember ✭✭

    @Ben.2646 said:
    Is there a way to do that with a UWP renderer ?

    I'm also wondering if anyone has found a way to do this in UWP?

  • MJ_AhmedMJ_Ahmed BHMember ✭✭

    @GuiWaltricke said:
    Make it with custom renderer:

    IOS:

    var attr = new NSAttributedStringDocumentAttributes();
    var nsError = new NSError();
    attr.DocumentType = NSDocumentType.HTML;
    
    var myHtmlData = NSData.FromString(label.Text, NSStringEncoding.Unicode);
    this.Control.AttributedText = new NSAttributedString(myHtmlData, attr, ref nsError);
    

    Android:

    Html.FromHtml(label.Text).ToString().Trim();
    

    Can u show a quick example cuz Im confused on where shall I put the code?

  • ClayBrinleeClayBrinlee USMember ✭✭

    @Joel.7378 @Ben.2646 @OyebodeOridota
    For UWP I did the following.

    Note: You'll have to read all these instructions fully!

    Because I already had HTML label working with the above approaches for Android/iOS I didn't want to scrap it and install a plugin. So instead I grabbed 3 files from this repository

    Behavior.cs --> from here
    HtmlTextBehavior.cs --> from here
    LabelRendererHelper.cs --> from here

    I put all of these files into my UWP project so I didn't have to touch my existing shared project.

    Install Microsoft.Xaml.Behaviors.Uwp.Managed v2.0 into your UWP project

    Create a custom HtmlLabelRenderer.cs
    Here is the code:

    using Microsoft.Xaml.Interactivity;
    using Xamarin.Forms;
    
    [assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
    namespace Target.UWP.Renderers
    {
        public class HtmlLabelRenderer : LabelRenderer
        {
            protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
            {
                base.OnElementChanged(e);
    
                if (Control != null)
                {
                    UpdateTextOnControl();
                }
            }
            void UpdateTextOnControl()
            {
                if (Control == null || Element == null) return;
    
                if (Element is HtmlLabel formsElement)
                {
                    var helper = new LabelRendererHelper(Element, formsElement.Html);
                    Control.Text = helper.ToString();
    
                    var behavior = new HtmlTextBehavior((HtmlLabel)Element);
                    var behaviors = Interaction.GetBehaviors(Control);
                    behaviors.Clear();
                    behaviors.Add(behavior);
                }
            }
        }
    

    Comment out a couple of the lines from the HtmlTextBehavior.cs file (untested):

    link.Click += (Hyperlink sender, HyperlinkClickEventArgs e) =>
                    {
                        sender.NavigateUri = null;
                        if (href == null) return;
    
                        var args = new Xamarin.Forms.WebNavigatingEventArgs(Xamarin.Forms.WebNavigationEvent.NewPage, new Xamarin.Forms.UrlWebViewSource { Url = href.Value }, href.Value);
                        //label.SendNavigating(args);
    
                        if (args.Cancel)
                            return;
    
                        Xamarin.Forms.Device.OpenUri(new Uri(href.Value));
                        //label.SendNavigated(args);
                    };
    

    Then edit LabelRendererHelper.cs:

    if (string.IsNullOrWhiteSpace(_text))
                    return string.Empty;
    
  • Hikari91Hikari91 ITMember ✭✭

    @MaximeLefebvre said:
    Thank you ! Here is my implementation if someone has the same problem :

    I created a new view extending the Label

    using Xamarin.Forms;
    
    namespace YourNamespace
    {
      public class HtmlLabel : Label
      {
      }
    }
    

    And I implemented the renderer in my Android project

    using System.ComponentModel;
    using Android.Text;
    using Android.Widget;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.Android;
    
    [assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
    
    namespace YourNamespace.Droid
    {
      class HtmlLabelRenderer : LabelRenderer
      {
          protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
          {
              base.OnElementChanged(e);
              
              Control?.SetText(Html.FromHtml(Element.Text), TextView.BufferType.Spannable);
          }
    
          protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
          {
              base.OnElementPropertyChanged(sender, e);
    
              if (e.PropertyName == Label.TextProperty.PropertyName)
              {
                  Control?.SetText(Html.FromHtml(Element.Text), TextView.BufferType.Spannable);
              }
          }
      }
    }
    

    I did not yet implement the iOS renderer but I think it is straightforward with Guillerme's code snippet.

    Edit : If you are not familiar with C#6 you can change "Control?.SetText..." to "if (Control != null) Control.SetText..."

    Have a nice day,
    Max.

    Hi,
    I have used this code for Android 7, now in Android 8 the Text property is always empty.

    Does anyone have this issue too?

    Thanks :smiley:

Sign In or Register to comment.