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

[Android] Entire words omitted & letters truncated from Label display #17884

Closed
jonmdev opened this issue Oct 7, 2023 · 14 comments · Fixed by #27179
Closed

[Android] Entire words omitted & letters truncated from Label display #17884

jonmdev opened this issue Oct 7, 2023 · 14 comments · Fixed by #27179
Labels
area-controls-label Label, Span area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing platform/android 🤖 s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage t/bug Something isn't working
Milestone

Comments

@jonmdev
Copy link

jonmdev commented Oct 7, 2023

Description

With specific settings, entire words are not rendered in Labels in Android. And in other circumstances letters are being truncated off. This is obviously a major problem as we cannot have random words and letters not displayed.

For example, here is my test code which reproduces both a missing word and a missing letter, to replace the code in App.xaml.cs in a default program:

using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Platform;
using System.Diagnostics;

namespace Label_Shadow_Bug {
    public partial class App : Application {

        public event Action screenSizeChanged = null;
        public double screenWidth = 0;
        public double screenHeight = 0;
        ContentPage mainPage;
        public App() {

            InitializeComponent();

            //=========
            //LAYOUT
            //=========
            mainPage = new();
            mainPage.Background = Colors.MediumAquamarine;
            MainPage = mainPage;
            mainPage.SizeChanged += delegate {
                invokeScreenSizeChangeEvent();
            };

            AbsoluteLayout abs = new();
            mainPage.Content = abs;

            VerticalStackLayout vert = new();
            abs.Add(vert);

            Label label = new();
            label.Text = "CHAT MISSING_WORD";
            label.TextColor = Colors.White;
            label.FontSize = 23; //changing font size can fix bug
            label.FontFamily = "MontserratBold";
            label.Shadow = new() { Offset = new Point(0, 1), Radius = 1, Brush = Colors.Aqua }; 
            label.MaxLines = 1;
            label.LineBreakMode = LineBreakMode.NoWrap;
            label.HorizontalOptions = LayoutOptions.Center;

            Border newBorder = new();
            newBorder.HorizontalOptions = LayoutOptions.Center;
            newBorder.Content = label;
            newBorder.BackgroundColor = Colors.DarkCyan;
            newBorder.Padding = new Thickness(6, 2);
            newBorder.Margin = new Thickness(0, 4);
            newBorder.StrokeShape = new RoundRectangle() { CornerRadius = 12 };
            newBorder.StrokeThickness = 0.5;
            newBorder.HandlerChanged += delegate {
#if ANDROID
                Android.Views.View androidView = ElementExtensions.ToPlatform(newBorder, newBorder.Handler.MauiContext);
                newBorder.Shadow = new() { Offset = new Point(0, androidView.Context.ToPixels(5)), Radius = androidView.Context.ToPixels(4) }; //android needs re-scaling of shadows due to other reported bug elsewhere
#else
                newBorder.Shadow = new() { Offset = new Point(0, 5), Radius = 4 };
#endif
            };
            
            newBorder.Content = label;
            vert.Add(newBorder);

            Label label2 = new();
            label2.Text = "ALEXANDRA";
            label2.FontFamily = "NotoSansBold";
            label2.FontSize = 42;
            label2.HorizontalOptions = LayoutOptions.Center;
            label2.TextColor = Colors.White;
            label2.Margin = new Thickness(0, -5, 0, 10);
            label2.HandlerChanged += delegate {
#if ANDROID
                Android.Views.View androidView = ElementExtensions.ToPlatform(label2, label2.Handler.MauiContext);
                label2.Shadow = new() { Offset = new Point(0, androidView.Context.ToPixels(5)), Radius = androidView.Context.ToPixels(7) }; //android needs re-scaling of shadows due to other reported bug elsewhere
#else
                label2.Shadow = new() { Offset = new Point(0, 5), Radius = 7 };
#endif
            };
            vert.Add(label2);

            //==================
            //RESIZE FUNCTION
            //==================
            screenSizeChanged += delegate {
                vert.HeightRequest = screenHeight;
                vert.WidthRequest = screenWidth;
            };
            Debug.WriteLine("FINISHED BUILD OKAY");

        }
        private void invokeScreenSizeChangeEvent() {
            if (mainPage.Width > 0 && mainPage.Height > 0) {
                screenWidth = mainPage.Width;
                screenHeight = mainPage.Height;
                Debug.WriteLine("main page size changed | width: " + screenWidth + " height: " + screenHeight);
                screenSizeChanged?.Invoke();
            }
        }
    }
    
}

