Shell flyout content template (#13190) fixes #6293

* Flyout Content

* - uitest

* - ios fix

* - fix ios

* - fix ios

* - fix offset

* - fix measuring issues

* - fix ScrollView Check

* - Cleanup iOS

* - remove comments

* - fix up ui test

* Remove UWP for UI Tests

* - fix merge

* - Add Flyout Items

* - fix uwp flyout items from rebinding

* - cleanup code

* Update ShellFlyoutContentRenderer.cs
This commit is contained in:
Shane Neuville 2020-12-31 08:30:50 -06:00 коммит произвёл GitHub
Родитель 75f9a3fdf9
Коммит 4048fab83b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
24 изменённых файлов: 1330 добавлений и 405 удалений

Просмотреть файл

@ -397,7 +397,7 @@
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<ProjectExtensions>
<VisualStudio>
<UserProperties XamarinHotReloadUnhandledDeviceExceptionXamarinFormsControlGalleryAndroidHideInfoBar="True" TriggeredFromHotReload="False" />
<UserProperties TriggeredFromHotReload="False" XamarinHotReloadUnhandledDeviceExceptionXamarinFormsControlGalleryAndroidHideInfoBar="True" />
</VisualStudio>
</ProjectExtensions>
</Project>

Просмотреть файл

@ -110,8 +110,11 @@ namespace Xamarin.Forms.Controls.Issues
{
Device.InvokeOnMainThreadAsync(() =>
{
var page = AddBottomTab("Success");
page.Content = new Label() { Text = "Success" };
var page = AddBottomTab("Flyout Item");
page.Content = new Label()
{
Text = "Success"
};
});
}

Просмотреть файл

@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
#if UITEST
using Xamarin.UITest;
using NUnit.Framework;
using Xamarin.Forms.Core.UITests;
#endif
namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.None, 0, "Shell Flyout Content",
PlatformAffected.All)]
#if UITEST
[NUnit.Framework.Category(UITestCategories.Shell)]
[NUnit.Framework.Category(UITestCategories.UwpIgnore)]
#endif
public class ShellFlyoutContent : TestShell
{
protected override void Init()
{
var page = new ContentPage();
this.BindingContext = this;
AddFlyoutItem(page, "Flyout Item Top");
for (int i = 0; i < 50; i++)
{
AddFlyoutItem($"Flyout Item :{i}");
Items[i].AutomationId = "Flyout Item";
}
Items.Add(new MenuItem() { Text = "Menu Item" });
AddFlyoutItem("Flyout Item Bottom");
var layout = new StackLayout()
{
Children =
{
new Label()
{
Text = "Open the Flyout and Toggle the Content, Header and Footer. If it changes after each click test has passed",
AutomationId = "PageLoaded"
}
}
};
page.Content = layout;
layout.Children.Add(new Button()
{
Text = "Toggle Flyout Content Template",
Command = new Command(() =>
{
if (FlyoutContentTemplate == null)
{
FlyoutContentTemplate = new DataTemplate(() =>
{
var collectionView = new CollectionView();
collectionView.SetBinding(CollectionView.ItemsSourceProperty, "FlyoutItems");
collectionView.IsGrouped = true;
collectionView.ItemTemplate =
new DataTemplate(() =>
{
var label = new Label();
label.SetBinding(Label.TextProperty, "Title");
var button = new Button()
{
Text = "Click to Reset",
AutomationId = "ContentView",
Command = new Command(() =>
{
FlyoutContentTemplate = null;
})
};
return new StackLayout()
{
Children =
{
label,
button
}
};
});
return collectionView;
});
}
else if (FlyoutContentTemplate != null)
{
FlyoutContentTemplate = null;
}
}),
AutomationId = "ToggleFlyoutContentTemplate"
});
layout.Children.Add(new Button()
{
Text = "Toggle Flyout Content",
Command = new Command(() =>
{
if (FlyoutContent != null)
{
FlyoutContent = null;
}
else
{
var stackLayout = new StackLayout()
{
Background = SolidColorBrush.Green
};
FlyoutContent = new ScrollView()
{
Content = stackLayout
};
AddButton("Top Button");
for (int i = 0; i < 50; i++)
{
AddButton("Content View");
}
AddButton("Bottom Button");
void AddButton(string text)
{
stackLayout.Children.Add(new Button()
{
Text = text,
AutomationId = "ContentView",
Command = new Command(() =>
{
FlyoutContent = null;
}),
TextColor = Color.White
});
}
}
}),
AutomationId = "ToggleContent"
});
layout.Children.Add(new Button()
{
Text = "Toggle Header/Footer View",
Command = new Command(() =>
{
if (FlyoutHeader != null)
{
FlyoutHeader = null;
FlyoutFooter = null;
}
else
{
FlyoutHeader = new StackLayout()
{
Children = {
new Label() { Text = "Header" }
},
AutomationId = "Header View",
Background = SolidColorBrush.Yellow
};
FlyoutFooter = new StackLayout()
{
Background = SolidColorBrush.Orange,
Orientation = StackOrientation.Horizontal,
Children = {
new Label() { Text = "Footer" }
},
AutomationId = "Footer View"
};
}
}),
AutomationId = "ToggleHeaderFooter"
});
}
#if UITEST
[Test]
public void FlyoutContentTests()
{
RunningApp.WaitForElement("PageLoaded");
TapInFlyout("Flyout Item");
RunningApp.Tap("ToggleContent");
TapInFlyout("ContentView");
TapInFlyout("Flyout Item");
RunningApp.Tap("ToggleFlyoutContentTemplate");
TapInFlyout("ContentView");
TapInFlyout("Flyout Item");
}
#endif
}
}

Просмотреть файл

@ -663,7 +663,7 @@ namespace Xamarin.Forms.Controls
ContentPage page = new ContentPage();
if (Items.Count == 0)
{
var item = AddContentPage(page);
var item = AddContentPage(page, title);
item.Items[0].Items[0].Title = title ?? page.Title;
item.Items[0].Title = title ?? page.Title;
return page;

Просмотреть файл

@ -12,6 +12,7 @@
<Compile Include="$(MSBuildThisFileDirectory)CollectionViewGroupTypeIssue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue11214.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue13109.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ShellFlyoutContent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue4720.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue10897.xaml.cs">
<DependentUpon>Issue10897.xaml</DependentUpon>

Просмотреть файл

@ -165,9 +165,7 @@ namespace Xamarin.Forms.Core.UnitTests
flyoutItem.CurrentItem.CurrentItem.MenuItems.Add(CreateNonVisibleMenuItem());
shell.Items.Add(flyoutItem);
IShellController shellController = (IShellController)shell;
var groups = shellController.GenerateFlyoutGrouping();
var groups = shell.Controller.GenerateFlyoutGrouping();
Assert.AreEqual(groups.SelectMany(x => x.OfType<IMenuItemController>()).Count(), 0);
}
@ -221,6 +219,89 @@ namespace Xamarin.Forms.Core.UnitTests
Assert.AreNotSame(flyoutItems, flyoutItems2);
}
[Test]
public void FlyoutItemsBasicSyncTest()
{
var shell = new TestShell();
shell.Items.Add(CreateShellItem<FlyoutItem>());
shell.Items.Add(CreateShellItem<FlyoutItem>());
shell.Items.Add(CreateShellItem<FlyoutItem>());
shell.Items.Add(CreateShellItem<FlyoutItem>());
shell.Items[3].IsVisible = false;
var flyoutItems = shell.GenerateTestFlyoutItems();
Assert.AreEqual(shell.Items[0], flyoutItems[0][0]);
Assert.AreEqual(shell.Items[1], flyoutItems[0][1]);
Assert.AreEqual(shell.Items[2], flyoutItems[0][2]);
Assert.AreEqual(3, flyoutItems[0].Count);
Assert.AreEqual(1, flyoutItems.Count);
}
[Test]
public void FlyoutItemsGroupTest()
{
var shell = new TestShell();
shell.Items.Add(CreateShellItem<FlyoutItem>());
shell.Items.Add(CreateShellItem<FlyoutItem>());
var sec1 = shell.Items[0].Items[0];
var sec2 = CreateShellSection<Tab>();
var sec3 = CreateShellSection<Tab>();
shell.Items[0].FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems;
shell.Items[0].Items.Add(sec2);
shell.Items[0].Items.Add(sec3);
var flyoutItems = shell.GenerateTestFlyoutItems();
Assert.AreEqual(sec1, flyoutItems[0][0]);
Assert.AreEqual(sec2, flyoutItems[0][1]);
Assert.AreEqual(sec3, flyoutItems[0][2]);
Assert.AreEqual(shell.Items[1], flyoutItems[1][0]);
}
[Test]
public void FlyoutItemsGroupTestWithRemove()
{
var shell = new TestShell();
shell.Items.Add(CreateShellItem<FlyoutItem>());
shell.Items.Add(CreateShellItem<FlyoutItem>());
var sec1 = shell.Items[0].Items[0];
var sec2 = CreateShellSection<Tab>();
var sec3 = CreateShellSection<Tab>();
shell.Items[0].FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems;
shell.Items[0].Items.Add(sec2);
shell.Items[0].Items.Add(sec3);
shell.Items.RemoveAt(0);
var flyoutItems = shell.GenerateTestFlyoutItems();
Assert.AreEqual(shell.Items[0], flyoutItems[0][0]);
Assert.AreEqual(1, flyoutItems.Count);
}
[Test]
public void FlyoutItemsGroupTestMoveGroup()
{
var shell = new TestShell();
shell.Items.Add(CreateShellItem<FlyoutItem>());
shell.Items.Add(CreateShellItem<FlyoutItem>());
var sec1 = shell.Items[0].Items[0];
var sec2 = CreateShellSection<Tab>();
var sec3 = CreateShellSection<Tab>();
shell.Items[0].FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems;
shell.Items[0].Items.Add(sec2);
shell.Items[0].Items.Add(sec3);
var item1 = shell.Items[0];
shell.Items.RemoveAt(0);
shell.Items.Add(item1);
var flyoutItems = shell.GenerateTestFlyoutItems();
Assert.AreEqual(sec1, flyoutItems[1][0]);
Assert.AreEqual(sec2, flyoutItems[1][1]);
Assert.AreEqual(sec3, flyoutItems[1][2]);
Assert.AreEqual(shell.Items[0], flyoutItems[0][0]);
}
MenuItem CreateNonVisibleMenuItem()
{
MenuItem item = new MenuItem();

Просмотреть файл

@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
@ -300,6 +301,18 @@ namespace Xamarin.Forms.Core.UnitTests
public IShellController Controller => this;
public List<List<Element>> GenerateTestFlyoutItems()
{
List<List<Element>> returnValue = new List<List<Element>>();
FlyoutItems
.OfType<IEnumerable>()
.ForEach(l => returnValue.Add(l.OfType<Element>().ToList()));
return returnValue;
}
public TestShell()
{
this.Navigated += (_, __) => NavigatedCount++;

Просмотреть файл

@ -165,7 +165,6 @@ namespace Xamarin.Forms
if (titleViewPart2TheNavBar != null)
yield return titleViewPart2TheNavBar;
}
}

