Line height does not work as expected
Description
The LineHeight property of Label is documented as the multiplier to apply to the default line height when displaying text
While it is counter-intuitive (I expected it to be an absolute value like the FontSize), it is possible to follow the documentation. However, this raises the next questions:
- how to calculate this multiplier?
- what is the default line height?
- why is the multiplier not simply calculated as the division of the absolute line height and the absolute font size?
With an absolute line height of 20, I would expect that 3 lines of text have a height of 60, so that text would not be clipped if I put it in a container that has a height of 60.
With a font size of 14, I would expect that the required LineHeight multiplier is 20/14 = 1.42857.
When I use those settings for a label, a Label item of 3 lines
- gets an inspected height of 66 if it is free-sized (10% too large)
- has clipped text if the
HeightRequestproperty is set to the expected value of 60.
When I use font size 14 and set the LineHeight multiplier to 1.25, a Label item of 3 lines
- gets an inspected height of 60 if it is free-sized (the correct height)
- has text that fits in the expected box if the
HeightRequestproperty is set to the expected value of 60.
Steps to Reproduce
- New Maui project
- Add
Labelstyling to theMainPage:
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontFamily"
Value="PoppinsRegular" />
<Setter Property="FontSize"
Value="14" />
<Setter Property="LineHeight"
Value="1.42857" />
<Setter Property="CharacterSpacing"
Value="-0.28" />
<Setter Property="LineBreakMode"
Value="WordWrap" />
<Setter Property="TextColor"
Value="Black" />
</Style>
</ContentPage.Resources>
- Add 2
Labels with aTextproperty that will span 3 lines (no need to insert line feeds or enters, it should flow automatically), likeLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
- Set one
Label'sBackgroundto some color, to visualize the element size - Set the colored
Label'sHeightRequestto60(3 lines with an absolute line height of 20). - See that the text in the colored
Labelis clipped. - Use
XAML Live Previewto see that theLabelnot havingHeightRequestset is actually higher than the expected value of 60
Link to public reproduction project repository
https://github.com/hansmbakker/bugrepro-lineheight
Version with bug
8.0.0-rc.1.9171
Is this a regression from previous behavior?
Not sure, did not test other versions
Last version that worked well
Unknown/Other
Affected platforms
Android
Affected platform versions
No response
Did you find any workaround?
Manually fiddling with the LineHeight, property which is a bad developer experience.
Relevant log output
No response
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.
@jsuarezruiz please note that while I reproduced it on Android, the root of the issue is across platforms - on other platforms the calculation is also not clear from the documentation.
Note: empirically we found that the default line height seems to be 16 (at least on Android), but it is unclear what that depends on. This assumption unfortunately does not work in all places in our app 🤦🏼♂️ and it is unclear what it depends on.
So if we apply the formula required lineheight in maui = lineheight in px / 16 then in some places we get deviations and we would need inline overrides.
Please also note, that the Android font scaling (users able to drag a slider in the Android Settings app, changing the default font size) seems to add another variable to the mix.
As a workaround I'm investigating a few Label control handlers to tame this control. They
- set line height in an absolute way
- on Android's
TextViewdepending onOperatingSystem.IsAndroidVersionAtLeast(...)-
textView.SetLineHeight((int)ComplexUnitType.Sp, (float)controlsLabel.LineHeight); -
textView.LineHeight = scaledPixels; -
textView.SetLineSpacing(scaledPixels, 0);
-
- on iOS's
UILabel
- on Android's
- set the
LabelsHeightRequestto the absolute lineheight in case the amount of lines is 1 (to work aroundOn Android, the Label.LineHeight property only changes the line height of text that wraps onto multiple lines.)
I would have appreciated more feedback from Microsoft here though - having to implement a design with unpredictable behavior is energy consuming, to put it mildly.
I just had the following issue: the first line as a greater height than this others.
So I found that using FormattedString just fixed it. I hope it can help someone:
<Label>
<Label.FormattedText>
<FormattedString>
<Span Text="TEXT HERE........." LineHeight="3"/>
</FormattedString>
</Label.FormattedText>
</Label>
@pierrebelin while I'm glad for you that it solves your problem, I believe it is not related to the original issue (how to determine the correct value for LineHeight and how to use LineHeight in a predictable way across platforms).
What you're seeing is uneven spacing between lines, which is a different thing. Without seeing your original code, the line height should be expected to be distributed evenly and using FormattedString this way should not be required to achieve that. Your issue might be the issue discussed in https://github.com/dotnet/maui/issues/24171.
I can repro this issue at Android platform on the latest 17.12.0 Preview 1.0(8.0.82 & 8.0.80).
First of all, I completely agree with the confusing nature of the LineHeight property. It being a multiplier is unexpected, and then determining how that is calculated is nigh impossible and would love to see either the implementation and/or documentation improved.
For others reading this, I did figure out what's happening for Windows (WinUI) specifically by digging into the MAUI source code. At this time the application I'm working on is exclusively for Windows so I have not investigated other platforms.
The underlying TextBlock has a LineHeight property that is an absolute value in pixels. The value assigned by Maui to the TextBlock's LineHeight is label.LineHeight * platformControl.FontSize
The source for that is: Microsoft.Maui.Platform.TextBlockExtensions.UpdateLineHeight.
So if your design indicates a font size of 28 and a line height of 36, the multiplier used in MAUI styles (at least for Windows) is: 36/28 = 1.2857.
Hope that helps someone out!
@PureWeen @jsuarezruiz what is the status of this? I see it's planned for .NET 10, have you done some investigation into it already?
Also: please not that this is not only Android-related (@jsuarezruiz added the Android tag but this issue is relevant to all platforms).
@jfversluis can you please look into this?
It would be great to confirm if this is still an issue for the latest .NET 9 release or even .NET 10 preview release. Maybe we already fixed this without knowing? Otherwise, we continually monitory our priorities and it seems this didn't quite make it to the top of the list yet, sorry.
@jfversluis there are multiple issues here:
- a developer productivity issue with having to fiddle to figure out the correct lineheight value. The Maui text api does not enable developers to build typography how designers design it (i.e. designers work in e.g. Figma and design a paragraph with font size and line height properties. The size of the paragraph should result from the text flowing). This api did not change I believe, so there is nothing to retest.
- the clipping / sizing issue. A reproduction is given at the top, it would be great if you could try it.
I added a summary at the opening post of the issue.
How is this not a priority? Text rendering on Android looks really bad because of it. I can’t even center text vertically without awkward workarounds. What’s also frustrating is that line heights differ between Android and iOS — on Android it’s noticeably larger for no apparent reason. And why do you keep asking whether the issue can be confirmed? Don’t you use your own framework?
I have verified this issue using .NET 9 and MAUI version 9.0.82, and unable to reproduce the issue on Android (Pixel Api 35), iOS ( iPhone 16 iOS 18.5), MacCatalyst and Windows platforms. I have attached the screenshot for reference.
This feels like wasting developers time - asking them to retry without changes having been made to this in the framework.
Just ran into this today. I am not familiar with how this feature should work but in my experience as someone getting a design in Figma and building it, iOS is working great, Android is double broken.
In my experience a designer designs it in Figma and they set the LineHeight to say, 120%. So we set LineHeight to 1.2 and on iOS this works (vertical text alignment is off as it needs to be centered to match Figma) but the actual height of the label is a 1:1 pixel match of what was designed. It does not matter if the label is 1 line or two.
With Android it does not work when the label is 1 line. It does not match iOS/Figma when it is multiple lines. IMO Android is double broken here.
Repro incoming.
TLDR: Overall I would say LineHeight is unusable in its current state. It does nothing for single line on Android, works great for multiline. But then iOS/MacCatalyst it makes the text bottom aligned and has a bug where it sometimes cuts off the last line. Windows does literally nothing with the property. I would hate to go down the path of character spacing and find mode skeletons. As mentioned earlier, it is incredibly difficult (impossible?) to take a design that has any form of LineHeight to actually working as intended.
============
This is my repro project. I think it shows the difficulties of transferring designs to apps, and then the bugs related to the apps not displaying correctly to each other.
https://github.com/beeradmoore/maui-issue-LineHeightBug
It is an app that makes a VerticalStackLayout that is centered and 350px wide.
At the top of it is a red box that is 100x100 (this is to help/confirm image size is scaled as expected)
Below this there are 8 labels:
- 4 with a font size of 15
- 4 with a font size of 30
Of those groupings:
- Half are setup to be single line
- The other half are multi-line
Of those groupings:
- Half of those have
LineHeightset to1.0, which I believe is136.2%in Figma - The other half have
LineHeightset to1.284, which I believe is175%in Figma.
Each of these Labels has black text and alternates with a different gray colour as BackgroundColor so we can see where they start/end.
The text in the label is {FontName} {FontSize}, {LineHeight}, {FigmaPercent}{Other text to wrap if required}
These odd scaling and LineHeight was calculated as follows.
I ran the app on iOS Simulator and loaded the font as a UIFont. Its LineHeight property was 20.427246 when its FontSize was set to 15. 20.427246 / 15 = 1.3618164, or 136.2%. Likewise 136.2 * X = 175, doing some math, X = 1.2850484103. I believe this is the same math that @hansmbakker was doing in their original post.
From these tests I ran the app on Android, iOS, macOS, and Windows. All the results and comments below will be broken down by OS. I took screenshots of each and put them in Figma, resized where required, and then overlayed text that should be the same size. The text in Figma is light blue with some transparency and has a semi-transparent red background so we can see where they start/end and how the text aligns with the Label underneath.
Here are all my results.
Android
Summary: In general works good for multiline, works bad for single line. Multiline is actually much much better than iOS shown later. There is some horizontal drift in the characters which is a minor issue.
First line is good, for some reason text is a little narrower than Figma.
Second line shows that LineHeight on a single line of text does nothing. This is noted in the docs, but IMO it should be fixed instead of noted that it just doesn't work.
Third line is perfect aside from some slight horizontal character drift. Non-issue IMO.
Fourth line is broken, again, this is noted in the docs but not ideal.
Fifth line is again perfect aside from horizontal character drift.
Sixth is the first time LineHeight tries to do anything. It is off from what we expect. I don't know if my LineHeight calculation is broken, or if I am meant to calculate LineHeight on a per-platform basis (notes on this in Additional Details below). Total Label height is larger and text (black from device) sits higher than expected.
Seventh line, perfect, horizontal drift, etc etc.
Eighth line shows the same issues as the sixth where text is higher than expected. The word wrapping is off slightly, but that it not the focus here.
iOS
Summary: LineHeight appears to set the height as intended. However text is vertically aligned to the bottom. Setting VerticalTextAlignment or VerticalOptions does not fix this. This worse than Android for multiline as now there is huge gap up the top which is not ideal. Interestingly the horizontal drift of characters was not present.
First line was perfect. No notes.
Second line has great height, however the text Is lower than expected.
Third line, perfect. No notes. This would make my designer so happy to see.
Fourth line, line height works pretty well. Text is much lower than expected. If you were to put this on a page it would look funny as there is a weird gap up the top. Unsure if there is a native iOS property we can toggle to fix this.
Fifth line, perfect, no notes.
Sixth line, text is lower than expected. Is not as noticeable with smaller font sizes.
Seventh line has a huge bug. Height looks great, text alignment looks great. But my last word "lines" is completely missing! IMO huge bug.
Eighth line again has the issue where the bottom is missing. This time "multiple lines" text is just gone. Alignment is lower than expected.
macOS
Summary: This screenshot is from when UIDeviceFamily=6. The repro still has it set to the default of 2. The difference here (and it's a real pinpoint of out of the box MAUI for MacCatalyst) is there if it is 2 the app is scaled down 30%. So that box that is 100x100 is actually 77x77 on screen. This has been reported on this repo a few times, as well as directly to David Ortinau on Twitter.
UIDeviceFamily aside, both labels in both modes all appear to be the same, and all appear to match what we saw on iOS (bugs and all!).
First line was perfect. No notes.
Second line has great height, however the text Is lower than expected.
Third line, perfect. No notes.
Fourth line, line height works pretty well. Text is much lower than expected. If you were to put this on a page it would look funny as there is a weird gap up the top. Unsure if there is a native iOS property we can toggle to fix this.
Fifth line, perfect, no notes.
Sixth line, text is lower than expected. Is not as noticeable with smaller font sizes.
Seventh line has a huge bug. Height looks great, text alignment looks great. But my last word "lines" is completely missing! IMO huge bug.
Eighth line again has the issue where the bottom is missing. This time "multiple lines" text is just gone. Alignment is lower than expected.
Windows
(Sorry for the yellow tint 😅)
Summary: Broken. Does not work at all.
First line text is a little lower than expected. But at size 15 we are talking like a pixel. Not a huge deal.
Second line is much like Android. It is mentioned in the docs that this property won't have any impact on single line labels. IMO I feel that this is a bug that should be resolved rather than just noted in the docs that it doesn't work.
Third line, perfect, no notes.
Fourth line much like the second, has issues because LineHeight does nothing on single line texts.
Fifth line, perfect, no notes.
Sixth line identical to the fifth line. This would be good if they didn't have different LineHeight set, but they do, and they are multi-line so they should not be the same.
Seventh, perfect. There is some vertical drift, but super minor.
Eighth line is identical to the seventh line which again, broken. LineHeight is not doing anything.
Additional notes
The math for LineHeight that I used comes from loading the font as a UIFont and then getting its property LineHeight. This is in my FontDetails class. However attempting to get the same details on Android there is not LineHeight on the TypeFace class. I don't know if some combinations for Top or Bottom or something is mean to mean the LineHeight and if doing the math again to get a different value to set as a LineHeight would yield better results.
In the constructor of MainPage I left in the code to load a font and then console log it (as json, heh) to see the properties that were loaded. Some of these properties are not set if the underlying tech didn't load it (eg. iOS does not have Bottom, Android does not have CapHeight)
With all fonts as FontSize=15` I get these different values.
| Property | Android | iOS | MacCatalyst | Windows |
|---|---|---|---|---|
| Ascender | -13.916016 | 16.032715 | 16.032715 | |
| Descender | 3.6621094 | -4.3945312 | -4.3945312 | |
| Leading | 0 | |||
| CapHeight | 10.708008 | 10.708008 | ||
| LineHeight | 20.427246 | 20 | 27.2363281 | |
| XHeight | 8.027344 | 8.027344 | ||
| Top | -15.842285 | |||
| Bottom | 4.0649414 |
Some takeaways from this. iOS and MacCatalyst loaded different LineHeight. 20.427246 vs 20 👀
I have seen claims that on Android that Math.Abs(Top) + Math.Abs(Bottom) should be LineHeight. If this is correct then Android has it as 19.9072264. This is different from 20.427246 from iOS and 20 from MacCatalyst, and from 27.2363281 on Windows. I am unsure if that means MacCatalyst default font is equivalent to 133.33% for MacCatalyst and 132.71% for Android, and 118.15% for Windows?
In our production app we actually set SetIncludeFontPadding to false for labels to get text to align correctly. We are using a different font there, FrutigerLTStd. Whatever that change was is not required for OpenSans.
For that production app I had added this to MainProgram.cs,
#if ANDROID
Microsoft.Maui.Handlers.LabelHandler.Mapper.AppendToMapping("AndroidRemoveFontPadding", (handler, view) =>
{
handler.PlatformView.SetIncludeFontPadding(false);
});
#endif