The code should display this as it does in Windows:

android missing words windows

This is what actually happens in Android:

android missing words android

As you can see, the word "MISSING_WORD" is missing. Yet space is still maintained for it. You just don't see the word. In fact, it seems to have wrapped the "MISSING_WORD" to the next line as extra space is maintained for it vertically.

And the name "ALEXANDRA" is now "ALEXANDR" - the last letter has been chopped off.

By contrast in Windows/iOS I have not seen this bug at all.

We obviously can't have entire words going missing in our Labels on Android. Even to just display an app's terms of service, this is a legal impossibility. A fix would be great ASAP.

Thanks for your help.

Steps to Reproduce

  1. Open GitHub repository.
  2. Play in standard Google Pixel 5 API33 Android emulator and see the words and letters missing.

Link to public reproduction project repository

https://github.com/jonmdev/Label-Shadow-Bug

Version with bug

7.0.92

Is this a regression from previous behavior?

No, this is something new

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

Android API 33, .NET 7.0, Google Pixel 5 Emulator

Did you find any workaround?

There seems to be none.

@jonmdev jonmdev added the t/bug Something isn't working label Oct 7, 2023
@jsuarezruiz jsuarezruiz added platform/android 🤖 area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing area-controls-label Label, Span labels Oct 9, 2023
@jsuarezruiz jsuarezruiz added this to the Backlog milestone Oct 9, 2023
@ghost ghost added the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label Oct 9, 2023
@ghost
Copy link

ghost commented Oct 9, 2023

We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.

@jonmdev jonmdev changed the title Entire words ommitted from Label display in Android with specific shadow/hierarchy settings (no issue in Windows/iOS) Entire words ommitted & letters truncated from Label display in Android under consistently reproducable conditions (Simple repro project included) Oct 11, 2023
@jonmdev
Copy link
Author

jonmdev commented Oct 11, 2023

I have revised my issue report, as I am now observing this issue also truncates letters off of words (previously I noted it was truncating words entirely - now today I see it can truncate words mid-way as well under other circumstances).

I simplified my code to < 100 lines and demonstrated the bug in both situations - a word being missing completely from one label and a word being partially truncated from another. I updated my post accordingly.

This is an absolutely crippling bug for Android. @jsuarezruiz @samhouts

There is no reasonable way someone can release an app for Android using MAUI when it is truncating letters and words. How could we possibly? Even just to display our app "Terms of Service" this is legally impossible. Words and letters cannot be missing even just from a legal standpoint. I don't believe the lawyers will permit it.

