Skip to content

Commit

Permalink
[Enhancement] Allow underline and strikethrough text decorations on l…
Browse files Browse the repository at this point in the history
…abels and spans (xamarin#2221)

* Fixes xamarin#1632

* Allow underline and strikethrought text decorations on labels and spans

* revert some files

* pr feedback adjustments

* remove docs

* rename interface

* reorder enum

* clean up whitespace

* adjust tizen renderer

* add gallery demo for setting both underline and strike

* allow multiple values of enum to be set in xaml/css

* use normal null check

* use nameof

* include paragraph style

* tab alignment

* rebase from upstream

* pass control to update method on UWP

* correct text decorations type converter

* reset run text instead of label text on UWP when spans are used

* add tests for text decoration converter
  • Loading branch information
alanag13 authored and PureWeen committed Jul 20, 2018
1 parent fd73303 commit 97d2f30
Show file tree
Hide file tree
Showing 19 changed files with 361 additions and 18 deletions.
10 changes: 10 additions & 0 deletions Xamarin.Forms.Controls/CoreGalleryPages/LabelCoreGalleryPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ protected override void Build (StackLayout stackLayout)
var namedSizeMediumItalicContainer = new ViewContainer<Label> (Test.Label.FontAttributesItalic, new Label { Text = "Medium Italic Font", Font = Font.SystemFontOfSize (NamedSize.Medium, FontAttributes.Italic) });
#pragma warning restore 618

#pragma warning disable 618
var namedSizeMediumUnderlineContainer = new ViewContainer<Label>(Test.Label.TextDecorationUnderline, new Label { Text = "Medium Underlined Font", Font = Font.SystemFontOfSize(NamedSize.Medium), TextDecorations = TextDecorations.Underline });
#pragma warning restore 618

#pragma warning disable 618
var namedSizeMediumStrikeContainer = new ViewContainer<Label>(Test.Label.TextDecorationStrike, new Label { Text = "Medium StrikeThrough Font", Font = Font.SystemFontOfSize(NamedSize.Medium), TextDecorations = TextDecorations.Strikethrough });
#pragma warning restore 618

#pragma warning disable 618
var namedSizeLargeContainer = new ViewContainer<Label> (Test.Label.FontNamedSizeLarge, new Label { Text = "Large Font", Font = Font.SystemFontOfSize (NamedSize.Large) });
#pragma warning restore 618
Expand Down Expand Up @@ -151,6 +159,8 @@ protected override void Build (StackLayout stackLayout)

Add (namedSizeMediumBoldContainer);
Add (namedSizeMediumItalicContainer);
Add (namedSizeMediumUnderlineContainer);
Add (namedSizeMediumStrikeContainer);
Add (namedSizeLargeContainer);
Add (namedSizeMediumContainer);
Add (namedSizeMicroContainer);
Expand Down
18 changes: 16 additions & 2 deletions Xamarin.Forms.Controls/GalleryPages/LabelGallery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public LabelGallery ()
var italicfont = new Label { Text = "Custom Italic Font" };
var boldfont = new Label { Text = "Custom Bold Font" };
var bolditalicfont = new Label { Text = "Custom Bold Italic Font" };
var toggleUnderline = new Label { Text = "Tap to toggle Underline", TextDecorations = TextDecorations.Underline };
toggleUnderline.GestureRecognizers.Add(new TapGestureRecognizer { Command = new Command(()=> { toggleUnderline.TextDecorations ^= TextDecorations.Underline; }) });
var toggleStrike = new Label { Text = "Tap to toggle StrikeThrough", TextDecorations = TextDecorations.Strikethrough };
toggleStrike.GestureRecognizers.Add(new TapGestureRecognizer { Command = new Command(() => { toggleStrike.TextDecorations ^= TextDecorations.Strikethrough; }) });
var toggleBoth = new Label { Text = "Tap to toggle both", TextDecorations = TextDecorations.Strikethrough | TextDecorations.Underline };
toggleBoth.GestureRecognizers.Add(new TapGestureRecognizer { Command = new Command(() => { toggleBoth.TextDecorations ^= TextDecorations.Strikethrough;
toggleBoth.TextDecorations ^= TextDecorations.Underline;
}) });
var huge = new Label {
Text = "This is the label that never ends, yes it go on and on my friend. " +
"Some people started catting it not knowing what it was, and they'll continue catting it forever just because...",
Expand All @@ -39,10 +47,13 @@ public LabelGallery ()
#pragma warning disable 618
new Span {Text="FormattedStrings ", TextColor=Color.Blue, BackgroundColor = Color.Yellow, Font = Font.BoldSystemFontOfSize (NamedSize.Large)},
#pragma warning restore 618
new Span {Text="are ", TextColor=Color.Red, BackgroundColor = Color.Gray},
new Span {Text="not pretty!", TextColor = Color.Green,},
}
} };
var underlineSpan = new Span { Text = "are ", TextColor = Color.Red, BackgroundColor = Color.Gray, TextDecorations = TextDecorations.Underline };
var strikeSpan = new Span { Text = "not pretty!", TextColor = Color.Green, TextDecorations = TextDecorations.Strikethrough };
formatted.FormattedText.Spans.Add(underlineSpan);
formatted.FormattedText.Spans.Add(strikeSpan);

