Skip invalidation on propagation (#25652)
* Skip Invalidation unless you're my immediate child * - fix override on test * Fix Legacy Layouts Invalidation Propagation * - use invalidation args to propagate depth
This commit is contained in:
Родитель
421e8d6c21
Коммит
1412b38245
|
@ -445,7 +445,7 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
|
|||
static void AssertEqualWithTolerance(double a, double b, double tolerance)
|
||||
{
|
||||
var diff = Math.Abs(a - b);
|
||||
Assert.True(diff <= tolerance);
|
||||
Assert.True(diff <= tolerance, $"a: {a} b: {b} tolerance: {tolerance}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
@ -130,5 +130,10 @@ namespace Microsoft.Maui.Controls
|
|||
this.ArrangeContent(bounds);
|
||||
return bounds.Size;
|
||||
}
|
||||
|
||||
private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
|
||||
{
|
||||
base.InvalidateMeasureLegacy(trigger, depth, 1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,15 @@ namespace Microsoft.Maui.Controls
|
|||
{
|
||||
Trigger = trigger;
|
||||
}
|
||||
public InvalidationEventArgs(InvalidationTrigger trigger, int depth) : this(trigger)
|
||||
{
|
||||
CurrentInvalidationDepth = depth;
|
||||
}
|
||||
|
||||
|
||||
public InvalidationTrigger Trigger { get; private set; }
|
||||
|
||||
|
||||
public int CurrentInvalidationDepth { set; get; }
|
||||
}
|
||||
}
|
|
@ -341,10 +341,10 @@ namespace Microsoft.Maui.Controls.Compatibility
|
|||
[Obsolete("Use ArrangeOverride")]
|
||||
protected abstract void LayoutChildren(double x, double y, double width, double height);
|
||||
|
||||
internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
|
||||
internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth)
|
||||
{
|
||||
// TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
|
||||
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
|
||||
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -356,8 +356,19 @@ namespace Microsoft.Maui.Controls.Compatibility
|
|||
/// <remarks>This method has a default implementation and application developers must call the base implementation.</remarks>
|
||||
protected void OnChildMeasureInvalidated(object sender, EventArgs e)
|
||||
{
|
||||
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
|
||||
OnChildMeasureInvalidated((VisualElement)sender, trigger);
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -531,7 +542,7 @@ namespace Microsoft.Maui.Controls.Compatibility
|
|||
child.Layout(region);
|
||||
}
|
||||
|
||||
internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
|
||||
internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth)
|
||||
{
|
||||
IReadOnlyList<Element> children = LogicalChildrenInternal;
|
||||
int count = children.Count;
|
||||
|
@ -557,13 +568,26 @@ namespace Microsoft.Maui.Controls.Compatibility
|
|||
}
|
||||
}
|
||||
|
||||
if (trigger == InvalidationTrigger.RendererReady)
|
||||
{
|
||||
InvalidateMeasureInternal(InvalidationTrigger.RendererReady);
|
||||
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
|
||||
{
|
||||
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
|
||||
FireMeasureChanged(trigger, depth);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ namespace Microsoft.Maui.Controls.Compatibility
|
|||
ComputeConstraintForView(view, false);
|
||||
}
|
||||
|
||||
internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
|
||||
internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
|
||||
{
|
||||
_layoutInformation = new LayoutInformation();
|
||||
base.InvalidateMeasureInternal(trigger);
|
||||
|
|
|
@ -500,10 +500,11 @@ namespace Microsoft.Maui.Controls
|
|||
SetInheritedBindingContext(TitleView, BindingContext);
|
||||
}
|
||||
|
||||
internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
|
||||
|
||||
internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth)
|
||||
{
|
||||
// TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
|
||||
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
|
||||
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -513,8 +514,19 @@ namespace Microsoft.Maui.Controls
|
|||
/// <param name="e">The event arguments.</param>
|
||||
protected virtual void OnChildMeasureInvalidated(object sender, EventArgs e)
|
||||
{
|
||||
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
|
||||
OnChildMeasureInvalidated((VisualElement)sender, trigger);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -593,7 +605,7 @@ namespace Microsoft.Maui.Controls
|
|||
}
|
||||
}
|
||||
|
||||
internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
|
||||
internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth)
|
||||
{
|
||||
var container = this as IPageContainer<Page>;
|
||||
if (container != null)
|
||||
|
@ -613,7 +625,14 @@ namespace Microsoft.Maui.Controls
|
|||
}
|
||||
}
|
||||
|
||||
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
|
||||
if (depth <= 1)
|
||||
{
|
||||
InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth));
|
||||
}
|
||||
else
|
||||
{
|
||||
FireMeasureChanged(trigger, depth);
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnAppearing(Action action)
|
||||
|
|
|
@ -474,5 +474,10 @@ namespace Microsoft.Maui.Controls
|
|||
|
||||
return bounds.Size;
|
||||
}
|
||||
|
||||
private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
|
||||
{
|
||||
base.InvalidateMeasureLegacy(trigger, depth, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Maui.Controls.Internals;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using Microsoft.Maui.Layouts;
|
||||
|
||||
|
@ -148,6 +149,12 @@ namespace Microsoft.Maui.Controls
|
|||
this.ArrangeContent(bounds);
|
||||
return bounds.Size;
|
||||
}
|
||||
|
||||
private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
|
||||
{
|
||||
base.InvalidateMeasureLegacy(trigger, depth, 1);
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
|
||||
}
|
||||
|
|
|
@ -279,13 +279,15 @@ namespace Microsoft.Maui.Controls
|
|||
public static readonly BindableProperty BackgroundProperty = BindableProperty.Create(nameof(Background), typeof(Brush), typeof(VisualElement), Brush.Default,
|
||||
propertyChanging: (bindable, oldvalue, newvalue) =>
|
||||
{
|
||||
if (oldvalue == null) return;
|
||||
if (oldvalue == null)
|
||||
return;
|
||||
|
||||
(bindable as VisualElement)?.StopNotifyingBackgroundChanges();
|
||||
},
|
||||
propertyChanged: (bindable, oldvalue, newvalue) =>
|
||||
{
|
||||
if (newvalue == null) return;
|
||||
if (newvalue == null)
|
||||
return;
|
||||
|
||||
(bindable as VisualElement)?.NotifyBackgroundChanges();
|
||||
});
|
||||
|
@ -318,7 +320,7 @@ namespace Microsoft.Maui.Controls
|
|||
_backgroundChanged ??= (sender, e) => OnPropertyChanged(nameof(Background));
|
||||
_backgroundProxy ??= new();
|
||||
_backgroundProxy.Subscribe(background, _backgroundChanged);
|
||||
|
||||
|
||||
OnParentResourcesChanged(this.GetMergedResources());
|
||||
((IElementDefinition)this).AddResourcesChangedListener(background.OnParentResourcesChanged);
|
||||
}
|
||||
|
@ -1369,14 +1371,19 @@ namespace Microsoft.Maui.Controls
|
|||
InvalidateMeasureInternal(trigger);
|
||||
}
|
||||
|
||||
internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger)
|
||||
internal void InvalidateMeasureInternal(InvalidationTrigger trigger)
|
||||
{
|
||||
InvalidateMeasureInternal(new InvalidationEventArgs(trigger, 0));
|
||||
}
|
||||
|
||||
internal virtual void InvalidateMeasureInternal(InvalidationEventArgs eventArgs)
|
||||
{
|
||||
_measureCache.Clear();
|
||||
|
||||
// TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and
|
||||
// VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead
|
||||
|
||||
switch (trigger)
|
||||
switch (eventArgs.Trigger)
|
||||
{
|
||||
case InvalidationTrigger.MarginChanged:
|
||||
case InvalidationTrigger.HorizontalOptionsChanged:
|
||||
|
@ -1388,11 +1395,28 @@ namespace Microsoft.Maui.Controls
|
|||
break;
|
||||
}
|
||||
|
||||
MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger));
|
||||
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger);
|
||||
FireMeasureChanged(eventArgs);
|
||||
}
|
||||
|
||||
internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
|
||||
|
||||
private protected void FireMeasureChanged(InvalidationTrigger trigger, int depth)
|
||||
{
|
||||
FireMeasureChanged(new InvalidationEventArgs(trigger, depth));
|
||||
}
|
||||
|
||||
|
||||
private protected void FireMeasureChanged(InvalidationEventArgs args)
|
||||
{
|
||||
var depth = args.CurrentInvalidationDepth;
|
||||
MeasureInvalidated?.Invoke(this, args);
|
||||
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, args.Trigger, ++depth);
|
||||
}
|
||||
|
||||
// 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; }
|
||||
|
||||
internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth)
|
||||
{
|
||||
switch (trigger)
|
||||
{
|
||||
|
@ -1404,17 +1428,14 @@ namespace Microsoft.Maui.Controls
|
|||
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);
|
||||
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)
|
||||
{
|
||||
// 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);
|
||||
FireMeasureChanged(InvalidationTrigger.MeasureChanged, depth);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -549,5 +549,93 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
|
|||
Assert.Contains(customControl, page.LogicalChildrenInternal);
|
||||
Assert.Contains(customControl, ((IVisualTreeElement)page).GetVisualChildren());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeasureInvalidatedPropagatesUpTree()
|
||||
{
|
||||
var label = new Label(){
|
||||
IsPlatformEnabled = true
|
||||
};
|
||||
|
||||
var scrollView = new ScrollViewInvalidationMeasureCheck()
|
||||
{
|
||||
Content = new VerticalStackLayout()
|
||||
{
|
||||
Children = { new ContentView { Content = label, IsPlatformEnabled = true } },
|
||||
IsPlatformEnabled = true
|
||||
},
|
||||
IsPlatformEnabled = true
|
||||
};
|
||||
|
||||
var page = new InvalidatePageInvalidateMeasureCheck()
|
||||
{
|
||||
Content = scrollView
|
||||
};
|
||||
|
||||
var window = new TestWindow(page);
|
||||
|
||||
int fired = 0;
|
||||
page.MeasureInvalidated += (sender, args) =>
|
||||
{
|
||||
fired++;
|
||||
};
|
||||
|
||||
page.InvalidateMeasureCount = 0;
|
||||
scrollView.InvalidateMeasureCount = 0;
|
||||
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, page.InvalidateMeasureCount);
|
||||
}
|
||||
|
||||
class LabelInvalidateMeasureCheck : Label
|
||||
{
|
||||
public int InvalidateMeasureCount { get; set; }
|
||||
|
||||
public LabelInvalidateMeasureCheck()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
|
||||
{
|
||||
base.InvalidateMeasureInternal(trigger);
|
||||
InvalidateMeasureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollViewInvalidationMeasureCheck : ScrollView
|
||||
{
|
||||
public int InvalidateMeasureCount { get; set; }
|
||||
|
||||
public ScrollViewInvalidationMeasureCheck()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
|
||||
{
|
||||
base.InvalidateMeasureInternal(trigger);
|
||||
InvalidateMeasureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidatePageInvalidateMeasureCheck : ContentPage
|
||||
{
|
||||
public int InvalidateMeasureCount { get; set; }
|
||||
|
||||
public InvalidatePageInvalidateMeasureCheck()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
|
||||
{
|
||||
base.InvalidateMeasureInternal(trigger);
|
||||
InvalidateMeasureCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче