Skip to content

Commit acc5226

Browse files
[android] improve FormattedString performance
Context: https://github.com/Redth/MauiCollectionViewGallery Fixes: dotnet#14222 This will conflict with: * dotnet#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 ce624a9 commit acc5226

File tree

4 files changed

+93
-82
lines changed

4 files changed

+93
-82
lines changed

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

Lines changed: 3 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,21 @@ internal static SpannableString ToSpannableStringNewWay(
102102

103103
// LineHeight
104104
if (span.LineHeight >= 0)
105-
spannable.SetSpan(new LineHeightSpan(span.LineHeight), start, end, SpanTypes.InclusiveExclusive);
105+
spannable.SetSpan(new PlatformLineHeightSpan((float)span.LineHeight), start, end, SpanTypes.InclusiveExclusive);
106106

107107
// CharacterSpacing
108108
var characterSpacing = span.CharacterSpacing >= 0
109109
? span.CharacterSpacing
110110
: defaultCharacterSpacing;
111111
if (characterSpacing >= 0)
112-
spannable.SetSpan(new LetterSpacingSpan(characterSpacing.ToEm()), start, end, SpanTypes.InclusiveInclusive);
112+
spannable.SetSpan(new PlatformFontSpan(characterSpacing.ToEm()), start, end, SpanTypes.InclusiveInclusive);
113113

114114
// Font
115115
var font = span.ToFont(defaultFontSize);
116116
if (font.IsDefault && defaultFont.HasValue)
117117
font = defaultFont.Value;
118118
if (!font.IsDefault)
119-
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);
120120

121121
// TextDecorations
122122
var textDecorations = span.IsSet(Span.TextDecorationsProperty)
@@ -224,84 +224,5 @@ public static void RecalculateSpanPositions(this TextView textView, Label elemen
224224
((ISpatialElement)span).Region = Region.FromRectangles(spanRectangles).Inflate(10);
225225
}
226226
}
227-
228-
class FontSpan : MetricAffectingSpan
229-
{
230-
readonly Font _font;
231-
readonly IFontManager _fontManager;
232-
readonly Context? _context;
233-
234-
public FontSpan(Font font, IFontManager fontManager, Context? context)
235-
{
236-
_font = font;
237-
_fontManager = fontManager;
238-
_context = context;
239-
}
240-
241-
public override void UpdateDrawState(TextPaint? tp)
242-
{
243-
if (tp != null)
244-
Apply(tp);
245-
}
246-
247-
public override void UpdateMeasureState(TextPaint p)
248-
{
249-
Apply(p);
250-
}
251-
252-
void Apply(TextPaint paint)
253-
{
254-
paint.SetTypeface(_font.ToTypeface(_fontManager));
255-
256-
paint.TextSize = TypedValue.ApplyDimension(
257-
_font.AutoScalingEnabled ? ComplexUnitType.Sp : ComplexUnitType.Dip,
258-
(float)_font.Size,
259-
(_context ?? AAplication.Context)?.Resources?.DisplayMetrics);
260-
}
261-
}
262-
263-
class LetterSpacingSpan : MetricAffectingSpan
264-
{
265-
readonly float _letterSpacing;
266-
267-
public LetterSpacingSpan(double letterSpacing)
268-
{
269-
_letterSpacing = (float)letterSpacing;
270-
}
271-
272-
public override void UpdateDrawState(TextPaint? tp)
273-
{
274-
if (tp != null)
275-
Apply(tp);
276-
}
277-
278-
public override void UpdateMeasureState(TextPaint p)
279-
{
280-
Apply(p);
281-
}
282-
283-
void Apply(TextPaint paint)
284-
{
285-
paint.LetterSpacing = _letterSpacing;
286-
}
287-
}
288-
289-
class LineHeightSpan : Java.Lang.Object, ILineHeightSpan
290-
{
291-
readonly double _relativeLineHeight;
292-
293-
public LineHeightSpan(double relativeLineHeight)
294-
{
295-
_relativeLineHeight = relativeLineHeight;
296-
}
297-
298-
public void ChooseHeight(Java.Lang.ICharSequence? text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt? fm)
299-
{
300-
if (fm is null)
301-
return;
302-
303-
fm.Ascent = (int)(fm.Top * _relativeLineHeight);
304-
}
305-
}
306227
}
307228
}
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+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.microsoft.maui;
2+
3+
import android.graphics.Paint;
4+
import android.text.style.LineHeightSpan;
5+
6+
/**
7+
* Class for setting a relativeLineHeight on a Span
8+
*/
9+
public class PlatformLineHeightSpan implements LineHeightSpan {
10+
private float relativeLineHeight;
11+
12+
public PlatformLineHeightSpan(float relativeLineHeight) {
13+
this.relativeLineHeight = relativeLineHeight;
14+
}
15+
16+
@Override
17+
public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt) {
18+
if (fontMetricsInt != null) {
19+
fontMetricsInt.ascent = (int)(fontMetricsInt.top * relativeLineHeight);
20+
}
21+
}
22+
}

src/Core/src/maui.aar

1.65 KB
Binary file not shown.

0 commit comments

Comments
 (0)