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

Labels (TextViews) in Android are bigger than they should be. Values returned by Label.Measure() are wrong. This is causing Maui Labels to clip and wrap abnormally. Any fix or help? #21474

Open
jonmdev opened this issue Mar 27, 2024 · 8 comments
Labels
area-controls-label Label, Span platform/android 🤖 t/bug Something isn't working
Milestone

Comments

@jonmdev
Copy link

jonmdev commented Mar 27, 2024

ISSUES:

1) Labels in Android clip and wrap inappropriately:

2) Label.Measure() returns wrong data in Android:

FURTHER INVESTIGATION:

I have created a plain .NET Android program in Visual Studio by File > New > Android Application. I have tested StaticLayout there (the native Android TextView/Label measurement method) and it returns the exact same results as it does in Maui with the same settings. This indicates StaticLayout is working fine in Maui.

I have also tested in a .NET Android Application creating a TextView manually and its size is always within 1 pixel (rounding to int) of the predicted StaticLayout size. This indicates StaticLayout works in Android Application.

However, in Maui, Label is always BIGGER than the native Android TextView/StaticLayout with identical settings.

This is true whether measuring via Label.Width, Label.Measure(), or getting the native Android TextView of the Label and checking its natural TextView.Width size (if you don't override it but just take it as it is from whatever Maui is doing).

This demonstrates the problem is definitely a Maui Label/Measure issue.

MAUI LABEL SIZING ERRORS:

Label.Measure() or Label.Width returns excessive sizes by up to 9-10+ pixels in width for a short string like "HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO" compared to the identical Android Project .NET TextView or Android/Maui StaticLayout.

PROOF:

I have made one .NET Android Project and one Maui Project. They both just put a Label on screen with identical settings and then measure it by view width/height and StaticLayout (the Android built in measurement method).

  • In both cases (Maui & .NET Android), the Static Layout returns the same size of 676.03125 x 28.
  • In .NET Android the label text object view measures within 1 pixel of this by width and height at 677 x 28.
  • In Maui Android, however, the label object measures way off, at 686.18 x 27

Maui is then off in width by around 9 pixels and it should be no surprise this is glitching things and causing abnormally wrapping and clipping.

REPRO PROJECTS:

1) .NET ANDROID:

2) MAUI:

CONCLUSION: LABEL.MEASURE() IS WRONG IN MAUI ANDROID:

It seems Label.Measure() code is wrong in Maui Android. It seems Label.Measure() is inflating the expected size of the Label, and thus wrapping/clipping the TextView when it is set to the correct size.

Label.Measure() should be using StaticLayout in Android Maui (that is the only way to measure multiline text in Android). If it is, it is implementing it wrong. And/or the parameters being stored and passed into it by Label are wrong.

However, I can't find or read the actual Maui code that Label.Measure() utilizes.

FIXING IT:

What is the Maui code that Label.Measure() uses exactly? How can we fix this? I cannot find it or read it to see the error.

@davidortinau

What can we do about this problem?

Thanks

@jonmdev jonmdev added the t/bug Something isn't working label Mar 27, 2024
@drasticactions drasticactions changed the title MAUI is creating TextViews (Labels) in Android which are bigger than they should be (bigger than in .NET Android Application or expected by StaticLayout), thus causing wrapping/clipping glitches. || What exact code does Label.Measure() use??? It is absolutely wrong in Android ... Can we see it to fix it??? TextViews (Labels) in Android which are bigger than they should be. Mar 27, 2024
@drasticactions
Copy link
Contributor

I've updated the title to make it clear what the issue is.

@jonmdev jonmdev changed the title TextViews (Labels) in Android which are bigger than they should be. TextViews (Labels) in Android are bigger than they should be. Values returned by Label.Measure() are wrong. What is the Android code for Label.Measure()? Mar 27, 2024
@dotnet-policy-service dotnet-policy-service bot added the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label Mar 27, 2024
@PureWeen PureWeen added this to the Backlog milestone Mar 27, 2024
@Eilon Eilon removed the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label May 10, 2024
@Domik234
Copy link

Domik234 commented Jun 4, 2024

This is still active and critical problem.

Most of the time it happens in these two cases:

Screenshot

image

  1. Trimmed letters
    Word: Ostatní is trimmed by one letter - So visible part is Ostatn - In this case it's label in HorizontalStackLayout -> ScrollView (Horizontal Orientation) (happens in many types of layouts)

  2. Last letter is on new line (propably the same as 1., but visible part is on new line and the first line is not visible (3 = 2023, 2 = 2022)). This is Grid with 3 columns and 2 rows. ColDef [auto, *, auto], RowDef [auto, auto]. Icon is set to RowSpan = 2 and VerticalOptions are set to Center.

Most of the time I am using custom Font - NunitoSans - Regular or Bold.

Just thinking: What if there is a conversion from double/float to int that rounds down value like 3.4 to 3 (missing 0.4 px), so text has not enough space to show and so it hides?

@jonmdev
Copy link
Author

jonmdev commented Jun 5, 2024

This is still active and critical problem.

Most of the time it happens in these two cases:
Screenshot

  1. Trimmed letters
    Word: Ostatní is trimmed by one letter - So visible part is Ostatn - In this case it's label in HorizontalStackLayout -> ScrollView (Horizontal Orientation) (happens in many types of layouts)

  2. Last letter is on new line (propably the same as 1., but visible part is on new line and the first line is not visible (3 = 2023, 2 = 2022)). This is Grid with 3 columns and 2 rows. ColDef [auto, *, auto], RowDef [auto, auto]. Icon is set to RowSpan = 2 and VerticalOptions are set to Center.

Most of the time I am using custom Font - NunitoSans - Regular or Bold.

Just thinking: What if there is a conversion from double/float to int that rounds down value like 3.4 to 3 (missing 0.4 px), so text has not enough space to show and so it hides?


Thank you for posting here as well. Yes, this is a huge problem that is causing random clipping of words and letters randomly. I have been severely troubled by this problem since I first posted about it 8 months ago. But I have heard nothing back from anyone at Microsoft regarding it so far. I am still hoping for some guidance or help.

I just now updated my OP in this thread with the demo projects I made proving the problem is Maui's measurement system in Android.