var missingfont = new Label { Text = "Missing font: use default" };

#pragma warning disable 618
Expand Down Expand Up @@ -148,6 +159,9 @@ public LabelGallery ()
bold,
italic,
bolditalic,
toggleUnderline,
toggleStrike,
toggleBoth,
customFont,
italicfont,
boldfont,
Expand Down
28 changes: 28 additions & 0 deletions Xamarin.Forms.Core.UnitTests/TextDecorationUnitTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Globalization;

using NUnit.Framework;


namespace Xamarin.Forms.Core.UnitTests
{
[TestFixture]
public class TextDecorationUnitTests : BaseTestFixture
{
[Test]
public void TestTextDecorationConverter()
{
var converter = new TextDecorationConverter();
TextDecorations both = TextDecorations.Strikethrough;
both |= TextDecorations.Underline;
Assert.True(converter.CanConvertFrom(typeof(string)));
Assert.AreEqual(TextDecorations.Strikethrough, converter.ConvertFromInvariantString("strikethrough"));
Assert.AreEqual(TextDecorations.Underline, converter.ConvertFromInvariantString("underline"));
Assert.AreEqual(TextDecorations.Strikethrough, converter.ConvertFromInvariantString("line-through"));
Assert.AreEqual(TextDecorations.None, converter.ConvertFromInvariantString("none"));
Assert.AreEqual(both, converter.ConvertFromInvariantString("strikethrough underline"));
Assert.AreEqual(both, converter.ConvertFromInvariantString("underline strikethrough"));
Assert.AreEqual(both, converter.ConvertFromInvariantString("underline line-through"));
Assert.AreEqual(both, converter.ConvertFromInvariantString("line-through underline"));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
Expand Down Expand Up @@ -149,6 +149,7 @@
<Compile Include="TapGestureRecognizerTests.cs" />
<Compile Include="TemplatedItemsListTests.cs" />
<Compile Include="TextCellTests.cs" />
<Compile Include="TextDecorationUnitTests.cs" />
<Compile Include="ThicknessTests.cs" />
<Compile Include="TimePickerUnitTest.cs" />
<Compile Include="ToolbarItemTests.cs" />
Expand Down Expand Up @@ -228,4 +229,4 @@
<LogicalName>Images/crimson.jpg</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Project>
</Project>
49 changes: 49 additions & 0 deletions Xamarin.Forms.Core/DecorableTextElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Xamarin.Forms
{
[Flags]
[TypeConverter(typeof(TextDecorationConverter))]
public enum TextDecorations
{
None = 0,
Underline = 1 << 0,
Strikethrough = 1 << 1,
}
static class DecorableTextElement
{
public static readonly BindableProperty TextDecorationsProperty = BindableProperty.Create(nameof(IDecorableTextElement.TextDecorations), typeof(TextDecorations), typeof(IDecorableTextElement), TextDecorations.None);
}

[Xaml.TypeConversion(typeof(TextDecorations))]
public class TextDecorationConverter : TypeConverter
{
public override object ConvertFromInvariantString(string value)
{
TextDecorations result = TextDecorations.None;
if (value == null)
throw new InvalidOperationException(string.Format("Cannot convert \"{0}\" into {1}", value, typeof(TextDecorations)));

var valueArr = value.Split(',');

if (valueArr.Length <= 1)
valueArr = value.Split(' ');

foreach (var item in valueArr)
{
if (Enum.TryParse(item.Trim(), true, out TextDecorations textDecorations))
result |= textDecorations;
else if (item.Equals("line-through", StringComparison.OrdinalIgnoreCase))
result |= TextDecorations.Strikethrough;
else
throw new InvalidOperationException(string.Format("Cannot convert \"{0}\" into {1}", item, typeof(TextDecorations)));
}

return result;
}
}

}

11 changes: 11 additions & 0 deletions Xamarin.Forms.Core/IDecorableTextElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Xamarin.Forms
{
public interface IDecorableTextElement
{
TextDecorations TextDecorations { get; set; }
}
}
10 changes: 9 additions & 1 deletion Xamarin.Forms.Core/Label.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Xamarin.Forms
{
[ContentProperty("Text")]
[RenderWith(typeof(_LabelRenderer))]
public class Label : View, IFontElement, ITextElement, ITextAlignmentElement, ILineHeightElement, IElementConfiguration<Label>
public class Label : View, IFontElement, ITextElement, ITextAlignmentElement, ILineHeightElement, IElementConfiguration<Label>, IDecorableTextElement
{
public static readonly BindableProperty HorizontalTextAlignmentProperty = TextAlignmentElement.HorizontalTextAlignmentProperty;

Expand All @@ -36,6 +36,8 @@ public class Label : View, IFontElement, ITextElement, ITextAlignmentElement, IL

public static readonly BindableProperty FontAttributesProperty = FontElement.FontAttributesProperty;

public static readonly BindableProperty TextDecorationsProperty = DecorableTextElement.TextDecorationsProperty;

public static readonly BindableProperty FormattedTextProperty = BindableProperty.Create(nameof(FormattedText), typeof(FormattedString), typeof(Label), default(FormattedString),
propertyChanging: (bindable, oldvalue, newvalue) =>
{
Expand Down Expand Up @@ -155,6 +157,12 @@ public FontAttributes FontAttributes
set { SetValue(FontAttributesProperty, value); }
}

public TextDecorations TextDecorations
{
get { return (TextDecorations)GetValue(TextDecorationsProperty); }
set { SetValue(TextDecorationsProperty, value); }
}

public string FontFamily
{
get { return (string)GetValue(FontFamilyProperty); }
Expand Down
1 change: 1 addition & 0 deletions Xamarin.Forms.Core/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
[assembly: StyleProperty("padding-right", typeof(IPaddingElement), nameof(PaddingElement.PaddingRightProperty), PropertyOwnerType = typeof(PaddingElement))]
[assembly: StyleProperty("padding-bottom", typeof(IPaddingElement), nameof(PaddingElement.PaddingBottomProperty), PropertyOwnerType = typeof(PaddingElement))]
[assembly: StyleProperty("text-align", typeof(ITextAlignmentElement), nameof(TextAlignmentElement.HorizontalTextAlignmentProperty), Inherited = true)]
[assembly: StyleProperty("text-decoration", typeof(IDecorableTextElement), nameof(DecorableTextElement.TextDecorationsProperty))]
[assembly: StyleProperty("visibility", typeof(VisualElement), nameof(VisualElement.IsVisibleProperty), Inherited = true)]
[assembly: StyleProperty("width", typeof(VisualElement), nameof(VisualElement.WidthRequestProperty))]
[assembly: StyleProperty("line-height", typeof(ILineHeightElement), nameof(LineHeightElement.LineHeightProperty), Inherited = true)]
Expand Down
10 changes: 9 additions & 1 deletion Xamarin.Forms.Core/Span.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Xamarin.Forms
{
[ContentProperty("Text")]
public sealed class Span : GestureElement, IFontElement, ITextElement, ILineHeightElement
public sealed class Span : GestureElement, IFontElement, ITextElement, ILineHeightElement, IDecorableTextElement
{
internal readonly MergedStyle _mergedStyle;

Expand All @@ -16,6 +16,8 @@ public Span()
public static readonly BindableProperty StyleProperty = BindableProperty.Create(nameof(Style), typeof(Style), typeof(Span), default(Style),
propertyChanged: (bindable, oldvalue, newvalue) => ((Span)bindable)._mergedStyle.Style = (Style)newvalue, defaultBindingMode: BindingMode.OneTime);

public static readonly BindableProperty TextDecorationsProperty = DecorableTextElement.TextDecorationsProperty;

public Style Style
{
get { return (Style)GetValue(StyleProperty); }
Expand Down Expand Up @@ -95,6 +97,12 @@ public double FontSize
set { SetValue(FontElement.FontSizeProperty, value); }
}

public TextDecorations TextDecorations
{
get { return (TextDecorations)GetValue(TextDecorationsProperty); }
set { SetValue(TextDecorationsProperty, value); }
}

public double LineHeight {
get { return (double)GetValue(LineHeightElement.LineHeightProperty); }
set { SetValue(LineHeightElement.LineHeightProperty, value); }
Expand Down
2 changes: 2 additions & 0 deletions Xamarin.Forms.CustomAttributes/TestAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,8 @@ public enum Label
FormattedText,
FontAttibutesBold,
FontAttributesItalic,
TextDecorationUnderline,
TextDecorationStrike,
FontNamedSizeMicro,
FontNamedSizeSmall,
FontNamedSizeMedium,
Expand Down
18 changes: 18 additions & 0 deletions Xamarin.Forms.Platform.Android/FastRenderers/LabelRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,24 @@ void UpdateFont()
}
}

void UpdateTextDecorations()
{
if (!Element.IsSet(Label.TextDecorationsProperty))
return;

var textDecorations = Element.TextDecorations;

if ((textDecorations & TextDecorations.Strikethrough) == 0)
PaintFlags &= ~PaintFlags.StrikeThruText;
else
PaintFlags |= PaintFlags.StrikeThruText;

if ((textDecorations & TextDecorations.Underline) == 0)
PaintFlags &= ~PaintFlags.UnderlineText;
else
PaintFlags |= PaintFlags.UnderlineText;
}

void UpdateGravity()
{
Label label = Element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public static SpannableString ToAttributed(this FormattedString formattedString,
#pragma warning restore 618
else
spannable.SetSpan(new FontSpan(defaultFont, view), start, end, SpanTypes.InclusiveInclusive);
if (span.IsSet(Span.TextDecorationsProperty))
spannable.SetSpan(new TextDecorationSpan(span), start, end, SpanTypes.InclusiveInclusive);
}
return spannable;
}
Expand Down Expand Up @@ -95,6 +97,34 @@ void Apply(Paint paint)
paint.TextSize = TypedValue.ApplyDimension(ComplexUnitType.Sp, value, TextView.Resources.DisplayMetrics);
}
}

class TextDecorationSpan : MetricAffectingSpan
{
public TextDecorationSpan(Span span)
{
Span = span;
}

public Span Span { get; }

public override void UpdateDrawState(TextPaint tp)
{
Apply(tp);
}

public override void UpdateMeasureState(TextPaint p)
{
Apply(p);
}

void Apply(Paint paint)
{
var textDecorations = Span.TextDecorations;
paint.UnderlineText = (textDecorations & TextDecorations.Underline) != 0;
paint.StrikeThruText = (textDecorations & TextDecorations.Strikethrough) != 0;
}
}

class LineHeightSpan : Java.Lang.Object, ILineHeightSpan
{
private double _lineHeight;
Expand All @@ -114,7 +144,6 @@ public void ChooseHeight(Java.Lang.ICharSequence text, int start, int end, int s
fm.Ascent = (int) (_ascent * _lineHeight);
fm.Descent = (int) (_descent * _lineHeight);
}

}
}
}
22 changes: 21 additions & 1 deletion Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
if (e.OldElement.HorizontalTextAlignment != e.NewElement.HorizontalTextAlignment || e.OldElement.VerticalTextAlignment != e.NewElement.VerticalTextAlignment)
UpdateGravity();
}

UpdateTextDecorations();
_motionEventHelper.UpdateElement(e.NewElement);
}

Expand All @@ -134,6 +134,8 @@ protected override void OnElementPropertyChanged(object sender, PropertyChangedE
UpdateText();
else if (e.PropertyName == Label.LineBreakModeProperty.PropertyName)
UpdateLineBreakMode();
else if (e.PropertyName == Label.TextDecorationsProperty.PropertyName)
UpdateTextDecorations();
else if (e.PropertyName == Label.TextProperty.PropertyName || e.PropertyName == Label.FormattedTextProperty.PropertyName)
UpdateText();
else if (e.PropertyName == Label.LineHeightProperty.PropertyName)
Expand Down Expand Up @@ -174,6 +176,24 @@ void UpdateFont()
}
}

void UpdateTextDecorations()
{
if (!Element.IsSet(Label.TextDecorationsProperty))
return;

var textDecorations = Element.TextDecorations;

if ((textDecorations & TextDecorations.Strikethrough) == 0)
_view.PaintFlags &= ~PaintFlags.StrikeThruText;
else
_view.PaintFlags |= PaintFlags.StrikeThruText;

if ((textDecorations & TextDecorations.Underline) == 0)
_view.PaintFlags &= ~PaintFlags.UnderlineText;
else
_view.PaintFlags |= PaintFlags.UnderlineText;
}

void UpdateGravity()
{
Label label = Element;
Expand Down
Loading

0 comments on commit 97d2f30

Please sign in to comment.