Skip to content

Commit 538463e

Browse files
[Android] Fix Label with MaxLines truncating text in horizontal ScrollView (dotnet#34279)
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause PR dotnet#33281 added a `GetDesiredSize()` override in `LabelHandler.Android.cs` to fix issue dotnet#31782 (WordWrap labels reporting full constraint width instead of actual text width). The fix computes the longest wrapped line and returns that as the desired width. This causes a regression when `MaxLines` is set on the label: 1. `GetDesiredSize()` is called at the full available width — text wraps cleanly within MaxLines limit 2. The fix returns the shorter "longest line" width 3. The label is arranged at that narrower width 4. At the narrower width, the same text needs more lines — exceeding MaxLines → text is clipped ### Description of Change The `GetDesiredSize()` override now uses a double-measurement strategy: 1. **Entry guard**: Only applies the width-narrowing when `Ellipsize == null` (no active truncation). 2. **Compute candidate width**: Finds the widest rendered line as before. 3. **Safety check** (only when `MaxLines` is explicitly set): Re-measures the TextView at exactly the narrowed pixel width. If the re-measurement shows the text would now exceed `MaxLines`, the original full width is returned instead. 4. **Narrow when safe**: If the re-measurement confirms the same or fewer lines, the narrowed width is returned — preserving the dotnet#31782 alignment fix even for labels with explicit `MaxLines`. This avoids both regressions: - Labels without `MaxLines` behave as before (alignment fix preserved, no second measure). - Labels with `MaxLines` that have line-count headroom also get the alignment fix. ### Issues Fixed Fixes dotnet#34120 ### Tested platforms - [x] Android - [x] Windows - [x] iOS - [x] Mac **Files Changed in this PR:** | File | Change | |------|--------| | `src/Core/src/Handlers/Label/LabelHandler.Android.cs` | Double-measurement fix (~20 lines) | | `src/Controls/tests/TestCases.HostApp/Issues/Issue34120.cs` | New UI test HostApp page | | `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34120.cs` | New NUnit UI test | **Regression Reference:** - Regressed by: PR dotnet#33281 - Introduced in: 10.0.40 - Works in: 10.0.30, 10.0.31 - Platform: Android only ### Screenshots |Before|After| |--|--| |<img width="540" alt="image" src="https://github.com/user-attachments/assets/4c365c06-6aa9-4471-9553-d46983ec66c7" >|<img width="540" alt="image" src="https://github.com/user-attachments/assets/d67723d9-fd79-4dcc-8451-f1537f8b3668" >|
1 parent 3e2acb4 commit 538463e

8 files changed

Lines changed: 132 additions & 1 deletion

File tree

296 KB
Loading
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
namespace Maui.Controls.Sample.Issues;
2+
3+
[Issue(IssueTracker.Github, 34120, "Label text truncated in ScrollView when MaxLines is set", PlatformAffected.Android)]
4+
public class Issue34120 : ContentPage
5+
{
6+
// Reproduces the N3_Navigation layout: horizontal ScrollView with BindableLayout,
7+
// 200×200 Border cards, Image (HeightRequest=120), and a Label with MaxLines=2.
8+
record Issue34120MonkeyItem(string Name, string ImageUrl);
9+
10+
public Issue34120()
11+
{
12+
// Long names ("Golden Snub-nosed Monkey", "Tonkin Snub-nosed Monkey") are the ones
13+
// that triggered truncation; "Baboon" is a short-name reference card.
14+
var monkeys = new List<Issue34120MonkeyItem>
15+
{
16+
new("Golden Snub-nosed Monkey", "golden.jpg"),
17+
new("Baboon", "papio.jpg"),
18+
new("Tonkin Snub-nosed Monkey", "bluemonkey.jpg"),
19+
new("Howler Monkey", "alouatta.jpg"),
20+
new("Squirrel Monkey", "saimiri.jpg"),
21+
};
22+
23+
var itemTemplate = new DataTemplate(() =>
24+
{
25+
var image = new Image
26+
{
27+
Aspect = Aspect.AspectFit,
28+
HeightRequest = 120,
29+
HorizontalOptions = LayoutOptions.Center,
30+
VerticalOptions = LayoutOptions.Center,
31+
};
32+
image.SetBinding(Image.SourceProperty, "ImageUrl");
33+
34+
var nameLabel = new Label
35+
{
36+
FontSize = 14,
37+
FontAttributes = FontAttributes.Bold,
38+
BackgroundColor = Color.FromArgb("#AAFFFFFF"),
39+
TextColor = Colors.Black,
40+
Padding = new Thickness(4, 2),
41+
HorizontalOptions = LayoutOptions.Center,
42+
VerticalOptions = LayoutOptions.Center,
43+
HorizontalTextAlignment = TextAlignment.Center,
44+
LineBreakMode = LineBreakMode.WordWrap,
45+
MaxLines = 2,
46+
};
47+
nameLabel.SetBinding(Label.TextProperty, "Name");
48+
nameLabel.SetBinding(Label.AutomationIdProperty, "Name");
49+
50+
var card = new Border
51+
{
52+
Padding = new Thickness(10),
53+
Stroke = Colors.LightGray,
54+
StrokeThickness = 1,
55+
WidthRequest = 200,
56+
HeightRequest = 200,
57+
BackgroundColor = Colors.White,
58+
Content = new VerticalStackLayout
59+
{
60+
Spacing = 10,
61+
Children = { image, nameLabel }
62+
}
63+
};
64+
return card;
65+
});
66+
67+
var horizontalStack = new HorizontalStackLayout
68+
{
69+
Spacing = 15,
70+
Padding = new Thickness(5),
71+
};
72+
BindableLayout.SetItemTemplate(horizontalStack, itemTemplate);
73+
BindableLayout.SetItemsSource(horizontalStack, monkeys);
74+
75+
Content = new ScrollView
76+
{
77+
Orientation = ScrollOrientation.Horizontal,
78+
Content = horizontalStack,
79+
};
80+
}
81+
}
107 KB
Loading
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using NUnit.Framework;
2+
using UITest.Appium;
3+
using UITest.Core;
4+
5+
namespace Microsoft.Maui.TestCases.Tests.Issues;
6+
7+
public class Issue34120 : _IssuesUITest
8+
{
9+
public override string Issue => "Label text truncated in ScrollView when MaxLines is set";
10+
11+
public Issue34120(TestDevice device) : base(device) { }
12+
13+
[Test]
14+
[Category(UITestCategories.Label)]
15+
public void LabelNotTruncatedWithMaxLines()
16+
{
17+
// Wait for the page to load, then verify labels are not truncated.
18+
App.WaitForElement("Golden Snub-nosed Monkey");
19+
VerifyScreenshot();
20+
}
21+
}
128 KB
Loading
325 KB
Loading
325 KB
Loading

src/Core/src/Handlers/Label/LabelHandler.Android.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra
2222

2323
// Android TextView reports full available width instead of actual text width when
2424
// text wraps to multiple lines, causing incorrect positioning for non-Fill alignments.
25+
// We narrow the desired width to the widest rendered line, but only when that narrowing
26+
// won't cause re-wrapping that exceeds MaxLines and truncates visible text.
2527
if (VirtualView.HorizontalLayoutAlignment != Primitives.LayoutAlignment.Fill &&
2628
PlatformView?.Layout is Layout layout &&
27-
layout.LineCount > 1)
29+
layout.LineCount > 1 &&
30+
PlatformView.Ellipsize == null)
2831
{
2932
float maxLineWidth = 0;
3033
for (int i = 0; i < layout.LineCount; i++)
@@ -38,7 +41,33 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra
3841
{
3942
var actualWidth = Context.FromPixels(maxLineWidth + PlatformView.PaddingLeft + PlatformView.PaddingRight);
4043
if (actualWidth < size.Width)
44+
{
45+
// When MaxLines is constrained, verify that narrowing doesn't cause the text
46+
// to re-wrap into more lines than MaxLines allows (which would truncate text).
47+
// Re-measure at exactly the pixel width the view will be arranged at.
48+
if (PlatformView.MaxLines != int.MaxValue)
49+
{
50+
var narrowedPx = (int)Context.ToPixels(actualWidth);
51+
52+
// AtMost mirrors how the layout pass constrains width, ensuring the
53+
// re-measurement reflects the same wrapping behaviour the view will
54+
// experience when arranged at actualWidth.
55+
PlatformView.Measure(
56+
MeasureSpecMode.AtMost.MakeMeasureSpec(narrowedPx),
57+
MeasureSpecMode.Unspecified.MakeMeasureSpec(0));
58+
59+
// Fail-safe: if Layout is null after re-measurement we cannot verify
60+
// that truncation won't occur, so return the original size.
61+
var measuredLayout = PlatformView.Layout;
62+
63+
if (measuredLayout is null || measuredLayout.LineCount > PlatformView.MaxLines)
64+
{
65+
return size; // Narrowing causes truncation (or unverifiable); return original size
66+
}
67+
}
68+
4169
return new Size(actualWidth, size.Height);
70+
}
4271
}
4372
}
4473

0 commit comments

Comments
 (0)