formattedstring - spans tap gesture?

PippoCanePippoCane USMember ✭✭

Are there any ways to detect the tapping on a specific Span of a FormattedString? The only way I could possibly think right now to achieve a similar behavior is using several Labels stacked horizontally... But it's a bit of an overkill in terms of layouting and performance, am I right?

Posts

  • CraigDunnCraigDunn USXamarin Team Xamurai

    Is this a Xamarin.Forms question or related specifically to iOS?

  • PippoCanePippoCane USMember ✭✭

    Xamarin.Forms. Maybe I'm wrong but ideally tapping over a span it's something I could easily imagine of a cross-platform nature.

  • CraigDunnCraigDunn USXamarin Team Xamurai

    Sadly there's no way to detect a tap with this precision in Xamarin.Forms. You can add a GestureRecogizer to the Label that is displaying the FormattedString but there's no easy way to know where in the string the tap occurred.

    Although stacking labels is a pain (like you said), it might be the best solution for now.

  • PippoCanePippoCane USMember ✭✭

    Thanks for the honest answer.
    Do you know of any platform dependent implementation of something like an instagram comment, where basically I can tap over a username, an hashtag, a quoted user. Maybe you could put me in the right direction to implement this kind of view with a custom renderer, that would be really helpful. thanks again

  • PippoCanePippoCane USMember ✭✭

    Now I've got this error:
    Xamarin.Forms.Xaml.XamlParseException: No Property of name Text found

    Is the Text property of Span not bindable? Why is that?

    <Label>
                <Label.FormattedText>
                    <FormattedString>
                        <FormattedString.Spans>
                            <Span Text="of" />
                            <Span Text="{Binding Action}" />
                        </FormattedString.Spans>
                    </FormattedString>
                </Label.FormattedText>
        </Label>
    
  • CraigDunnCraigDunn USXamarin Team Xamurai

    No, doesn't look like Span nor FormattedText have any bindable properties. Not sure if there's any particular reason other than it didn't make it into the release schedule yet. UserVoice is the best place to highlight stuff like this that you want us to work on.

  • CharlesHoranCharlesHoran USMember ✭✭

    Not only can we not bind to a span object....which makes NO sense.

    We can't create a shim since Span is a **sealed **class!

    So Labels with formatted text are now effectively...well...ummm useless.

  • RichardGutierrezRichardGutierrez USMember
    edited November 2014

    PippoCane,
    as a work around, you can do this:
    <Label FormattedText="{Binding TipFormattedString}" />
    then in your viewmodel do this:
    private FormattedString _mTipFormattedString; public FormattedString TipFormattedString { get { return _mTipFormattedString; } set { _mTipFormattedString = value; OnPropertyChanged("TipFormattedString"); } }

    then set it in viewmodel like this:
    string mystring = GetDynamicValue(... TipFormattedString = new FormattedString(); TipFormattedString.Spans.Add(new Span { Text = mystring, ForegroundColor = Color.White });

  • TroyWillmotTroyWillmot NZMember, University

    Just encountered the inability to bind span.text myself. Very disappointing.

  • AliRFarahnakAliRFarahnak DKMember ✭✭

    I need a bindable SPAN as well to optimize my listview!

  • DarrelSchreyerDarrelSchreyer ZAMember
    edited April 2015

    Try creating a special ValueConverter ... it worked on iOS, Droid and WP for me:

    <Label FontSize="Micro" FormattedText="{Binding ValueToBind Converter={StaticResource formatConverter} }" />

    public class FormatLabelConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { var formattedString = new FormattedString(); formattedString.Spans.Add(new Span() { Text = "Prefix " }); formattedString.Spans.Add(new Span() { Text = value.ToString(), FontAttributes = FontAttributes.Bold }); formattedString.Spans.Add(new Span() { Text = " PostFix" }); return formattedString; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }
    In addition to the above you should pass in a parameter to your ValueConverter and then parse it out and apply a prefix and postfix parameter:

    <Label FontSize="Micro" FormattedText="{Binding Converter={StaticResource formatConverter}, ConverterParameter='Pre |{0}| post!'}" />

    Don't forget to add the ResourceDictionary to the Page resources:

    <ResourceDictionary> <local:FormatLabelLabelConverter x:Key="formatConverter" /> </ResourceDictionary>

  • Mark.9492Mark.9492 USMember ✭✭

    The lack of gestures on Span is indeed disappointing

  • AhmedAlejoAhmedAlejo USMember ✭✭
    edited March 2016

    @CraigDunn @PippoCane @DarrelSchreyer @RichardGutierrez

    I formulated something that actually works( for Databindable Spans):

    <Label BindingContext="{Binding Feedback}">
        <Label.FormattedText>
            <custom:FormattedString>
                <custom:Span
                    Text="{Binding Type, Converter={StaticResource EnumToLocalizedStringConverter} }"
                    FontAttributes="Bold"
                    BackgroundColor="{Binding Type, Converter={StaticResource FeedbackTypeToColorConverter}}" />
               <custom:Span
                    Text=" - W - "
                    FontAttributes="Bold"
                    FontSize="16" />
               <custom:Span Text="{x:Static resx:String.Important}" />
               <custom:Span
                    Text="!"
                    FontAttributes="Bold"
                    FontSize="{Binding Size}"  />
            </custom:FormattedString>
        </Label.FormattedText>
    </Label>
    

    One thing to know is that though a Span isn´t a BindableObject it does implement INotifyPropertyChanged
    and any changes on it reflects on the Label.

    So the basic idea is to create a bindable Span which passes changes to an inner Xamarin.Forms.Span
    which triggers a "Spans" property change on FormattedText which in turn cause the Label to be updated

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using Xamarin.Forms;
    
    namespace Xamarin.Forms.Custom
    {
        [ContentProperty("BindableSpans")]
        public class FormattedString : Xamarin.Forms.FormattedString
        {
            private ObservableCollection<Span> _bindableSpans = new ObservableCollection<Span>();
    
            public IList<Span> BindableSpans { get { return this._bindableSpans; } }
    
            public FormattedString()
            {
                this._bindableSpans.CollectionChanged += OnCollectionChanged;
            }
    
            private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                if (e.OldItems != null)
                {
                    foreach (var bindableSpan in e.OldItems.Cast<Span>())
                        base.Spans.Remove(bindableSpan);
                }
                if (e.NewItems != null)
                {
                    foreach (var bindableSpan in e.NewItems.Cast<Span>())
                        base.Spans.Add(bindableSpan);
                }
            }
    
            /// <param name="text">To be added.</param>
            /// <summary>Cast a string to a FromattedString that contains a single span with no attribute set.</summary>
            /// <returns>To be added.</returns>
            /// <remarks>To be added.</remarks>
            public static implicit operator FormattedString(string text)
            {
                return new FormattedString
                {
                    Spans = { new Xamarin.Forms.Span { Text = text } }
                };
            }
            /// <param name="formatted">To be added.</param>
            /// <summary>Cast the FormattedString to a string, stripping all the attributes.</summary>
            /// <returns>To be added.</returns>
            /// <remarks>To be added.</remarks>
            public static explicit operator string(FormattedString formatted)
            {
                return formatted.ToString();
            }
        }
        //
        // Summary:
        //     Represents a part of a FormattedString.
        //
        // Remarks:
        //     To be added.
        [ContentProperty("Text")]
        public sealed class Span : BindableObject
        {
            Xamarin.Forms.Span _innerSpan = new Xamarin.Forms.Span();
            public static readonly BindableProperty BackgroundColorProperty =
                BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(Span), Color.Default);
            //
            // Summary:
            //     Gets or sets the Color of the span background.
            //
            // Remarks:
            //     Not supported on WindowsPhone.        
            public Color BackgroundColor
            {
                get { return (Color)GetValue(BackgroundColorProperty); }
                set { SetValue(BackgroundColorProperty, value); }
            }
    
            public static readonly BindableProperty FontAttributesProperty =
                BindableProperty.Create(nameof(FontAttributes), typeof(FontAttributes), typeof(BindableSpan), FontAttributes.None);
            //
            // Summary:
            //     Gets a value that indicates whether the font for the span is bold, italic, or
            //     neither.
            //
            // Remarks:
            //     To be added.
            public FontAttributes FontAttributes { get; set; }
    
            public static readonly BindableProperty FontFamilyProperty =
                BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(Span), string.Empty);
            //
            // Summary:
            //     Gets the font family to which the font for the text in the span belongs.
            //
            // Remarks:
            //     To be added.
            public string FontFamily { get; set; }
    
            public static readonly BindableProperty FontSizeProperty =
                BindableProperty.Create(nameof(FontSize), typeof(double), typeof(Span), -1.0);
            //
            // Summary:
            //     Gets the size of the font for the text in the span.
            //
            // Remarks:
            //     To be added.
            [TypeConverter(typeof(FontSizeConverter))]
            public double FontSize { get; set; }
    
            public static readonly BindableProperty ForegroundColorProperty =
                BindableProperty.Create(nameof(ForegroundColor), typeof(Color), typeof(Span), Color.Default);
            //
            // Summary:
            //     Gets or sets the Color for the text in the span.
            //
            // Remarks:
            //     To be added.
            public Color ForegroundColor
            {
                get { return (Color)GetValue(ForegroundColorProperty); }
                set { SetValue(ForegroundColorProperty, value); }
            }
    
            public static readonly BindableProperty TextProperty =
                BindableProperty.Create(nameof(Text), typeof(string), typeof(Span), string.Empty);
            //
            // Summary:
            //     Gets or sets the text of the span.
            //
            // Remarks:
            //     To be added.
            public string Text
            {
                get { return (string)GetValue(TextProperty); }
                set { SetValue(TextProperty, value); }
            }
    
            public Xamarin.Forms.Span Span { get { return _innerSpan; } }
            /// <Summary>
            /// Updates the inner/original Spans properties
            /// </Summary>
            protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                base.OnPropertyChanged(propertyName);
                _innerSpan.BackgroundColor = this.BackgroundColor;
                _innerSpan.FontSize = this.FontSize;
                _innerSpan.FontAttributes = this.FontAttributes;
                _innerSpan.FontFamily = this.FontFamily;
                _innerSpan.ForegroundColor = this.ForegroundColor;
                _innerSpan.Text = this.Text;
            }
            public static implicit operator Xamarin.Forms.Span(Span bindableSpan)
            {
                return bindableSpan.Span;
            }
        }
    }
    
  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭✭

    @AhmedAlejo said:
    public static implicit operator Xamarin.Forms.Span(Span bindableSpan)
    {
    return bindableSpan.Span;
    }

    I love it!! Great trick!

  • FrancMoralesFrancMorales JPMember ✭✭

    @AhmedAlejo

    Thanks. A couple of things:

    BindableProperty.Create(nameof(FontAttributes), typeof(FontAttributes), typeof(BindableSpan), FontAttributes.None);

    should probably be:

    BindableProperty.Create(nameof(FontAttributes), typeof(FontAttributes), typeof(Span), FontAttributes.None);

    But the one that's throwing me off is this one...

    public Xamarin.Forms.Span Span { get { return _innerSpan; } }

    Isn't it?

    public Xamarin.Forms.Span InnerSpan { get { return _innerSpan; } }

  • AhmedAlejoAhmedAlejo USMember ✭✭
    edited June 2016

    @FrancMorales thanks for the interest, you´re completely right: This occured while doing (private)code clean up before posting here.

    to support inheriting BindingContext, FontAttributes, FontFamily e.t.c from it´s parent Label, i provide a functional and tested complete code:

    I would love to have a blog to post many of my awesome xamarin.forms only code drops(i accept invitation for free :wink: ).

    in Xaml:

    <core:Label
        FontSize="Small"
        TextColor="#333"
        HorizontalOptions="FillAndExpand"
        LineBreakMode="MiddleTruncation">
        <Label.FormattedText>
            <core:FormattedString>
                <core:Span Text="{Binding EditedByName}" FontAttributes="Bold"/>
                <core:Span Text="{Binding Comment, StringFormat=' {0}'}"/>
            </core:FormattedString>
        </Label.FormattedText>
    </core:Label>
    

    The code:

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using Xamarin.Forms;
    
    namespace Mobile.Forms.Core
    {
        public class Label : Xamarin.Forms.Label
        {
            protected override void OnBindingContextChanged()
            {
                base.OnBindingContextChanged();
                UpdateFormattedTextBindingContext();
            }
    
            protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                base.OnPropertyChanged(propertyName);
    
                if (propertyName == FormattedTextProperty.PropertyName)
                    UpdateFormattedTextBindingContext();
            }
    
            private void UpdateFormattedTextBindingContext()
            {
                var formattedText = this.FormattedText as FormattedString;
    
                if (formattedText == null)
                    return;
    
                foreach (var span in formattedText.BindableSpans)
                    span.BindingContext = this.BindingContext;
            }
    
        }
    
        [ContentProperty("BindableSpans")]
        public class FormattedString : Xamarin.Forms.FormattedString
        {
            private ObservableCollection<Span> _bindableSpans = new ObservableCollection<Span>();
    
            public IList<Span> BindableSpans { get { return this._bindableSpans; } }
    
            public FormattedString()
            {
                this._bindableSpans.CollectionChanged += OnCollectionChanged;
            }
    
            private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                if (e.OldItems != null)
                {
                    foreach (var bindableSpan in e.OldItems.Cast<Span>())
                        base.Spans.Remove(bindableSpan);
                }
                if (e.NewItems != null)
                {
                    foreach (var bindableSpan in e.NewItems.Cast<Span>())
                        base.Spans.Add(bindableSpan);
                }
            }
    
            /// <param name="text">To be added.</param>
            /// <summary>Cast a string to a FromattedString that contains a single span with no attribute set.</summary>
            /// <returns>To be added.</returns>
            /// <remarks>To be added.</remarks>
            public static implicit operator FormattedString(string text)
            {
                return new FormattedString
                {
                    BindableSpans = { new Span { Text = text ?? "" } }
                };
            }
            /// <param name="formatted">To be added.</param>
            /// <summary>Cast the FormattedString to a string, stripping all the attributes.</summary>
            /// <returns>To be added.</returns>
            /// <remarks>To be added.</remarks>
            public static explicit operator string(FormattedString formatted)
            {
                return formatted.ToString();
            }
        }
        //
        // Summary:
        //     Represents a part of a FormattedString.
        //
        // Remarks:
        //     To be added.
        [ContentProperty("Text")]
        public sealed class Span : BindableObject
        {
            Xamarin.Forms.Span _innerSpan;
    
            public Span()
                : this(new Xamarin.Forms.Span())
            { }
            public Span(Xamarin.Forms.Span span)
            {
                _innerSpan = span;
                //important for triggering property inheritance from parent Label
                this.BackgroundColor = this._innerSpan.BackgroundColor;
                this.FontSize = this._innerSpan.FontSize;
                this.FontAttributes = this._innerSpan.FontAttributes;
                this.FontFamily = this._innerSpan.FontFamily;
                this.ForegroundColor = this._innerSpan.ForegroundColor;
                this.Text = this._innerSpan.Text ?? "";
            }
            public static readonly BindableProperty BackgroundColorProperty =
                BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(Span), Color.Default);
            //
            // Summary:
            //     Gets or sets the Color of the span background.
            //
            // Remarks:
            //     Not supported on WindowsPhone.        
            public Color BackgroundColor
            {
                get { return (Color)GetValue(BackgroundColorProperty); }
                set { SetValue(BackgroundColorProperty, value); }
            }
    
            public static readonly BindableProperty FontAttributesProperty =
                BindableProperty.Create(nameof(FontAttributes), typeof(FontAttributes), typeof(Span), FontAttributes.None);
            //
            // Summary:
            //     Gets a value that indicates whether the font for the span is bold, italic, or
            //     neither.
            //
            // Remarks:
            //     To be added.
            public FontAttributes FontAttributes
            {
                get { return (FontAttributes)GetValue(FontAttributesProperty); }
                set { SetValue(FontAttributesProperty, value); }
            }
    
            public static readonly BindableProperty FontFamilyProperty =
                BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(Span), string.Empty);
            //
            // Summary:
            //     Gets the font family to which the font for the text in the span belongs.
            //
            // Remarks:
            //     To be added.
            public string FontFamily
            {
                get { return (string)GetValue(FontFamilyProperty); }
                set { SetValue(FontFamilyProperty, value); }
            }
    
            public static readonly BindableProperty FontSizeProperty =
                BindableProperty.Create(nameof(FontSize), typeof(double), typeof(Span), -1.0, BindingMode.OneWay, null, null, null, null, bindable => Device.GetNamedSize(NamedSize.Default, typeof(Label)));
            //
            // Summary:
            //     Gets the size of the font for the text in the span.
            //
            // Remarks:
            //     To be added.
            [TypeConverter(typeof(FontSizeConverter))]
            public double FontSize
            {
                get { return (double)GetValue(FontSizeProperty); }
                set { SetValue(FontSizeProperty, value); }
            }
    
            public static readonly BindableProperty ForegroundColorProperty =
                BindableProperty.Create(nameof(ForegroundColor), typeof(Color), typeof(Span), Color.Default);
            //
            // Summary:
            //     Gets or sets the Color for the text in the span.
            //
            // Remarks:
            //     To be added.
            public Color ForegroundColor
            {
                get { return (Color)GetValue(ForegroundColorProperty); }
                set { SetValue(ForegroundColorProperty, value); }
            }
    
            public static readonly BindableProperty TextProperty =
                BindableProperty.Create(nameof(Text), typeof(string), typeof(Span), string.Empty);
            //
            // Summary:
            //     Gets or sets the text of the span.
            //
            // Remarks:
            //     To be added.
            public string Text
            {
                get { return (string)GetValue(TextProperty); }
                set { SetValue(TextProperty, value); }
            }
    
            public Xamarin.Forms.Span InnerSpan { get { return _innerSpan; } }
    
            protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                base.OnPropertyChanged(propertyName);
                _innerSpan.BackgroundColor = this.BackgroundColor;
                _innerSpan.FontSize = this.FontSize;
                _innerSpan.FontAttributes = this.FontAttributes;
                _innerSpan.FontFamily = this.FontFamily;
                _innerSpan.ForegroundColor = this.ForegroundColor;
                _innerSpan.Text = this.Text ?? "";
            }
    
            protected override void OnBindingContextChanged()
            {
                base.OnBindingContextChanged();
            }
    
            public static implicit operator Xamarin.Forms.Span(Span bindableSpan)
            {
                return bindableSpan.InnerSpan;
            }
    
            public static implicit operator Span(Xamarin.Forms.Span span)
            {
                return new Span(span);
            }
        }
    }
    
  • FrancMoralesFrancMorales JPMember ✭✭

    @AhmedAlejo

    Thank you very, very much for the quick reply. I don't have a blog but I would love to see a full example.

Sign In or Register to comment.