Make Label display HTML from a string (#4527)

* Use UpdateText

* Added missing helper method and UI test

* Added missing helper for UWP

* Added csproj entry for helper

* Resolved rebase conflicts

* Update LabelRenderer.cs

* Update LabelRenderer.cs

* Update LabelRenderer.cs

* iOS Merge error fix

* Feedback

* - uwp fixes

* - android fix empty text

* - ios fix null and setting text when texttype starts as html

* - set _perfectSizeValid = false; after changed AttributedText

Setting the AttributedText causes GetDesiredSize to get called which sets _perfectSizeValid to true but at this point this frame still hasn't adjusted to any size change from *LayoutSubViews*. This resets _perfectSizeValid so after the AttributedText set the desiredsize can get pulled again

* Renamed PlainText to Text

* Fixed initial no HTML styling
This commit is contained in:
Gerald Versluis 2019-08-28 13:25:02 +02:00 коммит произвёл GitHub
Родитель c0a681e852
Коммит 938a840e68
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 415 добавлений и 19 удалений

Просмотреть файл

@ -0,0 +1,88 @@
using System;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;
#if UITEST
using Xamarin.Forms.Core.UITests;
using Xamarin.UITest;
using NUnit.Framework;
using System.Linq;
#endif
namespace Xamarin.Forms.Controls.Issues
{
#if UITEST
[Category(UITestCategories.Label)]
#endif
[Preserve(AllMembers = true)]
[Issue(IssueTracker.None, 0, "Implementation of Label TextType", PlatformAffected.All)]
public class LabelTextType : TestContentPage
{
protected override void Init()
{
var label = new Label
{
AutomationId = "TextTypeLabel",
Text = "<h1>Hello World!</h1>"
};
var button = new Button
{
AutomationId = "ToggleTextTypeButton",
Text = "Toggle HTML/Plain"
};
button.Clicked += (s, a) =>
{
label.TextType = label.TextType == TextType.Html ? TextType.Text : TextType.Html;
};
Label htmlLabel = new Label() { TextType = TextType.Html };
Label normalLabel = new Label();
Label nullLabel = new Label() { TextType = TextType.Html };
Button toggle = new Button()
{
Text = "Toggle some more things",
Command = new Command(() =>
{
htmlLabel.Text = $"<b>{DateTime.UtcNow}</b>";
normalLabel.Text = $"<b>{DateTime.UtcNow}</b>";
if (String.IsNullOrWhiteSpace(nullLabel.Text))
nullLabel.Text = "hi there";
else
nullLabel.Text = null;
})
};
var stacklayout = new StackLayout();
stacklayout.Children.Add(label);
stacklayout.Children.Add(button);
stacklayout.Children.Add(htmlLabel);
stacklayout.Children.Add(normalLabel);
stacklayout.Children.Add(nullLabel);
stacklayout.Children.Add(toggle);
Content = stacklayout;
}
#if UITEST
[Test]
public void LabelToggleHtmlAndPlainTextTest()
{
RunningApp.WaitForElement ("TextTypeLabel");
RunningApp.Screenshot ("I see plain text");
Assert.IsTrue(RunningApp.Query("TextTypeLabel").FirstOrDefault()?.Text == "<h1>Hello World!</h1>");
RunningApp.Tap("ToggleTextTypeButton");
RunningApp.Screenshot ("I see HTML text");
Assert.IsFalse(RunningApp.Query("TextTypeLabel").FirstOrDefault()?.Text.Contains("<h1>") ?? true);
}
#endif
}
}

Просмотреть файл

@ -1029,6 +1029,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Issue6738.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GitHub6926.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue5503.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LabelTextType.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Bugzilla22229.xaml">

Просмотреть файл

@ -237,6 +237,40 @@ namespace Xamarin.Forms.Controls
}
);
var htmlLabelContainer = new ViewContainer<Label>(Test.Label.TextType,
new Label
{
Text = "<h1>Hello world!</h1>",
TextType = TextType.Html
});
var htmlLabelMultipleLinesContainer = new ViewContainer<Label>(Test.Label.TextType,
new Label
{
Text = "<h1>Hello world!</h1><p>Lorem <strong>ipsum</strong> bla di bla <i>blabla</i> blablabl&nbsp;ablabla & blablablablabl ablabl ablablabl ablablabla blablablablablablab lablablabla blablab lablablabla blablabl ablablablab lablabla blab lablablabla blablab lablabla blablablablab lablabla blablab lablablabl ablablabla blablablablablabla blablabla</p>",
TextType = TextType.Html,
MaxLines = 3
});
var toggleLabel = new Label
{
TextType = TextType.Html,
Text = "<h1 style=\"color: red;\">Hello world!</h1><p>Lorem <strong>ipsum</strong></p>"
};
var gestureRecognizer = new TapGestureRecognizer();
gestureRecognizer.Tapped += (s, a) =>
{
toggleLabel.TextType = toggleLabel.TextType == TextType.Html ? TextType.Text : TextType.Html;
};
toggleLabel.GestureRecognizers.Add(gestureRecognizer);
var toggleHtmlPlainTextLabelContainer = new ViewContainer<Label>(Test.Label.TextType,
toggleLabel);
Add (namedSizeMediumBoldContainer);
Add (namedSizeMediumItalicContainer);
Add (namedSizeMediumUnderlineContainer);
@ -272,6 +306,9 @@ namespace Xamarin.Forms.Controls
Add (maxlinesTailTruncContainer);
Add (maxlinesWordWrapContainer);
Add(paddingContainer);
Add (htmlLabelContainer);
Add (htmlLabelMultipleLinesContainer);
Add (toggleHtmlPlainTextLabelContainer);
}
}
}

