Skip to content

Commit

Permalink
Resolve Layout changes during native measure/arrange pass (xamarin#12017
Browse files Browse the repository at this point in the history
)

* Process layout changes during measure/arrange rather than queueing on the UI thread

* Add disposed check on invalidates

* Attempt to fix occasional GroupableItemsViewController disposed crash
  • Loading branch information
hartez authored Sep 25, 2020
1 parent 7484457 commit e8d06f3
Show file tree
Hide file tree
Showing 13 changed files with 134 additions and 14 deletions.
7 changes: 7 additions & 0 deletions Xamarin.Forms.Core/Device.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public static IPlatformServices PlatformServices
set { s_platformServices = value; }
}

public static IPlatformInvalidate PlatformInvalidator { get; set; }

[EditorBrowsable(EditorBrowsableState.Never)]
public static IReadOnlyList<string> Flags { get; private set; }

Expand Down Expand Up @@ -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);
}
}
}
8 changes: 8 additions & 0 deletions Xamarin.Forms.Core/IPlatformInvalidate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Xamarin.Forms.Internals
{
public interface IPlatformInvalidate

{
void Invalidate(VisualElement visualElement);
}
}
28 changes: 20 additions & 8 deletions Xamarin.Forms.Core/Layout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,32 +326,44 @@ internal virtual void OnChildMeasureInvalidated(VisualElement child, Invalidatio
}

s_resolutionList.Add(new KeyValuePair<Layout, int>(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);
}
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<KeyValuePair<Layout, int>> copy = s_resolutionList;
s_resolutionList = new List<KeyValuePair<Layout, int>>();
s_relayoutInProgress = false;

foreach (KeyValuePair<Layout, int> kvp in copy)
{
Expand Down
3 changes: 3 additions & 0 deletions Xamarin.Forms.Core/VisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,10 @@ public void BatchCommit()
{
_batched = Math.Max(0, _batched - 1);
if (!Batched)
{
BatchCommitted?.Invoke(this, new EventArg<VisualElement>(this));
Device.Invalidate(this);
}
}

ResourceDictionary _resources;
Expand Down
20 changes: 18 additions & 2 deletions Xamarin.Forms.Platform.Android/Forms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -610,7 +614,7 @@ protected override Expression VisitMember(MemberExpression node)
}
}

class AndroidPlatformServices : IPlatformServices
class AndroidPlatformServices : IPlatformServices, IPlatformInvalidate
{
double _buttonDefaultSize;
double _editTextDefaultSize;
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions Xamarin.Forms.Platform.Android/Platform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions Xamarin.Forms.Platform.Android/VisualElementTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion Xamarin.Forms.Platform.UAP/Forms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
6 changes: 6 additions & 0 deletions Xamarin.Forms.Platform.UAP/LayoutRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
13 changes: 12 additions & 1 deletion Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ protected override void Dispose(bool disposing)

_emptyUIView?.Dispose();
_emptyUIView = null;

_emptyViewFormsElement = null;

ItemsViewLayout?.Dispose();
Expand Down
21 changes: 20 additions & 1 deletion Xamarin.Forms.Platform.iOS/Forms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -238,6 +242,9 @@ protected override Expression VisitMember(MemberExpression node)
}

class IOSPlatformServices : IPlatformServices
#if __MOBILE__
, IPlatformInvalidate
#endif
{
readonly double _fontScalingFactor = 1;
public IOSPlatformServices()
Expand Down Expand Up @@ -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
}
}
Expand Down
20 changes: 20 additions & 0 deletions Xamarin.Forms.Platform.iOS/Platform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit e8d06f3

Please sign in to comment.