diff --git a/src/Controls/src/Core/Label/Label.cs b/src/Controls/src/Core/Label/Label.cs
index cf90933bbb35..d210b63dd87c 100644
--- a/src/Controls/src/Core/Label/Label.cs
+++ b/src/Controls/src/Core/Label/Label.cs
@@ -62,7 +62,8 @@ public partial class Label : View, IFontElement, ITextElement, ITextAlignmentEle
formattedString.Parent = null;
label.RemoveSpans(formattedString.Spans);
}
- }, propertyChanged: (bindable, oldvalue, newvalue) =>
+ },
+ propertyChanged: (bindable, oldvalue, newvalue) =>
{
var label = ((Label)bindable);
@@ -76,7 +77,8 @@ public partial class Label : View, IFontElement, ITextElement, ITextAlignmentEle
label.SetupSpans(formattedString.Spans);
}
- label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+ label.InvalidateMeasureIfLabelSizeable();
+
if (newvalue != null)
label.Text = null;
});
@@ -94,26 +96,21 @@ public virtual string UpdateFormsText(string source, TextTransform textTransform
/// Bindable property for .
public static readonly BindableProperty LineBreakModeProperty = BindableProperty.Create(nameof(LineBreakMode), typeof(LineBreakMode), typeof(Label), LineBreakMode.WordWrap,
- propertyChanged: (bindable, oldvalue, newvalue) => ((Label)bindable).InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged));
+ propertyChanged: (bindable, oldvalue, newvalue) => ((Label)bindable).InvalidateMeasureIfLabelSizeable());
/// Bindable property for .
public static readonly BindableProperty LineHeightProperty = LineHeightElement.LineHeightProperty;
/// Bindable property for .
- public static readonly BindableProperty MaxLinesProperty = BindableProperty.Create(nameof(MaxLines), typeof(int), typeof(Label), -1, propertyChanged: (bindable, oldvalue, newvalue) =>
- {
- if (bindable != null)
- {
- ((Label)bindable).InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
- }
- });
+ public static readonly BindableProperty MaxLinesProperty = BindableProperty.Create(nameof(MaxLines), typeof(int), typeof(Label), -1,
+ propertyChanged: (bindable, oldvalue, newvalue) => ((Label)bindable).InvalidateMeasureIfLabelSizeable());
/// Bindable property for .
public static readonly BindableProperty PaddingProperty = PaddingElement.PaddingProperty;
/// Bindable property for .
public static readonly BindableProperty TextTypeProperty = BindableProperty.Create(nameof(TextType), typeof(TextType), typeof(Label), TextType.Text,
- propertyChanged: (bindable, oldvalue, newvalue) => ((Label)bindable).InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged));
+ propertyChanged: (bindable, oldvalue, newvalue) => ((Label)bindable).InvalidateMeasureIfLabelSizeable());
readonly Lazy> _platformConfigurationRegistry;
@@ -260,24 +257,22 @@ void IFontElement.OnFontAutoScalingEnabledChanged(bool oldValue, bool newValue)
void HandleFontChanged()
{
Handler?.UpdateValue(nameof(ITextStyle.Font));
- InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+ InvalidateMeasureIfLabelSizeable();
}
void ILineHeightElement.OnLineHeightChanged(double oldValue, double newValue) =>
- InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
-
- void OnFormattedTextChanging(object sender, PropertyChangingEventArgs e)
- {
- OnPropertyChanging(nameof(FormattedText));
- }
+ InvalidateMeasureIfLabelSizeable();
void ITextElement.OnTextTransformChanged(TextTransform oldValue, TextTransform newValue) =>
- InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+ InvalidateMeasureIfLabelSizeable();
+
+ void OnFormattedTextChanging(object sender, PropertyChangingEventArgs e) =>
+ OnPropertyChanging(nameof(FormattedText));
void OnFormattedTextChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(nameof(FormattedText));
- InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+ InvalidateMeasureIfLabelSizeable();
}
void SetupSpans(IEnumerable spans)
@@ -357,18 +352,19 @@ void Span_GestureRecognizer_CollectionChanged(object sender, NotifyCollectionCha
void ITextAlignmentElement.OnHorizontalTextAlignmentPropertyChanged(TextAlignment oldValue, TextAlignment newValue)
{
+ // This is a no-op since the horizontal text alignment does not affect bounds or
+ // any other property that would require a measure invalidation.
}
static void OnTextPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var label = (Label)bindable;
- LineBreakMode breakMode = label.LineBreakMode;
- bool isVerticallyFixed = (label.Constraint & LayoutConstraint.VerticallyFixed) != 0;
- bool isSingleLine = !(breakMode == LineBreakMode.CharacterWrap || breakMode == LineBreakMode.WordWrap);
- if (!isVerticallyFixed || !isSingleLine)
- ((Label)bindable).InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+
+ if (TextChangedShouldInvalidateMeasure(label))
+ label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+
if (newvalue != null)
- ((Label)bindable).FormattedText = null;
+ label.FormattedText = null;
}
///
@@ -379,12 +375,12 @@ public IPlatformElementConfiguration On() where T : IConfigPlatform
void ITextElement.OnTextColorPropertyChanged(Color oldValue, Color newValue)
{
+ // This is a no-op since the text color does not affect bounds or
+ // any other property that would require a measure invalidation.
}
- void ITextElement.OnCharacterSpacingPropertyChanged(double oldValue, double newValue)
- {
- InvalidateMeasure();
- }
+ void ITextElement.OnCharacterSpacingPropertyChanged(double oldValue, double newValue) =>
+ InvalidateMeasureIfLabelSizeable();
internal bool HasFormattedTextSpans
=> (FormattedText?.Spans?.Count ?? 0) > 0;
@@ -411,16 +407,75 @@ public override IList GetChildElements(Point point)
return spans;
}
- Thickness IPaddingElement.PaddingDefaultValueCreator()
- {
- return default(Thickness);
- }
+ Thickness IPaddingElement.PaddingDefaultValueCreator() => default;
+
+ void IPaddingElement.OnPaddingPropertyChanged(Thickness oldValue, Thickness newValue) =>
+ InvalidateMeasureIfLabelSizeable();
+
+ Font ITextStyle.Font => this.ToFont();
- void IPaddingElement.OnPaddingPropertyChanged(Thickness oldValue, Thickness newValue)
+ ///
+ /// This method prevents unnecessary measure invalidations when the label is not
+ /// sizeable. If the label has a fixed width and height, then no matter what the
+ /// text is, the label will never change size.
+ ///
+ void InvalidateMeasureIfLabelSizeable()
{
+ if (!IsLabelSizeable(this))
+ return;
+
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
}
- Font ITextStyle.Font => this.ToFont();
+ ///
+ /// Determines if the label can grow in any direction based on the constraints. If the
+ /// label cannot grow in any direction, then we usually don't need to do anything.
+ ///
+ internal static bool IsLabelSizeable(Label label)
+ {
+ // Determine in which direction the label can grow/shrink.
+ var constraint = label.Constraint;
+ var isVerticallySizeable = (constraint & LayoutConstraint.VerticallyFixed) == 0;
+ var isHorizontallySizeable = (constraint & LayoutConstraint.HorizontallyFixed) == 0;
+ var isSizeable = isVerticallySizeable || isHorizontallySizeable;
+
+ // If the label cannot grow in any direction, then we usually don't need to do anything.
+ if (!isSizeable)
+ return false;
+
+ // The label may grow/shrink based on the constraints, so we may need to invalidate.
+ return true;
+ }
+
+ ///
+ /// Determines if the text has changed in a way that would require a measure invalidation.
+ /// Unlike FormattedText changes, Text changes may not always require invalidation because
+ /// the text size and spacing is all uniform. Formatted text may have a case where even
+ /// though the label is a single line, the font size of a span may cause the label to grow
+ /// vertically.
+ ///
+ internal static bool TextChangedShouldInvalidateMeasure(Label label)
+ {
+ // If the label cannot grow in any direction, then we don't need to invalidate.
+ var isSizeable = IsLabelSizeable(label);
+ if (!isSizeable)
+ return false;
+
+ // Determine if the label can grow vertically (wrapping means it may grow vertically).
+ var constraint = label.Constraint;
+ var breakMode = label.LineBreakMode;
+ var isHorizontallySizeable = (constraint & LayoutConstraint.HorizontallyFixed) == 0;
+ var isMultiline = breakMode == LineBreakMode.CharacterWrap || breakMode == LineBreakMode.WordWrap;
+ var isSingleLine = !isMultiline;
+
+ // If the label cannot grow horizontally and is only single line,
+ // then we don't need to invalidate since the only direction it can grow in
+ // is vertically but it never will.
+ if (!isHorizontallySizeable && isSingleLine)
+ return false;
+
+ // The label may grow/shrink based on the constraints, so we need to invalidate.
+ return true;
+ }
}
}
diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs
index 70c46aa8b1dc..077ebc0ad1ee 100644
--- a/src/Controls/src/Core/VisualElement/VisualElement.cs
+++ b/src/Controls/src/Core/VisualElement/VisualElement.cs
@@ -1628,11 +1628,11 @@ static void OnRequestChanged(BindableObject bindable, object oldvalue, object ne
{
var constraint = LayoutConstraint.None;
var element = (VisualElement)bindable;
- if (element.WidthRequest >= 0 && element.MinimumWidthRequest >= 0)
+ if (element.WidthRequest >= 0)
{
constraint |= LayoutConstraint.HorizontallyFixed;
}
- if (element.HeightRequest >= 0 && element.MinimumHeightRequest >= 0)
+ if (element.HeightRequest >= 0)
{
constraint |= LayoutConstraint.VerticallyFixed;
}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/ReduceInvalidateMeasure.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/ReduceInvalidateMeasure.cs
new file mode 100644
index 000000000000..f8b940d70f1f
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/ReduceInvalidateMeasure.cs
@@ -0,0 +1,57 @@
+#if IOS
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+ internal class ReduceInvalidateMeasure : _IssuesUITest
+ {
+ public ReduceInvalidateMeasure(TestDevice device)
+ : base(device)
+ { }
+
+ public override string Issue => "https://github.com/dotnet/maui/pull/21801";
+
+ [Test]
+ public void ReduceInvalidateMeasuresUpdatingLabel()
+ {
+ App.WaitForElement("UpdateTextLabel");
+
+ const int repeats = 2;
+
+ for (int i = 0; i < repeats; i++)
+ {
+ App.Tap("UpdateTextButton");
+ }
+
+ for (int i = 0; i < repeats; i++)
+ {
+ App.Tap("UpdateSizeButton");
+ }
+
+ for (int i = 0; i < repeats; i++)
+ {
+ App.Tap("UpdateFontSizeButton");
+ }
+
+ for (int i = 0; i < repeats; i++)
+ {
+ App.Tap("UpdateLineBreakModeButton");
+ }
+
+ for (int i = 0; i < repeats; i++)
+ {
+ App.Tap("UpdateLineHeightButton");
+ }
+
+ for (int i = 0; i < repeats; i++)
+ {
+ App.Tap("UpdateVisibilityButton");
+ }
+
+ VerifyScreenshot();
+ }
+ }
+}
+#endif
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ReduceInvalidateMeasuresUpdatingLabel.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ReduceInvalidateMeasuresUpdatingLabel.png
new file mode 100644
index 000000000000..dac780870b17
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ReduceInvalidateMeasuresUpdatingLabel.png differ
diff --git a/src/Controls/tests/TestCases/Issues/ReduceInvalidateMeasure.xaml b/src/Controls/tests/TestCases/Issues/ReduceInvalidateMeasure.xaml
new file mode 100644
index 000000000000..7d27d847257e
--- /dev/null
+++ b/src/Controls/tests/TestCases/Issues/ReduceInvalidateMeasure.xaml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases/Issues/ReduceInvalidateMeasure.xaml.cs b/src/Controls/tests/TestCases/Issues/ReduceInvalidateMeasure.xaml.cs
new file mode 100644
index 000000000000..682df45a6966
--- /dev/null
+++ b/src/Controls/tests/TestCases/Issues/ReduceInvalidateMeasure.xaml.cs
@@ -0,0 +1,56 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Maui.Controls.Sample.Issues
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ [Issue(IssueTracker.ManualTest, "ReduceInvalidateMeasure", "https://github.com/dotnet/maui/pull/21801", PlatformAffected.iOS)]
+ public partial class ReduceInvalidateMeasure : ContentPage
+ {
+ int _currentLineBreakMode;
+
+ public ReduceInvalidateMeasure()
+ {
+ InitializeComponent();
+ }
+
+ void OnUpdateTextButtonClicked(object sender, EventArgs e)
+ {
+ UpdateTextLabel.Text += " consectetur adipiscing elit";
+ }
+
+ void OnUpdateSizeButtonClicked(object sender, EventArgs e)
+ {
+ UpdateSizeLabel.WidthRequest += 50;
+ }
+
+ void OnUpdateFontSizeButtonClicked(object sender, EventArgs e)
+ {
+ UpdateFontSizeLabel.FontSize += 4;
+ }
+
+ void OnUpdateLineBreakModeButtonClicked(object sender, EventArgs e)
+ {
+ Array lineBreakModes = Enum.GetValues(typeof(LineBreakMode));
+
+ if (_currentLineBreakMode >= lineBreakModes.Length)
+ _currentLineBreakMode = 0;
+
+ UpdateLineBreakModeLabel.LineBreakMode = (LineBreakMode)lineBreakModes.GetValue(_currentLineBreakMode);
+
+ _currentLineBreakMode++;
+ }
+
+ void OnUpdateLineHeightButtonClicked(object sender, EventArgs e)
+ {
+ UpdateLineHeightLabel.LineHeight++;
+ }
+
+ void OnUpdateVisibilityButtonClicked(object sender, EventArgs e)
+ {
+ UpdateVisibilityLabel.IsVisible = !UpdateVisibilityLabel.IsVisible;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/src/Platform/iOS/MauiLabel.cs b/src/Core/src/Platform/iOS/MauiLabel.cs
index 5708f04f74c9..9b4893612c86 100644
--- a/src/Core/src/Platform/iOS/MauiLabel.cs
+++ b/src/Core/src/Platform/iOS/MauiLabel.cs
@@ -66,24 +66,6 @@ RectangleF AlignVertical(RectangleF rect)
return rect;
}
- public override void InvalidateIntrinsicContentSize()
- {
- base.InvalidateIntrinsicContentSize();
-
- if (Frame.Width == 0 && Frame.Height == 0)
- {
- // The Label hasn't actually been laid out on screen yet; no reason to request a layout
- return;
- }
-
- if (!Frame.Size.IsCloseTo(AddInsets(IntrinsicContentSize), (nfloat)0.001))
- {
- // The text or its attributes have changed enough that the size no longer matches the set Frame. It's possible
- // that the Label needs to be laid out again at a different size, so we request that the parent do so.
- Superview?.SetNeedsLayout();
- }
- }
-
public override SizeF SizeThatFits(SizeF size)
{
var requestedSize = base.SizeThatFits(size);
diff --git a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
index bb6889c295aa..1152b0f568a1 100644
--- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -160,4 +160,5 @@ Microsoft.Maui.Platform.MauiView.IsMeasureValid(double widthConstraint, double h
*REMOVED*override Microsoft.Maui.Platform.ContentView.SetNeedsLayout() -> void
Microsoft.Maui.Platform.UIEdgeInsetsExtensions
static Microsoft.Maui.Platform.UIEdgeInsetsExtensions.ToThickness(this UIKit.UIEdgeInsets insets) -> Microsoft.Maui.Thickness
-override Microsoft.Maui.Handlers.BorderHandler.PlatformArrange(Microsoft.Maui.Graphics.Rect rect) -> void
\ No newline at end of file
+override Microsoft.Maui.Handlers.BorderHandler.PlatformArrange(Microsoft.Maui.Graphics.Rect rect) -> void
+*REMOVED*override Microsoft.Maui.Platform.MauiLabel.InvalidateIntrinsicContentSize() -> void
\ No newline at end of file
diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
index 90a3bfd766e5..827532828734 100644
--- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -160,4 +160,5 @@ virtual Microsoft.Maui.MauiUIApplicationDelegate.PerformFetch(UIKit.UIApplicatio
*REMOVED*override Microsoft.Maui.Platform.LayoutView.SizeThatFits(CoreGraphics.CGSize size) -> CoreGraphics.CGSize
*REMOVED*override Microsoft.Maui.Platform.ContentView.SetNeedsLayout() -> void
Microsoft.Maui.Platform.UIEdgeInsetsExtensions
-static Microsoft.Maui.Platform.UIEdgeInsetsExtensions.ToThickness(this UIKit.UIEdgeInsets insets) -> Microsoft.Maui.Thickness
\ No newline at end of file
+static Microsoft.Maui.Platform.UIEdgeInsetsExtensions.ToThickness(this UIKit.UIEdgeInsets insets) -> Microsoft.Maui.Thickness
+*REMOVED*override Microsoft.Maui.Platform.MauiLabel.InvalidateIntrinsicContentSize() -> void