Is there any way this might be possible to set at a high priority? If we could get this fixed for .NET 8.0 (along with an Editor fix for iOS which is now p/1 and thanks for that: #17757) then at least .NET 8.0 may be reliably usable which would be great.

Thanks for all your help.

@XamlTest XamlTest added s/verified Verified / Reproducible Issue ready for Engineering Triage s/triaged Issue has been reviewed labels Oct 12, 2023
@XamlTest
Copy link

XamlTest commented Oct 12, 2023

Verified this on Visual Studio Enterprise 17.8.0 Preview 3.0(8.0.0-rc.2.9373). Repro on Android 13.0-API33, not repro on Windows 11 and iOS 16.4 with below Project:
Label Shadow Bug.zip

@samhouts samhouts changed the title Entire words ommitted & letters truncated from Label display in Android under consistently reproducable conditions (Simple repro project included) [Android] Entire words omitted & letters truncated from Label display Oct 14, 2023
@tschramme86
Copy link

Probably related to #11358 - although this issue was closed as fixed, I still can reproduce it under certain conditions. Multiline labels are cutting words on Android, because apparently their height is calculated wrong.

@williambuchanan2
Copy link

I have had a lot of these issues. Most of the time I have managed to work around them by changing the container (i.e. the border or stack layout).

For example, a recent issue I had where a label was just vanishing off the right side of the screen rather than wrapping was caused by it being in a grid with the columndefinition=Auto setting. Changing it to columndefinition=* fixed the problem.

I have also noticed that using VerticalStackLayout and HorizontalStackLayout cause problems when you want to use the full screen, so I mostly just use StackLayout which fixes most of these problems. Also need to play a bit with horizontalalignment - most of the time you need to specify FillAndExpand otherwise it doesn't fill...

@jonmdev
Copy link
Author

jonmdev commented Jun 13, 2024

This appears to be due to rounding errors in the measurement system.

Swapping the demo project App.xaml.cs for this "fixes" the problem and you can see the missizing that triggers the issue with the debugs also by turning the "fix" bool on/off. You can see the "fix" function by building to Pixel 5 emulator. If false, the words will clip. If true, they won't.

#if ANDROID
using Android.Content;
#endif
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using System.Diagnostics;

namespace Label_Shadow_Bug {
#if ANDROID
    class CMauiTextView: MauiTextView {
        
        bool applyFix = true; //TO APPLY FIX FOR TEXT CLIPPING IN ANDROID, VISIBLE ON PIXEL 5 EMULATOR

        public CMauiTextView(Context context) : base(context) {
            System.Diagnostics.Debug.WriteLine("CREATE CMAUITEXTVIEW");
        }
        protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) {

            //https://stackoverflow.com/questions/14493732/what-are-widthmeasurespec-and-heightmeasurespec-in-android-custom-views

            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);

            var scaledDensity = DeviceDisplay.Current.MainDisplayInfo.Density; //= 2.75 on Pixel 5 emulator
            System.Diagnostics.Debug.WriteLine("LABEL: " + Text + " ANDROID WIDTH RAW " + + widthSize + " SCALED " + widthSize / scaledDensity);

            base.OnMeasure(newWidthSpec, heightMeasureSpec); //must be for cases where difference is greater than one char (layout width limit)
            //base.OnMeasure(widthMeasureSpec, heightMeasureSpec); //must be for cases where difference is greater than one char (layout width limit)
        }
    }
#endif
    public partial class App : Application {

        public event Action screenSizeChanged = null;
        public double screenWidth = 0;
        public double screenHeight = 0;
        ContentPage mainPage;

        
        public App() {

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

            //=========
            //LAYOUT
            //=========
            mainPage = new();
            mainPage.Background = Colors.MediumAquamarine;
            MainPage = mainPage;
            mainPage.SizeChanged += delegate {
                invokeScreenSizeChangeEvent();
            };

            AbsoluteLayout abs = new();
            mainPage.Content = abs;

            VerticalStackLayout vert = new();
            abs.Add(vert);

            Label label = new();
            label.Text = "CHAT MISSING_WORD";
            label.TextColor = Colors.White;
            label.FontSize = 23; //changing font size can fix bug
            label.FontFamily = "MontserratBold";
            label.Shadow = new() { Offset = new Point(0, 1), Radius = 1, Brush = Colors.Aqua }; 
            label.MaxLines = 1;
            label.LineBreakMode = LineBreakMode.NoWrap;
            label.HorizontalOptions = LayoutOptions.Center;

            Border newBorder = new();
            newBorder.HorizontalOptions = LayoutOptions.Center;
            newBorder.Content = label;
            newBorder.BackgroundColor = Colors.DarkCyan;
            newBorder.Padding = new Thickness(6, 2);
            newBorder.Margin = new Thickness(0, 4);
            newBorder.StrokeShape = new RoundRectangle() { CornerRadius = 12 };
            newBorder.StrokeThickness = 0.5;
            newBorder.HandlerChanged += delegate {
#if ANDROID
                Android.Views.View androidView = ElementExtensions.ToPlatform(newBorder, newBorder.Handler.MauiContext);
                newBorder.Shadow = new() { Offset = new Point(0, androidView.Context.ToPixels(5)), Radius = androidView.Context.ToPixels(4) }; //android needs re-scaling of shadows due to other reported bug elsewhere
#else
                newBorder.Shadow = new() { Offset = new Point(0, 5), Radius = 4 };
#endif
            };
            
            newBorder.Content = label;
            vert.Add(newBorder);
            
            Label label2 = new();
            label2.Text = "ALEXANDRA";
            label2.FontFamily = "NotoSansBold";
            label2.FontSize = 42;
            label2.HorizontalOptions = LayoutOptions.Center;
            label2.TextColor = Colors.White;
            label2.Margin = new Thickness(0, -5, 0, 10);
            label2.HandlerChanged += delegate {
#if ANDROID
                Android.Views.View androidView = ElementExtensions.ToPlatform(label2, label2.Handler.MauiContext);
                label2.Shadow = new() { Offset = new Point(0, androidView.Context.ToPixels(5)), Radius = androidView.Context.ToPixels(7) }; //android needs re-scaling of shadows due to other reported bug elsewhere
#else
                label2.Shadow = new() { Offset = new Point(0, 5), Radius = 7 };
#endif
            };
            label2.SizeChanged += delegate {
                Debug.WriteLine("LABEL: " + label2.Text + " SIZE W " + label2.Width + " H " + label2.Height);
            };
            vert.Add(label2);

            //==================
            //RESIZE FUNCTION
            //==================
            screenSizeChanged += delegate {
                vert.HeightRequest = screenHeight;
                vert.WidthRequest = screenWidth;
            };
            Debug.WriteLine("FINISHED BUILD OKAY");

        }
        private void invokeScreenSizeChangeEvent() {
            if (mainPage.Width > 0 && mainPage.Height > 0) {
                screenWidth = mainPage.Width;
                screenHeight = mainPage.Height;
                Debug.WriteLine("main page size changed | width: " + screenWidth + " height: " + screenHeight);
                screenSizeChanged?.Invoke();

                
            }
        }
    }
    
}