Просмотреть файл

@ -25,6 +25,8 @@ namespace Xamarin.Forms
View FlyoutFooter { get; }
View FlyoutContent { get; }
ImageSource FlyoutIcon { get; }
void AddAppearanceObserver(IAppearanceObserver observer, Element pivot);

Просмотреть файл

@ -1,15 +1,14 @@

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms.Internals;
using Xamarin.Forms.StyleSheets;
namespace Xamarin.Forms
{
@ -296,14 +295,13 @@ namespace Xamarin.Forms
event EventHandler IShellController.FlyoutItemsChanged
{
add { _flyoutItemsChanged += value; }
remove { _flyoutItemsChanged -= value; }
add { _flyoutManager.FlyoutItemsChanged += value; }
remove { _flyoutManager.FlyoutItemsChanged -= value; }
}
event EventHandler _flyoutItemsChanged;
View IShellController.FlyoutHeader => FlyoutHeaderView;
View IShellController.FlyoutFooter => FlyoutFooterView;
View IShellController.FlyoutContent => FlyoutContentView;
IShellController ShellController => this;
@ -440,7 +438,7 @@ namespace Xamarin.Forms
bool IShellController.ProposeNavigation(ShellNavigationSource source, ShellItem shellItem, ShellSection shellSection, ShellContent shellContent, IReadOnlyList<Page> stack, bool canCancel)
{
return _navigationManager.ProposeNavigationOutsideGotoAsync(source, shellItem, shellSection, shellContent, stack, canCancel);
return _navigationManager.ProposeNavigationOutsideGotoAsync(source, shellItem, shellSection, shellContent, stack, canCancel, true);
}
bool IShellController.RemoveAppearanceObserver(IAppearanceObserver observer)
@ -546,8 +544,8 @@ namespace Xamarin.Forms
View _flyoutHeaderView;
View _flyoutFooterView;
List<List<Element>> _currentFlyoutViews;
ShellNavigationManager _navigationManager;
ShellFlyoutItemsManager _flyoutManager;
public Shell()
{
@ -564,6 +562,7 @@ namespace Xamarin.Forms
OnNavigating(args);
};
_flyoutManager = new ShellFlyoutItemsManager(this);
Navigation = new NavigationImpl(this);
Route = Routing.GenerateImplicitRoute("shell");
Initialize();
@ -822,166 +821,18 @@ namespace Xamarin.Forms
if (FlyoutFooterView != null)
SetInheritedBindingContext(FlyoutFooterView, BindingContext);
if (FlyoutContentView != null)
SetInheritedBindingContext(FlyoutContentView, BindingContext);
}
internal void SendFlyoutItemsChanged()
{
if (UpdateFlyoutGroupings())
_flyoutItemsChanged?.Invoke(this, EventArgs.Empty);
}
internal void SendFlyoutItemsChanged() => _flyoutManager.CheckIfFlyoutItemsChanged();
List<List<Element>> IShellController.GenerateFlyoutGrouping()
{
if (_currentFlyoutViews == null)
UpdateFlyoutGroupings();
public IEnumerable FlyoutItems => _flyoutManager.FlyoutItems;
return _currentFlyoutViews;
}
bool UpdateFlyoutGroupings()
{
// The idea here is to create grouping such that the Flyout would
// render correctly if it renderered each item in the groups in order
// but put a line between the groups. This is needed because our grouping can
// actually go 3 layers deep.
// Ideally this lets us control where lines are drawn in the core code
// just by changing how we generate these groupings
var result = new List<List<Element>>();
var currentGroup = new List<Element>();
foreach (var shellItem in ShellController.GetItems())
{
if (!ShowInFlyoutMenu(shellItem))
continue;
if (Routing.IsImplicit(shellItem) || shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
{
if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
IncrementGroup();
foreach (var shellSection in (shellItem as IShellItemController).GetItems())
{
if (!ShowInFlyoutMenu(shellSection))
continue;
var shellContents = ((IShellSectionController)shellSection).GetItems();
if (Routing.IsImplicit(shellSection) || shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
{
foreach (var shellContent in shellContents)
{
if (!ShowInFlyoutMenu(shellContent))
continue;
currentGroup.Add(shellContent);
if (shellContent == shellSection.CurrentItem)
{
AddMenuItems(shellContent.MenuItems);
}
}
if (shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
IncrementGroup();
}
else
{
if (!(shellSection.Parent is TabBar))
{
if (Routing.IsImplicit(shellSection) && shellContents.Count == 1)
{
if (!ShowInFlyoutMenu(shellContents[0]))
continue;
currentGroup.Add(shellContents[0]);
}
else
currentGroup.Add(shellSection);
}
// If we have only a single child we will also show the items menu items
if (shellContents.Count == 1 && shellSection == shellItem.CurrentItem && shellSection.CurrentItem.MenuItems.Count > 0)
{
AddMenuItems(shellSection.CurrentItem.MenuItems);
}
}
}
if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
IncrementGroup();
}
else
{
if (!(shellItem is TabBar))
currentGroup.Add(shellItem);
}
}
IncrementGroup();
// If the flyout groupings haven't changed just return
// the same instance so the caller knows it hasn't changed
// at a later point this will all get converted to an observable collection
if (_currentFlyoutViews?.Count == result.Count)
{
bool hasChanged = false;
for (var i = 0; i < result.Count && !hasChanged; i++)
{
var topLevelNew = result[i];
var topLevelPrevious = _currentFlyoutViews[i];
if (topLevelNew.Count != topLevelPrevious.Count)
{
hasChanged = true;
break;
}
for (var j = 0; j > topLevelNew.Count; j++)
{
if (topLevelNew[j] != topLevelPrevious[j])
{
hasChanged = true;
break;
}
}
}
if (!hasChanged)
return false;
}
_currentFlyoutViews = result;
return true;
bool ShowInFlyoutMenu(BindableObject bo)
{
if (bo is MenuShellItem msi)
return Shell.GetFlyoutItemIsVisible(msi.MenuItem);
return Shell.GetFlyoutItemIsVisible(bo);
}
void AddMenuItems(MenuItemCollection menuItems)
{
foreach (var item in menuItems)
{
if (ShowInFlyoutMenu(item))
currentGroup.Add(item);
}
}
void IncrementGroup()
{
if (currentGroup.Count > 0)
{
result.Add(currentGroup);
currentGroup = new List<Element>();
}
}
}
List<List<Element>> IShellController.GenerateFlyoutGrouping() =>
_flyoutManager.GenerateFlyoutGrouping();
internal void SendStructureChanged()
{
@ -1070,7 +921,7 @@ namespace Xamarin.Forms
var shellSection = shellItem.CurrentItem;
var shellContent = shellSection.CurrentItem;
var stack = shellSection.Stack;
shell._navigationManager.ProposeNavigationOutsideGotoAsync(ShellNavigationSource.ShellItemChanged, shellItem, shellSection, shellContent, stack, false);
shell._navigationManager.ProposeNavigationOutsideGotoAsync(ShellNavigationSource.ShellItemChanged, shellItem, shellSection, shellContent, stack, false, true);
}
static void UpdateChecked(Element root, bool isChecked = true)
@ -1346,6 +1197,85 @@ namespace Xamarin.Forms
}
#region Shell Flyout Content
public static readonly BindableProperty FlyoutContentProperty =
BindableProperty.Create(nameof(FlyoutContent), typeof(object), typeof(Shell), null, BindingMode.OneTime, propertyChanging: OnFlyoutContentChanging);
public static readonly BindableProperty FlyoutContentTemplateProperty =
BindableProperty.Create(nameof(FlyoutContentTemplate), typeof(DataTemplate), typeof(Shell), null, BindingMode.OneTime, propertyChanging: OnFlyoutContentTemplateChanging);
View _flyoutContentView;
public View FlyoutContent
{
get => (View)GetValue(FlyoutContentProperty);
set => SetValue(FlyoutContentProperty, value);
}
public DataTemplate FlyoutContentTemplate
{
get => (DataTemplate)GetValue(FlyoutContentTemplateProperty);
set => SetValue(FlyoutContentTemplateProperty, value);
}
View FlyoutContentView
{
get => _flyoutContentView;
set
{
if (_flyoutContentView == value)
return;
if (_flyoutContentView != null)
OnChildRemoved(_flyoutContentView, -1);
_flyoutContentView = value;
if (_flyoutContentView != null)
OnChildAdded(_flyoutContentView);
}
}
void OnFlyoutContentChanged(object oldVal, object newVal)
{
if (FlyoutContentTemplate == null)
{
if (newVal is View newFlyoutContent)
FlyoutContentView = newFlyoutContent;
else
FlyoutContentView = null;
}
}
void OnFlyoutContentTemplateChanged(DataTemplate oldValue, DataTemplate newValue)
{
if (newValue == null)
{
if (FlyoutContent is View flyoutContentView)
FlyoutContentView = flyoutContentView;
else
FlyoutContentView = null;
}
else
{
var newContentView = (View)newValue.CreateContent(FlyoutContent, this);
FlyoutContentView = newContentView;
}
}
static void OnFlyoutContentChanging(BindableObject bindable, object oldValue, object newValue)
{
var shell = (Shell)bindable;
shell.OnFlyoutContentChanged(oldValue, newValue);
}
static void OnFlyoutContentTemplateChanging(BindableObject bindable, object oldValue, object newValue)
{
var shell = (Shell)bindable;
shell.OnFlyoutContentTemplateChanged((DataTemplate)oldValue, (DataTemplate)newValue);
}
#endregion
[EditorBrowsable(EditorBrowsableState.Never)]
public static void VerifyShellUWPFlagEnabled(

Просмотреть файл

@ -0,0 +1,248 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Xamarin.Forms
{
internal class ShellFlyoutItemsManager
{
readonly Shell _shell;
List<List<Element>> _lastGeneratedFlyoutItems;
public event EventHandler FlyoutItemsChanged;
IShellController ShellController => _shell;
ReadOnlyObservableCollectionWithSource<IReadOnlyList<Element>> _flyoutItemsReadonly;
public IEnumerable FlyoutItems => _flyoutItemsReadonly;
public ShellFlyoutItemsManager(Shell shell)
{
_shell = shell;
_flyoutItemsReadonly = new ReadOnlyObservableCollectionWithSource<IReadOnlyList<Element>>();
}
void SyncFlyoutItemsToReadOnlyCollection()
{
var flyoutItems = _flyoutItemsReadonly.List;
// sync the number of groups
for (var i = flyoutItems.Count; i < _lastGeneratedFlyoutItems.Count; i++)
flyoutItems.Add(new ReadOnlyObservableCollectionWithSource<Element>());
for (var i = _lastGeneratedFlyoutItems.Count; i < flyoutItems.Count; i++)
flyoutItems.RemoveAt(i);
for (var i = 0; i < _lastGeneratedFlyoutItems.Count; i++)
{
var source = _lastGeneratedFlyoutItems[i];
var dest = ((ReadOnlyObservableCollectionWithSource<Element>)flyoutItems[i]).List;
for (var j = dest.Count - 1; j >= 0; j--)
{
var item = dest[j];
if (!source.Contains(item))
dest.RemoveAt(j);
}
for (var j = 0; j < source.Count; j++)
{
var item = source[j];
var destIndex = dest.IndexOf(item);
if (destIndex == -1)
{
if (j < dest.Count)
dest.Insert(j, item);
else
dest.Add(item);
}
else
{
if (j < dest.Count)
{
if(destIndex != j)
dest.Move(destIndex, j);
}
else
dest.Add(item);
}
}
}
}
public void CheckIfFlyoutItemsChanged()
{
if (UpdateFlyoutGroupings())
{
FlyoutItemsChanged?.Invoke(this, EventArgs.Empty);
SyncFlyoutItemsToReadOnlyCollection();
}
}
public List<List<Element>> GenerateFlyoutGrouping()
{
if (_lastGeneratedFlyoutItems == null)
UpdateFlyoutGroupings();
return _lastGeneratedFlyoutItems;
}
bool UpdateFlyoutGroupings()
{
// The idea here is to create grouping such that the Flyout would
// render correctly if it renderered each item in the groups in order
// but put a line between the groups. This is needed because our grouping can
// actually go 3 layers deep.
// Ideally this lets us control where lines are drawn in the core code
// just by changing how we generate these groupings
var result = new List<List<Element>>();
var currentGroup = new List<Element>();
foreach (var shellItem in ShellController.GetItems())
{
if (!ShowInFlyoutMenu(shellItem))
continue;
if (Routing.IsImplicit(shellItem) || shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
{
if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
IncrementGroup();
foreach (var shellSection in (shellItem as IShellItemController).GetItems())
{
if (!ShowInFlyoutMenu(shellSection))
continue;
var shellContents = ((IShellSectionController)shellSection).GetItems();
if (Routing.IsImplicit(shellSection) || shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
{
foreach (var shellContent in shellContents)
{
if (!ShowInFlyoutMenu(shellContent))
continue;
currentGroup.Add(shellContent);
if (shellContent == shellSection.CurrentItem)
{
AddMenuItems(shellContent.MenuItems);
}
}
if (shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
IncrementGroup();
}
else
{
if (!(shellSection.Parent is TabBar))
{
if (Routing.IsImplicit(shellSection) && shellContents.Count == 1)
{
if (!ShowInFlyoutMenu(shellContents[0]))
continue;
currentGroup.Add(shellContents[0]);
}
else
currentGroup.Add(shellSection);
}
// If we have only a single child we will also show the items menu items
if (shellContents.Count == 1 && shellSection == shellItem.CurrentItem && shellSection.CurrentItem.MenuItems.Count > 0)
{
AddMenuItems(shellSection.CurrentItem.MenuItems);
}
}
}
if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
IncrementGroup();
}
else
{
if (!(shellItem is TabBar))
currentGroup.Add(shellItem);
}
}
IncrementGroup();
// If the flyout groupings haven't changed just return
// the same instance so the caller knows it hasn't changed
// at a later point this will all get converted to an observable collection
if (_lastGeneratedFlyoutItems?.Count == result.Count)
{
bool hasChanged = false;
for (var i = 0; i < result.Count && !hasChanged; i++)
{
var topLevelNew = result[i];
var topLevelPrevious = _lastGeneratedFlyoutItems[i];
if (topLevelNew.Count != topLevelPrevious.Count)
{
hasChanged = true;
break;
}
for (var j = 0; j > topLevelNew.Count; j++)
{
if (topLevelNew[j] != topLevelPrevious[j])
{
hasChanged = true;
break;
}
}
}
if (!hasChanged)
return false;
}
_lastGeneratedFlyoutItems = result;
return true;
bool ShowInFlyoutMenu(BindableObject bo)
{
if (bo is MenuShellItem msi)
return Shell.GetFlyoutItemIsVisible(msi.MenuItem);
return Shell.GetFlyoutItemIsVisible(bo);
}
void AddMenuItems(MenuItemCollection menuItems)
{
foreach (var item in menuItems)
{
if (ShowInFlyoutMenu(item))
currentGroup.Add(item);
}
}
void IncrementGroup()
{
if (currentGroup.Count > 0)
{
result.Add(currentGroup);
currentGroup = new List<Element>();
}
}
}
class ReadOnlyObservableCollectionWithSource<T> : ReadOnlyObservableCollection<T>
{
public ReadOnlyObservableCollectionWithSource() : this(new ObservableCollection<T>())
{
}
public ReadOnlyObservableCollectionWithSource(ObservableCollection<T> list) : base(list)
{
List = list;
}
public ObservableCollection<T> List { get; }
}
}
}

Просмотреть файл

@ -51,7 +51,7 @@ namespace Xamarin.Forms
// This scenario only comes up from UI iniated navigation (i.e. switching tabs)
if (deferredArgs == null)
{
var navigatingArgs = ProposeNavigation(source, state, _shell.CurrentState != null);
var navigatingArgs = ProposeNavigation(source, state, _shell.CurrentState != null, animate ?? true);
bool accept = !navigatingArgs.NavigationDelayedOrCancelled;
if (navigatingArgs.DeferredTask != null)
@ -273,13 +273,20 @@ namespace Xamarin.Forms
// This is used for cases where the user is navigating via native UI navigation i.e. clicking on Tabs
// If the user defers this type of navigation we generate the equivalent GotoAsync call
// so when the deferral is completed the same navigation can complete
public bool ProposeNavigationOutsideGotoAsync(ShellNavigationSource source, ShellItem shellItem, ShellSection shellSection, ShellContent shellContent, IReadOnlyList<Page> stack, bool canCancel)
public bool ProposeNavigationOutsideGotoAsync(
ShellNavigationSource source,
ShellItem shellItem,
ShellSection shellSection,
ShellContent shellContent,
IReadOnlyList<Page> stack,
bool canCancel,
bool isAnimated)
{
if (_accumulateNavigatedEvents)
return true;
var proposedState = GetNavigationState(shellItem, shellSection, shellContent, stack, shellSection.Navigation.ModalStack);
var navArgs = ProposeNavigation(source, proposedState, canCancel);
var navArgs = ProposeNavigation(source, proposedState, canCancel, isAnimated);
if (navArgs.DeferralCount > 0)
{
@ -302,12 +309,20 @@ namespace Xamarin.Forms
return !navArgs.NavigationDelayedOrCancelled;
}
ShellNavigatingEventArgs ProposeNavigation(ShellNavigationSource source, ShellNavigationState proposedState, bool canCancel)
ShellNavigatingEventArgs ProposeNavigation(
ShellNavigationSource source,
ShellNavigationState proposedState,
bool canCancel,
bool isAnimated)
{
if (_accumulateNavigatedEvents)
return null;
var navArgs = new ShellNavigatingEventArgs(_shell.CurrentState, proposedState, source, canCancel);
var navArgs = new ShellNavigatingEventArgs(_shell.CurrentState, proposedState, source, canCancel)
{
Animate = isAnimated
};
HandleNavigating(navArgs);
return navArgs;

Просмотреть файл

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Text;
using Android.Content;
using Android.Runtime;
using Android.Util;
using AndroidX.CoordinatorLayout.Widget;
namespace Xamarin.Forms.Platform.Android
{
class ShellFlyoutLayout : CoordinatorLayout
{
public ShellFlyoutLayout(Context context) : base(context)
{
}
public ShellFlyoutLayout(Context context, IAttributeSet attrs) : base(context, attrs)
{
}
public ShellFlyoutLayout(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
{
}
protected ShellFlyoutLayout(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}
public Action LayoutChanging { get; set; }
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
LayoutChanging?.Invoke();
base.OnLayout(changed, left, top, right, bottom);
}
protected override void Dispose(bool disposing)
{
if (disposing)
LayoutChanging = null;
base.Dispose(disposing);
}
}
}

Просмотреть файл

@ -31,12 +31,13 @@ namespace Xamarin.Forms.Platform.Android
Drawable _defaultBackgroundColor;
ImageView _bgImage;
AppBarLayout _appBar;
RecyclerView _recycler;
ShellFlyoutRecyclerAdapter _adapter;
AView _flyoutContentView;
ShellViewRenderer _contentView;
View _flyoutHeader;
ShellViewRenderer _footerView;
int _actionBarHeight;
ScrollLayoutManager _layoutManager;
int _flyoutHeight;
int _flyoutWidth;
protected IShellContext ShellContext => _shellContext;
protected AView FooterView => _footerView?.NativeView;
@ -62,10 +63,7 @@ namespace Xamarin.Forms.Platform.Android
return;
}
var coordinator = LayoutInflater.FromContext(context).Inflate(Resource.Layout.FlyoutContent, null);
Profile.FramePartition("Find Recycler");
_recycler = coordinator.FindViewById<RecyclerView>(Resource.Id.flyoutcontent_recycler);
var coordinator = (ViewGroup)LayoutInflater.FromContext(context).Inflate(Resource.Layout.FlyoutContent, null);
Profile.FramePartition("Find AppBar");
_appBar = coordinator.FindViewById<AppBarLayout>(Resource.Id.flyoutcontent_appbar);
@ -79,12 +77,7 @@ namespace Xamarin.Forms.Platform.Android
_actionBarHeight = (int)context.ToPixels(56);
UpdateFlyoutHeader();
Profile.FramePartition("Recycler.SetAdapter");
_adapter = new ShellFlyoutRecyclerAdapter(shellContext, OnElementSelected);
_recycler.SetClipToPadding(false);
_recycler.SetLayoutManager(_layoutManager = new ScrollLayoutManager(context, (int)Orientation.Vertical, false));
_recycler.SetLayoutManager(new LinearLayoutManager(context, (int)Orientation.Vertical, false));
_recycler.SetAdapter(_adapter);
UpdateFlyoutContent();
Profile.FramePartition("Initialize BgImage");
var metrics = context.Resources.DisplayMetrics;
@ -121,6 +114,9 @@ namespace Xamarin.Forms.Platform.Android
UpdateFlyoutFooter();
Profile.FrameEnd();
if (View is ShellFlyoutLayout sfl)
sfl.LayoutChanging += OnFlyoutViewLayoutChanged;
}
void OnFlyoutHeaderMeasureInvalidated(object sender, EventArgs e)
@ -154,6 +150,65 @@ namespace Xamarin.Forms.Platform.Android
Shell.FlyoutFooterProperty,
Shell.FlyoutFooterTemplateProperty))
UpdateFlyoutFooter();
else if (e.IsOneOf(
Shell.FlyoutContentProperty,
Shell.FlyoutContentTemplateProperty))
UpdateFlyoutContent();
}
protected virtual void UpdateFlyoutContent()
{
if (!_rootView.IsAlive())
return;
var index = 0;
if (_flyoutContentView != null)
{
index = _rootView.IndexOfChild(_flyoutContentView);
_rootView.RemoveView(_flyoutContentView);
}
_flyoutContentView = CreateFlyoutContent(_rootView);
if (_flyoutContentView == null)
return;
_rootView.AddView(_flyoutContentView, index);
UpdateContentLayout();
}
AView CreateFlyoutContent(ViewGroup rootView)
{
_rootView = rootView;
if (_contentView != null)
{
var oldContentView = _contentView;
_contentView = null;
oldContentView.TearDown();
}
var content = ((IShellController)ShellContext.Shell).FlyoutContent;
if (content == null)
{
var lp = new CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MatchParent, CoordinatorLayout.LayoutParams.MatchParent);
lp.Behavior = new AppBarLayout.ScrollingViewBehavior();
var context = ShellContext.AndroidContext;
Profile.FramePartition("Recycler.SetAdapter");
var recyclerView = new RecyclerViewContainer(context, new ShellFlyoutRecyclerAdapter(ShellContext, OnElementSelected))
{
LayoutParameters = lp
};
return recyclerView;
}
_contentView = new ShellViewRenderer(ShellContext.AndroidContext, content);
_contentView.NativeView.LayoutParameters = new CoordinatorLayout.LayoutParams(LP.MatchParent, LP.MatchParent)
{
Behavior = new AppBarLayout.ScrollingViewBehavior()
};
return _contentView.NativeView;
}
protected virtual void UpdateFlyoutHeader()
@ -186,6 +241,8 @@ namespace Xamarin.Forms.Platform.Android
};
_appBar.AddView(_headerView);
UpdateFlyoutHeaderBehavior();
UpdateContentLayout();
}
protected virtual void UpdateFlyoutFooter()
@ -205,23 +262,76 @@ namespace Xamarin.Forms.Platform.Android
_footerView = new ShellViewRenderer(_shellContext.AndroidContext, footer);
_footerView.NativeView.LayoutParameters = new CoordinatorLayout.LayoutParams(LP.MatchParent, LP.WrapContent)
{
Gravity = (int)(GravityFlags.Bottom | GravityFlags.End)
};
_footerView.LayoutView(_shellContext.AndroidContext.FromPixels(_rootView.LayoutParameters.Width), double.PositiveInfinity);
_rootView.AddView(_footerView.NativeView);
if (_recycler?.LayoutParameters is CoordinatorLayout.LayoutParams cl)
if (_footerView.NativeView.LayoutParameters is CoordinatorLayout.LayoutParams cl)
cl.Gravity = (int)(GravityFlags.Bottom | GravityFlags.End);
UpdateFooterLayout();
UpdateContentLayout();
UpdateContentBottomMargin();
}
void UpdateFooterLayout()
{
if (_footerView != null)
{
cl.BottomMargin = (int)_shellContext.AndroidContext.ToPixels(_footerView.View.Height);
_footerView.LayoutView(_shellContext.AndroidContext.FromPixels(_rootView.LayoutParameters.Width), double.PositiveInfinity);
}
}
void UpdateContentLayout()
{
if (_contentView != null)
{
if (_contentView == null)
return;
var height =
(View.MeasuredHeight) -
(FooterView?.MeasuredHeight ?? 0) -
(_headerView?.MeasuredHeight ?? 0);
var width = View.MeasuredWidth;
_contentView.LayoutView(
ShellContext.AndroidContext.FromPixels(width),
ShellContext.AndroidContext.FromPixels(height));
}
}
void UpdateContentBottomMargin()
{
if (_flyoutContentView?.LayoutParameters is CoordinatorLayout.LayoutParams cl)
{
cl.BottomMargin = (int)_shellContext.AndroidContext.ToPixels(_footerView?.View.Height ?? 0);
}
}
void OnFlyoutViewLayoutChanged()
{
if (View?.MeasuredHeight > 0 &&
View?.MeasuredWidth > 0 &&
(_flyoutHeight != View.MeasuredHeight ||
_flyoutWidth != View.MeasuredWidth)
)
{
_flyoutHeight = View.MeasuredHeight;
_flyoutWidth = View.MeasuredWidth;
UpdateFooterLayout();
UpdateContentLayout();
UpdateContentBottomMargin();
}
}
void UpdateVerticalScrollMode()
{
if (_layoutManager != null)
_layoutManager.ScrollVertically = _shellContext.Shell.FlyoutVerticalScrollMode;
if (_flyoutContentView is RecyclerView rv && rv.GetLayoutManager() is ScrollLayoutManager lm)
{
lm.ScrollVertically = _shellContext.Shell.FlyoutVerticalScrollMode;
}
}
protected virtual void UpdateFlyoutBackground()
@ -367,30 +477,25 @@ namespace Xamarin.Forms.Platform.Android
if (_rootView != null && _footerView?.NativeView != null)
_rootView.RemoveView(_footerView.NativeView);
if (_recycler != null)
{
_recycler.SetLayoutManager(null);
_recycler.SetAdapter(null);
_recycler.Dispose();
}
if (View != null && View is ShellFlyoutLayout sfl)
sfl.LayoutChanging -= OnFlyoutViewLayoutChanged;
_adapter?.Dispose();
_contentView?.TearDown();
_flyoutContentView?.Dispose();
_headerView.Dispose();
_footerView?.TearDown();
_rootView.Dispose();
_layoutManager?.Dispose();
_defaultBackgroundColor?.Dispose();
_bgImage?.Dispose();
_contentView = null;
_flyoutHeader = null;
_rootView = null;
_headerView = null;
_shellContext = null;
_appBar = null;
_recycler = null;
_adapter = null;
_flyoutContentView = null;
_defaultBackgroundColor = null;
_layoutManager = null;
_bgImage = null;
_footerView = null;
}
@ -478,6 +583,41 @@ namespace Xamarin.Forms.Platform.Android
}
}
class RecyclerViewContainer : RecyclerView
{
bool _disposed;
ShellFlyoutRecyclerAdapter _shellFlyoutRecyclerAdapter;
ScrollLayoutManager _layoutManager;
public RecyclerViewContainer(Context context, ShellFlyoutRecyclerAdapter shellFlyoutRecyclerAdapter) : base(context)
{
_shellFlyoutRecyclerAdapter = shellFlyoutRecyclerAdapter;
SetClipToPadding(false);
SetLayoutManager(_layoutManager = new ScrollLayoutManager(context, (int)Orientation.Vertical, false));
SetLayoutManager(new LinearLayoutManager(context, (int)Orientation.Vertical, false));
SetAdapter(_shellFlyoutRecyclerAdapter);
}
protected override void Dispose(bool disposing)
{
if (_disposed)
return;
_disposed = true;
if (disposing)
{
SetLayoutManager(null);
SetAdapter(null);
_shellFlyoutRecyclerAdapter?.Dispose();
_layoutManager?.Dispose();
_shellFlyoutRecyclerAdapter = null;
_layoutManager = null;
}
base.Dispose(disposing);
}
}
internal class ScrollLayoutManager : LinearLayoutManager
{
public ScrollMode ScrollVertically { get; set; } = ScrollMode.Auto;

Просмотреть файл

@ -128,6 +128,7 @@ namespace Xamarin.Forms.Platform.Android
Fragment IShellObservableFragment.Fragment => this;
public ShellSection ShellSection { get; set; }
protected IShellContext ShellContext => _shellContext;
IShellSectionController SectionController => (IShellSectionController)ShellSection;
public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

Просмотреть файл

@ -50,7 +50,7 @@ namespace Xamarin.Forms.Platform.Android
SearchHandler _searchHandler;
IShellSearchView _searchView;
ContainerView _titleViewContainer;
IShellContext _shellContext;
protected IShellContext ShellContext { get; private set; }
//assume the default
Color _tintColor = Color.Default;
Toolbar _toolbar;
@ -62,7 +62,7 @@ namespace Xamarin.Forms.Platform.Android
public ShellToolbarTracker(IShellContext shellContext, Toolbar toolbar, DrawerLayout drawerLayout)
{
_shellContext = shellContext ?? throw new ArgumentNullException(nameof(shellContext));
ShellContext = shellContext ?? throw new ArgumentNullException(nameof(shellContext));
_toolbar = toolbar ?? throw new ArgumentNullException(nameof(toolbar));
_drawerLayout = drawerLayout ?? throw new ArgumentNullException(nameof(drawerLayout));
_appBar = _toolbar.Parent.GetParentOfType<AppBarLayout>();
@ -70,7 +70,7 @@ namespace Xamarin.Forms.Platform.Android
_globalLayoutListener = new GenericGlobalLayoutListener(() => UpdateNavBarHasShadow(Page));
_appBar.ViewTreeObserver.AddOnGlobalLayoutListener(_globalLayoutListener);
_toolbar.SetNavigationOnClickListener(this);
((IShellController)_shellContext.Shell).AddFlyoutBehaviorObserver(this);
((IShellController)ShellContext.Shell).AddFlyoutBehaviorObserver(this);
}
public bool CanNavigateBack
@ -138,7 +138,7 @@ namespace Xamarin.Forms.Platform.Android
else if (CanNavigateBack)
OnNavigateBack();
else
_shellContext.Shell.FlyoutIsPresented = !_shellContext.Shell.FlyoutIsPresented;
ShellContext.Shell.FlyoutIsPresented = !ShellContext.Shell.FlyoutIsPresented;
}
}
@ -161,9 +161,9 @@ namespace Xamarin.Forms.Platform.Android
_toolbar.DisposeMenuItems(_currentToolbarItems, OnToolbarItemPropertyChanged);
((IShellController)_shellContext.Shell)?.RemoveFlyoutBehaviorObserver(this);
((IShellController)ShellContext.Shell)?.RemoveFlyoutBehaviorObserver(this);
UpdateTitleView(_shellContext.AndroidContext, _toolbar, null);
UpdateTitleView(ShellContext.AndroidContext, _toolbar, null);
if (_searchView != null)
{
@ -184,7 +184,7 @@ namespace Xamarin.Forms.Platform.Android
_globalLayoutListener = null;
_backButtonBehavior = null;
SearchHandler = null;
_shellContext = null;
ShellContext = null;
_drawerToggle = null;
_searchView = null;
Page = null;
@ -197,7 +197,7 @@ namespace Xamarin.Forms.Platform.Android
protected virtual IShellSearchView GetSearchView(Context context)
{
return new ShellSearchView(context, _shellContext);
return new ShellSearchView(context, ShellContext);
}
protected async virtual void OnNavigateBack()
@ -427,7 +427,7 @@ namespace Xamarin.Forms.Platform.Android
//this needs to be set after SyncState
UpdateToolbarIconAccessibilityText(toolbar, _shellContext.Shell);
UpdateToolbarIconAccessibilityText(toolbar, ShellContext.Shell);
}
@ -467,7 +467,7 @@ namespace Xamarin.Forms.Platform.Android
protected virtual void UpdateMenuItemIcon(Context context, IMenuItem menuItem, ToolbarItem toolBarItem)
{
_shellContext.ApplyDrawableAsync(toolBarItem, ToolbarItem.IconImageSourceProperty, baseDrawable =>
ShellContext.ApplyDrawableAsync(toolBarItem, ToolbarItem.IconImageSourceProperty, baseDrawable =>
{
if (baseDrawable != null)
{
@ -547,12 +547,12 @@ namespace Xamarin.Forms.Platform.Android
var menu = toolbar.Menu;
var sortedItems = page.ToolbarItems.OrderBy(x => x.Order);
toolbar.UpdateMenuItems(sortedItems, _shellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems);
toolbar.UpdateMenuItems(sortedItems, ShellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems);
SearchHandler = Shell.GetSearchHandler(page);
if (SearchHandler != null && SearchHandler.SearchBoxVisibility != SearchBoxVisibility.Hidden)
{
var context = _shellContext.AndroidContext;
var context = ShellContext.AndroidContext;
if (_searchView == null)
{
_searchView = GetSearchView(context);
@ -609,7 +609,7 @@ namespace Xamarin.Forms.Platform.Android
void OnToolbarItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var sortedItems = Page.ToolbarItems.OrderBy(x => x.Order).ToList();
_toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, sortedItems, _shellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems);
_toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, sortedItems, ShellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems);
}
void OnSearchViewAttachedToWindow(object sender, AView.ViewAttachedToWindowEventArgs e)
@ -636,12 +636,12 @@ namespace Xamarin.Forms.Platform.Android
void UpdateLeftBarButtonItem()
{
UpdateLeftBarButtonItem(_shellContext.AndroidContext, _toolbar, _drawerLayout, Page);
UpdateLeftBarButtonItem(ShellContext.AndroidContext, _toolbar, _drawerLayout, Page);
}
void UpdateTitleView()
{
UpdateTitleView(_shellContext.AndroidContext, _toolbar, Shell.GetTitleView(Page));
UpdateTitleView(ShellContext.AndroidContext, _toolbar, Shell.GetTitleView(Page));
}
void UpdateToolbarItems()

Просмотреть файл

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<xamarin.forms.platform.android.ShellFlyoutLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@ -14,10 +14,4 @@
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/flyoutcontent_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</xamarin.forms.platform.android.ShellFlyoutLayout>

Просмотреть файл

@ -18,6 +18,8 @@ namespace Xamarin.Forms.Platform.UWP
new PropertyMetadata(default(bool), IsSelectedChanged));
View _content;
object _previousDataContext;
double _previousWidth;
FrameworkElement FrameworkElement { get; set; }
public ShellFlyoutItemRenderer()
@ -34,6 +36,11 @@ namespace Xamarin.Forms.Platform.UWP
void OnDataContextChanged(Windows.UI.Xaml.FrameworkElement sender, Windows.UI.Xaml.DataContextChangedEventArgs args)
{
if (_previousDataContext == args.NewValue)
return;
_previousWidth = -1;
_previousDataContext = args.NewValue;
if (_content != null)
{
if (_content.BindingContext is INotifyPropertyChanged inpc)
@ -95,12 +102,6 @@ namespace Xamarin.Forms.Platform.UWP
OnMeasureInvalidated();
}
protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize)
{
return base.MeasureOverride(availableSize);
}
double _previousWidth;
private void OnLayoutUpdated(object sender, object e)
{
if (this.ActualWidth > 0 && this.ActualWidth != _content.Width && _previousWidth != this.ActualWidth)

Просмотреть файл

@ -14,6 +14,7 @@ namespace Xamarin.Forms.Platform.iOS
UIView _footerView;
View _footer;
ShellTableViewController _tableViewController;
ShellFlyoutLayoutManager _shellFlyoutContentManager;
public event EventHandler WillAppear;
public event EventHandler WillDisappear;
@ -22,10 +23,10 @@ namespace Xamarin.Forms.Platform.iOS
{
_shellContext = context;
_tableViewController = CreateShellTableViewController();
_shellFlyoutContentManager = _tableViewController?.ShellFlyoutContentManager;
AddChildViewController(_tableViewController);
context.Shell.PropertyChanged += HandleShellPropertyChanged;
}
protected virtual ShellTableViewController CreateShellTableViewController()
@ -55,6 +56,12 @@ namespace Xamarin.Forms.Platform.iOS
{
UpdateFlyoutFooter();
}
else if (e.IsOneOf(
Shell.FlyoutContentProperty,
Shell.FlyoutContentTemplateProperty))
{
UpdateFlyoutContent();
}
}
void UpdateFlowDirection()
@ -123,7 +130,6 @@ namespace Xamarin.Forms.Platform.iOS
View.AddSubview(_footerView);
_footerView.ClipsToBounds = true;
_tableViewController.FooterView = _footerView;
_footer.MeasureInvalidated += OnFooterMeasureInvalidated;
}
@ -170,6 +176,7 @@ namespace Xamarin.Forms.Platform.iOS
{
base.ViewWillLayoutSubviews();
UpdateFooterPosition();
UpdateFlyoutContent();
}
protected virtual void UpdateBackground()
@ -251,7 +258,6 @@ namespace Xamarin.Forms.Platform.iOS
{
base.ViewDidLoad();
View.AddSubview(_tableViewController.View);
UpdateFlyoutHeader();
UpdateFlyoutFooter();
@ -273,6 +279,19 @@ namespace Xamarin.Forms.Platform.iOS
UpdateFlowDirection();
}
void UpdateFlyoutContent()
{
var view = (_shellContext.Shell as IShellController).FlyoutContent;
if (view != null)
_shellFlyoutContentManager.SetCustomContent(view);
else
_shellFlyoutContentManager.SetDefaultContent(_tableViewController.TableView);
if(_shellFlyoutContentManager.ContentView != null)
View.InsertSubview(_shellFlyoutContentManager.ContentView, 0);
}
public override void ViewWillAppear(bool animated)
{
UpdateFlowDirection();

Просмотреть файл

@ -0,0 +1,332 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using CoreAnimation;
using CoreGraphics;
using Foundation;
using UIKit;
namespace Xamarin.Forms.Platform.iOS
{
class ShellFlyoutLayoutManager
{
double _headerMin = 56;
double _headerOffset = 0;
UIView _contentView;
UIScrollView ScrollView { get; set; }
UIContainerView _headerView;
UIView _footerView;
double _headerSize;
readonly IShellContext _context;
Action removeScolledEvent;
IShellController ShellController => _context.Shell;
public ShellFlyoutLayoutManager(IShellContext context)
{
_context = context;
_context.Shell.PropertyChanged += OnShellPropertyChanged;
ShellController.StructureChanged += OnStructureChanged;
}
public void SetCustomContent(View content)
{
if (content == Content)
return;
removeScolledEvent?.Invoke();
removeScolledEvent = null;
if (Content != null)
{
var oldRenderer = Platform.GetRenderer(Content);
var oldContentView = ContentView;
var oldContent = Content;
Content = null;
ContentView = null;
oldContent.ClearValue(Platform.RendererProperty);
oldContentView?.RemoveFromSuperview();
oldRenderer?.Dispose();
}
// If the user hasn't defined custom content then only the ContentView is set
else if(ContentView != null)
{
var oldContentView = ContentView;
ContentView = null;
oldContentView.RemoveFromSuperview();
}
Content = content;
if (Content != null)
{
var renderer = Platform.CreateRenderer(Content);
ContentView = renderer.NativeView;
Platform.SetRenderer(Content, renderer);
ContentView.ClipsToBounds = true;
// not sure if there's a more efficient way to do this
// I can test the native control to see if it inherits from UIScrollView
// But the CollectionViewRenderer doesn't inherit from UIScrollView
if (Content is ScrollView sv)
{
sv.Scrolled += ScrollViewScrolled;
removeScolledEvent = () => sv.Scrolled -= ScrollViewScrolled;
void ScrollViewScrolled(object sender, ScrolledEventArgs e) =>
OnScrolled((nfloat)sv.ScrollY);
}
else if(Content is CollectionView cv)
{
cv.Scrolled += CollectionViewScrolled;
removeScolledEvent = () => cv.Scrolled -= CollectionViewScrolled;
void CollectionViewScrolled(object sender, ItemsViewScrolledEventArgs e) =>
OnScrolled((nfloat)e.VerticalOffset);
}
else if (Content is ListView lv)
{
lv.Scrolled += ListViewScrolled;
removeScolledEvent = () => lv.Scrolled -= ListViewScrolled;
void ListViewScrolled(object sender, ScrolledEventArgs e) =>
OnScrolled((nfloat)e.ScrollY);
}
}
}
public void SetDefaultContent(UIView view)
{
if (ContentView == view)
return;
SetCustomContent(null);
ContentView = view;
}
public View Content
{
get;
private set;
}
public UIView ContentView
{
get
{
return _contentView;
}
private set
{
_contentView = value;
if (ContentView is UIScrollView sv1)
ScrollView = sv1;
else if (ContentView is IVisualElementRenderer ver && ver.NativeView is UIScrollView uIScroll)
ScrollView = uIScroll;
if (ScrollView != null && Forms.IsiOS11OrNewer)
ScrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
if (ScrollView != null)
{
LayoutParallax();
SetHeaderContentInset();
}
}
}
public virtual UIContainerView HeaderView
{
get => _headerView;
set
{
if (_headerView == value)
return;
if (_headerView != null)
_headerView.HeaderSizeChanged -= OnHeaderFooterSizeChanged;
_headerView = value;
if (_headerView != null)
_headerView.HeaderSizeChanged += OnHeaderFooterSizeChanged;
}
}
public virtual UIView FooterView
{
get => _footerView;
set
{
if (_footerView == value)
return;
_footerView = value;
}
}
void OnHeaderFooterSizeChanged(object sender, EventArgs e)
{
_headerSize = HeaderMax;
SetHeaderContentInset();
LayoutParallax();
}
internal void SetHeaderContentInset()
{
if (ScrollView == null)
return;
var offset = ScrollView.ContentInset.Top;
if (HeaderView != null)
ScrollView.ContentInset = new UIEdgeInsets((nfloat)HeaderMax, 0, 0, 0);
else
ScrollView.ContentInset = new UIEdgeInsets(Platform.SafeAreaInsetsForWindow.Top, 0, 0, 0);
offset -= ScrollView.ContentInset.Top;
ScrollView.ContentOffset =
new CGPoint(ScrollView.ContentOffset.X, ScrollView.ContentOffset.Y + offset);
UpdateVerticalScrollMode();
}
public void UpdateVerticalScrollMode()
{
if (ScrollView == null)
return;
switch (_context.Shell.FlyoutVerticalScrollMode)
{
case ScrollMode.Auto:
ScrollView.ScrollEnabled = true;
ScrollView.AlwaysBounceVertical = false;
break;
case ScrollMode.Enabled:
ScrollView.ScrollEnabled = true;
ScrollView.AlwaysBounceVertical = true;
break;
case ScrollMode.Disabled:
ScrollView.ScrollEnabled = false;
ScrollView.AlwaysBounceVertical = false;
break;
}
}
public void LayoutParallax()
{
var parent = ContentView?.Superview;
if (parent == null)
return;
nfloat footerHeight = 0;
if (FooterView != null)
footerHeight = FooterView.Frame.Height;
var contentViewYOffset = HeaderView?.Frame.Height ?? 0;
if (ScrollView != null)
{
if (Content == null)
{
ContentView.Frame =
new CGRect(parent.Bounds.X, HeaderTopMargin, parent.Bounds.Width, parent.Bounds.Height - HeaderTopMargin - footerHeight);
}
else
{
ContentView.Frame =
new CGRect(parent.Bounds.X, HeaderTopMargin, parent.Bounds.Width, parent.Bounds.Height - HeaderTopMargin - footerHeight);
if (Content != null)
Layout.LayoutChildIntoBoundingRegion(Content, new Rectangle(0, 0, ContentView.Frame.Width, ContentView.Frame.Height - contentViewYOffset));
}
}
else
{
ContentView.Frame =
new CGRect(parent.Bounds.X, HeaderTopMargin + contentViewYOffset, parent.Bounds.Width, parent.Bounds.Height - HeaderTopMargin - footerHeight - contentViewYOffset);
if (Content != null)
Layout.LayoutChildIntoBoundingRegion(Content, new Rectangle(0, 0, ContentView.Frame.Width, ContentView.Frame.Height));
}
if (HeaderView != null && !double.IsNaN(_headerSize))
{
var margin = HeaderView.Margin;
var leftMargin = margin.Left - margin.Right;
HeaderView.Frame = new CGRect(leftMargin, _headerOffset, parent.Frame.Width, _headerSize + HeaderTopMargin);
if (_context.Shell.FlyoutHeaderBehavior == FlyoutHeaderBehavior.Scroll && HeaderTopMargin > 0 && _headerOffset < 0)
{
var headerHeight = Math.Max(_headerMin, _headerSize + _headerOffset);
CAShapeLayer shapeLayer = new CAShapeLayer();
CGRect rect = new CGRect(0, _headerOffset * -1, parent.Frame.Width, headerHeight);
var path = CGPath.FromRect(rect);
shapeLayer.Path = path;
HeaderView.Layer.Mask = shapeLayer;
}
else if (HeaderView.Layer.Mask != null)
HeaderView.Layer.Mask = null;
}
}
void OnStructureChanged(object sender, EventArgs e) => UpdateVerticalScrollMode();
void OnShellPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.Is(Shell.FlyoutHeaderBehaviorProperty))
{
SetHeaderContentInset();
LayoutParallax();
}
else if (e.Is(Shell.FlyoutVerticalScrollModeProperty))
UpdateVerticalScrollMode();
}
public void ViewDidLoad()
{
HeaderView?.MeasureIfNeeded();
SetHeaderContentInset();
}
public void OnScrolled(nfloat contentOffsetY)
{
var headerBehavior = _context.Shell.FlyoutHeaderBehavior;
switch (headerBehavior)
{
case FlyoutHeaderBehavior.Default:
case FlyoutHeaderBehavior.Fixed:
_headerSize = HeaderMax;
_headerOffset = 0;
break;
case FlyoutHeaderBehavior.Scroll:
_headerSize = HeaderMax;
_headerOffset = Math.Min(0, -(HeaderMax + contentOffsetY));
break;
case FlyoutHeaderBehavior.CollapseOnScroll:
_headerSize = Math.Max(_headerMin, -contentOffsetY);
_headerOffset = 0;
break;
}
LayoutParallax();
}
double HeaderMax => HeaderView?.MeasuredHeight ?? 0;
double HeaderTopMargin => (HeaderView != null) ? HeaderView.Margin.Top - HeaderView.Margin.Bottom : 0;
public void TearDown()
{
_context.Shell.PropertyChanged -= OnShellPropertyChanged;
ShellController.StructureChanged -= OnStructureChanged;
SetCustomContent(null);
ContentView = null;
HeaderView = null;
FooterView = null;
}
}
}

Просмотреть файл

@ -11,60 +11,48 @@ namespace Xamarin.Forms.Platform.iOS
{
readonly IShellContext _context;
readonly ShellTableViewSource _source;
double _headerMin = 56;
double _headerOffset = 0;
double _headerSize;
bool _isDisposed;
Action<Element> _onElementSelected;
UIContainerView _headerView;
UIView _footerView;
IShellController ShellController => ((IShellController)_context.Shell);
IShellController ShellController => _context.Shell;
public ShellTableViewController(IShellContext context, UIContainerView headerView, Action<Element> onElementSelected) : this(context, onElementSelected)
{
ShellFlyoutContentManager = new ShellFlyoutLayoutManager(context);
HeaderView = headerView;
}
public ShellTableViewController(IShellContext context, Action<Element> onElementSelected)
{
ShellFlyoutContentManager = ShellFlyoutContentManager ?? new ShellFlyoutLayoutManager(context);
_context = context;
_onElementSelected = onElementSelected;
_source = CreateShellTableViewSource();
_source.ScrolledEvent += OnScrolled;
ShellController.FlyoutItemsChanged += OnFlyoutItemsChanged;
_context.Shell.PropertyChanged += OnShellPropertyChanged;
_source.ScrolledEvent += OnScrolled;
}
internal ShellFlyoutLayoutManager ShellFlyoutContentManager
{
get;
set;
}
void OnScrolled(object sender, UIScrollView e)
{
ShellFlyoutContentManager.OnScrolled(e.ContentOffset.Y);
}
public virtual UIContainerView HeaderView
{
get => _headerView;
set
{
if (_headerView == value)
return;
if (_headerView != null)
_headerView.HeaderSizeChanged -= OnHeaderFooterSizeChanged;
_headerView = value;
if (_headerView != null)
_headerView.HeaderSizeChanged += OnHeaderFooterSizeChanged;
}
get => ShellFlyoutContentManager.HeaderView;
set => ShellFlyoutContentManager.HeaderView = value;
}
public virtual UIView FooterView
{
get => _footerView;
set
{
if (_footerView == value)
return;
_footerView = value;
}
get => ShellFlyoutContentManager.FooterView;
set => ShellFlyoutContentManager.FooterView = value;
}
protected ShellTableViewSource CreateShellTableViewSource()
@ -72,106 +60,26 @@ namespace Xamarin.Forms.Platform.iOS
return new ShellTableViewSource(_context, _onElementSelected);
}
void OnShellPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.Is(Shell.FlyoutHeaderBehaviorProperty))
{
SetHeaderContentInset();
LayoutParallax();
}
else if (e.Is(Shell.FlyoutVerticalScrollModeProperty))
UpdateVerticalScrollMode();
}
void OnHeaderFooterSizeChanged(object sender, EventArgs e)
{
_headerSize = HeaderMax;
SetHeaderContentInset();
LayoutParallax();
}
void OnFlyoutItemsChanged(object sender, EventArgs e)
{
_source.ClearCache();
TableView.ReloadData();
UpdateVerticalScrollMode();
ShellFlyoutContentManager.UpdateVerticalScrollMode();
}
void UpdateVerticalScrollMode()
{
switch (_context.Shell.FlyoutVerticalScrollMode)
{
case ScrollMode.Auto:
TableView.ScrollEnabled = true;
TableView.AlwaysBounceVertical = false;
break;
case ScrollMode.Enabled:
TableView.ScrollEnabled = true;
TableView.AlwaysBounceVertical = true;
break;
case ScrollMode.Disabled:
TableView.ScrollEnabled = false;
TableView.AlwaysBounceVertical = false;
break;
}
}
public void LayoutParallax()
{
if (TableView?.Superview == null)
return;
var parent = TableView.Superview;
nfloat footerHeight = 0;
if (FooterView != null)
footerHeight = FooterView.Frame.Height;
TableView.Frame =
new CGRect(parent.Bounds.X, HeaderTopMargin, parent.Bounds.Width, parent.Bounds.Height - HeaderTopMargin - footerHeight);
if (HeaderView != null && !double.IsNaN(_headerSize))
{
var margin = HeaderView.Margin;
var leftMargin = margin.Left - margin.Right;
HeaderView.Frame = new CGRect(leftMargin, _headerOffset + HeaderTopMargin, parent.Frame.Width, _headerSize);
if (_context.Shell.FlyoutHeaderBehavior == FlyoutHeaderBehavior.Scroll && HeaderTopMargin > 0 && _headerOffset < 0)
{
var headerHeight = Math.Max(_headerMin, _headerSize + _headerOffset);
CAShapeLayer shapeLayer = new CAShapeLayer();
CGRect rect = new CGRect(0, _headerOffset * -1, parent.Frame.Width, headerHeight);
var path = CGPath.FromRect(rect);
shapeLayer.Path = path;
HeaderView.Layer.Mask = shapeLayer;
}
else if (HeaderView.Layer.Mask != null)
HeaderView.Layer.Mask = null;
}
}
void SetHeaderContentInset()
{
if (HeaderView != null)
TableView.ContentInset = new UIEdgeInsets((nfloat)HeaderMax, 0, 0, 0);
else
TableView.ContentInset = new UIEdgeInsets(Platform.SafeAreaInsetsForWindow.Top, 0, 0, 0);
UpdateVerticalScrollMode();
}
public void LayoutParallax() =>
ShellFlyoutContentManager.LayoutParallax();
public override void ViewDidLoad()
{
{
base.ViewDidLoad();
HeaderView?.MeasureIfNeeded();
TableView.SeparatorStyle = UITableViewCellSeparatorStyle.None;
if (Forms.IsiOS11OrNewer)
TableView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
SetHeaderContentInset();
TableView.Source = _source;
ShellFlyoutContentManager.ViewDidLoad();
}
protected override void Dispose(bool disposing)
@ -187,50 +95,12 @@ namespace Xamarin.Forms.Platform.iOS
if (_source != null)
_source.ScrolledEvent -= OnScrolled;
if (HeaderView != null)
HeaderView.HeaderSizeChanged -= OnHeaderFooterSizeChanged;
_context.Shell.PropertyChanged -= OnShellPropertyChanged;
ShellFlyoutContentManager.TearDown();
_onElementSelected = null;
}
_isDisposed = true;
base.Dispose(disposing);
}
void OnScrolled(object sender, UIScrollView e)
{
if (HeaderView == null)
return;
var headerBehavior = _context.Shell.FlyoutHeaderBehavior;
switch (headerBehavior)
{
case FlyoutHeaderBehavior.Default:
case FlyoutHeaderBehavior.Fixed:
_headerSize = HeaderMax;
_headerOffset = 0;
break;
case FlyoutHeaderBehavior.Scroll:
_headerSize = HeaderMax;
_headerOffset = Math.Min(0, -(HeaderMax + e.ContentOffset.Y));
break;
case FlyoutHeaderBehavior.CollapseOnScroll:
_headerSize = Math.Max(_headerMin, -e.ContentOffset.Y);
_headerOffset = 0;
break;
}
LayoutParallax();
}
double HeaderMax => HeaderView?.MeasuredHeight ?? 0;
double HeaderTopMargin => (HeaderView != null) ? HeaderView.Margin.Top - HeaderView.Margin.Bottom : 0;
}
}