Просмотреть файл

@ -90,6 +90,9 @@ namespace Xamarin.Forms
public static readonly BindableProperty PaddingProperty = PaddingElement.PaddingProperty;
public static readonly BindableProperty TextTypeProperty = BindableProperty.Create(nameof(TextType), typeof(TextType), typeof(Label), TextType.Text,
propertyChanged: (bindable, oldvalue, newvalue) => ((Label)bindable).InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged));
readonly Lazy<PlatformConfigurationRegistry<Label>> _platformConfigurationRegistry;
public Label()
@ -213,6 +216,12 @@ namespace Xamarin.Forms
set { SetValue(PaddingProperty, value); }
}
public TextType TextType
{
get => (TextType)GetValue(TextTypeProperty);
set => SetValue(TextTypeProperty, value);
}
double IFontElement.FontSizeDefaultValueCreator() =>
Device.GetNamedSize(NamedSize.Default, (Label)this);

Просмотреть файл

@ -0,0 +1,8 @@
namespace Xamarin.Forms
{
public enum TextType
{
Text,
Html
}
}

Просмотреть файл

@ -635,7 +635,8 @@ namespace Xamarin.Forms.CustomAttributes
VerticalTextAlignmentStart,
VerticalTextAlignmentCenter,
VerticalTextAlignmentEnd,
MaxLines
MaxLines,
TextType
}
public enum MasterDetailPage

Просмотреть файл

@ -251,6 +251,7 @@ namespace Xamarin.Forms.Platform.Android.FastRenderers
UpdateGravity();
if (e.OldElement?.MaxLines != e.NewElement.MaxLines)
UpdateMaxLines();
UpdatePadding();
ElevationHelper.SetElevation(this, e.NewElement);
@ -263,7 +264,8 @@ namespace Xamarin.Forms.Platform.Android.FastRenderers
if (e.PropertyName == Label.HorizontalTextAlignmentProperty.PropertyName || e.PropertyName == Label.VerticalTextAlignmentProperty.PropertyName)
UpdateGravity();
else if (e.PropertyName == Label.TextColorProperty.PropertyName)
else if (e.PropertyName == Label.TextColorProperty.PropertyName ||
e.PropertyName == Label.TextTypeProperty.PropertyName)
UpdateText();
else if (e.PropertyName == Label.FontProperty.PropertyName)
UpdateText();
@ -380,7 +382,23 @@ namespace Xamarin.Forms.Platform.Android.FastRenderers
SetTextColor(_labelTextColorDefault);
_lastUpdateColor = Color.Default;
}
switch (Element.TextType)
{
case TextType.Html:
if (Forms.IsNougatOrNewer)
Control.SetText(Html.FromHtml(Element.Text ?? string.Empty, FromHtmlOptions.ModeCompact), BufferType.Spannable);
else
#pragma warning disable CS0618 // Type or member is obsolete
Control.SetText(Html.FromHtml(Element.Text ?? string.Empty), BufferType.Spannable);
#pragma warning restore CS0618 // Type or member is obsolete
break;
default:
Text = Element.Text;
break;
}
UpdateColor();
UpdateFont();