@Marioo1357
Copy link
Contributor

So crutial error, would be nice to fix it from Maui's framework side

@kubaflo
Copy link
Contributor

kubaflo commented Dec 28, 2024

Hi! It seems to be working in NET9. Maybe try again?

@kubaflo kubaflo added the s/try-latest-version Please try to reproduce the potential issue on the latest public version label Dec 28, 2024
@dotnet-policy-service dotnet-policy-service bot removed this from the Backlog milestone Jan 5, 2025
@jonmdev
Copy link
Author

jonmdev commented Jan 15, 2025

This has not been fixed. Nothing has changed. I updated the posted bug project to .NET 9 and this is same result:

Image

@jonmdev
Copy link
Author

jonmdev commented Jan 15, 2025

@kubaflo Please reopen this.

@dotnet-policy-service dotnet-policy-service bot removed the s/try-latest-version Please try to reproduce the potential issue on the latest public version label Jan 15, 2025
@kubaflo kubaflo reopened this Jan 15, 2025
@kubaflo
Copy link
Contributor

kubaflo commented Jan 15, 2025

@jonmdev I've created a simple application in Android studio to learn how the native TextView behaves

MAUI

<VerticalStackLayout VerticalOptions="Center">
          <Label Text="CHAT MISSING_WORD"
                 TextColor="Red"
                 FontSize="23"
                 WidthRequest="{Binding Value, Source={x:Reference slider}}"
                 MaxLines="1"
                 LineBreakMode="WordWrap"
                 HorizontalOptions="Center">
          </Label>
          <Slider
               x:Name="slider"
               Minimum="0"
               Maximum="300"/>
     </VerticalStackLayout>

Android Studio aka native android app

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="CHAT MISSING_WORD"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="23sp"
        android:maxLines="1"
        android:gravity="center" />

    <SeekBar
        android:id="@+id/seekBar"
        android:layout_width="400dp"
        android:layout_height="wrap_content"
        android:min="0"
        android:max="1000" />
</LinearLayout>

As you can see the issue boils down to how Android deals with one-line-forced-text. What I would recommend is to set LineBreakMode to TailTruncation or explicitly set width of your label. I know that it might not be what you want, but it seems to be 'android' thing

@jfversluis @mattleibow @PureWeen thoughts?

MAUI Native
Screen.Recording.2025-01-15.at.18.05.27.mov
Screen.Recording.2025-01-15.at.18.07.36.mov

@kubaflo
Copy link
Contributor

kubaflo commented Jan 15, 2025

You can also remove MaxLines="1"

@kubaflo
Copy link
Contributor

kubaflo commented Jan 15, 2025

There's actually one more hacky solution if you really want MaxLines="1" and no truncation

