Skip to content

Commit 4aec060

Browse files
[android] improve FormattedString performance (#21712)
Context: https://github.com/Redth/MauiCollectionViewGallery Fixes: #14222 This will conflict with: * #20352 But I will rework this after it is merged. Profiling the above sample while scrolling on a Pixel 5, a lot of time is spent in `FormattedStringExtensions.RecalculateSpanPositions()`: (20%) Microsoft.Maui.Controls!Microsoft.Maui.Controls.Platform.FormattedStringExtensions.RecalculateSpanPositions(Android.Widget.TextView,Microsoft.Maui.Controls.Label,Android.Text.SpannableString,Microsoft.Maui.SizeRequest) (11%) Microsoft.Maui.Controls!Microsoft.Maui.Controls.Platform.FormattedStringExtensions.FontSpan.UpdateDrawState(Android.Text.TextPaint) (11%) Microsoft.Maui.Controls!Microsoft.Maui.Controls.Platform.FormattedStringExtensions.FontSpan.Apply(Android.Text.TextPaint) (6.3%) MauiCollectionViewGallery!PoolMathApp.Helpers.FormattedTextExtensions.ToFormattedString(PoolMathApp.Models.FormattedTex The key contributors are `FontSpan` and `LineHeightSpan` which: * Implement `MetricAffectingSpan`, an abstract class that allows to change the metrics of the text. * Implement `UpdateDrawState()` and `Apply()` methods that are called during draw. Causing frequent Java -> C# interop. To fix this, let's move the following types from C# to Java: * `FontSpan` -> `PlatformFontSpan` * `LetterSpacingSpan` -> `PlatformFontSpan` (use different ctor) * `LineHeightSpan` -> `PlatformLineHeightSpan` `PlatformLineHeightSpan` is similar, as it is an implementation of the `LineHeightSpan` interface. With these changes, I see a nice improvement while scrolling: (5.5%) Microsoft.Maui.Controls!Microsoft.Maui.Controls.Platform.FormattedStringExtensions.RecalculateSpanPositions(Android.Widget.TextView,Microsoft.Maui.Controls.Label,Android.Text.SpannableString,Microsoft.Maui.SizeRequest) (4.0%) MauiCollectionViewGallery!PoolMathApp.Helpers.FormattedTextExtensions.ToFormattedString(PoolMathApp.Models.FormattedText `RecalculateSpanPositions` overall, went from 20% to 5.5%! Comparing the new types, the times spent calling the constructors are also improved: Before: (1.1%) Microsoft.Maui.Controls!Microsoft.Maui.Controls.Platform.FormattedStringExtensions.FontSpan..ctor(Microsoft.Maui.Font,M (1.5%) Microsoft.Maui.Controls!Microsoft.Maui.Controls.Platform.FormattedStringExtensions.LetterSpacingSpan..ctor(double) After: (1.0%) Microsoft.Maui!Microsoft.Maui.PlatformFontSpan..ctor(Android.Content.Context,Android.Graphics.Typeface,bool,single) (0.82%) Microsoft.Maui!Microsoft.Maui.PlatformFontSpan..ctor(single) This should be reasonable for .NET 8servicing, as there should be no behavior changes and no API changes. In a future PR, it looks like `FormattedStringExtensions` could be improved further, but this is a decent starting point that makes it a lot better.
1 parent 5f21882 commit 4aec060

File tree

5 files changed

+101
-89
lines changed

5 files changed

+101
-89
lines changed

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

Lines changed: 4 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ internal static SpannableString ToSpannableStringNewWay(
6464

6565
var builder = new StringBuilder();
6666

67-
var fontMetrics = PlatformInterop.GetFontMetrics(context, defaultFontSize);
68-
6967
for (int i = 0; i < formattedString.Spans.Count; i++)
7068
{
7169
Span span = formattedString.Spans[i];
@@ -103,22 +101,22 @@ internal static SpannableString ToSpannableStringNewWay(
103101
spannable.SetSpan(new BackgroundColorSpan(span.BackgroundColor.ToPlatform()), start, end, SpanTypes.InclusiveExclusive);
104102

105103
// LineHeight
106-
if (span.LineHeight >= 0 && fontMetrics is not null)
107-
spannable.SetSpan(new LineHeightSpan(span.LineHeight, fontMetrics.Top), start, end, SpanTypes.InclusiveExclusive);
104+
if (span.LineHeight >= 0)
105+
spannable.SetSpan(new PlatformLineHeightSpan(context, (float)span.LineHeight, (float)defaultFontSize), start, end, SpanTypes.InclusiveExclusive);
108106

109107
// CharacterSpacing
110108
var characterSpacing = span.CharacterSpacing >= 0
111109
? span.CharacterSpacing
112110
: defaultCharacterSpacing;
113111
if (characterSpacing >= 0)
114-
spannable.SetSpan(new LetterSpacingSpan(characterSpacing.ToEm()), start, end, SpanTypes.InclusiveInclusive);
112+
spannable.SetSpan(new PlatformFontSpan(characterSpacing.ToEm()), start, end, SpanTypes.InclusiveInclusive);
115113

116114
// Font
117115
var font = span.ToFont(defaultFontSize);
118116
if (font.IsDefault && defaultFont.HasValue)
119117
font = defaultFont.Value;
120118
if (!font.IsDefault)
121-
spannable.SetSpan(new FontSpan(font, fontManager, context), start, end, SpanTypes.InclusiveInclusive);
119+
spannable.SetSpan(new PlatformFontSpan(context ?? AAplication.Context, font.ToTypeface(fontManager), font.AutoScalingEnabled, (float)font.Size), start, end, SpanTypes.InclusiveInclusive);
122120

123121
// TextDecorations
124122
var textDecorations = span.IsSet(Span.TextDecorationsProperty)
@@ -226,86 +224,5 @@ public static void RecalculateSpanPositions(this TextView textView, Label elemen
226224
((ISpatialElement)span).Region = Region.FromRectangles(spanRectangles).Inflate(10);
227225
}
228226
}
229-
230-
class FontSpan : MetricAffectingSpan
231-
{
232-
readonly Font _font;
233-
readonly IFontManager _fontManager;
234-
readonly Context? _context;
235-
236-
public FontSpan(Font font, IFontManager fontManager, Context? context)
237-
{
238-
_font = font;
239-
_fontManager = fontManager;
240-
_context = context;
241-
}
242-
243-
public override void UpdateDrawState(TextPaint? tp)
244-
{
245-
if (tp != null)
246-
Apply(tp);
247-
}
248-
249-
public override void UpdateMeasureState(TextPaint p)
250-
{
251-
Apply(p);
252-
}
253-
254-
void Apply(TextPaint paint)
255-
{
256-
paint.SetTypeface(_font.ToTypeface(_fontManager));
257-
258-
paint.TextSize = TypedValue.ApplyDimension(
259-
_font.AutoScalingEnabled ? ComplexUnitType.Sp : ComplexUnitType.Dip,
260-
(float)_font.Size,
261-
(_context ?? AAplication.Context)?.Resources?.DisplayMetrics);
262-
}
263-
}
264-
265-
class LetterSpacingSpan : MetricAffectingSpan
266-
{
267-
readonly float _letterSpacing;
268-
269-
public LetterSpacingSpan(double letterSpacing)
270-
{
271-
_letterSpacing = (float)letterSpacing;
272-
}
273-
274-
public override void UpdateDrawState(TextPaint? tp)
275-
{
276-
if (tp != null)
277-
Apply(tp);
278-
}
279-
280-
public override void UpdateMeasureState(TextPaint p)
281-
{
282-
Apply(p);
283-
}
284-
285-
void Apply(TextPaint paint)
286-
{
287-
paint.LetterSpacing = _letterSpacing;
288-
}
289-
}
290-
291-
class LineHeightSpan : Java.Lang.Object, ILineHeightSpan
292-
{
293-
readonly double _relativeLineHeight;
294-
readonly double _originalTop;
295-
296-
public LineHeightSpan(double relativeLineHeight, double originalTop)
297-
{
298-
_relativeLineHeight = relativeLineHeight;
299-
_originalTop = originalTop;
300-
}
301-
302-
public void ChooseHeight(Java.Lang.ICharSequence? text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt? fm)
303-
{
304-
if (fm is null)
305-
return;
306-
307-
fm.Ascent = (int)(_originalTop * _relativeLineHeight);
308-
}
309-
}
310227
}
311228
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.microsoft.maui;
2+
3+
import android.content.Context;
4+
import android.graphics.Typeface;
5+
import android.text.TextPaint;
6+
import android.text.style.MetricAffectingSpan;
7+
import android.util.TypedValue;
8+
9+
import androidx.annotation.NonNull;
10+
11+
/**
12+
* Class for setting letterSpacing, textSize, or typeface on a Span
13+
*/
14+
public class PlatformFontSpan extends MetricAffectingSpan {
15+
// NOTE: java.lang.Float is a "nullable" float
16+
private Float letterSpacing;
17+
private Float textSize;
18+
private Typeface typeface;
19+
20+
/**
21+
* Constructor for setting letterSpacing-only
22+
* @param letterSpacing
23+
*/
24+
public PlatformFontSpan(float letterSpacing) {
25+
this.letterSpacing = letterSpacing;
26+
}
27+
28+
/**
29+
* Constructor for setting typeface and computing textSize
30+
* @param context
31+
* @param typeface
32+
* @param autoScalingEnabled
33+
* @param fontSize
34+
*/
35+
public PlatformFontSpan(@NonNull Context context, Typeface typeface, boolean autoScalingEnabled, float fontSize) {
36+
this.typeface = typeface;
37+
textSize = TypedValue.applyDimension(
38+
autoScalingEnabled ? TypedValue.COMPLEX_UNIT_SP : TypedValue.COMPLEX_UNIT_DIP,
39+
fontSize,
40+
context.getResources().getDisplayMetrics()
41+
);
42+
}
43+
44+
@Override
45+
public void updateDrawState(TextPaint textPaint) {
46+
if (textPaint != null) {
47+
apply(textPaint);
48+
}
49+
}
50+
51+
@Override
52+
public void updateMeasureState(@NonNull TextPaint textPaint) {
53+
apply(textPaint);
54+
}
55+
56+
void apply(TextPaint textPaint)
57+
{
58+
if (typeface != null) {
59+
textPaint.setTypeface(typeface);
60+
}
61+
if (textSize != null) {
62+
textPaint.setTextSize(textSize);
63+
}
64+
if (letterSpacing != null) {
65+
textPaint.setLetterSpacing(letterSpacing);
66+
}
67+
}
68+
}

src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ public static Rect getCurrentWindowMetrics(Activity activity) {
598598
* @param defaultFontSize
599599
* @return FontMetrics object or null if context or display metrics is null
600600
*/
601-
public static Paint.FontMetrics getFontMetrics(Context context, double defaultFontSize) {
601+
public static Paint.FontMetrics getFontMetrics(Context context, float defaultFontSize) {
602602
if (context == null)
603603
return null;
604604

@@ -608,7 +608,7 @@ public static Paint.FontMetrics getFontMetrics(Context context, double defaultFo
608608
setTextSize(
609609
TypedValue.applyDimension(
610610
TypedValue.COMPLEX_UNIT_SP,
611-
(float) defaultFontSize,
611+
defaultFontSize,
612612
metrics
613613
));
614614
}}.getFontMetrics();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.microsoft.maui;
2+
3+
import android.content.Context;
4+
import android.graphics.Paint;
5+
import android.text.style.LineHeightSpan;
6+
7+
/**
8+
* Class for setting a relativeLineHeight on a Span
9+
*/
10+
public class PlatformLineHeightSpan implements LineHeightSpan {
11+
private final float relativeLineHeight;
12+
private final Float top; //NOTE: nullable float
13+
14+
public PlatformLineHeightSpan(Context context, float relativeLineHeight, float defaultFontSize) {
15+
this.relativeLineHeight = relativeLineHeight;
16+
Paint.FontMetrics fontMetrics = PlatformInterop.getFontMetrics(context, defaultFontSize);
17+
this.top = fontMetrics != null ? fontMetrics.top : null;
18+
}
19+
20+
@Override
21+
public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt) {
22+
if (fontMetricsInt != null) {
23+
float top = this.top != null ? this.top : fontMetricsInt.top;
24+
fontMetricsInt.ascent = (int)(top * relativeLineHeight);
25+
}
26+
}
27+
}

src/Core/src/maui.aar

1.92 KB
Binary file not shown.

0 commit comments

Comments
 (0)