StaticLayout (Android's built in measurement system) works the same whether it is a plain .NET Android project or a Maui project. And in Android .NET the StaticLayout measuring method returns within 1 pixel of the actual text view's dimensions on screen.

However, in Maui, the Label is up to 9-10 pixels off in width. This severe sizing error is causing things to clip and wrap and behave abnormally.

I cannot figure out how Maui is getting these terribly wrong sizes. I have looked through the code as far as I can and can't find where the error is occurring as I can't find the function that actually does the measuring.

I have submitted numerous bug reports and repro projections on this trying to narrow it all down and I have come this far but can't go further.

It is now a great fear for me as I have a major Maui project otherwise reaching conclusion. But like your project example, words are getting randomly clipped and truncated in Android and it is not workable.

@davidortinau Any help or suggestion?

@jonmdev jonmdev changed the title TextViews (Labels) in Android are bigger than they should be. Values returned by Label.Measure() are wrong. What is the Android code for Label.Measure()? TextViews (Labels) in Android are bigger than they should be. Values returned by Label.Measure() are wrong. This is causing Maui Labels to clip and wrap abnormally. Any fix or help? Jun 5, 2024
@jonmdev jonmdev changed the title TextViews (Labels) in Android are bigger than they should be. Values returned by Label.Measure() are wrong. This is causing Maui Labels to clip and wrap abnormally. Any fix or help? Labels (TextViews) in Android are bigger than they should be. Values returned by Label.Measure() are wrong. This is causing Maui Labels to clip and wrap abnormally. Any fix or help? Jun 5, 2024
@jonmdev
Copy link
Author

jonmdev commented Jun 6, 2024

LIKELY EXPLANATION

I have managed to narrow down the likely explanation I think.

Android rounds all font sizes down (Floor) to the int.

The problem is then density scalar must be used in the proper way to get the expected results (ie.

var scaledDensity = DeviceDisplay.Current.MainDisplayInfo.Density; //= 2.75 on Pixel 5 emulator

This means if you use a given fontSize and:

  1. Divide the font size by the density scalar when setting it to a native Android view (ie. textView.TextSize = fontSize / scaledDensity
  2. Use the font size at the full given value, and then instead divide the Label/TextView dimensions by the density value for layout (ie. scaledWidth = textView.MeasuredWidth / scaledDensity)

These two approaches will give APPROXIMATE but not the same values. They can be off by up to 10-11 pixels which will easily account for truncating or wrapping a letter wrong.

Accommodating for this (using correct order of operations for scaling method) allows one to get a StaticLayout's measurement to match a Maui Label or native TextView in Maui. So it is possible.

Likely the clipping and wrapping issues are due to this issue. Labels are mis-sized compared to their "equivalent" .NET Android labels due to this discrepancy. Maui applies the scales to font sizes differently from .NET Android in terms of where and how it uses the scaling factors (ie. ToPixels & FromPixels).

It is hard for me to fully explain how this is messing things up on Maui's layout system (as I don't understand Maui that well and I have not fully thought it through). But this demonstrably accounts for the sizing discrepancies, and I can only presume the sizing discrepancies are then what are causing the clipping and wrapping.

I have used this finding to work around the issue in my app as I am doing my own layout system (not using Maui's really - just using AbsoluteLayouts). I can now get consistent layouts without clipping. But this only helps me not others.

But for it to be fixed in Maui, this will likely need to be where the issue is investigated. ie.

In Android, due to native floor int rounding of font sizes, scaling the font size is not the same as scaling the outputted Label/TextView dimensions. These two methods create similar but different results. If any system is mismatched in how it is scaling (in measurement and layout) things will clip or not fit.

Just FYI @jonathanpeppers perhaps or @davidortinau if you ever want to try to fix this for other users' sake.

@AlleSchonWeg
Copy link

The .measure() is bugy since years. Never fixed issues in xf. It must completely rewrite. There are problems with floor, ceiling and rounding in the Framework, which results in incorrect values.

@Domik234
Copy link

Domik234 commented Jun 11, 2024

I was playing with the label measuring and solved my problem for all my cases on Android. Not the ideal way but works for me.

How To
  1. Create new file to Project/Platforms/Android/Views/CMauiTextView.cs
using Android.Content;
using Microsoft.Maui.Platform;

namespace App.Platforms.Android.Views
{
    internal class CMauiTextView : MauiTextView
    {
        public CMauiTextView(Context context) : base(context)
        {

        }

        public override void SetLayerPaint(global::Android.Graphics.Paint paint)
        {
            base.SetLayerPaint(paint);
            measuredCharZ = null; //remove saved measurement of char Z because of changing paint
        }

        private float? measuredCharZ; //value for saving measurement of char Z for difference comparison

        protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
        {
#if DEBUG
            var text = Text?.Substring(0, Text.Length > 16 ? 16 : Text.Length);
#endif
            var allowed = measuredCharZ ??= Paint.MeasureText("Z");
            var measured = Paint.MeasureText(Text);
            var widthSize = widthMeasureSpec.GetSize();

            var diff = Math.Abs(widthSize - measured); //difference of measured text natively vs. MAUI
            if (diff > 0 && diff < allowed) //if there is difference and difference is smaller than character Z
            {
#if DEBUG
                Console.WriteLine($"Remeasuring: {widthSize} width changed to {measured} with allowed difference {diff}/{allowed} for text: {text} (capped)");
#endif
                base.OnMeasure((int)Math.Ceiling(measured), heightMeasureSpec);
                return;
            }

            base.OnMeasure(widthMeasureSpec, heightMeasureSpec); //must be for cases where difference is greater than one char (layout width limit)
        }
    }
}
  1. Create new file to Project/Platforms/Android/CLabelHandler.cs
using AndroidX.AppCompat.Widget;
using Microsoft.Maui.Handlers;
using App.Platforms.Android.Views;

namespace App.Platforms.Android
{
    internal class CLabelHandler : LabelHandler
    {
        protected override AppCompatTextView CreatePlatformView()
        {
            return new CMauiTextView(Context); //Create custom MauiTextView for CLabelHandler
        }
    }
}
  1. Configure usage of new CLabelHandler for Label (Android Only)
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureMauiHandlers(handlers => //MUST be after UseMauiApp to replace LabelHandler!
            {
#if ANDROID
                handlers.AddHandler(typeof(Label), typeof(CLabelHandler)); //Replace LabelHandler with CLabelHandler for Label
#endif
            });

        return builder.Build();
    }

Everytime it was only ONE character that made mess in my case. I've tried to edit OnMeasure on Android and add there to widthConstraint size + 16 (16 because as @jonmdev said earlier - it is up to 10-11 pixels so closest higher power number of 2). This has broke some views but non visible text was visible.

Because it was always ONE character - I've measured one character (in my case Z - looked like biggest one when I wrote it but propably it should be W 🤣🤣 + also this has to be done for every textview solo because there can be different setting of text (font, characterSpacing)) using Paint.MeasureText() and used it as allowed overflowing value. So if difference of measured whole text and original widthConstraint from MAUI was not greater than allowed difference of one character - use measured whole text for widthConstraint instead of the original one.


Results

Before:
image

After:
image

@jonmdev
Copy link
Author

jonmdev commented Jun 13, 2024

Based on your method you posted and my work from it, @Domik234, I think we are dealing with multiple issues:

1) Shadow Label Issue

When you apply a Shadow to a Label in Android there is a slight mis-size which can be "solved" by just going up by 1 int point in the OnMeasure width. I demonstrated the "solution" to my shadow clipping bug project here based on your code idea:

#17884 (comment)

2) Static Layout Issue