.ConfigureMauiHandlers(handlers =>
{
    Microsoft.Maui.Handlers.LabelHandler.Mapper.AppendToMapping(nameof(Label), (handler, view) =>
    {
#if ANDROID
    handler.PlatformView.SetSingleLine(true);
#endif
    });
})
Screen.Recording.2025-01-15.at.20.10.33.mov

@jonmdev
Copy link
Author

jonmdev commented Jan 15, 2025

As you can see the issue boils down to how Android deals with one-line-forced-text. What I would recommend is to set LineBreakMode to TailTruncation or explicitly set width of your label. I know that it might not be what you want, but it seems to be 'android' thing

The issue as I have documented across numerous threads is that Android and Maui are not measuring Labels correctly.

The "solution" I used was:

(1) Create my own "label measurer" class that can reliably tell me how big a label will be based on font, size, text, and max width in Windows/iOS/Android - this was a pain in the ass but it worked.

There were numerous issues to contend with. You have to scale based on the ToPixels or FromPixels multiplier or in WIndows the scalar in a particular order of operations. There is also in Android I believe I recall a rounding on the font size, which also must be applied in a particular order of operations.

Eventually though it is possible to correctly anticipate Label sizes in all OS's from Maui.

(2) Stop using VerticalStackLayout or HorizontalStackLayout or setting Label as content of Border. I now design everything flat like:

AbsoluteLayout
--Border
--Label

Then measure my labels and do my own layout accordingly.

Thus I no longer need a "solution" as I have "solved" this. But I think this is still a massive issue. I have been shocked no one at Maui wants to even talk about it for over a year now. I believe this type of thing left unfixed is hugely sabotaging the project.

It is absolutely unacceptable in any app to have words going missing. That is why I developed such an extensive workaround. My workaround is favorable to me as it has also solved many of the innumerable layout glitches like these you see here: #21580 (comment)

I can reliably layout my text and Borders and everything always works based on my manual "measure" then "layout" method. In theory this also makes my design more agnostic. If Maui died tomorrow I could port it to something else as long as it has something like an AbsoluteLayout and I can predict label sizes. So I don't mind in the end. It probably helps me long term.

But no one else is going to do all this work. No one should have to.

The average user or developer is going to see words going missing in their app or objects not lining up and they will simply quit assuming the whole system is broken. This hurts all of us. It hurts Microsoft Maui team because if there isn't ongoing interest in the project, Microsoft has less incentive to keep it funded. And it hurts all of us who hitched our wagon to Maui. We need a robust community to keep it alive as well. Or we have to find another system to port over to in 3-5 years when it is abandoned. None of us want to do that.

The biggest issues with Maui have been in my opinion the endless layout glitches, of which this is one. I am happy at least there is starting to be some acknowledgment of how broken the layout system is and guys like @albyrock87 have proposed some fixes that are very slowly working through.

This entire project is shooting itself in the foot by not working on fixing these basic layout types of things. Maui is nothing if it can't even layout text or images correctly and efficiently. And without doing crazy work like I have to avoid the built in intended layout system, it still can't.

I don't personally understand how the Maui layout system works so I will be no help in fixing it. But I can clearly and easily reproduce problems, which should be enough for someone who does understand it to look deeper if there is interest.

albyrock87 added a commit to albyrock87/maui that referenced this issue Jan 25, 2025
albyrock87 added a commit to albyrock87/maui that referenced this issue Jan 27, 2025
PureWeen pushed a commit to albyrock87/maui that referenced this issue Jan 30, 2025
@jsuarezruiz jsuarezruiz added this to the .NET 9 SR4 milestone Feb 3, 2025
github-actions bot pushed a commit to albyrock87/maui that referenced this issue Feb 3, 2025
PureWeen added a commit that referenced this issue Feb 4, 2025
* Fix #17884 Android TextView truncated under some conditions

* Remove workaround as rounding by epsilon fixed the issue

* Update screenshots

* Update more tests snapshots

* Update more snapshots

* Added a new test to validate Issue 22853

* Pending snapshots

* - update screen shots

* Add conditional compilation for NUnit import.

* Update preprocessor directive for MACCATALYST test

* - update screen shots

---------

Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com>
Co-authored-by: Shane Neuville <shneuvil@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-controls-label Label, Span area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing platform/android 🤖 s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage t/bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants