Xamarin.forms: [Enhancement] Allow for commandable span regions

Created on 26 Jan 2018  Â·  8Comments  Â·  Source: xamarin/Xamarin.Forms

Rationale

Embedding hyperlinks into a label is useful for many many reasons but the simplest to imagine is an article which contains hyperlinks in the text. It is impractical to expect the text be split up into multiple labels.

API

This will depend on the Bindable Span spec being implemented https://github.com/xamarin/Xamarin.Forms/issues/1340.

public class Span : Base {
  //<Snip/>

  public static readonly BindableProperty CommandProperty;

  public ICommand Command { get; set; } // BindableProperty backed
}

Expected Result

When a command is set and its CanExecute method evaluates to true, the span with a Command should become tappable and execute the command when tapped.

Implementation details

Android

Without having looked in any real depth it looks liek this can be achieved with a combination of attributed text and LinkMovementMethod.

iOS

A TapGestureRecognizer will need to be used in conjunction with checking which part of the label it actually hit. This will be annoying to implement to say the least.

UWP

https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/hyperlinks

Implications for CSS

Not relevant to CSS

Backward Compatibility

Shouldn't be any issues here.

Difficulty : Hard

The iOS and Android implementations of this are likely to be quite difficult. Worse we want to make sure that this behavior sits "on top" of any tap gesture recognizer added to the label as a whole. So if you tap a hyperlink, the link should open, if you tap other parts of the label, the gesture recognizer should fire.

F100 community-sprint enhancement âž•

Most helpful comment

Managed to get it working on all resizing and different lines on UWP at least. A bit of cleanup, then on to iOS and Android :)

gesturespan

All 8 comments

UITextView for iOS works well with this

UITextView for iOS works well with this

Yep. Unfortunately, XF uses UILabel :(

For the iOS renderer, this gist might be helpful: https://gist.github.com/hartez/819483f40916cf8835e0c9d0b2244b3e

It demonstrates how to create tappable links in a UILabel based on markdown links, but the technique should be applicable to arbitrary spans.

@davidortinau - I'll attempt this one now. Most likely going to regret this :)

@adamped but the rest of us appreciate your pain...

Ok, I have delved into this, and come across some design decisions that I need to run past you.

1) Having a Command on a Span didn't make sense when a Label has GestureRecognizers not commands. As such I was thinking of having GestureRecognizers on the Span instead. e.g.

<Label VerticalOptions="CenterAndExpand" 
            HorizontalOptions="CenterAndExpand">
    <Label.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding HowdyChickenCommand}" />
    </Label.GestureRecognizers>
    <Label.FormattedText>
        <FormattedString>
            <Span Text="{Binding SpanOne}" Style="{StaticResource Test}">
                <Span.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding SpanOneCommand}" />
                </Span.GestureRecognizers>
            </Span>
            <Span Text=" - " />
            <Span Text="{Binding SpanTwo}" />
        </FormattedString>
    </Label.FormattedText>
</Label>

2) I would then create a new SpanGestureRecognizer class. This would look something like this.

Please note, this is rough draft code. Obviously refactoring is going to take place :)

    public class SpanGestureRecognizer: GestureRecognizer
    {
        public IGestureRecognizer WrappedGestureRecognizer { get; set; }
        public double StartX { get; set; }
        public double EndX { get; set; }
        public SpanGestureRecognizer(IGestureRecognizer wrappedGestureRecognizer, double startX, double endX)
        {
            WrappedGestureRecognizer = wrappedGestureRecognizer;
            StartX = startX;
            EndX = endX;
        }
    }

What I do at the LabelRenderer level is take any GestureRecognizers and load them into the Label GestureRecognizers (or maybe I could have a separate internal property would be better here).

foreach (var recognizer in formatted.Spans[i].GestureRecognizers)
    Element.GestureRecognizers.Add(new SpanGestureRecognizer(recognizer, beforeWidth, textBlock.ActualWidth - beforeWidth));

The startX and endX will have to be refactored, as they need to update when the label changes size.

Then in the VisualElementTracker, when a Tap is clicked I can do this.

IEnumerable<SpanGestureRecognizer> spanGestures = view.GestureRecognizers.GetGesturesFor<SpanGestureRecognizer>();
            foreach (SpanGestureRecognizer recognizer in spanGestures)
            {
                var position = e.GetPosition(Control);
                if (position.X >= recognizer.StartX && position.X <= recognizer.EndX)
                {
                    (recognizer.WrappedGestureRecognizer as TapGestureRecognizer).SendTapped(view);
                    e.Handled = true;
                }
            }

            if (e.Handled)
                return;

I check to see if the click is in the right area for the span, if so I activate that and call it handled. If not it passes on to the other GestureRecognizers to be handled if any are there.

3) Unfortunately the Hyperlink option isn't available in UWP for adding to an Inline. Also looking at the other ones, it looks the most feasible to determine where in the label that was clicked, to see if it should be triggered.

This is what I did with UWP, and it does work.
commandspan

Does this approach have your tick of approval?

Managed to get it working on all resizing and different lines on UWP at least. A bit of cleanup, then on to iOS and Android :)

gesturespan

Was this page helpful?
0 / 5 - 0 ratings