diff --git a/src/Controls/src/Core/ContentPresenter.cs b/src/Controls/src/Core/ContentPresenter.cs index 590a0802e913..fb517a905715 100644 --- a/src/Controls/src/Core/ContentPresenter.cs +++ b/src/Controls/src/Core/ContentPresenter.cs @@ -130,10 +130,5 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds) this.ArrangeContent(bounds); return bounds.Size; } - - private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate) - { - base.InvalidateMeasureLegacy(trigger, depth, 1); - } } } \ No newline at end of file diff --git a/src/Controls/src/Core/InvalidationEventArgs.cs b/src/Controls/src/Core/InvalidationEventArgs.cs index 701d725c5290..050c7548fd63 100644 --- a/src/Controls/src/Core/InvalidationEventArgs.cs +++ b/src/Controls/src/Core/InvalidationEventArgs.cs @@ -10,15 +10,7 @@ public InvalidationEventArgs(InvalidationTrigger trigger) { Trigger = trigger; } - public InvalidationEventArgs(InvalidationTrigger trigger, int depth) : this(trigger) - { - CurrentInvalidationDepth = depth; - } - public InvalidationTrigger Trigger { get; private set; } - - - public int CurrentInvalidationDepth { set; get; } } } \ No newline at end of file diff --git a/src/Controls/src/Core/LegacyLayouts/Layout.cs b/src/Controls/src/Core/LegacyLayouts/Layout.cs index 8b9ac2371c06..e02c311f545b 100644 --- a/src/Controls/src/Core/LegacyLayouts/Layout.cs +++ b/src/Controls/src/Core/LegacyLayouts/Layout.cs @@ -202,8 +202,12 @@ public override SizeRequest Measure(double widthConstraint, double heightConstra SizeRequest size = base.Measure(widthConstraint - Padding.HorizontalThickness, heightConstraint - Padding.VerticalThickness, flags); #pragma warning restore CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete - return new SizeRequest(new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness), - new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness)); + var request = new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness); + var minimum = new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness); + + DesiredSize = request; + + return new SizeRequest(request, minimum); #pragma warning restore CS0618 // Type or member is obsolete } #pragma warning restore CS0672 // Member overrides obsolete member @@ -320,7 +324,7 @@ public void RaiseChild(View view) [Obsolete("Use InvalidateMeasure depending on your scenario")] protected virtual void InvalidateLayout() { - _hasDoneLayout = false; + SetNeedsLayout(); InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); if (!_hasDoneLayout) { @@ -328,6 +332,11 @@ protected virtual void InvalidateLayout() } } + void SetNeedsLayout() + { + _hasDoneLayout = false; + } + /// /// Positions and sizes the children of a layout. /// @@ -341,10 +350,18 @@ protected virtual void InvalidateLayout() [Obsolete("Use ArrangeOverride")] protected abstract void LayoutChildren(double x, double y, double width, double height); - internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth) + internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger) { - // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly - OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth)); + SetNeedsLayout(); + InvalidateMeasureCache(); + + OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger)); + + var propagatedTrigger = GetPropagatedTrigger(trigger); + InvokeMeasureInvalidated(propagatedTrigger); + + // Behavior of legacy layouts is to always propagate the measure invalidation to the parent + (Parent as VisualElement)?.OnChildMeasureInvalidated(this, propagatedTrigger); } /// @@ -356,19 +373,6 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In /// This method has a default implementation and application developers must call the base implementation. protected void OnChildMeasureInvalidated(object sender, EventArgs e) { - var depth = 0; - InvalidationTrigger trigger; - if (e is InvalidationEventArgs args) - { - trigger = args.Trigger; - depth = args.CurrentInvalidationDepth; - } - else - { - trigger = InvalidationTrigger.Undefined; - } - - OnChildMeasureInvalidated((VisualElement)sender, trigger, depth); OnChildMeasureInvalidated(); } @@ -542,55 +546,6 @@ internal static void LayoutChildIntoBoundingRegion(View child, Rect region, Size child.Layout(region); } - internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth) - { - IReadOnlyList children = LogicalChildrenInternal; - int count = children.Count; - for (var index = 0; index < count; index++) - { - if (LogicalChildrenInternal[index] is VisualElement v && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent)) - { - return; - } - } - - if (child is View view) - { - // we can ignore the request if we are either fully constrained or when the size request changes and we were already fully constrained - if ((trigger == InvalidationTrigger.MeasureChanged && view.Constraint == LayoutConstraint.Fixed) || - (trigger == InvalidationTrigger.SizeRequestChanged && view.ComputedConstraint == LayoutConstraint.Fixed)) - { - return; - } - if (trigger == InvalidationTrigger.HorizontalOptionsChanged || trigger == InvalidationTrigger.VerticalOptionsChanged) - { - ComputeConstraintForView(view); - } - } - - InvalidateMeasureLegacy(trigger, depth, int.MaxValue); - } - - // This lets us override the rules for invalidation on MAUI controls that unfortunately still inheirt from the legacy layout - private protected virtual void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate) - { - if (depth <= depthLeveltoInvalidate) - { - if (trigger == InvalidationTrigger.RendererReady) - { - InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.RendererReady, depth)); - } - else - { - InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth)); - } - } - else - { - FireMeasureChanged(trigger, depth); - } - } - internal override void OnIsVisibleChanged(bool oldValue, bool newValue) { base.OnIsVisibleChanged(oldValue, newValue); @@ -708,19 +663,6 @@ bool ShouldLayoutChildren() return true; } - protected override void InvalidateMeasureOverride() - { - base.InvalidateMeasureOverride(); - - foreach (var child in ((IElementController)this).LogicalChildren) - { - if (child is IView fe) - { - fe.InvalidateMeasure(); - } - } - } - protected override Size ArrangeOverride(Rect bounds) { base.ArrangeOverride(bounds); diff --git a/src/Controls/src/Core/LegacyLayouts/StackLayout.cs b/src/Controls/src/Core/LegacyLayouts/StackLayout.cs index 146826a97502..3b948478a6a0 100644 --- a/src/Controls/src/Core/LegacyLayouts/StackLayout.cs +++ b/src/Controls/src/Core/LegacyLayouts/StackLayout.cs @@ -92,12 +92,18 @@ protected override SizeRequest OnMeasure(double widthConstraint, double heightCo return result; } + internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger) + { + _layoutInformation = new LayoutInformation(); + base.OnChildMeasureInvalidated(child, trigger); + } + internal override void ComputeConstraintForView(View view) { ComputeConstraintForView(view, false); } - internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger) + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) { _layoutInformation = new LayoutInformation(); base.InvalidateMeasureInternal(trigger); diff --git a/src/Controls/src/Core/Page/Page.cs b/src/Controls/src/Core/Page/Page.cs index 941f47c52dfb..985bc26842cf 100644 --- a/src/Controls/src/Core/Page/Page.cs +++ b/src/Controls/src/Core/Page/Page.cs @@ -506,10 +506,11 @@ protected override void OnBindingContextChanged() } - internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth) + internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger) { - // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly - OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth)); + OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger)); + var propagatedTrigger = GetPropagatedTrigger(trigger); + InvokeMeasureInvalidated(propagatedTrigger); } /// @@ -519,19 +520,6 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In /// The event arguments. protected virtual void OnChildMeasureInvalidated(object sender, EventArgs e) { - var depth = 0; - InvalidationTrigger trigger; - if (e is InvalidationEventArgs args) - { - trigger = args.Trigger; - depth = args.CurrentInvalidationDepth; - } - else - { - trigger = InvalidationTrigger.Undefined; - } - - OnChildMeasureInvalidated((VisualElement)sender, trigger, depth); } /// @@ -610,36 +598,6 @@ protected void UpdateChildrenLayout() } } - internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth) - { - var container = this as IPageContainer; - if (container != null) - { - Page page = container.CurrentPage; - if (page != null && page.IsVisible && (!page.IsPlatformEnabled || !page.IsPlatformStateConsistent)) - return; - } - else - { - var logicalChildren = this.InternalChildren; - for (var i = 0; i < logicalChildren.Count; i++) - { - var v = logicalChildren[i] as VisualElement; - if (v != null && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent)) - return; - } - } - - if (depth <= 1) - { - InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth)); - } - else - { - FireMeasureChanged(trigger, depth); - } - } - internal void OnAppearing(Action action) { if (_hasAppeared) diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index f48b2b26b2c2..5b8f9021d63e 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -69,6 +69,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.Embedding.EmbeddingExtensions Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 63a6c7ddd87a..4eeaee9652d9 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -205,6 +205,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.Embedding.EmbeddingExtensions Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.Dispose() -> void *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.OnLayoutSubviews() -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 4d313d1b0c85..6610d9f6732e 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -206,6 +206,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.Embedding.EmbeddingExtensions Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.Dispose() -> void *REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.OnLayoutSubviews() -> void diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 275b44d25503..4ee051439ef7 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string! const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index b1dbe9d0aa07..c2190ccab4e5 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -70,6 +70,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.Embedding.EmbeddingExtensions Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 27745f220371..0950e21053b6 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string! const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 275b44d25503..4ee051439ef7 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string! const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string! Microsoft.Maui.Controls.HandlerProperties +*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void Microsoft.Maui.Controls.HybridWebView Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string? Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void diff --git a/src/Controls/src/Core/ScrollView/ScrollView.cs b/src/Controls/src/Core/ScrollView/ScrollView.cs index 23489e87fb54..7c226a3b7565 100644 --- a/src/Controls/src/Core/ScrollView/ScrollView.cs +++ b/src/Controls/src/Core/ScrollView/ScrollView.cs @@ -478,11 +478,6 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds) return bounds.Size; } - private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate) - { - base.InvalidateMeasureLegacy(trigger, depth, 1); - } - private protected override string GetDebuggerDisplay() { var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(Content), Content); diff --git a/src/Controls/src/Core/TemplatedView/TemplatedView.cs b/src/Controls/src/Core/TemplatedView/TemplatedView.cs index 23ef916eafbd..3e8cd53943bf 100644 --- a/src/Controls/src/Core/TemplatedView/TemplatedView.cs +++ b/src/Controls/src/Core/TemplatedView/TemplatedView.cs @@ -150,11 +150,6 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds) return bounds.Size; } - private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate) - { - base.InvalidateMeasureLegacy(trigger, depth, 1); - } - #nullable disable } diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs index 6161fcd8a8c1..74e941ce8a6b 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement.cs @@ -1384,6 +1384,7 @@ internal void ComputeConstrainsForChildren() } } + // TODO: .NET10 this should be made public so whoever implements a custom layout can leverage this internal virtual void ComputeConstraintForView(View view) => view.ComputedConstraint = LayoutConstraint.None; /// @@ -1408,23 +1409,25 @@ public void InvalidateMeasureNonVirtual(InvalidationTrigger trigger) InvalidateMeasureInternal(trigger); } - internal void InvalidateMeasureInternal(InvalidationTrigger trigger) + internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger) { - InvalidateMeasureInternal(new InvalidationEventArgs(trigger, 0)); - } - - internal virtual void InvalidateMeasureInternal(InvalidationEventArgs eventArgs) - { - _measureCache.Clear(); + InvalidateMeasureCache(); - // TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and - // VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead - switch (eventArgs.Trigger) + switch (trigger) { case InvalidationTrigger.MarginChanged: + ParentView?.InvalidateMeasure(); + break; case InvalidationTrigger.HorizontalOptionsChanged: case InvalidationTrigger.VerticalOptionsChanged: + if (this is View thisView && Parent is VisualElement visualParent) + { + visualParent.ComputeConstraintForView(thisView); + } + + // TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and + // VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead ParentView?.InvalidateMeasure(); break; default: @@ -1432,50 +1435,47 @@ internal virtual void InvalidateMeasureInternal(InvalidationEventArgs eventArgs) break; } - FireMeasureChanged(eventArgs); + InvokeMeasureInvalidated(trigger); +#pragma warning disable CS0618 // Type or member is obsolete + (Parent as VisualElement)?.OnChildMeasureInvalidated(this, trigger); +#pragma warning restore CS0618 // Type or member is obsolete } - private protected void FireMeasureChanged(InvalidationTrigger trigger, int depth) + private protected void InvokeMeasureInvalidated(InvalidationTrigger trigger) { - FireMeasureChanged(new InvalidationEventArgs(trigger, depth)); + MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger)); } + /// + /// A flag that determines whether the measure invalidated event should not be propagated up the visual tree. + /// + /// + /// Propagation will still occur within legacy layout subtrees. + /// + internal static bool SkipMeasureInvalidatedPropagation { get; set /* for testing purpose */; } = + AppContext.TryGetSwitch("Microsoft.Maui.RuntimeFeature.SkipMeasureInvalidatedPropagation", out var enabled) && enabled; - private protected void FireMeasureChanged(InvalidationEventArgs args) + internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger) { - var depth = args.CurrentInvalidationDepth; - MeasureInvalidated?.Invoke(this, args); - (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, args.Trigger, ++depth); + if (SkipMeasureInvalidatedPropagation) + { + return; + } + + var propagatedTrigger = GetPropagatedTrigger(trigger); + InvokeMeasureInvalidated(propagatedTrigger); + (Parent as VisualElement)?.OnChildMeasureInvalidated(this, propagatedTrigger); } - // We don't want to change the execution path of Page or Layout when they are calling "InvalidationMeasure" - // If you look at page it calls OnChildMeasureInvalidated from OnChildMeasureInvalidatedInternal - // Because OnChildMeasureInvalidated is public API and the user might override it, we need to keep it as is - //private protected int CurrentInvalidationDepth { get; set; } + private protected static InvalidationTrigger GetPropagatedTrigger(InvalidationTrigger trigger) + { + var propagatedTrigger = trigger == InvalidationTrigger.RendererReady ? trigger : InvalidationTrigger.MeasureChanged; + return propagatedTrigger; + } - internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth) + private protected void InvalidateMeasureCache() { - switch (trigger) - { - case InvalidationTrigger.VerticalOptionsChanged: - case InvalidationTrigger.HorizontalOptionsChanged: - // When a child changes its HorizontalOptions or VerticalOptions - // the size of the parent won't change, so we don't have to invalidate the measure - return; - case InvalidationTrigger.RendererReady: - // Undefined happens in many cases, including when `IsVisible` changes - case InvalidationTrigger.Undefined: - FireMeasureChanged(trigger, depth); - return; - default: - // When visibility changes `InvalidationTrigger.Undefined` is used, - // so here we're sure that visibility didn't change - if (child.IsVisible) - { - FireMeasureChanged(InvalidationTrigger.MeasureChanged, depth); - } - return; - } + _measureCache.Clear(); } /// diff --git a/src/Controls/tests/Core.UnitTests/PageTests.cs b/src/Controls/tests/Core.UnitTests/PageTests.cs index 7c530229ec8f..22b758d94205 100644 --- a/src/Controls/tests/Core.UnitTests/PageTests.cs +++ b/src/Controls/tests/Core.UnitTests/PageTests.cs @@ -567,16 +567,16 @@ public void LogicalChildrenDontAddToPagesInternalChildren() } [Fact] - public void MeasureInvalidatedPropagatesUpTree() + public void MeasureInvalidatedPropagatesUpTreeWithCompatibilityLayouts() { - var label = new Label() + var label = new LabelInvalidateMeasureCheck { IsPlatformEnabled = true }; - var scrollView = new ScrollViewInvalidationMeasureCheck() + var scrollView = new ScrollViewInvalidationMeasureCheck { - Content = new VerticalStackLayout() + Content = new Compatibility.StackLayout { Children = { new ContentView { Content = label, IsPlatformEnabled = true } }, IsPlatformEnabled = true @@ -584,74 +584,171 @@ public void MeasureInvalidatedPropagatesUpTree() IsPlatformEnabled = true }; - var page = new InvalidatePageInvalidateMeasureCheck() + var page = new InvalidatePageInvalidateMeasureCheck { Content = scrollView }; - var window = new TestWindow(page); - - int fired = 0; - page.MeasureInvalidated += (sender, args) => - { - fired++; - }; + // Set up the window + _ = new TestWindow(page); + // Reset counters + label.InvalidateMeasureCount = 0; + label.PlatformInvalidateMeasureCount = 0; page.InvalidateMeasureCount = 0; + page.PlatformInvalidateMeasureCount = 0; scrollView.InvalidateMeasureCount = 0; + scrollView.PlatformInvalidateMeasureCount = 0; + + // Invalidate the label label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); - Assert.Equal(1, fired); - Assert.Equal(0, page.InvalidateMeasureCount); - Assert.Equal(0, scrollView.InvalidateMeasureCount); - page.Content.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); + Assert.Equal(1, label.InvalidateMeasureCount); + Assert.Equal(1, label.PlatformInvalidateMeasureCount); Assert.Equal(1, page.InvalidateMeasureCount); + Assert.Equal(0, page.PlatformInvalidateMeasureCount); + Assert.Equal(1, scrollView.InvalidateMeasureCount); + Assert.Equal(0, scrollView.PlatformInvalidateMeasureCount); + + // Invalidate page content + page.Content.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); + Assert.Equal(2, page.InvalidateMeasureCount); + Assert.Equal(0, page.PlatformInvalidateMeasureCount); + } + + [Theory] + [InlineData(true, 0)] + [InlineData(false, 1)] + public void MeasureInvalidatedPropagatesUpTreeOnAppSwitch(bool skipMeasureInvalidatedPropagation, int expectedAncestorMeasureInvalidatedEvents) + { + try + { + VisualElement.SkipMeasureInvalidatedPropagation = skipMeasureInvalidatedPropagation; + + var label = new LabelInvalidateMeasureCheck { IsPlatformEnabled = true }; + + var contentView = new ContentViewInvalidationMeasureCheck { Content = label, IsPlatformEnabled = true }; + + var scrollView = new ScrollViewInvalidationMeasureCheck + { + // VerticalStackLayout is not a CompatibilityLayout so it will not propagate the MeasureInvalidated + // event up the tree unless VisualElement.IsMeasureInvalidatedPropagationEnabled switch is set to true + Content = new VerticalStackLayout + { + Children = { contentView }, + IsPlatformEnabled = true + }, + IsPlatformEnabled = true + }; + + var page = new InvalidatePageInvalidateMeasureCheck { Content = scrollView }; + + // Set up the window + _ = new TestWindow(page); + + // Reset counters + label.InvalidateMeasureCount = 0; + label.PlatformInvalidateMeasureCount = 0; + contentView.InvalidateMeasureCount = 0; + contentView.PlatformInvalidateMeasureCount = 0; + scrollView.InvalidateMeasureCount = 0; + scrollView.PlatformInvalidateMeasureCount = 0; + page.InvalidateMeasureCount = 0; + page.PlatformInvalidateMeasureCount = 0; + + // Invalidate the label + label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); + Assert.Equal(1, label.InvalidateMeasureCount); + Assert.Equal(1, label.PlatformInvalidateMeasureCount); + Assert.Equal(1, contentView.InvalidateMeasureCount); + Assert.Equal(0, contentView.PlatformInvalidateMeasureCount); + Assert.Equal(expectedAncestorMeasureInvalidatedEvents, scrollView.InvalidateMeasureCount); + Assert.Equal(0, scrollView.PlatformInvalidateMeasureCount); + Assert.Equal(expectedAncestorMeasureInvalidatedEvents, page.InvalidateMeasureCount); + Assert.Equal(0, page.PlatformInvalidateMeasureCount); + } + finally + { + VisualElement.SkipMeasureInvalidatedPropagation = false; + } } class LabelInvalidateMeasureCheck : Label { + public int PlatformInvalidateMeasureCount { get; set; } public int InvalidateMeasureCount { get; set; } public LabelInvalidateMeasureCheck() { + MeasureInvalidated += (sender, args) => + { + InvalidateMeasureCount++; + }; + } + + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) + { + base.InvalidateMeasureInternal(trigger); + PlatformInvalidateMeasureCount++; + } + } + class ContentViewInvalidationMeasureCheck : ContentView + { + public int PlatformInvalidateMeasureCount { get; set; } + public int InvalidateMeasureCount { get; set; } + + public ContentViewInvalidationMeasureCheck() + { + MeasureInvalidated += (sender, args) => + { + InvalidateMeasureCount++; + }; } - internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger) + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) { base.InvalidateMeasureInternal(trigger); - InvalidateMeasureCount++; + PlatformInvalidateMeasureCount++; } } class ScrollViewInvalidationMeasureCheck : ScrollView { + public int PlatformInvalidateMeasureCount { get; set; } public int InvalidateMeasureCount { get; set; } public ScrollViewInvalidationMeasureCheck() { - + MeasureInvalidated += (sender, args) => + { + InvalidateMeasureCount++; + }; } - internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger) + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) { base.InvalidateMeasureInternal(trigger); - InvalidateMeasureCount++; + PlatformInvalidateMeasureCount++; } } class InvalidatePageInvalidateMeasureCheck : ContentPage { + public int PlatformInvalidateMeasureCount { get; set; } public int InvalidateMeasureCount { get; set; } public InvalidatePageInvalidateMeasureCheck() { - + MeasureInvalidated += (sender, args) => + { + InvalidateMeasureCount++; + }; } - internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger) + internal override void InvalidateMeasureInternal(InvalidationTrigger trigger) { base.InvalidateMeasureInternal(trigger); - InvalidateMeasureCount++; + PlatformInvalidateMeasureCount++; } } }