diff --git a/Xamarin.Forms.Core/Device.cs b/Xamarin.Forms.Core/Device.cs index 5a7d51a89ce..3aaafe835c6 100644 --- a/Xamarin.Forms.Core/Device.cs +++ b/Xamarin.Forms.Core/Device.cs @@ -90,6 +90,8 @@ public static IPlatformServices PlatformServices set { s_platformServices = value; } } + public static IPlatformInvalidate PlatformInvalidator { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] public static IReadOnlyList Flags { get; private set; } @@ -283,5 +285,10 @@ public static class Styles public static readonly Style CaptionStyle = new Style(typeof(Label)) { BaseResourceKey = CaptionStyleKey }; } + + public static void Invalidate(VisualElement visualElement) + { + PlatformInvalidator?.Invalidate(visualElement); + } } } diff --git a/Xamarin.Forms.Core/IPlatformInvalidate.cs b/Xamarin.Forms.Core/IPlatformInvalidate.cs new file mode 100644 index 00000000000..012b21206cc --- /dev/null +++ b/Xamarin.Forms.Core/IPlatformInvalidate.cs @@ -0,0 +1,8 @@ +namespace Xamarin.Forms.Internals +{ + public interface IPlatformInvalidate + + { + void Invalidate(VisualElement visualElement); + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Core/Layout.cs b/Xamarin.Forms.Core/Layout.cs index 68c835b9fac..594bb6857bf 100644 --- a/Xamarin.Forms.Core/Layout.cs +++ b/Xamarin.Forms.Core/Layout.cs @@ -326,15 +326,16 @@ internal virtual void OnChildMeasureInvalidated(VisualElement child, Invalidatio } s_resolutionList.Add(new KeyValuePair(this, GetElementDepth(this))); - if (!s_relayoutInProgress) - { - s_relayoutInProgress = true; - // Rather than recomputing the layout for each change as it happens, we accumulate them in + if (Device.PlatformInvalidator == null && !s_relayoutInProgress) + { + // Rather than recomputing the layout for each change as it happens, we accumulate them in // s_resolutionList and schedule a single layout update operation to handle them all at once. // This avoids a lot of unnecessary layout operations if something is triggering many property // changes at once (e.g., a BindingContext change) + s_relayoutInProgress = true; + if (Dispatcher != null) { Dispatcher.BeginInvokeOnMainThread(ResolveLayoutChanges); @@ -342,16 +343,27 @@ internal virtual void OnChildMeasureInvalidated(VisualElement child, Invalidatio else { Device.BeginInvokeOnMainThread(ResolveLayoutChanges); - } + } + } + else + { + // If the platform supports PlatformServices2, queueing is unnecessary; the layout changes + // will be handled during the Layout's next Measure/Arrange pass + Device.Invalidate(this); } } - internal void ResolveLayoutChanges() + public void ResolveLayoutChanges() { - // if thread safety mattered we would need to lock this and compareexchange above + s_relayoutInProgress = false; + + if (s_resolutionList.Count == 0) + { + return; + } + IList> copy = s_resolutionList; s_resolutionList = new List>(); - s_relayoutInProgress = false; foreach (KeyValuePair kvp in copy) { diff --git a/Xamarin.Forms.Core/VisualElement.cs b/Xamarin.Forms.Core/VisualElement.cs index 7fa40773a5d..242b16644ff 100644 --- a/Xamarin.Forms.Core/VisualElement.cs +++ b/Xamarin.Forms.Core/VisualElement.cs @@ -663,7 +663,10 @@ public void BatchCommit() { _batched = Math.Max(0, _batched - 1); if (!Batched) + { BatchCommitted?.Invoke(this, new EventArg(this)); + Device.Invalidate(this); + } } ResourceDictionary _resources; diff --git a/Xamarin.Forms.Platform.Android/Forms.cs b/Xamarin.Forms.Platform.Android/Forms.cs index 9bb1d4de031..be94fc2846e 100644 --- a/Xamarin.Forms.Platform.Android/Forms.cs +++ b/Xamarin.Forms.Platform.Android/Forms.cs @@ -296,7 +296,11 @@ static void SetupInit( // We want this to be updated when we have a new activity (e.g. on a configuration change) // because AndroidPlatformServices needs a current activity to launch URIs from Profile.FramePartition("Device.PlatformServices"); - Device.PlatformServices = new AndroidPlatformServices(activity); + + var androidServices = new AndroidPlatformServices(activity); + + Device.PlatformServices = androidServices; + Device.PlatformInvalidator = androidServices; // use field and not property to avoid exception in getter if (Device.info != null) @@ -610,7 +614,7 @@ protected override Expression VisitMember(MemberExpression node) } } - class AndroidPlatformServices : IPlatformServices + class AndroidPlatformServices : IPlatformServices, IPlatformInvalidate { double _buttonDefaultSize; double _editTextDefaultSize; @@ -917,6 +921,18 @@ public SizeRequest GetNativeSize(VisualElement view, double widthConstraint, dou return Platform.Android.Platform.GetNativeSize(view, widthConstraint, heightConstraint); } + public void Invalidate(VisualElement visualElement) + { + var renderer = visualElement.GetRenderer(); + if (renderer == null || renderer.View.IsDisposed()) + { + return; + } + + renderer.View.Invalidate(); + renderer.View.RequestLayout(); + } + public OSAppTheme RequestedTheme { get diff --git a/Xamarin.Forms.Platform.Android/Platform.cs b/Xamarin.Forms.Platform.Android/Platform.cs index 785fd2fa65c..b4dccc6f093 100644 --- a/Xamarin.Forms.Platform.Android/Platform.cs +++ b/Xamarin.Forms.Platform.Android/Platform.cs @@ -1312,6 +1312,16 @@ protected override void Dispose(bool disposing) bool ILayoutChanges.HasLayoutOccurred => _hasLayoutOccurred; + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + if (Element is Layout layout) + { + layout.ResolveLayoutChanges(); + } + + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + } + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) { base.OnLayout(changed, left, top, right, bottom); diff --git a/Xamarin.Forms.Platform.Android/VisualElementTracker.cs b/Xamarin.Forms.Platform.Android/VisualElementTracker.cs index f027c5b6a2d..0ca1f23988e 100644 --- a/Xamarin.Forms.Platform.Android/VisualElementTracker.cs +++ b/Xamarin.Forms.Platform.Android/VisualElementTracker.cs @@ -336,6 +336,9 @@ void UpdateIsVisible() aview.Visibility = ViewStates.Visible; if (!view.IsVisible && aview.Visibility != ViewStates.Gone) aview.Visibility = ViewStates.Gone; + + aview.Invalidate(); + aview.RequestLayout(); } void UpdateNativeView(object sender, EventArgs e) diff --git a/Xamarin.Forms.Platform.UAP/Forms.cs b/Xamarin.Forms.Platform.UAP/Forms.cs index c970c34601d..8c81219820d 100644 --- a/Xamarin.Forms.Platform.UAP/Forms.cs +++ b/Xamarin.Forms.Platform.UAP/Forms.cs @@ -49,7 +49,12 @@ public static void Init(IActivatedEventArgs launchActivatedEventArgs, IEnumerabl Device.SetIdiom(TargetIdiom.Tablet); Device.SetFlowDirection(GetFlowDirection()); - Device.PlatformServices = new WindowsPlatformServices(Window.Current.Dispatcher); + + var platformServices = new WindowsPlatformServices(Window.Current.Dispatcher); + + Device.PlatformServices = platformServices; + Device.PlatformInvalidator = platformServices; + Device.SetFlags(s_flags); Device.Info = new WindowsDeviceInfo(); diff --git a/Xamarin.Forms.Platform.UAP/LayoutRenderer.cs b/Xamarin.Forms.Platform.UAP/LayoutRenderer.cs index b0240ce948b..ef4509be5d5 100644 --- a/Xamarin.Forms.Platform.UAP/LayoutRenderer.cs +++ b/Xamarin.Forms.Platform.UAP/LayoutRenderer.cs @@ -80,5 +80,11 @@ void UpdateClipToBounds() Clip = new RectangleGeometry { Rect = new WRect(0, 0, ActualWidth, ActualHeight) }; } } + + protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize) + { + Element?.ResolveLayoutChanges(); + return base.MeasureOverride(availableSize); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs b/Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs index 27bca80046a..b386509f89e 100644 --- a/Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs +++ b/Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs @@ -25,7 +25,7 @@ namespace Xamarin.Forms.Platform.UWP { - internal abstract class WindowsBasePlatformServices : IPlatformServices + internal abstract class WindowsBasePlatformServices : IPlatformServices, IPlatformInvalidate { const string WrongThreadError = "RPC_E_WRONG_THREAD"; readonly CoreDispatcher _dispatcher; @@ -256,6 +256,17 @@ await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { return await taskCompletionSource.Task; } + public void Invalidate(VisualElement visualElement) + { + var renderer = Platform.GetRenderer(visualElement); + if (renderer == null) + { + return; + } + + renderer.ContainerElement.InvalidateMeasure(); + } + public OSAppTheme RequestedTheme => Windows.UI.Xaml.Application.Current.RequestedTheme == ApplicationTheme.Dark ? OSAppTheme.Dark : OSAppTheme.Light; } } diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs index 8fd566ab57f..8409d41f87d 100644 --- a/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs +++ b/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs @@ -66,7 +66,7 @@ protected override void Dispose(bool disposing) _emptyUIView?.Dispose(); _emptyUIView = null; - + _emptyViewFormsElement = null; ItemsViewLayout?.Dispose(); diff --git a/Xamarin.Forms.Platform.iOS/Forms.cs b/Xamarin.Forms.Platform.iOS/Forms.cs index 6a49ed53bf1..b761ec1db52 100644 --- a/Xamarin.Forms.Platform.iOS/Forms.cs +++ b/Xamarin.Forms.Platform.iOS/Forms.cs @@ -191,8 +191,12 @@ public static void Init() } #endif Device.SetFlags(s_flags); - Device.PlatformServices = new IOSPlatformServices(); + var platformServices = new IOSPlatformServices(); + + Device.PlatformServices = platformServices; + #if __MOBILE__ + Device.PlatformInvalidator = platformServices; Device.Info = new IOSDeviceInfo(); #else Device.Info = new Platform.macOS.MacDeviceInfo(); @@ -238,6 +242,9 @@ protected override Expression VisitMember(MemberExpression node) } class IOSPlatformServices : IPlatformServices +#if __MOBILE__ + , IPlatformInvalidate +#endif { readonly double _fontScalingFactor = 1; public IOSPlatformServices() @@ -793,6 +800,18 @@ static UIViewController GetCurrentViewController(bool throwIfNull = true) return viewController; } + + public void Invalidate(VisualElement visualElement) + { + var renderer = Platform.iOS.Platform.GetRenderer(visualElement); + + if (renderer == null) + { + return; + } + + renderer.NativeView.SetNeedsLayout(); + } #endif } } diff --git a/Xamarin.Forms.Platform.iOS/Platform.cs b/Xamarin.Forms.Platform.iOS/Platform.cs index abb20685f79..7867ec7267f 100644 --- a/Xamarin.Forms.Platform.iOS/Platform.cs +++ b/Xamarin.Forms.Platform.iOS/Platform.cs @@ -582,6 +582,26 @@ public override UIView HitTest(CGPoint point, UIEvent uievent) return result; } + + void ResolveLayoutChanges() + { + if (Element is Layout layout) + { + layout.ResolveLayoutChanges(); + } + } + + public override void LayoutSubviews() + { + ResolveLayoutChanges(); + base.LayoutSubviews(); + } + + public override CGSize SizeThatFits(CGSize size) + { + ResolveLayoutChanges(); + return base.SizeThatFits(size); + } } internal static string ResolveMsAppDataUri(Uri uri)