Pages

Friday, 11 January 2019

Hyperlink in Xamarin.Forms Label

Showing part of a label as a clickable link, has been a long desired feature of Xamarin.Forms. With the release of 3.2.0, this is now possible. his is currently supported for Android, iOS, MacOS, UWP and WPF.

Tappable Span

Create a Label, with a FormattedText property and spans as shown below. For those not familar, labels, have a property called FormattedText. It allows you to split text into separate sections, so you can format each of them differently.

<Label HorizontalOptions="Center"
       VerticalOptions="CenterAndExpand">
    <Label.FormattedText>
        <FormattedString>
            <Span Text="Hello " />
            <Span Text="Click Me!"
                  TextColor="Blue"
                  TextDecorations="Underline">
                <Span.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding ClickCommand}"
                                          CommandParameter="https://xamarin.com" />
                </Span.GestureRecognizers>
            </Span>
            <Span Text=" Some more text." />
        </FormattedString>
    </Label.FormattedText>
</Label>
 
 

In your ViewModel, add the following Command.
public ICommand ClickCommand => new Command<string>((url) =>
{
    Device.OpenUri(new System.Uri(url));
});
This will result in a label, with Click Me! text highlighted. Tapping the text, will result in the browser opening and going to xamarin.com.




Note: 3.2.0-pre1 currently doesn’t have the underline capability, but does include everything else. Here I have used 3.3.0.685826-nightly to bring in the new TextDecorations property.

HtmlLabelConverter

The above solution works, but can become tedious or messy, when you want to deal with a large chunk of text. For this approach, we can use a converter, to automatically convert some HTML, into a Label, with Spans that are clickable.
Add the following HtmlLabelConverter. This supports multiple <a href=””></a> links in a body of text, but not any other HTML elements.
public class HtmlLabelConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var formatted = new FormattedString();

        foreach (var item in ProcessString((string)value))
            formatted.Spans.Add(CreateSpan(item));

        return formatted;
    }

    private Span CreateSpan(StringSection section)
    {
        var span = new Span()
        {
            Text = section.Text
        };

        if (!string.IsNullOrEmpty(section.Link))
        {
            span.GestureRecognizers.Add(new TapGestureRecognizer()
            {
                Command = _navigationCommand,
                CommandParameter = section.Link
            });
            span.TextColor = Color.Blue;
            // Underline coming soon from https://github.com/xamarin/Xamarin.Forms/pull/2221
            // Currently available in Nightly builds if you wanted to try, it does work :)
            // As of 2018-07-22. But not avail in 3.2.0-pre1.
            // span.TextDecorations = TextDecorations.Underline;
        }

        return span;
    }

    public IList<StringSection> ProcessString(string rawText)
    {
        const string spanPattern = @"(<a.*?>.*?</a>)";

        MatchCollection collection = Regex.Matches(rawText, spanPattern, RegexOptions.Singleline);

        var sections = new List<StringSection>();

        var lastIndex = 0;

        foreach (Match item in collection)
        {
            var foundText = item.Value;
            sections.Add(new StringSection() { Text = rawText.Substring(lastIndex, item.Index) });
            lastIndex += item.Index + item.Length;

            // Get HTML href 
            var html = new StringSection()
            {
                Link = Regex.Match(item.Value, "(?<=href=\\\")[\\S]+(?=\\\")").Value,
                Text = Regex.Replace(item.Value, "<.*?>", string.Empty)
            };

            sections.Add(html);
        }

        sections.Add(new StringSection() { Text = rawText.Substring(lastIndex) });

        return sections;
    }

    public class StringSection
    {
        public string Text { get; set; }
        public string Link { get; set; }
    }

     private ICommand _navigationCommand = new Command<string>((url) =>
     {
         Device.OpenUri(new Uri(url));
     });

     public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
     {
         throw new NotImplementedException();
     }
}
Then in your XAML, you would call it like so.
<ContentPage.Resources>
    <ResourceDictionary>
        <local:HtmlLabelConverter x:Key="HtmlLabelConverter" />
    </ResourceDictionary>
</ContentPage.Resources>


<Label FormattedText="{Binding Html, Converter={StaticResource HtmlLabelConverter}}" />
 
 

Framework Changes

Why a GestureRecognizer and not a Command? Initially it was requested we just place a Command property in the Span, and it will be executed once the Span was tapped. But this felt rather limiting and I didn’t want to block future enhancements.

After a discussion with some members of the XF team, gesture recognizers for a Span were approved, as scenario’s such as LongPress on a Span were considered valid scenarios.
This XF update, also brings in a new class called GestureElement. This is now inherited from Span.

Span -> GestureElement -> Element
 
 
This was done to ensure that the added Gesture Handling capabilities, were available for possible controls, such as an ImageMap. Because a Label has multiple sections, that can have their own Gesture handling.

Because of the need to handle all types of Gestures on a Span, we couldn’t use any platform specific span control to handle any clicks. But what I did do may sound a little crazy but it was necessary to provide the awesome feature of handling any type of gesture. I could calculate the region that the span text is contained in, inside the label. When the Gesture Handler is triggered on the main control, it detects if it is hitting a span. If that span handles the gesture, it actions it, otherwise it goes up to the main Label’s GestureRecognizer list to see if its handled there.

Span’s have priority on handling a gesture. As of this post, only TapGestureHandler is natively handled, but nothing is stopping it being expanded.

Due to this, XF I also added a neat new class added called Region. You can create a new Region.FromLines, that creates a region that encompasses a collection of boxes. It is like the Rectangle class but supports non-rectangular shapes. This might be enhanced in the future to support curves, it was designed with that in mind, but that is not currently developed.

Please reply on comment if you have any problem.


No comments:

Post a Comment