diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyTextIsNotMissing.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyTextIsNotMissing.png new file mode 100644 index 000000000000..8a99aedaf819 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifyTextIsNotMissing.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue17884.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue17884.xaml new file mode 100644 index 000000000000..835f8c186991 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue17884.xaml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue17884.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue17884.xaml.cs new file mode 100644 index 000000000000..b2fc8ba1fecc --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue17884.xaml.cs @@ -0,0 +1,17 @@ + +namespace Maui.Controls.Sample.Issues +{ + [Issue(IssueTracker.Github, 17884, "[Android] Entire words omitted & letters truncated from Label display", PlatformAffected.Android)] + public partial class Issue17884 : ContentPage + { + public Issue17884() + { + InitializeComponent(); + } + + private void OnStubLabelTapped(object sender, EventArgs e) + { + ++StubLabel.FontSize; + } + } +} diff --git a/src/Controls/tests/TestCases.HostApp/MauiProgram.cs b/src/Controls/tests/TestCases.HostApp/MauiProgram.cs index d10a58ca8c51..7fadcbae7c19 100644 --- a/src/Controls/tests/TestCases.HostApp/MauiProgram.cs +++ b/src/Controls/tests/TestCases.HostApp/MauiProgram.cs @@ -21,6 +21,7 @@ public static MauiApp CreateMauiApp() fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("FontAwesome.ttf", "FA"); fonts.AddFont("ionicons.ttf", "Ion"); + fonts.AddFont("Montserrat-Bold.otf", "MontserratBold"); }) .RenderingPerformanceAddMappers() .Issue21109AddMappers() diff --git a/src/Controls/tests/TestCases.HostApp/Resources/Fonts/Montserrat-Bold.otf b/src/Controls/tests/TestCases.HostApp/Resources/Fonts/Montserrat-Bold.otf new file mode 100644 index 000000000000..cdfb83df2f2f Binary files /dev/null and b/src/Controls/tests/TestCases.HostApp/Resources/Fonts/Montserrat-Bold.otf differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17884.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17884.cs new file mode 100644 index 000000000000..c867e7c3a3bc --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17884.cs @@ -0,0 +1,25 @@ +#if ANDROID +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues +{ + public class Issue17884 : _IssuesUITest + { + public Issue17884(TestDevice device) + : base(device) + { } + + public override string Issue => "[Android] Entire words omitted & letters truncated from Label display"; + + [Test] + [Category(UITestCategories.Visual)] + public void VerifyTextIsNotMissing() + { + App.WaitForElement("StubLabel"); + VerifyScreenshot(); + } + } +} +#endif \ No newline at end of file diff --git a/src/Core/src/Handlers/ViewHandlerExtensions.Android.cs b/src/Core/src/Handlers/ViewHandlerExtensions.Android.cs index 654a33f46629..061411fcd618 100644 --- a/src/Core/src/Handlers/ViewHandlerExtensions.Android.cs +++ b/src/Core/src/Handlers/ViewHandlerExtensions.Android.cs @@ -102,10 +102,10 @@ internal static void PlatformArrangeHandler(this IViewHandler viewHandler, Rect { var platformView = viewHandler.ToPlatform(); - var Context = viewHandler.MauiContext?.Context; - var MauiContext = viewHandler.MauiContext; + var context = viewHandler.MauiContext?.Context; + var mauiContext = viewHandler.MauiContext; - if (platformView == null || MauiContext == null || Context == null) + if (platformView == null || mauiContext == null || context == null) { return; } @@ -116,10 +116,7 @@ internal static void PlatformArrangeHandler(this IViewHandler viewHandler, Rect return; } - var left = Context.ToPixels(frame.Left); - var top = Context.ToPixels(frame.Top); - var bottom = Context.ToPixels(frame.Bottom); - var right = Context.ToPixels(frame.Right); + var (left, top, right, bottom) = context.ToPixels(frame); var viewParent = platformView.Parent; if (viewParent?.LayoutDirection == LayoutDirection.Rtl && viewParent is View parentView) @@ -130,7 +127,7 @@ internal static void PlatformArrangeHandler(this IViewHandler viewHandler, Rect right = left + width; } - platformView.Layout((int)left, (int)top, (int)right, (int)bottom); + platformView.Layout(left, top, right, bottom); viewHandler.Invoke(nameof(IView.Frame), frame); } diff --git a/src/Core/src/Platform/Android/ContextExtensions.cs b/src/Core/src/Platform/Android/ContextExtensions.cs index 88ead155d5dd..5d2a40a3fdb6 100644 --- a/src/Core/src/Platform/Android/ContextExtensions.cs +++ b/src/Core/src/Platform/Android/ContextExtensions.cs @@ -104,15 +104,36 @@ public static float ToPixels(this Context? self, double dp) return (float)Math.Ceiling(dp * s_displayDensity); } + /// + /// Converts dp to pixels, rounding to the nearest whole pixel. + /// + /// + /// This is especially useful when converting coordinates to the nearest pixel. + /// Do not use this method when setting sizes, as it may lead to visual truncation of the content + /// when rounding down. + /// + internal static float ToPixelsRound(this Context? self, double dp) + { + EnsureMetrics(self); + + return (float)Math.Round(dp * s_displayDensity, MidpointRounding.AwayFromZero); + } + public static (int left, int top, int right, int bottom) ToPixels(this Context context, Graphics.Rect rectangle) { - return - ( - (int)context.ToPixels(rectangle.Left), - (int)context.ToPixels(rectangle.Top), - (int)context.ToPixels(rectangle.Right), - (int)context.ToPixels(rectangle.Bottom) - ); + // Use `ToPixels` to get enough pixels to display the whole content + var width = (int)context.ToPixels(rectangle.Width); + var height = (int)context.ToPixels(rectangle.Height); + // Use `ToPixelsRound` to position the content on the nearest pixel + var left = (int)context.ToPixelsRound(rectangle.Left); + var top = (int)context.ToPixelsRound(rectangle.Top); + + // Compute the right and bottom edges based on platform-specific sizes + // Do NOT compute them based on the rectangle's right and bottom as it may lead to visual truncation + var right = left + width; + var bottom = top + height; + + return (left, top, right, bottom); } public static Thickness ToPixels(this Context context, Thickness thickness)