Skip to content
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

Fix Android TextView being truncated under some conditions #27179

Merged
merged 11 commits into from
Feb 4, 2025

Conversation

albyrock87
Copy link
Contributor

@albyrock87 albyrock87 commented Jan 16, 2025

Detailed Issue information

Android emulators (e.g., with a display density multiplier of 2.625) expose an issue when arranging views.
Consider this label arrangement:

X = 112.00000000000001dp
Y = 318.66666666666669dp
Width = 187.42857142857142dp
Height = 22.095238095238095dp

When converted to pixels (Math.Ceiling used by MAUI):

X = 294.00000000000006 => 295px
Y = 836.5 => 837px
Width = 491.99999999999994 => 492px
Height = 58px

Expected platform rectangle width: 492px.
Actual width due to rounding error: 491px, causing content to be cut off.

Issues Identified

  1. Rounding Error
    The calculation of X + Width involves independent conversion of dp values to px, e.g.:
    (187.42857142857142 + 112.00000000000001) * 2.625 = 786px
    
    Here, 786px - 295px = 491px, which is 1px short of the expected width.
  2. Layout Calculation
    The use of dp instead of px for layout leads to inaccuracies when scaling.
    A DeviceTest has been added to show what I'm talking about (failing Assert has been commented):
    image
    When converting a Rect to a px one we basically have two possible strategies
    • [current MAUI strategy] Slightly truncate content by converting left and right.
      Less visually disruptive, except for text as explained by @kubaflo here.
    • Allow overlap by 1px to match desired size by converting left and width.
      Undesirable for grid layouts like ColumnDefinitions=",,," where content is positioned side by side.

Proposed Fixes

To prevent truncation:

  1. Apply a better rounding strategy by verifying if the value is close enough to the lower integer.
    For example, 294.00000000000006 should round to 294px instead of 295px because 0.00000000000006 is less than a given Epsilon = 0.0000000001.
  2. Account for the missing pixel in the measure pass for TextView by adding 1px to its measured size:
    internal static Size GetDesiredSizeFromHandler(this IViewHandler viewHandler, double widthConstraint, double heightConstraint)
    {
        // ...
        if (viewHandler.PlatformView is TextView)
        {
            measuredWidth += 1;
        }
    }

Description of Change

We can fix this specific issue by using any of these solutions, so I decided to keep only the first one and defer the second one in case the issue reappears under some special conditions.

Issues Fixed

Fixes #17884
Fixes #26619

@albyrock87 albyrock87 requested a review from a team as a code owner January 16, 2025 13:43
@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Jan 16, 2025
@veikkoeeva
Copy link

@albyrock87 Just a random person here, but that looks like a solid explanation to add also in the code so it's easily discoverable also later to whoever reads the code.

@albyrock87
Copy link
Contributor Author

I've added the comments in the code.

That said, I'm afraid this PR will generate UITests failures everywhere and it'll take forever to capture all the screenshots.

@albyrock87
Copy link
Contributor Author

albyrock87 commented Jan 16, 2025

We have another option to not break all the UI tests: keep the Ceiling rounding for coordinates too, and change this behavior only with .NET10.

@rmarinho
Copy link
Member

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@rmarinho
Copy link
Member

In theory it should generated the needed screenshots and we can just replace, or we can ask for help to finish that tedious work of updating the screenshots.

@PureWeen
Copy link
Member

I've added the comments in the code.

That said, I'm afraid this PR will generate UITests failures everywhere and it'll take forever to capture all the screenshots.

once we see the screenshots that'll give us an idea of the magnitude of change. There's definitely a lot :-)
We'll see how subtle/large it is once those are uploaded

@PureWeen
Copy link
Member

PureWeen commented Jan 17, 2025

I think this change makes sense...

I'm a little curious about the initial conversion from px to dp and if we should be rounding that

Like, if a DP value is 294.0000000000001 it seems like we should just convert that to 294 when doing the PX to DP conversion

I'm also wondering if we should midpoint round the width and height also. It seems like that's how Android suggests always doing the rounding https://developer.android.com/training/multiscreen/screendensities#dips-pels

@albyrock87
Copy link
Contributor Author

albyrock87 commented Jan 17, 2025

Like, if a DP value is 294.0000000000001 it seems like we should just convert that to 294 when doing the PX to DP conversion

I'm afraid that value comes from the centering algorithm (in this specific example) and not from PX to DP conversion.

Obviously the centering algorithm uses DP coming from a measure pass which is in PX, but that's a plain px / density without any kind of rounding and that seems correct to me.

I also wonder why we're going through float while converting DP double to PX int.

@jsuarezruiz jsuarezruiz added area-layout StackLayout, GridLayout, ContentView, AbsoluteLayout, FlexLayout, ContentPresenter platform/android 🤖 labels Jan 17, 2025
{
EnsureMetrics(self);

return (float)Math.Round(dp * s_displayDensity, MidpointRounding.AwayFromZero);
Copy link
Contributor

Choose a reason for hiding this comment

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

Apart from the UITest, could be nice to include some device tests checking conversions.

@PureWeen
Copy link
Member

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@rmarinho
Copy link
Member

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@PureWeen
Copy link
Member

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@jonmdev
Copy link

jonmdev commented Feb 1, 2025

Proposed Fixes

To prevent truncation:

  1. Apply a better rounding strategy by verifying if the value is close enough to the lower integer.
    For example, 294.00000000000006 should round to 294px instead of 295px because 0.00000000000006 is less than a given Epsilon = 0.0000000001.

  2. Account for the missing pixel in the measure pass for TextView by adding 1px to its measured size:
    cs internal static Size GetDesiredSizeFromHandler(this IViewHandler viewHandler, double widthConstraint, double heightConstraint) { // ... if (viewHandler.PlatformView is TextView) { measuredWidth += 1; } }

Description of Change

We can fix this specific issue by using any of these solutions, so I decided to keep only the first one and defer the second one in case the issue reappears under some special conditions.

Thanks @albyrock87 as always for looking into this and explaining it so succinctly. This adds more information to something that has puzzled me now for 14+ months.

I think it's interesting that fix # 2 appears to a variation of the same "fix" (workaround) I posted back in June 2024 (8 months ago now), while this issue has been entirely ignored by Microsoft formally since report in Oct 2023 (14 months ago).

My "workaround" posted here was to override the platform view type and then apply this same +1 you are doing to the width there:

#if ANDROID
            LabelHandler.PlatformViewFactory = (handler) => {
                return new CMauiTextView(handler.Context); //Create custom MauiTextView for CLabelHandler
            };
#endif 

#if ANDROID
class CMauiTextView: MauiTextView {
        
        bool applyFix = true; //TO APPLY FIX FOR TEXT CLIPPING IN ANDROID

        protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) {

            var measured = Paint.MeasureText(Text);

            int widthSize = widthMeasureSpec.GetSize();
            Android.Views.MeasureSpecMode widthMode = widthMeasureSpec.GetMode();

            if (applyFix) {
                widthSize += 1;
            }

            int newWidthSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec(widthSize, widthMode);
            base.OnMeasure(newWidthSpec, heightMeasureSpec); 
        }
}
#endif 

At the time I frankly thought this was the wrong approach as it felt so crude and dumb to just add a pixel to the width. I felt like the rounding errors should be fixed intrinsically in how Maui handles things so that we don't have to do this.

i.e. I feel like Maui should not be rounding things that it cannot safely be rounding.

Thanks as always for adding clarity to such things.

@PureWeen
Copy link
Member

PureWeen commented Feb 3, 2025

/rebase

@PureWeen
Copy link
Member

PureWeen commented Feb 3, 2025

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@PureWeen
Copy link
Member

PureWeen commented Feb 4, 2025

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@PureWeen
Copy link
Member

PureWeen commented Feb 4, 2025

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@albyrock87
Copy link
Contributor Author

Failing tests look unrelated

@PureWeen PureWeen enabled auto-merge (squash) February 4, 2025 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-layout StackLayout, GridLayout, ContentView, AbsoluteLayout, FlexLayout, ContentPresenter community ✨ Community Contribution platform/android 🤖
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Border label text cut off [Android] Entire words omitted & letters truncated from Label display
10 participants