Просмотреть файл

@ -56,6 +56,7 @@ namespace Xamarin.Forms
static BuildVersionCodes? s_sdkInt;
static bool? s_isLollipopOrNewer;
static bool? s_isMarshmallowOrNewer;
static bool? s_isNougatOrNewer;
[Obsolete("Context is obsolete as of version 2.5. Please use a local context instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
@ -98,6 +99,16 @@ namespace Xamarin.Forms
}
}
internal static bool IsNougatOrNewer
{
get
{
if (!s_isNougatOrNewer.HasValue)
s_isNougatOrNewer = (int)Build.VERSION.SdkInt >= 24;
return s_isNougatOrNewer.Value;
}
}
public static float GetFontSizeNormal(Context context)
{
float size = 50;

Просмотреть файл

@ -135,7 +135,6 @@ namespace Xamarin.Forms.Platform.Android
UpdateMaxLines();
if (e.OldElement.CharacterSpacing != e.NewElement.CharacterSpacing)
UpdateCharacterSpacing();
}
UpdateTextDecorations();
UpdatePadding();
@ -158,13 +157,13 @@ namespace Xamarin.Forms.Platform.Android
UpdateLineBreakMode();
else if (e.PropertyName == Label.TextDecorationsProperty.PropertyName)
UpdateTextDecorations();
else if (e.PropertyName == Label.TextProperty.PropertyName || e.PropertyName == Label.FormattedTextProperty.PropertyName)
else if (e.IsOneOf(Label.TextProperty, Label.FormattedTextProperty, Label.TextTypeProperty))
UpdateText();
else if (e.PropertyName == Label.LineHeightProperty.PropertyName)
UpdateLineHeight();
else if (e.PropertyName == Label.MaxLinesProperty.PropertyName)
UpdateMaxLines();
else if (e.PropertyName == ImageButton.PaddingProperty.PropertyName)
else if (e.PropertyName == Label.PaddingProperty.PropertyName)
UpdatePadding();
}
@ -242,7 +241,6 @@ namespace Xamarin.Forms.Platform.Android
}
}
void UpdateLineHeight()
{
_lastSizeRequest = null;
@ -274,7 +272,25 @@ namespace Xamarin.Forms.Platform.Android
_view.SetTextColor(_labelTextColorDefault);
_lastUpdateColor = Color.Default;
}
switch (Element.TextType)
{
case TextType.Html:
if (Forms.IsNougatOrNewer)
Control.SetText(Html.FromHtml(Element.Text ?? string.Empty, FromHtmlOptions.ModeCompact), TextView.BufferType.Spannable);
else
#pragma warning disable CS0618 // Type or member is obsolete
Control.SetText(Html.FromHtml(Element.Text ?? string.Empty), TextView.BufferType.Spannable);
#pragma warning restore CS0618 // Type or member is obsolete
break;
default:
_view.Text = Element.Text;
break;
}
UpdateColor();
UpdateFont();

Просмотреть файл