When using StaticLayout to predict the Maui sizes as noted, one must accommodate the scaling density factor in the right order of operations due to the floor rounding of font sizes. ie. multiply or divide the font size and max width vs. scale the measured size that is outputted.

This appears to be a separate issue which can lead to up to 10-12 pixels difference in manually calculated StaticLayout sizes for Labels vs their Maui counterparts but can be solved by adopting the same order that Maui does to get the same results.

3) Your Clipping Issue

Your clipping issue is different from my shadow clipping bug report, as my shadow clipping bug report can be solved by just a 1 int boost, while I presume from what you are demonstrating in your fix, you are seeing the bigger sizing issues like I am talking about in (2). I am not sure exactly how this all fits together. Perhaps it has something to do with the parent view or layout invoking StaticLayout using the wrong method as per point (2)? I can't say.


In any case, thanks for the interesting method and adding to the puzzle. FYI, your method can be shortened by cutting out the CLabelHandler. You can instead just run somewhere in your code before the first Label is made:

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

This works as a nifty easy way to override the method of creating the native view (CreatePlatformView) if that is all you are doing.

The full depth of the problems associated with this rounding/scaling issue and the correct solution is beyond me at this point. In any case, I have figured out as much as I need to to "solve" the issue for me. I am no longer personally experiencing problems from it.

There should also be enough info and methods here at this point for the Maui team between all these threads to track it down further and actually fix it if there is time and will.

@Domik234
Copy link

Domik234 commented Jun 13, 2024

Primary result of this experimenting/testing was to provide a solution to resolve combination of these problems for incoming people. I think that everyone is nervous about the situation with dismissed XF support and many of us accutely tries to solve these problems alone to be able to release new version of applications with closest performance and working UI as much as it it possible.

I've tried to clone maui repo but I am not able to build it cause of like 10k errors throwing at me at VS and Rider so I was not able to experiment directly on maui's source. (Maybe I have some trash in work PC?)

Many of these errors were "BINDINGSGENERATOR" object not found, many also duplicate attribute and many were completely different for each case.

I've used the custom handler because I thought that there will be need of overriding GetDesiredSize originally, but later I've found that it may not be neccessary.

Anyway let's see if this helps at least a bit to the MAUI team. 😄

Thanks for description and additional work on that, @jonmdev !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-controls-label Label, Span platform/android 🤖 t/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

6 participants