Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Modal page offset measuring for AdjustPan and AdjustResize #12661

Merged
merged 6 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,21 +123,17 @@ protected virtual void LoadView(IShellContext shellContext)
// so we want to delay loading to the latest possible point in time so
// it doesn't delay initial startup.
GenericGlobalLayoutListener ggll = null;
ggll = new GenericGlobalLayoutListener(InitialLoad);
sfl.ViewTreeObserver.AddOnGlobalLayoutListener(ggll);
ggll = new GenericGlobalLayoutListener(InitialLoad, sfl);

void InitialLoad()
void InitialLoad(GenericGlobalLayoutListener listener, AView view)
{
OnFlyoutViewLayoutChanging();

if (_flyoutContentView == null || ggll == null)
return;

var listener = ggll;
ggll = null;

// Once initial load has finished let's just attach to Layout Changing
sfl.ViewTreeObserver.RemoveOnGlobalLayoutListener(listener);
listener.Invalidate();
mattleibow marked this conversation as resolved.
Show resolved Hide resolved
sfl.LayoutChanging += OnFlyoutViewLayoutChanging;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ public ShellToolbarTracker(IShellContext shellContext, AToolbar toolbar, DrawerL
_drawerLayout = drawerLayout ?? throw new ArgumentNullException(nameof(drawerLayout));
_appBar = _platformToolbar.Parent.GetParentOfType<AppBarLayout>();

_globalLayoutListener = new GenericGlobalLayoutListener(() => UpdateNavBarHasShadow(Page));
_appBar.ViewTreeObserver.AddOnGlobalLayoutListener(_globalLayoutListener);
_globalLayoutListener = new GenericGlobalLayoutListener((_,_) => UpdateNavBarHasShadow(Page), _appBar);
_platformToolbar.SetNavigationOnClickListener(this);
((IShellController)ShellContext.Shell).AddFlyoutBehaviorObserver(this);
ShellContext.Shell.Toolbar.PropertyChanged += OnToolbarPropertyChanged;
Expand Down Expand Up @@ -175,9 +174,6 @@ protected override void Dispose(bool disposing)

if (disposing)
{
if (_appBar.IsAlive() && _appBar.ViewTreeObserver.IsAlive())
_appBar.ViewTreeObserver.RemoveOnGlobalLayoutListener(_globalLayoutListener);

_globalLayoutListener.Invalidate();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the invalidate call takes care of removing the listener


if (_backButtonBehavior != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
#nullable disable
using System;
using Android.Views;
using AView = Android.Views.View;
using Object = Java.Lang.Object;

namespace Microsoft.Maui.Controls.Platform
{
internal class GenericGlobalLayoutListener : Object, ViewTreeObserver.IOnGlobalLayoutListener
{
Action _callback;
Action<GenericGlobalLayoutListener, AView?>? _callback;
WeakReference<AView>? _targetView;

public GenericGlobalLayoutListener(Action callback)
public GenericGlobalLayoutListener(Action<GenericGlobalLayoutListener, AView?> callback, AView? targetView = null)
{
_callback = callback;

if (targetView?.ViewTreeObserver != null)
{
_targetView = new WeakReference<AView>(targetView);
targetView.ViewTreeObserver.AddOnGlobalLayoutListener(this);
}
}

public void OnGlobalLayout()
{
_callback?.Invoke();
AView? targetView = null;
_targetView?.TryGetTarget(out targetView);
_callback?.Invoke(this, targetView);
}

protected override void Dispose(bool disposing)
Expand All @@ -30,6 +39,16 @@ protected override void Dispose(bool disposing)
internal void Invalidate()
{
_callback = null;

if (_targetView != null &&
_targetView.TryGetTarget(out var targetView) &&
targetView.IsAlive() &&
targetView.ViewTreeObserver != null)
{
targetView.ViewTreeObserver.RemoveOnGlobalLayoutListener(this);
}

_targetView = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,39 @@
using Android.Views.Animations;
using AndroidX.Activity;
using AndroidX.AppCompat.App;
using AndroidX.AppCompat.Widget;
using AndroidX.Core.View;
using AndroidX.Fragment.App;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using AView = Android.Views.View;

namespace Microsoft.Maui.Controls.Platform
{
internal partial class ModalNavigationManager
{
ViewGroup GetModalParentView()
ViewGroup? _modalParentView;

// This is only here for the device tests to use.
// With the device tests we have a `FakeActivityRootView` and a `WindowTestFragment`
// that we use to replicate the `DecorView` and `MainActivity`
// The tests will set this to the `FakeActivityRootView` so that the `modals`
// are part of the correct testing space.
// If/When we move to opening new activities we can remove this code.
internal void SetModalParentView(ViewGroup viewGroup)
mattleibow marked this conversation as resolved.
Show resolved Hide resolved
{
var currentRootView = GetCurrentRootView() as ViewGroup;

if (_window?.PlatformActivity?.GetWindow() == _window)
{
currentRootView = _window?.PlatformActivity?.Window?.DecorView as ViewGroup;
}
_modalParentView = viewGroup;
}

return currentRootView ??
ViewGroup GetModalParentView()
{
return _modalParentView ??
mattleibow marked this conversation as resolved.
Show resolved Hide resolved
_window?.PlatformActivity?.Window?.DecorView as ViewGroup ??
throw new InvalidOperationException("Root View Needs to be set");
}

bool _navAnimationInProgress;
internal const string CloseContextActionsSignalName = "Xamarin.CloseContextActions";
Page CurrentPage => _navModel.CurrentPage;

// AFAICT this is specific to ListView and Context Items
internal bool NavAnimationInProgress
Expand Down Expand Up @@ -197,6 +206,10 @@ sealed class ModalContainer : ViewGroup
ModalFragment _modalFragment;
FragmentManager? _fragmentManager;
NavigationRootManager? NavigationRootManager => _modalFragment.NavigationRootManager;
int _currentRootViewHeight = 0;
int _currentRootViewWidth = 0;
GenericGlobalLayoutListener? _rootViewLayoutListener;
AView? _rootView;

AView GetWindowRootView() =>
_windowMauiContext
Expand All @@ -212,7 +225,6 @@ public ModalContainer(
{
_windowMauiContext = windowMauiContext;
Modal = modal;

_backgroundView = new AView(_windowMauiContext.Context);
UpdateBackgroundColor();
AddView(_backgroundView);
Expand All @@ -228,22 +240,110 @@ public ModalContainer(
.BeginTransaction()
.Add(this.Id, _modalFragment)
.Commit();
}

protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
UpdateMargin();
UpdateRootView(GetWindowRootView());
}

protected override void OnDetachedFromWindow()
{
base.OnDetachedFromWindow();
UpdateRootView(null);
}

void UpdateRootView(AView? rootView)
{
if (_rootView.IsAlive() && _rootView != null)
{
_rootView.LayoutChange -= OnRootViewLayoutChanged;
_rootView = null;
}

if (rootView.IsAlive() && rootView != null)
{
rootView.LayoutChange += OnRootViewLayoutChanged;
_rootView = rootView;
_currentRootViewHeight = _rootView.MeasuredHeight;
_currentRootViewWidth = _rootView.MeasuredWidth;
}
}

// If the RootView changes sizes that means we also need to change sizes
// This will typically happen when the user is opening the soft keyboard
// which sometimes causes the available window size to change
void OnRootViewLayoutChanged(object? sender, LayoutChangeEventArgs e)
{
if (Modal == null || sender is not AView view)
return;

var modalStack = Modal?.Navigation?.ModalStack;
if (modalStack == null ||
modalStack.Count == 0 ||
modalStack[modalStack.Count - 1] != Modal)
{
return;
}

if ((_currentRootViewHeight != view.MeasuredHeight || _currentRootViewWidth != view.MeasuredWidth)
&& this.ViewTreeObserver != null)
{
// When the keyboard closes Android calls layout but doesn't call remeasure.
// MY guess is that this is due to the modal not being part of the FitSystemWindowView
// The modal is added to the decor view so its dimensions don't get updated.
// So, here we are waiting for the layout pass to finish and then we remeasure the modal
//
// For .NET 8 we'll convert this all over to using a DialogFragment
// which means we can delete most of the awkward code here
_currentRootViewHeight = view.MeasuredHeight;
_currentRootViewWidth = view.MeasuredWidth;
if (!this.IsInLayout)
{
this.InvalidateMeasure(Modal);
return;
}

_rootViewLayoutListener ??= new GenericGlobalLayoutListener((listener, view) =>
{
if (view != null && !this.IsInLayout)
{
listener.Invalidate();
_rootViewLayoutListener = null;
this.InvalidateMeasure(Modal);
}
}, this);
}
}

void UpdateMargin()
{
// This sets up the modal container to be offset from the top of window the same
// amount as the view it's covering. This will make it so the
// ModalContainer takes into account the statusbar or lack thereof
var rootView = GetWindowRootView();
int y = (int)rootView.GetLocationOnScreenPx().Y;
// ModalContainer takes into account the StatusBar or lack thereof
var decorView = Context?.GetActivity()?.Window?.DecorView;
mattleibow marked this conversation as resolved.
Show resolved Hide resolved

if (this.LayoutParameters is ViewGroup.MarginLayoutParams mlp &&
mlp.TopMargin != y)
if (decorView != null && this.LayoutParameters is ViewGroup.MarginLayoutParams mlp)
{
mlp.TopMargin = y;
var windowInsets = ViewCompat.GetRootWindowInsets(decorView);
if (windowInsets != null)
{
var barInsets = windowInsets.GetInsetsIgnoringVisibility(WindowInsetsCompat.Type.SystemBars());

if (mlp.TopMargin != barInsets.Top)
mlp.TopMargin = barInsets.Top;

if (mlp.LeftMargin != barInsets.Left)
mlp.LeftMargin = barInsets.Left;

if (mlp.RightMargin != barInsets.Right)
mlp.RightMargin = barInsets.Right;

if (mlp.BottomMargin != barInsets.Bottom)
mlp.BottomMargin = barInsets.Bottom;
}
}
}

Expand All @@ -261,8 +361,8 @@ protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
return;
}

var rootView = GetWindowRootView();
UpdateMargin();
var rootView = GetWindowRootView();

widthMeasureSpec = MeasureSpecMode.Exactly.MakeMeasureSpec(rootView.MeasuredWidth);
heightMeasureSpec = MeasureSpecMode.Exactly.MakeMeasureSpec(rootView.MeasuredHeight);
Expand Down Expand Up @@ -313,6 +413,10 @@ public void Destroy()

Modal.Handler = null;

UpdateRootView(null);
_rootViewLayoutListener?.Invalidate();
_rootViewLayoutListener = null;

_fragmentManager
.BeginTransaction()
.Remove(_modalFragment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ public override AView OnCreateView(ALayoutInflater inflater, AViewGroup containe
FakeActivityRootView.AddView(handler.PlatformViewUnderTest);
handler.PlatformViewUnderTest.LayoutParameters = new FitWindowsFrameLayout.LayoutParams(AViewGroup.LayoutParams.MatchParent, AViewGroup.LayoutParams.MatchParent);

if (_window is Window window)
{
window.ModalNavigationManager.SetModalParentView(FakeActivityRootView);
}

return FakeActivityRootView;
}

Expand Down
Loading