@ -0,0 +1,112 @@
using System.Xml.Linq;
using Windows.UI.Xaml.Documents;
namespace Xamarin.Forms.Platform.UAP
{
internal static class LabelHtmlHelper
{
// All the supported HTML tags
internal const string ElementB = "B";
internal const string ElementBr = "BR";
internal const string ElementEm = "EM";
internal const string ElementI = "I";
internal const string ElementP = "P";
internal const string ElementStrong = "STRONG";
internal const string ElementU = "U";
internal const string ElementUl = "UL";
internal const string ElementLi = "LI";
internal const string ElementDiv = "DIV";
public static void ParseText(XElement element, InlineCollection inlines, Label label)
{
if (element == null)
return;
var currentInlines = inlines;
var elementName = element.Name.ToString().ToUpper();
switch (elementName)
{
case ElementB:
case ElementStrong:
var bold = new Bold();
inlines.Add(bold);
currentInlines = bold.Inlines;
break;
case ElementI:
case ElementEm:
var italic = new Italic();
inlines.Add(italic);
currentInlines = italic.Inlines;
break;
case ElementU:
var underline = new Underline();
inlines.Add(underline);
currentInlines = underline.Inlines;
break;
case ElementBr:
inlines.Add(new LineBreak());
break;
case ElementP:
// Add two line breaks, one for the current text and the second for the gap.
if (AddLineBreakIfNeeded(inlines))
{
inlines.Add(new LineBreak());
}
var paragraphSpan = new Windows.UI.Xaml.Documents.Span();
inlines.Add(paragraphSpan);
currentInlines = paragraphSpan.Inlines;
break;
case ElementLi:
inlines.Add(new LineBreak());
inlines.Add(new Run { Text = " • " });
break;
case ElementUl:
case ElementDiv:
AddLineBreakIfNeeded(inlines);
var divSpan = new Windows.UI.Xaml.Documents.Span();
inlines.Add(divSpan);
currentInlines = divSpan.Inlines;
break;
}
foreach (var node in element.Nodes())
{
if (node is XText textElement)
{
currentInlines.Add(new Run { Text = textElement.Value });
}
else
{
ParseText(node as XElement, currentInlines, label);
}
}
// Add newlines for paragraph tags
if (elementName == "ElementP")
{
currentInlines.Add(new LineBreak());
}
}
static bool AddLineBreakIfNeeded(InlineCollection inlines)
{
if (inlines.Count <= 0)
return false;
var lastInline = inlines[inlines.Count - 1];
while ((lastInline is Windows.UI.Xaml.Documents.Span))
{
var span = (Windows.UI.Xaml.Documents.Span)lastInline;
if (span.Inlines.Count > 0)
{
lastInline = span.Inlines[span.Inlines.Count - 1];
}
}
if (lastInline is LineBreak)
return false;
inlines.Add(new LineBreak());
return true;
}
}
}

Просмотреть файл

