Skip to content

[Android] Span line height fix #20352

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 1, 2024

Conversation

kubaflo
Copy link
Contributor

@kubaflo kubaflo commented Feb 4, 2024

Description of Change

The fm.Top value was constantly increasing so that the height of each line grew progressively. Instead, the line height should be adjusted based on the initial top value of font metrics

Issues Fixed

Fixes #19592

Before After

@kubaflo kubaflo requested a review from a team as a code owner February 4, 2024 19:17
@ghost ghost added the community ✨ Community Contribution label Feb 4, 2024
@ghost
Copy link

ghost commented Feb 4, 2024

Hey there @kubaflo! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@jsuarezruiz
Copy link
Contributor

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@ghost ghost added the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label Feb 5, 2024
// The line height should be the same for each line
// of the paragraph, 1.5 and 2.5 respectively,
// as opposed to progressively growing
App.Screenshot();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use here App.VerifyScreenshot? This will generate a snapshot to compare it with a reference one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

@jsuarezruiz jsuarezruiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test NativeFormattedStringContainsSpan seems to be failing with the changes
Java.Lang.NullPointerException : Attempt to read from field 'float android.util.DisplayMetrics.scaledDensity' on a null object reference

@kubaflo
Copy link
Contributor Author

kubaflo commented Feb 5, 2024

@jsuarezruiz I have fixed the falling test

@PureWeen
Copy link
Member

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@@ -64,6 +64,12 @@ public static SpannableString ToSpannableString(this Label label)

var builder = new StringBuilder();

var fontMetrics = context?.Resources?.DisplayMetrics != null ? new Paint()
{
TextSize = TypedValue.ApplyDimension(ComplexUnitType.Sp, (float)defaultFontSize, context.Resources.DisplayMetrics)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line looks like it would throw if context.Resources returned null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#I'm not sure why it would throw🤔
The only place that fontMetrics is here

if (span.LineHeight >= 0 && fontMetrics is not null)
and there's a null check

Comment on lines 67 to 71
var fontMetrics = context?.Resources?.DisplayMetrics != null ? new Paint()
{
TextSize = TypedValue.ApplyDimension(ComplexUnitType.Sp, (float)defaultFontSize, context.Resources.DisplayMetrics)
}.GetFontMetrics()
: null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we write this code in Java, it looks like it interops between C# and Java a lot. I've seen some past issue complaining about Android FormattedString performance like:

So we'd make a version of this method that returns "Font metrics" instead of "Window metrics":

/**
* Computes the current WindowMetrics' bounds
* @param activity
* @return Rect value of the bounds
*/
@NonNull
public static Rect getCurrentWindowMetrics(Activity activity) {
return WindowMetricsCalculator.Companion
.getOrCreate()
.computeCurrentWindowMetrics(activity)
.getBounds();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be beneficial, but I'm not sure how to write this code in JAVA and how to use it in MAUI

Copy link
Member

@jonathanpeppers jonathanpeppers Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add a method in this file, it will magically appear on the PlatformInterop static class, you can call from C# like:

//signature
public static Rect GetCurrentWindowMetrics(Activity activity);

//usage
var rect = PlatformInterop.GetCurrentWindowMetrics(myActivity);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I pushed a new commit with this functionality

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made one small change, but it seems like this should be ready if we fix up the screenshot.

@kubaflo kubaflo force-pushed the span-line-height-android-fix branch from 778d523 to 12d9c14 Compare March 5, 2024 15:47
jonathanpeppers
jonathanpeppers previously approved these changes Mar 5, 2024
Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me if the new test & screenshot work. 👍

@PureWeen
Copy link
Member

/rebase

@github-actions github-actions bot force-pushed the span-line-height-android-fix branch from f32a663 to ce1b722 Compare March 19, 2024 19:02
@jsuarezruiz
Copy link
Contributor

/azp run MAUI-UITests-public

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

jsuarezruiz
jsuarezruiz previously approved these changes Mar 27, 2024
@jonathanpeppers jonathanpeppers dismissed stale reviews from jsuarezruiz and themself via 18fdf8f March 27, 2024 13:49
Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the merge conflict.

@PureWeen PureWeen enabled auto-merge (squash) March 28, 2024 23:39
@PureWeen
Copy link
Member

Not sure why UI Tests aren't triggering
Manual run here
https://dev.azure.com/xamarin/public/_build/results?buildId=112277&view=results

jonathanpeppers added a commit to jonathanpeppers/maui that referenced this pull request Mar 29, 2024
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.
@PureWeen
Copy link
Member

PureWeen commented Apr 1, 2024

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@PureWeen PureWeen merged commit 8359359 into dotnet:main Apr 1, 2024
jonathanpeppers added a commit to jonathanpeppers/maui that referenced this pull request Apr 8, 2024
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.
jonathanpeppers added a commit to jonathanpeppers/maui that referenced this pull request Apr 8, 2024
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.
rmarinho pushed a commit that referenced this pull request Apr 9, 2024
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.
@github-actions github-actions bot locked and limited conversation to collaborators May 2, 2024
@Eilon Eilon removed the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label May 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Span LineHeight Wrong on Android
6 participants