Просмотреть файл

@ -12,7 +12,6 @@ namespace Xamarin.Forms.Platform.iOS
readonly Action<Element> _onElementSelected;
List<List<Element>> _groups;
Dictionary<Element, UIContainerCell> _cells;
IShellController ShellController => _context.Shell;
public ShellTableViewSource(IShellContext context, Action<Element> onElementSelected)
@ -213,4 +212,4 @@ namespace Xamarin.Forms.Platform.iOS
}
}
}
}
}

Просмотреть файл

@ -22,6 +22,11 @@ namespace Xamarin.Forms.Platform.iOS
ClipsToBounds = true;
view.MeasureInvalidated += OnMeasureInvalidated;
MeasuredHeight = double.NaN;
_view.BatchCommitted += _view_BatchCommitted;
}
private void _view_BatchCommitted(object sender, Internals.EventArg<VisualElement> e)
{
}
internal View View => _view;
@ -30,7 +35,10 @@ namespace Xamarin.Forms.Platform.iOS
internal bool MeasureIfNeeded()
{
if (double.IsNaN(MeasuredHeight))
if (View == null)
return false;
if (double.IsNaN(MeasuredHeight) || Frame.Width != View.Width)
{
ReMeasure();
return true;
@ -44,7 +52,12 @@ namespace Xamarin.Forms.Platform.iOS
{
if(!_view.IsSet(View.MarginProperty))
{
_view.Margin = new Thickness(0, (float)Platform.SafeAreaInsetsForWindow.Top, 0, 0);
var newMargin = new Thickness(0, (float)Platform.SafeAreaInsetsForWindow.Top, 0, 0);
if (newMargin != _view.Margin)
{
_view.Margin = newMargin;
}
}
return _view.Margin;
@ -54,7 +67,6 @@ namespace Xamarin.Forms.Platform.iOS
void ReMeasure()
{
var request = _view.Measure(Frame.Width, double.PositiveInfinity, MeasureFlags.None);
Layout.LayoutChildIntoBoundingRegion(_view, new Rectangle(0, 0, Frame.Width, request.Request.Height));
MeasuredHeight = request.Request.Height;
HeaderSizeChanged?.Invoke(this, EventArgs.Empty);
}
@ -62,12 +74,18 @@ namespace Xamarin.Forms.Platform.iOS
void OnMeasureInvalidated(object sender, System.EventArgs e)
{
ReMeasure();
LayoutSubviews();
}
public override void WillMoveToSuperview(UIView newsuper)
{
base.WillMoveToSuperview(newsuper);
ReMeasure();
}
public override void LayoutSubviews()
{
if(!MeasureIfNeeded())
_view.Layout(Bounds.ToRectangle());
_view.Layout(new Rectangle(0, Margin.Top, Frame.Width, MeasuredHeight));
}
protected override void Dispose(bool disposing)

Просмотреть файл

@ -192,6 +192,7 @@
<Compile Include="Renderers\PageContainer.cs" />
<Compile Include="Renderers\CheckBoxRendererBase.cs" />
<Compile Include="Renderers\PhoneFlyoutPageRenderer.cs" />
<Compile Include="Renderers\ShellFlyoutLayoutManager.cs" />
<Compile Include="Renderers\TabletFlyoutPageRenderer.cs" />
<Compile Include="Renderers\WkWebViewRenderer.cs" />
<Compile Include="Renderers\ElementSelectedEventArgs.cs" />