[Android] Fix Label with MaxLines truncating text in horizontal ScrollView#34279
[Android] Fix Label with MaxLines truncating text in horizontal ScrollView#34279PureWeen merged 7 commits intodotnet:mainfrom
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34279Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34279" |
🤖 AI Summary📊 Expand Full Review🔍 Pre-Flight — Context & Validation📝 Review Session — Update LabelHandler.Android.cs ·
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34279 | Double-measurement with MaxLines safety check | ✅ PASS (Gate) | LabelHandler.Android.cs |
Original PR |
🚦 Gate — Test Verification
📝 Review Session — Update LabelHandler.Android.cs · c91b66b
Result: ✅ PASSED
Platform: Android
Mode: Full Verification
- Tests FAIL without fix ✅ (truncation reproduced —
LabelNotTruncatedWithMaxLinesscreenshot differs) - Tests PASS with fix ✅ (label text displayed correctly within MaxLines=2)
Test: Issue34120.LabelNotTruncatedWithMaxLines — screenshot verification against snapshots/android/LabelNotTruncatedWithMaxLines.png
Note: Gate was verified in a prior agent session. PureWeen (MAUI team member) approved the PR on 2026-03-04 after reviewing the implementation.
🔧 Fix — Analysis & Comparison
📝 Review Session — Update LabelHandler.Android.cs · c91b66b
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-sonnet) | Pre-check lineCount < MaxLines + Ellipsize == null |
✅ PASS | 1 file | Simplest; zero extra measurements; small theoretical edge-case gap |
| 2 | try-fix (claude-opus) | Width-adjustment buffers (Math.Ceiling, GetLineMax) | ❌ FAIL | 1 file | DIP↔pixel round-trip makes width adjustments unreliable |
| 3 | try-fix (gpt-5.2) | Skip narrowing when inside HorizontalScrollView | ✅ PASS | 1 file | Too narrow in scope — bug could occur outside ScrollView |
| 4 | try-fix (gpt-5.3-codex) | StaticLayout simulation for non-mutating validation | ❌ FAIL | 1 file | Same DIP precision issue as attempt 2 |
| 5 | try-fix (gemini) | Skip narrowing entirely when MaxLines is set | ✅ PASS | 1 file | Too aggressive — loses alignment fix for all MaxLines labels |
| PR | PR #34279 | Double-measurement with MaxLines safety re-check | ✅ PASS (Gate) | 1 file + tests | Most robust; handles all edge cases; small measurement overhead |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| explore (round 2) | 2 | No (variations only) | Paint-based validation (similar to failed attempt 4), deferred PlatformArrange narrowing (adds complexity) |
Exhausted: Yes — all reasonable approach categories explored (pre-check, width adjustment, context check, simulation, skip entirely, re-measurement)
Approach Comparison
Key Insight: Two fundamental strategies work:
- Guard-based (attempts 1, 3, 5): Don't narrow when truncation is likely. Trade: may skip narrowing unnecessarily.
- Verify-based (PR): Narrow, then verify. Trade: extra measurement pass but guaranteed correctness.
Width-adjustment approaches (attempts 2, 4) are provably insufficient due to DIP↔pixel precision loss in the measurement→arrangement pipeline.
Selected Fix: PR's fix
Reason: The PR's double-measurement approach is the most robust:
- Handles ALL edge cases including the theoretical scenario where
lineCount < MaxLinesat full width butlineCount > MaxLinesat narrowed width - The extra measurement cost is negligible (one re-measure only when MaxLines is explicitly set AND width would narrow)
- The
Ellipsize == nullguard avoids the re-measure entirely for labels with active truncation
Attempt 1 is a strong simpler alternative but has a small theoretical gap. For a framework-level handler, the PR's extra safety check is worth the small cost.
📋 Report — Final Recommendation
📝 Review Session — Update LabelHandler.Android.cs · c91b66b
✅ Final Recommendation: APPROVE
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Context gathered, prior blocked reviews imported |
| Gate | ✅ PASSED | Android: tests FAIL without fix, PASS with fix |
| Try-Fix | ✅ COMPLETE | 5 attempts (3 passing, 2 failing), PR's fix selected as best |
| Report | ✅ COMPLETE | PR finalization verified |
Summary
PR #34279 fixes a regression from PR #33281 where GetDesiredSize() width-narrowing for WordWrap labels causes text truncation when MaxLines is set. The fix uses a double-measurement strategy: when narrowing would apply to a label with explicit MaxLines, it re-measures at the candidate width and returns the original width if re-wrapping would exceed MaxLines.
Root Cause
PR #33281 added GetDesiredSize() override to return actual text width instead of full available width for non-Fill aligned wrapping labels. This narrowing causes re-wrapping at arrangement time, and when MaxLines is set, the additional lines are clipped.
Fix Quality
Excellent. The PR's approach is the most robust of all 6 candidates explored:
- 3 simpler alternatives passed tests but have scope limitations (too narrow, too aggressive, or theoretical edge-case gaps)
- 2 width-adjustment approaches failed (DIP↔pixel precision loss is fundamental)
- The double-measurement approach guarantees correctness with negligible overhead (re-measure only when MaxLines is set AND width would narrow)
- Well-commented code explains the rationale for each guard
Ellipsize == nullentry guard avoids unnecessary work
PR Finalization
- Title: ✅ Accurate —
[Android] Fix Label with MaxLines truncating text in horizontal ScrollView - Description: ✅ Exemplary — includes root cause, fix strategy, regression provenance, tested platforms, screenshots
- Tests: ✅ Adequate — UI test reproduces exact bug scenario with screenshot verification
- Code quality: ✅ Good — well-structured, defensive, consistent with codebase patterns
Selected Fix: PR's fix
PR's double-measurement approach is preferred over simpler alternatives because it handles all edge cases at framework-handler level where correctness must be guaranteed.
Human Review
PureWeen (MAUI team member) approved this PR on 2026-03-04 after reviewing the implementation.
📋 Expand PR Finalization Review
Title: ✅ Good
Current: [Android] Fix Label with MaxLines truncating text in horizontal ScrollView
Description: ✅ Excellent
Description needs updates. See details below.
Code Review: ✅ Passed
Code Review — PR #34279
File: src/Core/src/Handlers/Label/LabelHandler.Android.cs
🟡 Medium: Re-measurement Leaves View in Modified State on Bail-out Path
Location: LabelHandler.Android.cs, lines 55–66
PlatformView.Measure(
MeasureSpecMode.AtMost.MakeMeasureSpec(narrowedPx),
MeasureSpecMode.Unspecified.MakeMeasureSpec(0));
var measuredLayout = PlatformView.Layout;
if (measuredLayout is null || measuredLayout.LineCount > PlatformView.MaxLines)
{
return size; // Narrowing causes truncation (or unverifiable); return original size
}Observation:
When the safety check determines that narrowing would exceed MaxLines, the code returns size (the original full width). However, at this point PlatformView.Measure() has already been called with narrowedPx. The TextView's internal layout state is now computed for the narrowed width, while we return a size indicating the view should be arranged at the full original width.
In practice this resolves itself: MAUI's layout pass will call Measure() again with the correct constraint before PlatformArrange. However, between GetDesiredSize returning and the next Measure() call, the view is in an inconsistent state (measured at narrowed width, but reported desired size is full width). This could theoretically cause issues if any code reads PlatformView.Layout in that window.
Recommendation:
This is low-risk in normal MAUI layout flows, but adding a re-measure back to the original constraint before returning would be defensive:
if (measuredLayout is null || measuredLayout.LineCount > PlatformView.MaxLines)
{
// Restore measurement state to the original constraint before returning
PlatformView.Measure(
MeasureSpecMode.AtMost.MakeMeasureSpec((int)Context.ToPixels(size.Width)),
MeasureSpecMode.Unspecified.MakeMeasureSpec(0));
return size;
}This is a non-blocking suggestion. The current behavior is acceptable in the typical MAUI layout flow.
🟡 Low: iOS Snapshot Files for an Android-only Test
Files:
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/LabelNotTruncatedWithMaxLines.pngsrc/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/LabelNotTruncatedWithMaxLines.png
Observation:
The Issue34120 HostApp page is decorated with PlatformAffected.Android, which indicates the bug only manifests on Android. However, the NUnit test (LabelNotTruncatedWithMaxLines) has no platform guard ([Category(UITestCategories.Label)] only), which means it runs on all platforms including iOS.
The iOS snapshot files capture a "correct" state on iOS (where the bug never existed), which is harmless. But it means the test CI gates iOS screenshots that don't test any real behavior — they will pass trivially on iOS but add snapshot maintenance overhead.
Recommendation (non-blocking):
If the test is Android-specific, consider whether a platform guard is appropriate. The existing pattern for Android-only tests is:
// In the NUnit test, add before the assertion:
// if (App.GetTestDevice() != TestDevice.Android)
// Assert.Ignore("This fix is Android-specific.");Alternatively, removing the iOS/mac snapshot files (and not running the test on those platforms) would reduce noise. The current state is acceptable.
✅ Looks Good
-
Ellipsize == nullguard — Correct. When aTruncateAtstrategy is active, the narrowing optimization is skipped entirely. This prevents interaction between two competing layout adjustments. -
PlatformView.MaxLines != int.MaxValuesentinel — Correct. Android's default whenMaxLinesis not set isInteger.MAX_VALUE. Usingint.MaxValueas the "no limit" sentinel matches Android internals. -
measuredLayout is nullfail-safe — Good defensive code.TextView.Layoutcan return null before a view has been laid out; the null guard prevents an NPE and correctly falls back to the safe (original) size. -
MeasureSpecMode.AtMostfor re-measure — Correct choice.AT_MOSTmirrors the constraint passed during a normal layout pass within aScrollViewchild, so the re-measurement accurately predicts actual wrapping behavior. -
Outer
PlatformView?.Layout is Layout layoutnull check — The existing pattern safely skips the optimization if the layout is null, and the new MaxLines safety check also null-checksPlatformView.Layoutafter the extraMeasure(). Consistent and correct. -
Test page structure — The
Issue34120HostApp reproduces the exact real-world scenario reported in the issue: a horizontalScrollViewcontaining aHorizontalStackLayoutofBordercards withImage+Label(MaxLines=2). This is an accurate regression test. -
No API surface change — The fix is entirely internal to
GetDesiredSize. No public APIs changed. NoPublicAPI.Unshipped.txtupdates needed.
Addressed the concerns raised in the AI summary by adding an inline comment to clarify the use of MeasureSpecMode.AtMost and updating the logic to fail safely when Layout is null, ensuring the width is narrowed only when it’s verified to be safe. |
|
/azp run maui-pr-uitests, maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
|
/azp run maui-pr-uitests, maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
…nd layout options (#34533) <!-- Please let the below note in for people that find this PR --> > [!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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details On Android, a Label with LineBreakMode="WordWrap" placed inside a width-constrained layout may clip text on the right side instead of wrapping correctly. This behavior occurs depending on the combination of Flow ### Root Cause PR #33281 introduced a GetDesiredSize() override in LabelHandler.Android.cs to address issue #31782, where WordWrap labels reported the full width constraint instead of the actual text width. The fix narrowed the measured width by computing the longest wrapped line and returning that value as the desired width. However, narrowing the width without proper verification could cause additional line wrapping, leading to text clipping or incorrect layout, especially in RTL or bidirectional text scenarios. Later, PR #34279 restricted this logic to run only when the MaxLines property is explicitly set. As a result, when MaxLines is not defined, the width-narrowing verification is skipped, which can again cause incorrect wrapping and text clipping in certain alignment and layout configurations. ### Description of Change Improved the logic in LabelHandler.Android.cs so that when measuring a Label's desired size, the code now always checks if narrowing the width would cause the text to wrap into more lines than the original measurement. This prevents truncation or clipping of text. ### Validated the behaviour in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed: Fixes #34459 ### Screenshots | Before | After | |---------|--------| | <img height=600 width=300 src="https://github.com/user-attachments/assets/44222872-0093-4a97-af81-49b0e1be4aab"> | <img height=600 width=300 src="https://github.com/user-attachments/assets/27361bd2-8922-4b83-8d70-3d24b27fe9e1"> |
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 from this PR and let us know in a comment if this change resolves your issue. Thank you!
Root Cause
PR #33281 added a
GetDesiredSize()override inLabelHandler.Android.csto fix issue #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
MaxLinesis set on the label:GetDesiredSize()is called at the full available width — text wraps cleanly within MaxLines limitDescription of Change
The
GetDesiredSize()override now uses a double-measurement strategy:Ellipsize == null(no active truncation).MaxLinesis explicitly set): Re-measures the TextView at exactly the narrowed pixel width. If the re-measurement shows the text would now exceedMaxLines, the original full width is returned instead.MaxLines.This avoids both regressions:
MaxLinesbehave as before (alignment fix preserved, no second measure).MaxLinesthat have line-count headroom also get the alignment fix.Issues Fixed
Fixes #34120
Tested platforms
Files Changed in this PR:
src/Core/src/Handlers/Label/LabelHandler.Android.cssrc/Controls/tests/TestCases.HostApp/Issues/Issue34120.cssrc/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34120.csRegression Reference:
Screenshots