@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Windows.Foundation;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Documents;
using Xamarin.Forms.Platform.UAP;
using Xamarin.Forms.PlatformConfiguration.WindowsSpecific;
using Specifics = Xamarin.Forms.PlatformConfiguration.WindowsSpecific.Label;
using WThickness = Windows.UI.Xaml.Thickness;
@ -155,11 +158,8 @@ namespace Xamarin.Forms.Platform.UWP
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == Label.TextProperty.PropertyName ||
e.PropertyName == Label.FormattedTextProperty.PropertyName)
{
if (e.IsOneOf(Label.TextProperty, Label.FormattedTextProperty, Label.TextTypeProperty))
UpdateText(Control);
}
else if (e.PropertyName == Label.TextColorProperty.PropertyName)
UpdateColor(Control);
else if (e.PropertyName == Label.HorizontalTextAlignmentProperty.PropertyName || e.PropertyName == Label.VerticalTextAlignmentProperty.PropertyName)
@ -182,6 +182,7 @@ namespace Xamarin.Forms.Platform.UWP
UpdateMaxLines(Control);
else if (e.PropertyName == Label.PaddingProperty.PropertyName)
UpdatePadding(Control);
base.OnElementPropertyChanged(sender, e);
}
@ -327,10 +328,22 @@ namespace Xamarin.Forms.Platform.UWP
_perfectSizeValid = false;
if (textBlock == null)
{
return;
switch (Element.TextType)
{
case TextType.Html:
UpdateTextHtml(textBlock);
break;
default:
UpdateTextPlainText(textBlock);
break;
}
}
void UpdateTextPlainText(TextBlock textBlock)
{
Label label = Element;
if (label != null)
{
@ -360,6 +373,27 @@ namespace Xamarin.Forms.Platform.UWP
}
}
void UpdateTextHtml(TextBlock textBlock)
{
var text = Element.Text ?? String.Empty;
// Just in case we are not given text with elements.
var modifiedText = string.Format("<div>{0}</div>", text);
modifiedText = Regex.Replace(modifiedText, "<br>", "<br></br>", RegexOptions.IgnoreCase);
// reset the text because we will add to it.
Control.Inlines.Clear();
try
{
var element = XElement.Parse(modifiedText);
LabelHtmlHelper.ParseText(element, Control.Inlines, Element);
}
catch (Exception)
{
// if anything goes wrong just show the html
textBlock.Text = Windows.Data.Html.HtmlUtilities.ConvertToText(Element.Text);
}
}
void UpdateDetectReadingOrderFromContent(TextBlock textBlock)
{
if (Element.IsSet(Specifics.DetectReadingOrderFromContentProperty))

Просмотреть файл

@ -78,6 +78,7 @@
<Compile Include="ITitleViewRendererController.cs" />
<Compile Include="IToolbarProvider.cs" />
<Compile Include="IVisualNativeElementRenderer.cs" />
<Compile Include="LabelHtmlHelper.cs" />
<Compile Include="NativeBindingExtensions.cs" />
<Compile Include="NativeEventWrapper.cs" />
<Compile Include="NativePropertyListener.cs" />

Просмотреть файл

@ -39,7 +39,8 @@ namespace Xamarin.Forms.Platform.MacOS
Label.FormattedTextProperty.PropertyName,
Label.LineBreakModeProperty.PropertyName,
Label.LineHeightProperty.PropertyName,
Label.PaddingProperty.PropertyName
Label.PaddingProperty.PropertyName,
Label.TextTypeProperty.PropertyName
};
public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
@ -216,9 +217,10 @@ namespace Xamarin.Forms.Platform.MacOS
UpdateMaxLines();
else if (e.PropertyName == Label.PaddingProperty.PropertyName)
UpdatePadding();
else if (e.PropertyName == Label.TextTypeProperty.PropertyName)
UpdateText();
}
protected override NativeLabel CreateNativeControl()
{
#if __MOBILE__
@ -236,6 +238,9 @@ namespace Xamarin.Forms.Platform.MacOS
void UpdateTextDecorations()
{
if (Element?.TextType != TextType.Text)
return;
if (!Element.IsSet(Label.TextDecorationsProperty))
return;
@ -275,6 +280,7 @@ namespace Xamarin.Forms.Platform.MacOS
#else
Control.AttributedStringValue = newAttributedText;
#endif
_perfectSizeValid = false;
}
#if __MOBILE__
@ -372,14 +378,33 @@ namespace Xamarin.Forms.Platform.MacOS
{
#if __MOBILE__
if (Element?.TextType != TextType.Text)
return;
var textAttr = Control.AttributedText.AddCharacterSpacing(Element.Text, Element.CharacterSpacing);
if (textAttr != null)
Control.AttributedText = textAttr;
_perfectSizeValid = false;
#endif
}
void UpdateText()
{
switch (Element.TextType)
{
case TextType.Html:
UpdateTextHtml();
break;
default:
UpdateTextPlainText();
break;
}
}
void UpdateTextPlainText()
{
_formatted = Element.FormattedText;
if (_formatted == null && Element.LineHeight >= 0)
@ -407,10 +432,42 @@ namespace Xamarin.Forms.Platform.MacOS
#else
Control.AttributedStringValue = _formatted.ToAttributed(Element, Element.TextColor, Element.HorizontalTextAlignment, Element.LineHeight);
#endif
_perfectSizeValid = false;
}
void UpdateTextHtml()
{
string text = Element.Text ?? string.Empty;
#if __MOBILE__
var attr = new NSAttributedStringDocumentAttributes
{
DocumentType = NSDocumentType.HTML
};
NSError nsError = null;
Control.AttributedText = new NSAttributedString(text, attr, ref nsError);
#else
var attr = new NSAttributedStringDocumentAttributes
{
DocumentType = NSDocumentType.HTML
};
var htmlData = new NSMutableData();
htmlData.SetData(text);
Control.AttributedStringValue = new NSAttributedString(htmlData, attr, out _);
#endif
_perfectSizeValid = false;
}
void UpdateFont()
{
if (Element?.TextType != TextType.Text)
return;
if (IsTextFormatted)
{
UpdateFormattedText();
@ -427,6 +484,9 @@ namespace Xamarin.Forms.Platform.MacOS
void UpdateTextColor()
{
if (Element?.TextType != TextType.Text)
return;
if (IsTextFormatted)
{
UpdateFormattedText();