From 4bb54e7ceaff83c40151e6d559ef60b7ede28e8c Mon Sep 17 00:00:00 2001 From: Alberto Aldegheri Date: Fri, 14 Jun 2024 12:28:53 +0200 Subject: [PATCH] Make `MeasureInvalidated` event work correctly Fixes #15177 Fixes #20181 --- src/Controls/src/Core/LegacyLayouts/Layout.cs | 10 +++--- src/Controls/src/Core/Page/Page.cs | 24 ++++++++------ .../src/Core/VisualElement/VisualElement.cs | 33 +++++++++++++++++-- .../CollectionViewCoreGalleryPage.cs | 1 + 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/Controls/src/Core/LegacyLayouts/Layout.cs b/src/Controls/src/Core/LegacyLayouts/Layout.cs index e53929ada83c..5983a1c741a3 100644 --- a/src/Controls/src/Core/LegacyLayouts/Layout.cs +++ b/src/Controls/src/Core/LegacyLayouts/Layout.cs @@ -319,6 +319,12 @@ protected virtual void InvalidateLayout() /// It is suggested to still call the base method and modify its calculated results. protected abstract void LayoutChildren(double x, double y, double width, double height); + internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger) + { + // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly + OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger)); + } + /// /// Invoked whenever a child of the layout has emitted . /// Implement this method to add class handling for this event. @@ -603,14 +609,10 @@ void OnInternalAdded(View view) { InvalidateLayout(); } - - view.MeasureInvalidated += OnChildMeasureInvalidated; } void OnInternalRemoved(View view, int oldIndex) { - view.MeasureInvalidated -= OnChildMeasureInvalidated; - OnChildRemoved(view, oldIndex); if (ShouldInvalidateOnChildRemoved(view)) { diff --git a/src/Controls/src/Core/Page/Page.cs b/src/Controls/src/Core/Page/Page.cs index 007b91ce6c84..b5d0eab7d9bd 100644 --- a/src/Controls/src/Core/Page/Page.cs +++ b/src/Controls/src/Core/Page/Page.cs @@ -497,6 +497,12 @@ protected override void OnBindingContextChanged() if (TitleView != null) SetInheritedBindingContext(TitleView, BindingContext); } + + internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger) + { + // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly + OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger)); + } /// /// Indicates that the preferred size of a child has changed. @@ -706,9 +712,6 @@ void InternalChildrenOnCollectionChanged(object sender, NotifyCollectionChangedE for (var i = 0; i < e.OldItems.Count; i++) { var item = (Element)e.OldItems[i]; - if (item is VisualElement visual) - visual.MeasureInvalidated -= OnChildMeasureInvalidated; - RemoveLogicalChild(item); } } @@ -721,20 +724,21 @@ void InternalChildrenOnCollectionChanged(object sender, NotifyCollectionChangedE { int insertIndex = index; if (insertIndex < 0) + { insertIndex = InternalChildren.IndexOf(item); + } - if (item is VisualElement visual) + InsertLogicalChild(insertIndex, item); + + if (item is VisualElement) { - visual.MeasureInvalidated += OnChildMeasureInvalidated; - - InsertLogicalChild(insertIndex, visual); InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged); } - else - InsertLogicalChild(insertIndex, item); - + if (index >= 0) + { index++; + } } } } diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs index 38f9eabf7fec..24946eb7c3d5 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement.cs @@ -1252,9 +1252,7 @@ protected override void OnChildAdded(Element child) { base.OnChildAdded(child); - var view = child as View; - - if (view != null) + if (child is View view) { ComputeConstraintForView(view); } @@ -1377,6 +1375,35 @@ internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger) } MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger)); + (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger); + } + + internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger) + { + 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: + MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger)); + (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger); + return; + default: + // When visibility changes `InvalidationTrigger.Undefined` is used, + // so here we're sure that visibility didn't change + if (child.IsVisible) + { + // We need to invalidate measures only if child is actually visible + MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(InvalidationTrigger.MeasureChanged)); + (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, InvalidationTrigger.MeasureChanged); + } + return; + } } /// diff --git a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/CollectionViewCoreGalleryPage.cs b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/CollectionViewCoreGalleryPage.cs index cde20c877400..5e24472f0cfa 100644 --- a/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/CollectionViewCoreGalleryPage.cs +++ b/src/Controls/tests/TestCases.HostApp/Elements/CollectionView/CollectionViewCoreGalleryPage.cs @@ -47,6 +47,7 @@ public CollectionViewCoreGalleryContentPage() // SelectionShouldUpdateBinding (src\Compatibility\ControlGallery\src\Issues.Shared\CollectionViewBoundSingleSelection.cs) // ItemsFromViewModelShouldBeSelected (src\Compatibility\ControlGallery\src\Issues.Shared\CollectionViewBoundMultiSelection.cs) TestBuilder.NavButton("Selection Galleries", () => new SelectionGallery(), Navigation), + TestBuilder.NavButton("Item Size Galleries", () => new ItemsSizeGallery(), Navigation), } } };