Skip to content

Commit

Permalink
Fix Android FormattedText and related platforms (#7219)
Browse files Browse the repository at this point in the history
* Reduce line length so we can work

* Set initial Span.FontSize => double.NaN Fixes #6801

This initial value of Nan (or could be 0) indicates to the layout engine that the size must come from the Label. If we set it here, then there is no way of knowing that the size was not actually set so uses the size. If the size is Nan, it falls back to the "default font size" which is not really default but actually the font size of the label.

* Correctly apply span values to spans Fixes #7220

All:
- set the default parameter values for line height to be -1 as that is what is the default currently
Android:
- don't capture the TextView.Paint as that is always wrong initially since none of the other properties are set
- don't fall back to the Label.LineHeight to the spans as that is always applied - regardless of span values
- pass the Label.CharacterSpacing down to be consistent
- pass the Label.TextDecorations down as well
- for text decorations, use the platform spans
- split a LetterSpacingSpan out of the FontSpan so that they can be individually applied

* Update src/Controls/src/Core/Platform/Android/Extensions/FormattedStringExtensions.cs

Co-authored-by: campersau <buchholz.bastian@googlemail.com>
  • Loading branch information
mattleibow and campersau committed May 17, 2022
1 parent a6c12a1 commit a51243a
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 101 deletions.
2 changes: 1 addition & 1 deletion src/Controls/src/Core/FontExtensions.cs
Expand Up @@ -30,7 +30,7 @@ public static FontAttributes GetFontAttributes(this Font font)
public static Font ToFont(this IFontElement element, double? defaultSize = null)
{
var size = element.FontSize;
if (defaultSize.HasValue && (size == 0 || size == double.NaN))
if (defaultSize.HasValue && (size <= 0 || double.IsNaN(size)))
size = defaultSize.Value;

return Font.OfSize(element.FontFamily, size, enableScaling: element.FontAutoScalingEnabled).WithAttributes(element.FontAttributes);
Expand Down
Expand Up @@ -15,9 +15,27 @@ namespace Microsoft.Maui.Controls.Platform
public static class FormattedStringExtensions
{
public static SpannableString ToSpannableString(this Label label)
=> ToSpannableString(label.FormattedText, label.RequireFontManager(), (label.Handler?.PlatformView as TextView)?.Paint, label.Handler?.MauiContext?.Context, label.LineHeight, label.HorizontalTextAlignment, label.ToFont(), label.TextColor, label.TextTransform);

public static SpannableString ToSpannableString(this FormattedString formattedString, IFontManager fontManager, TextPaint? textPaint = null, Context? context = null, double defaultLineHeight = 0d, TextAlignment defaultHorizontalAlignment = TextAlignment.Start, Font? defaultFont = null, Graphics.Color? defaultColor = null, TextTransform defaultTextTransform = TextTransform.Default)
=> ToSpannableString(
label.FormattedText,
label.RequireFontManager(),
label.Handler?.MauiContext?.Context,
label.CharacterSpacing,
label.HorizontalTextAlignment,
label.ToFont(),
label.TextColor,
label.TextTransform,
label.TextDecorations);

public static SpannableString ToSpannableString(
this FormattedString formattedString,
IFontManager fontManager,
Context? context = null,
double defaultCharacterSpacing = 0d,
TextAlignment defaultHorizontalAlignment = TextAlignment.Start,
Font? defaultFont = null,
Graphics.Color? defaultColor = null,
TextTransform defaultTextTransform = TextTransform.Default,
TextDecorations defaultTextDecorations = TextDecorations.None)
{
if (formattedString == null)
return new SpannableString(string.Empty);
Expand Down Expand Up @@ -53,46 +71,43 @@ public static SpannableString ToSpannableString(this FormattedString formattedSt
int end = start + text.Length;
c = end;

// TextColor
var textColor = span.TextColor ?? defaultColor;

if (textColor != null)
{
if (textColor is not null)
spannable.SetSpan(new ForegroundColorSpan(textColor.ToPlatform()), start, end, SpanTypes.InclusiveExclusive);
}

if (span.BackgroundColor != null)
{
// BackgroundColor
if (span.BackgroundColor is not null)
spannable.SetSpan(new BackgroundColorSpan(span.BackgroundColor.ToPlatform()), start, end, SpanTypes.InclusiveExclusive);
}

var lineHeight = span.LineHeight >= 0
? span.LineHeight
: defaultLineHeight;
// LineHeight
if (span.LineHeight >= 0)
spannable.SetSpan(new LineHeightSpan(span.LineHeight), start, end, SpanTypes.InclusiveExclusive);

if (lineHeight >= 0)
{
spannable.SetSpan(new LineHeightSpan(textPaint, lineHeight), start, end, SpanTypes.InclusiveExclusive);
}
// CharacterSpacing
var characterSpacing = span.CharacterSpacing >= 0
? span.CharacterSpacing
: defaultCharacterSpacing;
if (characterSpacing >= 0)
spannable.SetSpan(new LetterSpacingSpan(characterSpacing.ToEm()), start, end, SpanTypes.InclusiveInclusive);

// Font
var font = span.ToFont(defaultFontSize);
if (font.IsDefault && defaultFont.HasValue)
font = defaultFont.Value;

if (!font.IsDefault)
{
spannable.SetSpan(
new FontSpan(font, context, span.CharacterSpacing.ToEm(), defaultHorizontalAlignment, fontManager),
start,
end,
SpanTypes.InclusiveInclusive);
}

if (span.IsSet(Span.TextDecorationsProperty))
{
spannable.SetSpan(new TextDecorationSpan(span), start, end, SpanTypes.InclusiveInclusive);
}

spannable.SetSpan(new FontSpan(font, fontManager, context), start, end, SpanTypes.InclusiveInclusive);

// TextDecorations
var textDecorations = span.IsSet(Span.TextDecorationsProperty)
? span.TextDecorations
: defaultTextDecorations;
if (textDecorations.HasFlag(TextDecorations.Strikethrough))
spannable.SetSpan(new StrikethroughSpan(), start, end, SpanTypes.InclusiveInclusive);
if (textDecorations.HasFlag(TextDecorations.Underline))
spannable.SetSpan(new UnderlineSpan(), start, end, SpanTypes.InclusiveInclusive);
}

return spannable;
}

Expand All @@ -118,7 +133,6 @@ public static void RecalculateSpanPositions(this TextView textView, Label elemen
int count = 0;
IList<int> totalLineHeights = new List<int>();

#pragma warning disable CA1416 // https://github.com/xamarin/xamarin-android/issues/6962
for (int i = 0; i < spannableString.Length(); i = next)
{
var type = Java.Lang.Class.FromType(typeof(Java.Lang.Object));
Expand All @@ -136,7 +150,7 @@ public static void RecalculateSpanPositions(this TextView textView, Label elemen
// Get all spans in the range - Android can have overlapping spans
var spans = spannableString.GetSpans(i, next, type);

if (spans == null)
if (spans is null || spans.Length == 0)
continue;

var startSpan = spans[0];
Expand Down Expand Up @@ -179,26 +193,21 @@ public static void RecalculateSpanPositions(this TextView textView, Label elemen

((ISpatialElement)span).Region = Region.FromLines(lineHeights, labelWidth, startX, endX, yaxis).Inflate(10);
}
#pragma warning restore CA1416 // 'SpannableString.Length()' is only supported on: 'android' 29.0 and later
}

class FontSpan : MetricAffectingSpan
{
public FontSpan(Font font, Context? context, float characterSpacing, TextAlignment? horizontalTextAlignment, IFontManager fontManager)
readonly Font _font;
readonly IFontManager _fontManager;
readonly Context? _context;

public FontSpan(Font font, IFontManager fontManager, Context? context)
{
Font = font;
Context = context;
CharacterSpacing = characterSpacing;
FontManager = fontManager;
HorizontalTextAlignment = horizontalTextAlignment;
_font = font;
_fontManager = fontManager;
_context = context;
}

public readonly IFontManager FontManager;
public readonly Font Font;
public readonly Context? Context;
public readonly float CharacterSpacing;
public readonly TextAlignment? HorizontalTextAlignment;

public override void UpdateDrawState(TextPaint? tp)
{
if (tp != null)
Expand All @@ -210,28 +219,26 @@ public override void UpdateMeasureState(TextPaint p)
Apply(p);
}

void Apply(Paint paint)
void Apply(TextPaint paint)
{
paint.SetTypeface(Font.ToTypeface(FontManager));
float value = (float)Font.Size;
paint.SetTypeface(_font.ToTypeface(_fontManager));

paint.TextSize = TypedValue.ApplyDimension(
Font.AutoScalingEnabled ? ComplexUnitType.Sp : ComplexUnitType.Dip,
value, Context?.Resources?.DisplayMetrics ?? AAplication.Context.Resources!.DisplayMetrics);

paint.LetterSpacing = CharacterSpacing;
_font.AutoScalingEnabled ? ComplexUnitType.Sp : ComplexUnitType.Dip,
(float)_font.Size,
(_context ?? AAplication.Context)?.Resources?.DisplayMetrics);
}
}

class TextDecorationSpan : MetricAffectingSpan
class LetterSpacingSpan : MetricAffectingSpan
{
public TextDecorationSpan(Span span)
readonly float _letterSpacing;

public LetterSpacingSpan(double letterSpacing)
{
Span = span;
_letterSpacing = (float)letterSpacing;
}

public Span Span { get; }

public override void UpdateDrawState(TextPaint? tp)
{
if (tp != null)
Expand All @@ -243,35 +250,27 @@ public override void UpdateMeasureState(TextPaint p)
Apply(p);
}

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

class LineHeightSpan : Java.Lang.Object, ILineHeightSpan
{
private double _lineHeight;
private int _ascent;
private int _descent;
readonly double _relativeLineHeight;

public LineHeightSpan(TextPaint? paint, double lineHeight)
public LineHeightSpan(double relativeLineHeight)
{
_lineHeight = lineHeight;
var fm = paint?.GetFontMetricsInt();
_ascent = fm?.Ascent ?? 1;
_descent = fm?.Descent ?? 1;
_relativeLineHeight = relativeLineHeight;
}

public void ChooseHeight(Java.Lang.ICharSequence? text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt? fm)
public void ChooseHeight(Java.Lang.ICharSequence? text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt? fm)
{
if (fm != null)
{
fm.Ascent = (int)(_ascent * _lineHeight);
fm.Descent = (int)(_descent * _lineHeight);
}
if (fm is null)
return;

fm.Ascent = (int)(fm.Top * _relativeLineHeight);
}
}
}
Expand Down
Expand Up @@ -11,9 +11,25 @@ namespace Microsoft.Maui.Controls.Platform
public static class FormattedStringExtensions
{
public static void UpdateInlines(this TextBlock textBlock, Label label)
=> UpdateInlines(textBlock, label.RequireFontManager(false), label.FormattedText, label.LineHeight, label.HorizontalTextAlignment, label.ToFont(), label.TextColor, label.TextTransform);

public static void UpdateInlines(this TextBlock textBlock, IFontManager fontManager, FormattedString formattedString, double defaultLineHeight = 0d, TextAlignment defaultHorizontalAlignment = TextAlignment.Start, Font? defaultFont = null, Color? defaultColor = null, TextTransform defaultTextTransform = TextTransform.Default)
=> UpdateInlines(
textBlock,
label.RequireFontManager(false),
label.FormattedText,
label.LineHeight,
label.HorizontalTextAlignment,
label.ToFont(),
label.TextColor,
label.TextTransform);

public static void UpdateInlines(
this TextBlock textBlock,
IFontManager fontManager,
FormattedString formattedString,
double defaultLineHeight = -1d,
TextAlignment defaultHorizontalAlignment = TextAlignment.Start,
Font? defaultFont = null,
Color? defaultColor = null,
TextTransform defaultTextTransform = TextTransform.Default)
{
textBlock.Inlines.Clear();
// Have to implement a measure here, otherwise inline.ContentStart and ContentEnd will be null, when used in RecalculatePositions
Expand Down Expand Up @@ -57,7 +73,14 @@ public static void UpdateInlines(this TextBlock textBlock, IFontManager fontMana
}
}

public static IEnumerable<Tuple<Run, Color, Color>> ToRunAndColorsTuples(this FormattedString formattedString, IFontManager fontManager, double defaultLineHeight = 0d, TextAlignment defaultHorizontalAlignment = TextAlignment.Start, Font? defaultFont = null, Color? defaultColor = null, TextTransform defaultTextTransform = TextTransform.Default)
public static IEnumerable<Tuple<Run, Color, Color>> ToRunAndColorsTuples(
this FormattedString formattedString,
IFontManager fontManager,
double defaultLineHeight = -1d,
TextAlignment defaultHorizontalAlignment = TextAlignment.Start,
Font? defaultFont = null,
Color? defaultColor = null,
TextTransform defaultTextTransform = TextTransform.Default)
{
var runs = new List<Tuple<Run, Color, Color>>();

Expand All @@ -74,15 +97,22 @@ public static void UpdateInlines(this TextBlock textBlock, IFontManager fontMana
return runs;
}

public static Tuple<Run, Color, Color> ToRunAndColorsTuple(this Span span, IFontManager fontManager, Font? defaultFont = null, Color? defaultColor = null, TextTransform defaultTextTransform = TextTransform.Default)
public static Tuple<Run, Color, Color> ToRunAndColorsTuple(
this Span span,
IFontManager fontManager,
Font? defaultFont = null,
Color? defaultColor = null,
TextTransform defaultTextTransform = TextTransform.Default)
{
var defaultFontSize = defaultFont?.Size ?? fontManager.DefaultFontSize;

var transform = span.TextTransform != TextTransform.Default ? span.TextTransform : defaultTextTransform;

var text = TextTransformUtilites.GetTransformedText(span.Text, transform);

var run = new Run { Text = text ?? string.Empty };

var font = span.ToFont();
var font = span.ToFont(defaultFontSize);
if (font.IsDefault && defaultFont.HasValue)
font = defaultFont.Value;

Expand Down
Expand Up @@ -26,8 +26,14 @@ public static class FormattedStringExtensions
label.TextColor,
label.TextTransform);


public static NSAttributedString ToNSAttributedString(this FormattedString formattedString, IFontManager fontManager, double defaultLineHeight = 0d, TextAlignment defaultHorizontalAlignment = TextAlignment.Start, Font? defaultFont = null, Color? defaultColor = null, TextTransform defaultTextTransform = TextTransform.Default)
public static NSAttributedString ToNSAttributedString(
this FormattedString formattedString,
IFontManager fontManager,
double defaultLineHeight = -1d,
TextAlignment defaultHorizontalAlignment = TextAlignment.Start,
Font? defaultFont = null,
Color? defaultColor = null,
TextTransform defaultTextTransform = TextTransform.Default)
{
if (formattedString == null)
return new NSAttributedString(string.Empty);
Expand All @@ -45,8 +51,17 @@ public static NSAttributedString ToNSAttributedString(this FormattedString forma
return attributed;
}

public static NSAttributedString ToNSAttributedString(this Span span, IFontManager fontManager, double defaultLineHeight = 0d, TextAlignment defaultHorizontalAlignment = TextAlignment.Start, Font? defaultFont = null, Color? defaultColor = null, TextTransform defaultTextTransform = TextTransform.Default)
public static NSAttributedString ToNSAttributedString(
this Span span,
IFontManager fontManager,
double defaultLineHeight = -1d,
TextAlignment defaultHorizontalAlignment = TextAlignment.Start,
Font? defaultFont = null,
Color? defaultColor = null,
TextTransform defaultTextTransform = TextTransform.Default)
{
var defaultFontSize = defaultFont?.Size ?? fontManager.DefaultFontSize;

var transform = span.TextTransform != TextTransform.Default ? span.TextTransform : defaultTextTransform;

var text = TextTransformUtilites.GetTransformedText(span.Text, transform);
Expand All @@ -71,7 +86,7 @@ public static NSAttributedString ToNSAttributedString(this Span span, IFontManag
_ => UITextAlignment.Left
};

var font = span.ToFont();
var font = span.ToFont(defaultFontSize);
if (font.IsDefault && defaultFont.HasValue)
font = defaultFont.Value;

Expand Down Expand Up @@ -110,6 +125,5 @@ public static NSAttributedString ToNSAttributedString(this Span span, IFontManag

return attrString;
}

}
}
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Span.cs
Expand Up @@ -159,7 +159,7 @@ void IFontElement.OnFontSizeChanged(double oldValue, double newValue)
}

double IFontElement.FontSizeDefaultValueCreator() =>
this.GetDefaultFontSize();
double.NaN;

void IFontElement.OnFontAttributesChanged(FontAttributes oldValue, FontAttributes newValue)
{
Expand Down

0 comments on commit a51243a

Please sign in to comment.