--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+
+#if UITEST
+using Xamarin.Forms.Core.UITests;
+using Xamarin.UITest;
+using NUnit.Framework;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if UITEST
+ [Category(UITestCategories.CollectionView)]
+#endif
+
+ [Preserve(AllMembers = true)]
+ [Issue(IssueTracker.None, 4539135, "CollectionView: Grouping", PlatformAffected.All)]
+ public class CollectionViewGrouping : TestNavigationPage
+ {
+ protected override void Init()
+ {
+#if APP
+ Device.SetFlags(new List<string>(Device.Flags ?? new List<string>()) { "CollectionView_Experimental" });
+
+ PushAsync(new GalleryPages.CollectionViewGalleries.GroupingGalleries.ObservableGrouping());
+#endif
+ }
+
+#if UITEST && __IOS__ // Grouping is not implemented on Android yet
+ [Test]
+ public void RemoveSelectedItem()
+ {
+ RunningApp.WaitForElement("Hawkeye");
+ RunningApp.Tap("Hawkeye");
+ RunningApp.Tap("RemoveItem");
+ RunningApp.WaitForNoElement("Hawkeye");
+ }
+
+ [Test]
+ public void AddItem()
+ {
+ RunningApp.WaitForElement("Hawkeye");
+ RunningApp.Tap("Hawkeye");
+ RunningApp.Tap("AddItem");
+ RunningApp.WaitForElement("Spider-Man");
+ }
+
+ [Test]
+ public void ReplaceItem()
+ {
+ RunningApp.WaitForElement("Iron Man");
+ RunningApp.Tap("Iron Man");
+ RunningApp.Tap("ReplaceItem");
+ RunningApp.WaitForNoElement("Iron Man");
+ RunningApp.WaitForElement("Spider-Man");
+ }
+
+ [Test]
+ public void RemoveGroup()
+ {
+ RunningApp.WaitForElement("Avengers");
+ RunningApp.Tap("RemoveGroup");
+ RunningApp.WaitForNoElement("Avengers");
+ }
+
+ [Test]
+ public void AddGroup()
+ {
+ RunningApp.WaitForElement("AddGroup");
+ RunningApp.Tap("AddGroup");
+ RunningApp.WaitForElement("Excalibur");
+ }
+
+ [Test]
+ public void ReplaceGroup()
+ {
+ RunningApp.WaitForElement("Fantastic Four");
+ RunningApp.Tap("ReplaceGroup");
+ RunningApp.WaitForElement("Alpha Flight");
+ }
+
+ [Test]
+ public void MoveGroup()
+ {
+ RunningApp.WaitForElement("MoveGroup");
+ RunningApp.Tap("MoveGroup");
+ }
+
+
+#endif
+ }
+}
<Import_RootNamespace>Xamarin.Forms.Controls.Issues</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
+ <Compile Include="$(MSBuildThisFileDirectory)CollectionViewGrouping.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue5412.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\GarbageCollectionHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue4879.cs" />
using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.EmptyViewGalleries;
+using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries;
using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries;
-using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.ItemSizeGalleries;
-using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SpacingGalleries;
namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
{
GalleryBuilder.NavButton("EmptyView Galleries", () => new EmptyViewGallery(), Navigation),
GalleryBuilder.NavButton("Selection Galleries", () => new SelectionGallery(), Navigation),
GalleryBuilder.NavButton("Propagation Galleries", () => new PropagationGallery(), Navigation),
- GalleryBuilder.NavButton("Item Size Galleries", () => new ItemsSizeGallery(), Navigation),
- GalleryBuilder.NavButton("Spacing Galleries", () => new ItemsSpacingGallery(), Navigation),
+ GalleryBuilder.NavButton("Grouping Galleries", () => new GroupingGallery(), Navigation),
}
};
}
private bool ItemMatches(string filter, CollectionViewGalleryTestItem item)
{
- return item.Caption.ToLower().Contains(filter.ToLower());
+ filter = filter ?? "";
+ return item.Caption.ToLower().Contains(filter?.ToLower());
}
public void FilterItems(string filter)
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.BasicGrouping">
+ <ContentPage.Content>
+ <CollectionView x:Name="CollectionView" IsGrouped="True">
+
+ <CollectionView.ItemTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.ItemTemplate>
+
+ <CollectionView.GroupHeaderTemplate>
+ <DataTemplate>
+
+ <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+
+ </DataTemplate>
+ </CollectionView.GroupHeaderTemplate>
+
+ <CollectionView.GroupFooterTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange"
+ Margin="0,0,0,15"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.GroupFooterTemplate>
+
+ </CollectionView>
+ </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ [Preserve (AllMembers = true)]
+ public partial class BasicGrouping : ContentPage
+ {
+ public BasicGrouping ()
+ {
+ InitializeComponent ();
+
+ CollectionView.ItemsSource = new SuperTeams();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ class GroupingGallery : ContentPage
+ {
+ public GroupingGallery()
+ {
+ var descriptionLabel =
+ new Label { Text = "Grouping Galleries", Margin = new Thickness(2, 2, 2, 2) };
+
+ Title = "Grouping Galleries";
+
+ Content = new ScrollView
+ {
+ Content = new StackLayout
+ {
+ Children =
+ {
+ descriptionLabel,
+ GalleryBuilder.NavButton("Basic Grouping", () =>
+ new BasicGrouping(), Navigation),
+ GalleryBuilder.NavButton("Grouping, some empty groups", () =>
+ new SomeEmptyGroups(), Navigation),
+ GalleryBuilder.NavButton("Grouping, no templates", () =>
+ new GroupingNoTemplates(), Navigation),
+ GalleryBuilder.NavButton("Grouping, with selection", () =>
+ new GroupingPlusSelection(), Navigation),
+ GalleryBuilder.NavButton("Grouping, switchable", () =>
+ new SwitchGrouping(), Navigation),
+ GalleryBuilder.NavButton("Grouping, Observable", () =>
+ new ObservableGrouping(), Navigation),
+ }
+ }
+ };
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.GroupingNoTemplates">
+ <ContentPage.Content>
+ <CollectionView x:Name="CollectionView" IsGrouped="True" />
+ </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class GroupingNoTemplates : ContentPage
+ {
+ public GroupingNoTemplates()
+ {
+ InitializeComponent();
+ CollectionView.ItemsSource = new SuperTeams();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.GroupingPlusSelection">
+ <ContentPage.Content>
+ <CollectionView x:Name="CollectionView" IsGrouped="True"
+ SelectionMode="Single">
+
+ <CollectionView.ItemTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.ItemTemplate>
+
+ <CollectionView.GroupHeaderTemplate>
+ <DataTemplate>
+
+ <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+
+ </DataTemplate>
+ </CollectionView.GroupHeaderTemplate>
+
+ <CollectionView.GroupFooterTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange"
+ Margin="0,0,0,15"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.GroupFooterTemplate>
+
+ </CollectionView>
+ </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class GroupingPlusSelection : ContentPage
+ {
+ public GroupingPlusSelection ()
+ {
+ InitializeComponent ();
+ CollectionView.ItemsSource = new SuperTeams();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.MeasureFirstStrategy">
+ <ContentPage.Content>
+ <StackLayout>
+
+ <Label Text="Using ItemSizingStrategy.MeasureFirstItem. On scrolling, some items may disappear; this is a current known issue ()"></Label>
+
+ <CollectionView x:Name="CollectionView" ItemSizingStrategy="MeasureFirstItem" IsGrouped="True">
+
+ <CollectionView.ItemTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.ItemTemplate>
+
+ <CollectionView.GroupHeaderTemplate>
+ <DataTemplate>
+
+ <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+
+ </DataTemplate>
+ </CollectionView.GroupHeaderTemplate>
+
+ <CollectionView.GroupFooterTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange"
+ Margin="0,0,0,15"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.GroupFooterTemplate>
+
+ </CollectionView>
+ </StackLayout>
+ </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class MeasureFirstStrategy : ContentPage
+ {
+ public MeasureFirstStrategy()
+ {
+ InitializeComponent();
+
+ CollectionView.ItemsSource = new SuperTeams();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ internal class ObservableGrouping : ContentPage
+ {
+ public ObservableGrouping()
+ {
+ Title = "Observable Grouped List";
+
+ var buttonStyle = new Style(typeof(Button)) { };
+ buttonStyle.Setters.Add(new Setter() { Property = Button.HeightRequestProperty, Value = 20 });
+ buttonStyle.Setters.Add(new Setter() { Property = Button.FontSizeProperty, Value = 10 });
+
+ var layout = new Grid
+ {
+ RowDefinitions = new RowDefinitionCollection
+ {
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = GridLength.Star }
+ },
+ ColumnDefinitions = new ColumnDefinitionCollection
+ {
+ new ColumnDefinition(), new ColumnDefinition()
+ }
+
+ };
+
+ var collectionView = new CollectionView
+ {
+ ItemTemplate = ItemTemplate(),
+ GroupFooterTemplate = GroupFooterTemplate(),
+ GroupHeaderTemplate = GroupHeaderTemplate(),
+ IsGrouped = true,
+ SelectionMode = SelectionMode.Single
+ };
+
+ var itemsSource = new ObservableSuperTeams();
+
+ collectionView.ItemsSource = itemsSource;
+
+ var remover = new Button { Text = "Remove Selected", AutomationId = "RemoveItem", Style = buttonStyle };
+ remover.Clicked += (obj, args) => {
+ var selectedMember = collectionView.SelectedItem as Member;
+ var team = FindTeam(itemsSource, selectedMember);
+ team?.Remove(selectedMember);
+ };
+
+ var adder = new Button { Text = "Add After Selected", AutomationId = "AddItem", Style = buttonStyle };
+ adder.Clicked += (obj, args) => {
+ var selectedMember = collectionView.SelectedItem as Member;
+ var team = FindTeam(itemsSource, selectedMember);
+
+ if (team == null)
+ {
+ return;
+ }
+
+ team.Insert(team.IndexOf(selectedMember) + 1, new Member("Spider-Man"));
+ };
+
+ AddStuffToGridRow(layout, 0, remover, adder);
+
+ var replacer = new Button { Text = "Replace Selected", AutomationId = "ReplaceItem", Style = buttonStyle };
+ replacer.Clicked += (obj, args) => {
+ var selectedMember = collectionView.SelectedItem as Member;
+ var team = FindTeam(itemsSource, selectedMember);
+
+ if (team == null)
+ {
+ return;
+ }
+
+ team.Insert(team.IndexOf(selectedMember) + 1, new Member("Spider-Man"));
+ team.Remove(selectedMember);
+ };
+
+ var mover = new Button { Text = $"Move Selected To {itemsSource[0].Name}", AutomationId = "MoveItem",
+ Style = buttonStyle };
+ mover.Clicked += (obj, args) => {
+ var selectedMember = collectionView.SelectedItem as Member;
+ var team = FindTeam(itemsSource, selectedMember);
+
+ if (team == null || team == itemsSource[0])
+ {
+ return;
+ }
+
+ team.Remove(selectedMember);
+ itemsSource[0].Add(selectedMember);
+ };
+
+ AddStuffToGridRow(layout, 1, replacer, mover);
+
+ var groupRemover = new Button { Text = $"Remove {itemsSource[0].Name}", AutomationId = "RemoveGroup",
+ Style = buttonStyle };
+ groupRemover.Clicked += (obj, args) => {
+ itemsSource?.Remove(itemsSource[0]);
+ groupRemover.Text = $"Remove {itemsSource[0].Name}";
+ mover.Text = $"Move Selected To {itemsSource[0].Name}";
+ };
+
+ var groupAdder = new Button { Text = $"Insert New Group at position 2", AutomationId = "AddGroup",
+ Style = buttonStyle };
+ groupAdder.Clicked += (obj, args) => {
+ itemsSource?.Insert(1, new ObservableTeam("Excalibur", new List<Member>()));
+ };
+
+ AddStuffToGridRow(layout, 2, groupRemover, groupAdder);
+
+ var groupMover = new Button { Text = "Move 3rd Group to 1st", AutomationId = "MoveGroup", Style = buttonStyle };
+ groupMover.Clicked += (obj, args) => {
+ var group = itemsSource[2];
+ itemsSource.Remove(group);
+ itemsSource.Insert(0, group);
+ groupRemover.Text = $"Remove {itemsSource[0].Name}";
+ mover.Text = $"Move Selected To {itemsSource[0].Name}";
+ };
+
+ var groupReplacer = new Button { Text = "Replace 2nd Group", AutomationId = "ReplaceGroup", Style = buttonStyle };
+ groupReplacer.Clicked += (obj, args) => {
+ var group = itemsSource[1];
+ itemsSource.Remove(group);
+ itemsSource?.Insert(1, new ObservableTeam("Alpha Flight", new List<Member> { new Member("Guardian"),
+ new Member("Sasquatch"), new Member("Northstar") }));
+ };
+
+ AddStuffToGridRow(layout, 3, groupMover, groupReplacer);
+
+ layout.Children.Add(collectionView);
+ Grid.SetRow(collectionView, 6);
+ Grid.SetColumnSpan(collectionView, 2);
+
+ Content = layout;
+ }
+
+ void AddStuffToGridRow(Grid grid, int row, params View[] views)
+ {
+ var col = 0;
+
+ foreach (var view in views)
+ {
+ grid.Children.Add(view);
+ Grid.SetRow(view, row);
+ Grid.SetColumn(view, col);
+ col = col + 1;
+ }
+ }
+
+ DataTemplate ItemTemplate()
+ {
+ return new DataTemplate(() =>
+ {
+ var layout = new StackLayout();
+ var label = new Label()
+ {
+ Margin = new Thickness(5, 0, 0, 0),
+
+ };
+
+ label.SetBinding(Label.TextProperty, new Binding("Name"));
+ layout.Children.Add(label);
+
+ return layout;
+ });
+ }
+
+ DataTemplate GroupHeaderTemplate()
+ {
+ return new DataTemplate(() =>
+ {
+ var layout = new StackLayout();
+ var label = new Label()
+ {
+ FontSize = 16,
+ FontAttributes = FontAttributes.Bold,
+ BackgroundColor = Color.LightGreen
+
+ };
+
+ label.SetBinding(Label.TextProperty, new Binding("Name"));
+ layout.Children.Add(label);
+
+ return layout;
+ });
+ }
+
+ DataTemplate GroupFooterTemplate()
+ {
+ return new DataTemplate(() =>
+ {
+ var layout = new StackLayout();
+ var label = new Label()
+ {
+ Margin = new Thickness(0, 0, 0, 15),
+ BackgroundColor = Color.LightBlue
+ };
+
+ label.SetBinding(Label.TextProperty, new Binding("Count", stringFormat: "Total members: {0:D}"));
+ layout.Children.Add(label);
+
+ return layout;
+ });
+ }
+
+ ObservableTeam FindTeam(ObservableSuperTeams teams, Member member)
+ {
+ if (member == null)
+ {
+ return null;
+ }
+
+ for (int i = 0; i < teams.Count; i++)
+ {
+ var group = teams[i];
+
+ if (group.Contains(member))
+ {
+ return group;
+ }
+ }
+
+ return null;
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.SomeEmptyGroups">
+ <ContentPage.Content>
+
+ <StackLayout>
+
+ <Label x:Name="Description">
+ <Label.Text>
+ <OnPlatform x:TypeArguments="x:String"
+ Default="The CollectionView below should be grouped and some of the groups should be empty, but still display headers/footers.">
+ <On Platform="iOS">The CollectionView below should be grouped and some of the groups should be empty, but still display headers/footers. On iOS 10 and lower, the group headers/footers may all be clumped at the top; this is a known issue.</On>
+ </OnPlatform>
+ </Label.Text>
+
+ </Label>
+
+ <CollectionView x:Name="CollectionView" IsGrouped="True">
+
+ <CollectionView.ItemTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.ItemTemplate>
+
+ <CollectionView.GroupHeaderTemplate>
+ <DataTemplate>
+
+ <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+
+ </DataTemplate>
+ </CollectionView.GroupHeaderTemplate>
+
+ <CollectionView.GroupFooterTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange"
+ Margin="0,0,0,15"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.GroupFooterTemplate>
+
+ </CollectionView>
+
+ </StackLayout>
+ </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
--- /dev/null
+using System.Collections.Generic;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class SomeEmptyGroups : ContentPage
+ {
+ public SomeEmptyGroups()
+ {
+ InitializeComponent();
+
+ var teams = new List<Team>
+ {
+ new Team("Avengers", new List<Member>
+ {
+ new Member("Thor"),
+ new Member("Captain America")
+ }),
+
+ new Team("Thundercats", new List<Member>()),
+
+ new Team("Avengers", new List<Member>
+ {
+ new Member("Thor"),
+ new Member("Captain America")
+ }),
+
+ new Team("Bionic Six", new List<Member>()),
+
+ new Team("Fantastic Four", new List<Member>
+ {
+ new Member("The Thing"),
+ new Member("The Human Torch"),
+ new Member("The Invisible Woman"),
+ new Member("Mr. Fantastic"),
+ })
+ };
+
+ CollectionView.ItemsSource = teams;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.SwitchGrouping">
+ <ContentPage.Content>
+ <StackLayout>
+
+ <StackLayout Orientation="Horizontal">
+ <Label Text="Is Grouped:"></Label><Switch x:Name="GroupingSwitch"
+ BindingContext="{x:Reference Name=CollectionView}" IsToggled="{Binding IsGrouped}"></Switch>
+ </StackLayout>
+ <CollectionView x:Name="CollectionView">
+
+ <CollectionView.ItemTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.ItemTemplate>
+
+ <CollectionView.GroupHeaderTemplate>
+ <DataTemplate>
+
+ <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+
+ </DataTemplate>
+ </CollectionView.GroupHeaderTemplate>
+
+ <CollectionView.GroupFooterTemplate>
+ <DataTemplate>
+ <StackLayout>
+ <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange"
+ Margin="0,0,0,15"/>
+ </StackLayout>
+ </DataTemplate>
+ </CollectionView.GroupFooterTemplate>
+
+ </CollectionView>
+ </StackLayout>
+ </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class SwitchGrouping : ContentPage
+ {
+ public SwitchGrouping()
+ {
+ InitializeComponent();
+
+ CollectionView.ItemsSource = new SuperTeams();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+ [Preserve(AllMembers = true)]
+ class Team : List<Member>
+ {
+ public Team(string name, List<Member> members) : base(members)
+ {
+ Name = name;
+ }
+
+ public string Name { get; set; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ class Member
+ {
+ public Member(string name) => Name = name;
+
+ public string Name { get; set; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ class SuperTeams : List<Team>
+ {
+ public SuperTeams()
+ {
+ Add(new Team("Avengers",
+ new List<Member>
+ {
+ new Member("Thor"),
+ new Member("Captain America"),
+ new Member("Iron Man"),
+ new Member("The Hulk"),
+ new Member("Ant-Man"),
+ new Member("Wasp"),
+ new Member("Hawkeye"),
+ new Member("Black Panther"),
+ new Member("Black Widow"),
+ new Member("Doctor Druid"),
+ new Member("She-Hulk"),
+ new Member("Mockingbird"),
+ }
+ ));
+
+ Add(new Team("Fantastic Four",
+ new List<Member>
+ {
+ new Member("The Thing"),
+ new Member("The Human Torch"),
+ new Member("The Invisible Woman"),
+ new Member("Mr. Fantastic"),
+ }
+ ));
+
+ Add(new Team("Defenders",
+ new List<Member>
+ {
+ new Member("Doctor Strange"),
+ new Member("Namor"),
+ new Member("Hulk"),
+ new Member("Silver Surfer"),
+ new Member("Hellcat"),
+ new Member("Nighthawk"),
+ new Member("Yellowjacket"),
+ }
+ ));
+
+ Add(new Team("Heroes for Hire",
+ new List<Member>
+ {
+ new Member("Luke Cage"),
+ new Member("Iron Fist"),
+ new Member("Misty Knight"),
+ new Member("Colleen Wing"),
+ new Member("Shang-Chi"),
+ }
+ ));
+
+ Add(new Team("West Coast Avengers",
+ new List<Member>
+ {
+ new Member("Hawkeye"),
+ new Member("Mockingbird"),
+ new Member("War Machine"),
+ new Member("Wonder Man"),
+ new Member("Tigra"),
+ }
+ ));
+
+ Add(new Team("Great Lakes Avengers",
+ new List<Member>
+ {
+ new Member("Squirrel Girl"),
+ new Member("Dinah Soar"),
+ new Member("Mr. Immortal"),
+ new Member("Flatman"),
+ new Member("Doorman"),
+ }
+ ));
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ class ObservableTeam : ObservableCollection<Member>
+ {
+ public ObservableTeam(string name, List<Member> members) : base(members)
+ {
+ Name = name;
+ }
+
+ public string Name { get; set; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ class ObservableSuperTeams : ObservableCollection<ObservableTeam>
+ {
+ public ObservableSuperTeams ()
+ {
+ Add(new ObservableTeam("Avengers",
+ new List<Member>
+ {
+ new Member("Thor"),
+ new Member("Captain America"),
+ new Member("Iron Man"),
+ new Member("The Hulk"),
+ new Member("Ant-Man"),
+ new Member("Wasp"),
+ new Member("Hawkeye"),
+ new Member("Black Panther"),
+ new Member("Black Widow"),
+ new Member("Doctor Druid"),
+ new Member("She-Hulk"),
+ new Member("Mockingbird"),
+ }
+ ));
+
+ Add(new ObservableTeam("Fantastic Four",
+ new List<Member>
+ {
+ new Member("The Thing"),
+ new Member("The Human Torch"),
+ new Member("The Invisible Woman"),
+ new Member("Mr. Fantastic"),
+ }
+ ));
+
+ Add(new ObservableTeam("Defenders",
+ new List<Member>
+ {
+ new Member("Doctor Strange"),
+ new Member("Namor"),
+ new Member("Hulk"),
+ new Member("Silver Surfer"),
+ new Member("Hellcat"),
+ new Member("Nighthawk"),
+ new Member("Yellowjacket"),
+ }
+ ));
+
+ Add(new ObservableTeam("Heroes for Hire",
+ new List<Member>
+ {
+ new Member("Luke Cage"),
+ new Member("Iron Fist"),
+ new Member("Misty Knight"),
+ new Member("Colleen Wing"),
+ new Member("Shang-Chi"),
+ }
+ ));
+
+ Add(new ObservableTeam("West Coast Avengers",
+ new List<Member>
+ {
+ new Member("Hawkeye"),
+ new Member("Mockingbird"),
+ new Member("War Machine"),
+ new Member("Wonder Man"),
+ new Member("Tigra"),
+ }
+ ));
+
+ Add(new ObservableTeam("Great Lakes Avengers",
+ new List<Member>
+ {
+ new Member("Squirrel Girl"),
+ new Member("Dinah Soar"),
+ new Member("Mr. Immortal"),
+ new Member("Flatman"),
+ new Member("Doorman"),
+ }
+ ));
+ }
+ }
+}
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
+using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries;
namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
{
internal class MultiTestObservableCollection<T> : List<T>, INotifyCollectionChanged
{
+ public MultiTestObservableCollection(List<T> members) : base(members) { }
+
+ public MultiTestObservableCollection() { }
+
// This is a testing class which implements INotifyCollectionChanged and, unlike the regular
// ObservableCollection, will actually fire Add and Remove with multiple items at once
public void TestReset()
{
+ var random = new Random();
+ var randomized = GetRange(1, Count - 1).Select(item => new { Item = item, Index = random.Next(100000) })
+ .OrderBy(x => x.Index).Select(x => x.Item).ToList();
+
RemoveRange(0, Count);
+ InsertRange(0, randomized);
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
OnNotifyCollectionChanged(this, args);
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
+
+ <ItemGroup>
+ <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\BasicGrouping.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ </None>
+ <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\GroupingNoTemplates.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ </None>
+ <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\GroupingPlusSelection.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ </None>
+ <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\MeasureFirstStrategy.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ </None>
+ <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\SomeEmptyGroups.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ </None>
+ <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\SwitchGrouping.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ </None>
+ </ItemGroup>
<Target Name="CreateControllGalleryConfig" BeforeTargets="Build">
<CreateItem Include="blank.config">
<Output TaskParameter="Include" ItemName="ConfigFile" />
namespace Xamarin.Forms
{
[RenderWith(typeof(_CollectionViewRenderer))]
- public class CollectionView : SelectableItemsView
+ public class CollectionView : GroupableItemsView
{
internal const string CollectionViewExperimental = "CollectionView_Experimental";
--- /dev/null
+namespace Xamarin.Forms
+{
+ public class GroupableItemsView : SelectableItemsView
+ {
+ public static readonly BindableProperty IsGroupedProperty =
+ BindableProperty.Create(nameof(IsGrouped), typeof(bool), typeof(GroupableItemsView), false);
+
+ public bool IsGrouped
+ {
+ get => (bool)GetValue(IsGroupedProperty);
+ set => SetValue(IsGroupedProperty, value);
+ }
+
+ public static readonly BindableProperty GroupHeaderTemplateProperty =
+ BindableProperty.Create(nameof(GroupHeaderTemplate), typeof(DataTemplate), typeof(GroupableItemsView), default(DataTemplate));
+
+ public DataTemplate GroupHeaderTemplate
+ {
+ get => (DataTemplate)GetValue(GroupHeaderTemplateProperty);
+ set => SetValue(GroupHeaderTemplateProperty, value);
+ }
+
+ public static readonly BindableProperty GroupFooterTemplateProperty =
+ BindableProperty.Create(nameof(GroupFooterTemplate), typeof(DataTemplate), typeof(GroupableItemsView), default(DataTemplate));
+
+ public DataTemplate GroupFooterTemplate
+ {
+ get => (DataTemplate)GetValue(GroupFooterTemplateProperty);
+ set => SetValue(GroupFooterTemplateProperty, value);
+ }
+ }
+}
{
public class ListItemsLayout : ItemsLayout
{
- public ListItemsLayout(ItemsLayoutOrientation orientation) : base(orientation)
+ public ListItemsLayout([Parameter("Orientation")] ItemsLayoutOrientation orientation) : base(orientation)
{
}
namespace Xamarin.Forms.Platform.iOS
{
- public class CollectionViewRenderer : SelectableItemsViewRenderer { }
+ public class CollectionViewRenderer : GroupableItemsViewRenderer { }
}
\ No newline at end of file
using System;
+using Foundation;
namespace Xamarin.Forms.Platform.iOS
{
- sealed class EmptySource : IItemsViewSource
+ internal class EmptySource : IItemsViewSource
{
- public int Count => 0;
+ public int GroupCount => 0;
- public object this[int index] => throw new IndexOutOfRangeException("IItemsViewSource is empty");
+ public int ItemCount => 0;
+
+ public object this[NSIndexPath indexPath] => throw new IndexOutOfRangeException("IItemsViewSource is empty");
+
+ public int ItemCountInGroup(nint group)
+ {
+ return 0;
+ }
+
+ public object Group(NSIndexPath indexPath)
+ {
+ throw new IndexOutOfRangeException("IItemsViewSource is empty");
+ }
+
+ public NSIndexPath GetIndexForItem(object item)
+ {
+ throw new IndexOutOfRangeException("IItemsViewSource is empty");
+ }
public void Dispose()
{
--- /dev/null
+using System;
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+ public class GroupableItemsViewController : SelectableItemsViewController
+ {
+ GroupableItemsView GroupableItemsView => (GroupableItemsView)ItemsView;
+
+ // Keep a cached value for the current state of grouping around so we can avoid hitting the
+ // BindableProperty all the time
+ bool _isGrouped;
+
+ public GroupableItemsViewController(GroupableItemsView groupableItemsView, ItemsViewLayout layout)
+ : base(groupableItemsView, layout)
+ {
+ _isGrouped = GroupableItemsView.IsGrouped;
+ }
+
+ protected override IItemsViewSource CreateItemsViewSource()
+ {
+ // Use the BindableProperty here (instead of _isGroupingEnabled) because the cached value might not be set yet
+ if (GroupableItemsView.IsGrouped)
+ {
+ return ItemsSourceFactory.CreateGrouped(GroupableItemsView.ItemsSource, CollectionView);
+ }
+
+ return base.CreateItemsViewSource();
+ }
+
+ public override void UpdateItemsSource()
+ {
+ _isGrouped = GroupableItemsView.IsGrouped;
+ base.UpdateItemsSource();
+ }
+
+ protected override void RegisterViewTypes()
+ {
+ base.RegisterViewTypes();
+
+ RegisterSupplementaryViews(UICollectionElementKindSection.Header);
+ RegisterSupplementaryViews(UICollectionElementKindSection.Footer);
+ }
+
+ private void RegisterSupplementaryViews(UICollectionElementKindSection kind)
+ {
+ CollectionView.RegisterClassForSupplementaryView(typeof(HorizontalTemplatedSupplementalView),
+ kind, HorizontalTemplatedSupplementalView.ReuseId);
+ CollectionView.RegisterClassForSupplementaryView(typeof(VerticalTemplatedSupplementalView),
+ kind, VerticalTemplatedSupplementalView.ReuseId);
+ CollectionView.RegisterClassForSupplementaryView(typeof(HorizontalDefaultSupplementalView),
+ kind, HorizontalDefaultSupplementalView.ReuseId);
+ CollectionView.RegisterClassForSupplementaryView(typeof(VerticalDefaultSupplementalView),
+ kind, VerticalDefaultSupplementalView.ReuseId);
+ }
+
+ public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView,
+ NSString elementKind, NSIndexPath indexPath)
+ {
+ var reuseId = DetermineViewReuseId(elementKind);
+
+ var view = collectionView.DequeueReusableSupplementaryView(elementKind, reuseId, indexPath) as UICollectionReusableView;
+
+ switch (view)
+ {
+ case DefaultCell defaultCell:
+ UpdateDefaultSupplementaryView(defaultCell, elementKind, indexPath);
+ break;
+ case TemplatedCell templatedCell:
+ UpdateTemplatedSupplementaryView(templatedCell, elementKind, indexPath);
+ break;
+ }
+
+ return view;
+ }
+
+ void UpdateDefaultSupplementaryView(DefaultCell cell, NSString elementKind, NSIndexPath indexPath)
+ {
+ cell.Label.Text = ItemsSource.Group(indexPath).ToString();
+
+ if (cell is ItemsViewCell constrainedCell)
+ {
+ cell.ConstrainTo(ItemsViewLayout.ConstrainedDimension);
+ }
+ }
+
+ void UpdateTemplatedSupplementaryView(TemplatedCell cell, NSString elementKind, NSIndexPath indexPath)
+ {
+ ApplyTemplateAndDataContext(cell, elementKind, indexPath);
+
+ if (cell is ItemsViewCell constrainedCell)
+ {
+ cell.ConstrainTo(ItemsViewLayout.ConstrainedDimension);
+ }
+ }
+
+ void ApplyTemplateAndDataContext(TemplatedCell cell, NSString elementKind, NSIndexPath indexPath)
+ {
+ DataTemplate template;
+
+ if (elementKind == UICollectionElementKindSectionKey.Header)
+ {
+ template = GroupableItemsView.GroupHeaderTemplate;
+ }
+ else
+ {
+ template = GroupableItemsView.GroupFooterTemplate;
+ }
+
+ var templateElement = template.CreateContent() as View;
+ var renderer = CreateRenderer(templateElement);
+
+ BindableObject.SetInheritedBindingContext(renderer.Element, ItemsSource.Group(indexPath));
+ cell.SetRenderer(renderer);
+ }
+
+ string DetermineViewReuseId(NSString elementKind)
+ {
+ if (elementKind == UICollectionElementKindSectionKey.Header)
+ {
+ return DetermineViewReuseId(GroupableItemsView.GroupHeaderTemplate);
+ }
+
+ return DetermineViewReuseId(GroupableItemsView.GroupFooterTemplate);
+ }
+
+ string DetermineViewReuseId(DataTemplate template)
+ {
+ if (template == null)
+ {
+ // No template, fall back the the default supplemental views
+ return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
+ ? HorizontalDefaultSupplementalView.ReuseId
+ : VerticalDefaultSupplementalView.ReuseId;
+ }
+
+ return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
+ ? HorizontalTemplatedSupplementalView.ReuseId
+ : VerticalTemplatedSupplementalView.ReuseId;
+ }
+
+ internal CGSize GetReferenceSizeForHeader(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
+ {
+ if (!_isGrouped)
+ {
+ return CGSize.Empty;
+ }
+
+ // Currently we explicitly measure all of the headers/footers
+ // Long-term, we might want to look at performance hints (similar to ItemSizingStrategy) for
+ // headers/footers (if the dev knows for sure they'll all the be the same size)
+
+ var cell = GetViewForSupplementaryElement(collectionView, UICollectionElementKindSectionKey.Header,
+ NSIndexPath.FromItemSection(0, section)) as ItemsViewCell;
+
+ return cell.Measure();
+ }
+
+ internal CGSize GetReferenceSizeForFooter(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
+ {
+ if (!_isGrouped)
+ {
+ return CGSize.Empty;
+ }
+
+ var cell = GetViewForSupplementaryElement(collectionView, UICollectionElementKindSectionKey.Footer,
+ NSIndexPath.FromItemSection(0, section)) as ItemsViewCell;
+
+ return cell.Measure();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.ComponentModel;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+ public class GroupableItemsViewRenderer : SelectableItemsViewRenderer
+ {
+ GroupableItemsView GroupableItemsView => (GroupableItemsView)Element;
+ GroupableItemsViewController GroupableItemsViewController => (GroupableItemsViewController)ItemsViewController;
+
+ protected override ItemsViewController CreateController(ItemsView itemsView, ItemsViewLayout layout)
+ {
+ return new GroupableItemsViewController(itemsView as GroupableItemsView, layout);
+ }
+
+ protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs changedProperty)
+ {
+ base.OnElementPropertyChanged(sender, changedProperty);
+
+ if (changedProperty.Is(GroupableItemsView.IsGroupedProperty))
+ {
+ GroupableItemsViewController?.UpdateItemsSource();
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+ internal sealed class HorizontalDefaultSupplementalView : DefaultCell
+ {
+ public static NSString ReuseId = new NSString("Xamarin.Forms.Platform.iOS.HorizontalDefaultSupplementalView");
+
+ [Export("initWithFrame:")]
+ public HorizontalDefaultSupplementalView(CGRect frame) : base(frame)
+ {
+ Label.Font = UIFont.PreferredHeadline;
+
+ Constraint = Label.HeightAnchor.ConstraintEqualTo(Frame.Height);
+ Constraint.Active = true;
+ }
+
+ public override void ConstrainTo(CGSize constraint)
+ {
+ Constraint.Constant = constraint.Height;
+ }
+
+ public override CGSize Measure()
+ {
+ return new CGSize(Label.IntrinsicContentSize.Width, Constraint.Constant);
+ }
+ }
+
+}
\ No newline at end of file
--- /dev/null
+using CoreGraphics;
+using Foundation;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+ public class HorizontalTemplatedSupplementalView : TemplatedCell
+ {
+ public static NSString ReuseId = new NSString("Xamarin.Forms.Platform.iOS.HorizontalTemplatedSupplementalView");
+
+ [Export("initWithFrame:")]
+ public HorizontalTemplatedSupplementalView(CGRect frame) : base(frame)
+ {
+ }
+
+ public override CGSize Measure()
+ {
+ var measure = VisualElementRenderer.Element.Measure(double.PositiveInfinity,
+ ConstrainedDimension, MeasureFlags.IncludeMargins);
+
+ var width = VisualElementRenderer.Element.Width > 0
+ ? VisualElementRenderer.Element.Width : measure.Request.Width;
+
+ return new CGSize(width, ConstrainedDimension);
+ }
+
+ public override void ConstrainTo(CGSize constraint)
+ {
+ ConstrainedDimension = constraint.Height;
+ Layout(constraint);
+ }
+ }
+}
\ No newline at end of file
namespace Xamarin.Forms.Platform.iOS
{
- internal interface IItemsViewSource : IDisposable
+ public interface IItemsViewSource : IDisposable
{
- int Count { get; }
- object this[int index] { get; }
+ int ItemCount { get; }
+ int ItemCountInGroup(nint group);
+ int GroupCount { get; }
+ object this[Foundation.NSIndexPath indexPath] { get; }
+ object Group(Foundation.NSIndexPath indexPath);
+ Foundation.NSIndexPath GetIndexForItem(object item);
}
}
\ No newline at end of file
return new ListSource(itemsSource);
}
+
+ public static IItemsViewSource CreateGrouped(IEnumerable itemsSource, UICollectionView collectionView)
+ {
+ if (itemsSource == null)
+ {
+ return new EmptySource();
+ }
+
+ return new ObservableGroupedSource(itemsSource, collectionView);
+ }
}
}
\ No newline at end of file
// TODO hartez 2018/06/01 14:21:24 Add a method for updating the layout
public class ItemsViewController : UICollectionViewController
{
- IItemsViewSource _itemsSource;
- readonly ItemsView _itemsView;
- ItemsViewLayout _layout;
+ protected IItemsViewSource ItemsSource { get; set; }
+ public ItemsView ItemsView { get; }
+ protected ItemsViewLayout ItemsViewLayout { get; set; }
bool _initialConstraintsSet;
- bool _safeForReload;
- bool _wasEmpty;
+ bool _isEmpty;
bool _currentBackgroundIsEmptyView;
bool _disposed;
public ItemsViewController(ItemsView itemsView, ItemsViewLayout layout) : base(layout)
{
- _itemsView = itemsView;
- _itemsSource = ItemsSourceFactory.Create(_itemsView.ItemsSource, CollectionView);
-
- // If we already have data, the UICollectionView will have items and we'll be safe to call
- // ReloadData if the ItemsSource changes in the future (see UpdateItemsSource for more).
- _safeForReload = _itemsSource?.Count > 0;
+ ItemsView = itemsView;
+ ItemsSource = CreateItemsViewSource();
UpdateLayout(layout);
}
public void UpdateLayout(ItemsViewLayout layout)
{
- _layout = layout;
- _layout.GetPrototype = GetPrototype;
+ ItemsViewLayout = layout;
+ ItemsViewLayout.GetPrototype = GetPrototype;
// If we're updating from a previous layout, we should keep any settings for the SelectableItemsViewController around
var selectableItemsViewController = Delegator?.SelectableItemsViewController;
- Delegator = new UICollectionViewDelegator(_layout, this);
+ Delegator = new UICollectionViewDelegator(ItemsViewLayout, this);
CollectionView.Delegate = Delegator;
- if (CollectionView.CollectionViewLayout != _layout)
+ if (CollectionView.CollectionViewLayout != ItemsViewLayout)
{
// We're updating from a previous layout
// Make sure the new layout is sized properly
- _layout.ConstrainTo(CollectionView.Bounds.Size);
+ ItemsViewLayout.ConstrainTo(CollectionView.Bounds.Size);
- CollectionView.SetCollectionViewLayout(_layout, false);
+ CollectionView.SetCollectionViewLayout(ItemsViewLayout, false);
// Reload the data so the currently visible cells get laid out according to the new layout
CollectionView.ReloadData();
{
if (disposing)
{
- _itemsSource?.Dispose();
+ ItemsSource?.Dispose();
}
_disposed = true;
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
- var cell = collectionView.DequeueReusableCell(DetermineCellReusedId(), indexPath) as UICollectionViewCell;
+ var cell = collectionView.DequeueReusableCell(DetermineCellReuseId(), indexPath) as UICollectionViewCell;
switch (cell)
{
public override nint GetItemsCount(UICollectionView collectionView, nint section)
{
- var count = _itemsSource.Count;
+ var count = ItemsSource.ItemCountInGroup(section);
- if (_wasEmpty && count > 0)
- {
- // We've moved from no items to having at least one item; it's likely that the layout needs to update
- // its cell size/estimate
- _layout?.SetNeedCellSizeUpdate();
- }
+ CheckForEmptySource();
+
+ return count;
+ }
- _wasEmpty = count == 0;
+ void CheckForEmptySource()
+ {
+ var wasEmpty = _isEmpty;
- UpdateEmptyViewVisibility(_wasEmpty);
+ _isEmpty = ItemsSource.ItemCount == 0;
- return count;
+ if (wasEmpty != _isEmpty)
+ {
+ UpdateEmptyViewVisibility(_isEmpty);
+ }
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
AutomaticallyAdjustsScrollViewInsets = false;
- RegisterCells();
+ RegisterViewTypes();
}
public override void ViewWillLayoutSubviews()
// are set up the first time this method is called.
if (!_initialConstraintsSet)
{
- _layout.ConstrainTo(CollectionView.Bounds.Size);
+ ItemsViewLayout.ConstrainTo(CollectionView.Bounds.Size);
_initialConstraintsSet = true;
}
}
- public virtual void UpdateItemsSource()
+ protected virtual IItemsViewSource CreateItemsViewSource()
{
- if (_safeForReload)
- {
- UpdateItemsSourceAndReload();
- }
- else
- {
- // Okay, thus far this UICollectionView has never had any items in it. At this point, if
- // we set the ItemsSource and try to call ReloadData(), it'll crash. AFAICT this is a bug, but
- // until it's fixed (or we can figure out another way to go from empty -> having items), we'll
- // have to use this crazy workaround
- EmptyCollectionViewReloadWorkaround();
- }
+ return ItemsSourceFactory.Create(ItemsView.ItemsSource, CollectionView);
}
- void UpdateItemsSourceAndReload()
+ public virtual void UpdateItemsSource()
{
- _itemsSource = ItemsSourceFactory.Create(_itemsView.ItemsSource, CollectionView);
+ ItemsSource = CreateItemsViewSource();
CollectionView.ReloadData();
CollectionView.CollectionViewLayout.InvalidateLayout();
}
- void EmptyCollectionViewReloadWorkaround()
+ public override nint NumberOfSections(UICollectionView collectionView)
{
- var enumerator = _itemsView.ItemsSource.GetEnumerator();
-
- if (!enumerator.MoveNext())
- {
- // The source we're updating to is empty, so we can just update as normal; it won't crash
- UpdateItemsSourceAndReload();
- }
- else
- {
- // Grab the first item from the new ItemsSource and create a usable source for the UICollectionView
- // from that
- var firstItem = new List<object> { enumerator.Current };
- _itemsSource = ItemsSourceFactory.Create(firstItem, CollectionView);
-
- // Insert that item into the UICollectionView
- // TODO ezhart When we implement grouping, this will need to be the index of the first actual item
- // Which might not be zero,zero if we have empty groups
- var indexesToInsert = new NSIndexPath[1] { NSIndexPath.Create(0, 0) };
-
- UIView.PerformWithoutAnimation(() =>
- {
- CollectionView.InsertItems(indexesToInsert);
- });
-
- // Okay, from now on we can just call ReloadData and things will work fine
- _safeForReload = true;
- UpdateItemsSource();
- }
+ CheckForEmptySource();
+ return ItemsSource.GroupCount;
}
protected virtual void UpdateDefaultCell(DefaultCell cell, NSIndexPath indexPath)
{
- cell.Label.Text = _itemsSource[indexPath.Row].ToString();
+ cell.Label.Text = ItemsSource[indexPath].ToString();
if (cell is ItemsViewCell constrainedCell)
{
- _layout.PrepareCellForLayout(constrainedCell);
+ ItemsViewLayout.PrepareCellForLayout(constrainedCell);
}
}
if (cell is ItemsViewCell constrainedCell)
{
- _layout.PrepareCellForLayout(constrainedCell);
+ ItemsViewLayout.PrepareCellForLayout(constrainedCell);
}
}
public virtual NSIndexPath GetIndexForItem(object item)
{
- for (int n = 0; n < _itemsSource.Count; n++)
- {
- if (_itemsSource[n] == item)
- {
- return NSIndexPath.Create(0, n);
- }
- }
-
- return NSIndexPath.Create(-1, -1);
+ return ItemsSource.GetIndexForItem(item);
}
protected object GetItemAtIndex(NSIndexPath index)
{
- return _itemsSource[index.Row];
+ return ItemsSource[index];
}
void ApplyTemplateAndDataContext(TemplatedCell cell, NSIndexPath indexPath)
{
- var template = _itemsView.ItemTemplate;
- var item = _itemsSource[indexPath.Row];
+ var template = ItemsView.ItemTemplate;
+ var item = ItemsSource[indexPath];
// Run this through the extension method in case it's really a DataTemplateSelector
- template = template.SelectDataTemplate(item, _itemsView);
+ template = template.SelectDataTemplate(item, ItemsView);
// Create the content and renderer for the view and
var view = template.CreateContent() as View;
cell.SetRenderer(renderer);
// Bind the view to the data item
- view.BindingContext = _itemsSource[indexPath.Row];
+ view.BindingContext = ItemsSource[indexPath];
// And make sure it's a "child" of the ItemsView
- _itemsView.AddLogicalChild(view);
+ ItemsView.AddLogicalChild(view);
cell.ContentSizeChanged += CellContentSizeChanged;
}
if (oldView != null)
{
oldView.BindingContext = null;
- _itemsView.RemoveLogicalChild(oldView);
+ ItemsView.RemoveLogicalChild(oldView);
}
templatedCell.PrepareForRemoval();
}
}
- IVisualElementRenderer CreateRenderer(View view)
+ protected IVisualElementRenderer CreateRenderer(View view)
{
if (view == null)
{
return renderer;
}
- string DetermineCellReusedId()
+ string DetermineCellReuseId()
{
- if (_itemsView.ItemTemplate != null)
+ if (ItemsView.ItemTemplate != null)
{
- return _layout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
+ return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
? HorizontalTemplatedCell.ReuseId
: VerticalTemplatedCell.ReuseId;
}
- return _layout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
+ return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
? HorizontalDefaultCell.ReuseId
: VerticalDefaultCell.ReuseId;
}
UICollectionViewCell GetPrototype()
{
- if (_itemsSource.Count == 0)
+ if (ItemsSource.ItemCount == 0)
{
return null;
}
- // TODO hartez assuming this works, we'll need to evaluate using this nsindexpath (what about groups?)
- var indexPath = NSIndexPath.Create(0, 0);
+ var group = 0;
+
+ if (ItemsSource.GroupCount > 1)
+ {
+ // If we're in a grouping situation, then we need to make sure we find an actual data item
+ // to use for our prototype cell. It's possible that we have empty groups.
+ for (int n = 0; n < ItemsSource.GroupCount; n++)
+ {
+ if (ItemsSource.ItemCountInGroup(n) > 0)
+ {
+ group = n;
+ break;
+ }
+ }
+ }
+
+ var indexPath = NSIndexPath.Create(group, 0);
+
return GetCell(CollectionView, indexPath);
}
- void RegisterCells()
+ protected virtual void RegisterViewTypes()
{
CollectionView.RegisterClassForCell(typeof(HorizontalDefaultCell), HorizontalDefaultCell.ReuseId);
CollectionView.RegisterClassForCell(typeof(VerticalDefaultCell), VerticalDefaultCell.ReuseId);
internal void UpdateEmptyView()
{
// Is EmptyView set on the ItemsView?
- var emptyView = _itemsView?.EmptyView;
+ var emptyView = ItemsView?.EmptyView;
if (emptyView == null)
{
{
// Create the native renderer for the EmptyView, and keep the actual Forms element (if any)
// around for updating the layout later
- var (NativeView, FormsElement) = RealizeEmptyView(emptyView, _itemsView.EmptyViewTemplate);
+ var (NativeView, FormsElement) = RealizeEmptyView(emptyView, ItemsView.EmptyViewTemplate);
_emptyUIView = NativeView;
_emptyViewFormsElement = FormsElement;
}
// If the empty view is being displayed, we might need to update it
- UpdateEmptyViewVisibility(_itemsSource?.Count == 0);
+ UpdateEmptyViewVisibility(ItemsSource?.ItemCount == 0);
}
void UpdateEmptyViewVisibility(bool isEmpty)
if (emptyViewTemplate != null)
{
// Run this through the extension method in case it's really a DataTemplateSelector
- emptyViewTemplate = emptyViewTemplate.SelectDataTemplate(emptyView, _itemsView);
+ emptyViewTemplate = emptyViewTemplate.SelectDataTemplate(emptyView, ItemsView);
// We have a template; turn it into a Forms view
var templateElement = emptyViewTemplate.CreateContent() as View;
readonly ItemsLayout _itemsLayout;
bool _determiningCellSize;
bool _disposed;
- bool _needCellSizeUpdate;
protected ItemsViewLayout(ItemsLayout itemsLayout)
{
public abstract void ConstrainTo(CGSize size);
- public virtual void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath path)
- {
- if (_needCellSizeUpdate)
- {
- // Our cell size/estimate is out of date, probably because we moved from zero to one item; update it
- _needCellSizeUpdate = false;
- DetermineCellSize();
- }
- }
-
public virtual UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
nint section)
{
}
}
+ if (Forms.IsiOS11OrNewer)
+ {
+ return base.ShouldInvalidateLayout(preferredAttributes, originalAttributes);
+ }
+
+ // For iOS 10 and lower, we have to invalidate on header/footer changes here; otherwise, all of the
+ // headers and footers will draw on top of one another
+ if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Header
+ || preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Footer)
+ {
+ return true;
+ }
+
return base.ShouldInvalidateLayout(preferredAttributes, originalAttributes);
}
UpdateCellConstraints();
}
- public void SetNeedCellSizeUpdate()
- {
- _needCellSizeUpdate = true;
- }
-
public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
{
var snapPointsType = _itemsLayout.SnapPointsType;
InvalidateLayout();
}
+
+ public override UICollectionViewLayoutInvalidationContext GetInvalidationContext(UICollectionViewLayoutAttributes preferredAttributes, UICollectionViewLayoutAttributes originalAttributes)
+ {
+ if (Forms.IsiOS11OrNewer)
+ {
+ return base.GetInvalidationContext(preferredAttributes, originalAttributes);
+ }
+
+ var indexPath = preferredAttributes.IndexPath;
+
+ try
+ {
+ UICollectionViewLayoutInvalidationContext invalidationContext =
+ base.GetInvalidationContext(preferredAttributes, originalAttributes);
+
+ // Ensure that if this invalidation was triggered by header/footer changes, the header/footer
+ // are being invalidated
+ if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Header)
+ {
+ invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Header,
+ new[] { indexPath });
+ }
+ else if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Footer)
+ {
+ invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Footer,
+ new[] { indexPath });
+ }
+
+ return invalidationContext;
+ }
+ catch (MonoTouchException)
+ {
+ // This happens on iOS 10 if we have any empty groups in our ItemsSource. Catching here and
+ // returning a UICollectionViewFlowLayoutInvalidationContext means that the application does not
+ // crash, though any group headers/footers will initially draw in the wrong location. It's possible to
+ // work around this problem by forcing a full layout update after the headers/footers have been
+ // drawn in the wrong places
+ }
+
+ return new UICollectionViewFlowLayoutInvalidationContext();
+ }
+
+ public override UICollectionViewLayoutAttributes LayoutAttributesForSupplementaryView(NSString kind, NSIndexPath indexPath)
+ {
+ if (Forms.IsiOS11OrNewer)
+ {
+ return base.LayoutAttributesForSupplementaryView(kind, indexPath);
+ }
+
+ // iOS 10 and lower doesn't create these and will throw an exception in GetViewForSupplementaryElement
+ // without them, so we need to do it manually here
+ return UICollectionViewLayoutAttributes.CreateForSupplementaryView(kind, indexPath);
+ }
}
}
-using System.Collections;
+using System;
+using System.Collections;
using System.Collections.Generic;
+using Foundation;
namespace Xamarin.Forms.Platform.iOS
{
{
}
+
+ public object this[NSIndexPath indexPath]
+ {
+ get
+ {
+ if (indexPath.Section > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(indexPath));
+ }
+
+ return this[indexPath.Row];
+ }
+ }
+
+ public int GroupCount => Count == 0 ? 0 : 1;
+
+ public int ItemCount => Count;
+
+ public NSIndexPath GetIndexForItem(object item)
+ {
+ for (int n = 0; n < Count; n++)
+ {
+ if (this[n] == item)
+ {
+ return NSIndexPath.Create(0, n);
+ }
+ }
+
+ return NSIndexPath.Create(-1, -1);
+ }
+
+ public object Group(NSIndexPath indexPath)
+ {
+ return null;
+ }
+
+ public int ItemCountInGroup(nint group)
+ {
+ if (group > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(group));
+ }
+
+ return Count;
+ }
}
}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+ internal class ObservableGroupedSource : IItemsViewSource
+ {
+ readonly UICollectionView _collectionView;
+ readonly IList _groupSource;
+ bool _disposed;
+ List<ObservableItemsSource> _groups = new List<ObservableItemsSource>();
+
+ public ObservableGroupedSource(IEnumerable groupSource, UICollectionView collectionView)
+ {
+ _collectionView = collectionView;
+ _groupSource = groupSource as IList ?? new ListSource(groupSource);
+
+ if (_groupSource is INotifyCollectionChanged incc)
+ {
+ incc.CollectionChanged += CollectionChanged;
+ }
+
+ ResetGroupTracking();
+ }
+
+ public object this[NSIndexPath indexPath]
+ {
+ get
+ {
+ var group = (IList)_groupSource[indexPath.Section];
+
+ if (group.Count == 0)
+ {
+ return null;
+ }
+
+ return group[indexPath.Row];
+ }
+ }
+
+ public int GroupCount => _groupSource.Count;
+
+ int IItemsViewSource.ItemCount
+ {
+ get
+ {
+ // TODO hartez We should probably cache this value
+ var total = 0;
+
+ for (int n = 0; n < _groupSource.Count; n++)
+ {
+ var group = (IList)_groupSource[n];
+ total += group.Count;
+ }
+
+ return total;
+ }
+ }
+
+ public NSIndexPath GetIndexForItem(object item)
+ {
+ for (int i = 0; i < _groupSource.Count; i++)
+ {
+ var group = (IList)_groupSource[i];
+
+ for (int j = 0; j < group.Count; j++)
+ {
+ if (group[j] == item)
+ {
+ return NSIndexPath.Create(i, j);
+ }
+ }
+ }
+
+ return NSIndexPath.Create(-1, -1);
+ }
+
+ public object Group(NSIndexPath indexPath)
+ {
+ return _groupSource[indexPath.Section];
+ }
+
+ public int ItemCountInGroup(nint group)
+ {
+ return ((IList)_groupSource[(int)group]).Count;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (disposing)
+ {
+ ClearGroupTracking();
+ if (_groupSource is INotifyCollectionChanged incc)
+ {
+ incc.CollectionChanged -= CollectionChanged;
+ }
+ }
+ }
+
+ void ClearGroupTracking()
+ {
+ for (int n = _groups.Count - 1; n >= 0; n--)
+ {
+ _groups[n].Dispose();
+ _groups.RemoveAt(n);
+ }
+ }
+
+ void ResetGroupTracking()
+ {
+ ClearGroupTracking();
+
+ for (int n = 0; n < _groupSource.Count; n++)
+ {
+ if (_groupSource[n] is INotifyCollectionChanged incc && _groupSource[n] is IList list)
+ {
+ _groups.Add(new ObservableItemsSource(list, _collectionView, n));
+ }
+ }
+ }
+
+ void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
+ {
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ Add(args);
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ Remove(args);
+ break;
+ case NotifyCollectionChangedAction.Replace:
+ Replace(args);
+ break;
+ case NotifyCollectionChangedAction.Move:
+ Move(args);
+ break;
+ case NotifyCollectionChangedAction.Reset:
+ Reload();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ void Reload()
+ {
+ ResetGroupTracking();
+ _collectionView.ReloadData();
+ _collectionView.CollectionViewLayout.InvalidateLayout();
+ }
+
+ NSIndexSet CreateIndexSetFrom(int startIndex, int count)
+ {
+ return NSIndexSet.FromNSRange(new NSRange(startIndex, count));
+ }
+
+ void Add(NotifyCollectionChangedEventArgs args)
+ {
+ var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
+ var count = args.NewItems.Count;
+
+ // Adding a group will change the section index for all subsequent groups, so the easiest thing to do
+ // is to reset all the group tracking to get it up-to-date
+ ResetGroupTracking();
+
+ _collectionView.InsertSections(CreateIndexSetFrom(startIndex, count));
+ }
+
+ void Remove(NotifyCollectionChangedEventArgs args)
+ {
+ var startIndex = args.OldStartingIndex;
+
+ if (startIndex < 0)
+ {
+ // INCC implementation isn't giving us enough information to know where the removed items were in the
+ // collection. So the best we can do is a ReloadData()
+ Reload();
+ return;
+ }
+
+ // If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
+ var count = args.OldItems.Count;
+
+ // Removing a group will change the section index for all subsequent groups, so the easiest thing to do
+ // is to reset all the group tracking to get it up-to-date
+ ResetGroupTracking();
+
+ _collectionView.DeleteSections(CreateIndexSetFrom(startIndex, count));
+ }
+
+ void Replace(NotifyCollectionChangedEventArgs args)
+ {
+ var newCount = args.NewItems.Count;
+
+ if (newCount == args.OldItems.Count)
+ {
+ ResetGroupTracking();
+
+ var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
+
+ // We are replacing one set of items with a set of equal size; we can do a simple item range update
+ _collectionView.ReloadSections(CreateIndexSetFrom(startIndex, newCount));
+ return;
+ }
+
+ // The original and replacement sets are of unequal size; this means that everything currently in view will
+ // have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
+ Reload();
+ }
+
+ void Move(NotifyCollectionChangedEventArgs args)
+ {
+ var count = args.NewItems.Count;
+
+ ResetGroupTracking();
+
+ if (count == 1)
+ {
+ // For a single item, we can use MoveSection and get the animation
+ _collectionView.MoveSection(args.OldStartingIndex, args.NewStartingIndex);
+ return;
+ }
+
+ var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
+ var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
+
+ _collectionView.ReloadSections(CreateIndexSetFrom(start, end));
+ }
+ }
+
+}
internal class ObservableItemsSource : IItemsViewSource
{
readonly UICollectionView _collectionView;
+ readonly bool _grouped;
+ readonly int _section;
readonly IList _itemsSource;
bool _disposed;
- public ObservableItemsSource(IList itemSource, UICollectionView collectionView)
+ public ObservableItemsSource(IList itemSource, UICollectionView collectionView, int group = -1)
{
_collectionView = collectionView;
+
+ _section = group < 0 ? 0 : group;
+ _grouped = group >= 0;
+
_itemsSource = itemSource;
((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged;
}
}
+ public int ItemCountInGroup(nint group)
+ {
+ return _itemsSource.Count;
+ }
+
+ public object Group(NSIndexPath indexPath)
+ {
+ return null;
+ }
+
+ public NSIndexPath GetIndexForItem(object item)
+ {
+ for (int n = 0; n < _itemsSource.Count; n++)
+ {
+ if (this[n] == item)
+ {
+ return NSIndexPath.Create(_section, n);
+ }
+ }
+
+ return NSIndexPath.Create(-1, -1);
+ }
+
+ public int GroupCount => _itemsSource.Count == 0 ? 0 : 1;
+
+ public int ItemCount => _itemsSource.Count;
+
+ public object this[NSIndexPath indexPath]
+ {
+ get
+ {
+ if (indexPath.Section != _section)
+ {
+ throw new ArgumentOutOfRangeException(nameof(indexPath));
+ }
+
+ return this[indexPath.Row];
+ }
+ }
+
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
Move(args);
break;
case NotifyCollectionChangedAction.Reset:
- _collectionView.ReloadData();
- _collectionView.CollectionViewLayout.InvalidateLayout();
+ Reload();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
- void Move(NotifyCollectionChangedEventArgs args)
- {
- var count = args.NewItems.Count;
-
- if (count == 1)
- {
- // For a single item, we can use MoveItem and get the animation
- var oldPath = NSIndexPath.Create(0, args.OldStartingIndex);
- var newPath = NSIndexPath.Create(0, args.NewStartingIndex);
-
- _collectionView.MoveItem(oldPath, newPath);
- return;
- }
-
- var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
- var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
- _collectionView.ReloadItems(CreateIndexesFrom(start, end));
- }
-
- private void Replace(NotifyCollectionChangedEventArgs args)
+ void Reload()
{
- var newCount = args.NewItems.Count;
-
- if (newCount == args.OldItems.Count)
- {
- var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
-
- // We are replacing one set of items with a set of equal size; we can do a simple item range update
- _collectionView.ReloadItems(CreateIndexesFrom(startIndex, newCount));
- return;
- }
-
- // The original and replacement sets are of unequal size; this means that everything currently in view will
- // have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
_collectionView.ReloadData();
+ _collectionView.CollectionViewLayout.InvalidateLayout();
}
- static NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
+ NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
{
var result = new NSIndexPath[count];
for (int n = 0; n < count; n++)
{
- result[n] = NSIndexPath.Create(0, startIndex + n);
+ result[n] = NSIndexPath.Create(_section, startIndex + n);
}
return result;
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
var count = args.NewItems.Count;
- _collectionView.InsertItems(CreateIndexesFrom(startIndex, count));
+ _collectionView.PerformBatchUpdates(() =>
+ {
+ if (!_grouped && _collectionView.NumberOfSections() != GroupCount)
+ {
+ // We had an empty non-grouped list, and now we're trying to add an item;
+ // we need to give it a section as well
+ _collectionView.InsertSections(new NSIndexSet(0));
+ }
+
+ _collectionView.InsertItems(CreateIndexesFrom(startIndex, count));
+ }, null);
}
void Remove(NotifyCollectionChangedEventArgs args)
{
// INCC implementation isn't giving us enough information to know where the removed items were in the
// collection. So the best we can do is a ReloadData()
- _collectionView.ReloadData();
+ Reload();
return;
}
-
+
// If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
var count = args.OldItems.Count;
- _collectionView.DeleteItems(CreateIndexesFrom(startIndex, count));
+
+ _collectionView.PerformBatchUpdates(() =>
+ {
+ _collectionView.DeleteItems(CreateIndexesFrom(startIndex, count));
+
+ if (!_grouped && _collectionView.NumberOfSections() != GroupCount)
+ {
+ // We had a non-grouped list with items, and we're removing the last one;
+ // we also need to remove the group it was in
+ _collectionView.DeleteSections(new NSIndexSet(0));
+ }
+ }, null);
+ }
+
+ void Replace(NotifyCollectionChangedEventArgs args)
+ {
+ var newCount = args.NewItems.Count;
+
+ if (newCount == args.OldItems.Count)
+ {
+ var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
+
+ // We are replacing one set of items with a set of equal size; we can do a simple item range update
+ _collectionView.ReloadItems(CreateIndexesFrom(startIndex, newCount));
+ return;
+ }
+
+ // The original and replacement sets are of unequal size; this means that everything currently in view will
+ // have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
+ Reload();
+ }
+
+ void Move(NotifyCollectionChangedEventArgs args)
+ {
+ var count = args.NewItems.Count;
+
+ if (count == 1)
+ {
+ // For a single item, we can use MoveItem and get the animation
+ var oldPath = NSIndexPath.Create(_section, args.OldStartingIndex);
+ var newPath = NSIndexPath.Create(_section, args.NewStartingIndex);
+
+ _collectionView.MoveItem(oldPath, newPath);
+ return;
+ }
+
+ var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
+ var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
+ _collectionView.ReloadItems(CreateIndexesFrom(start, end));
}
}
-}
\ No newline at end of file
+}
{
public class SelectableItemsViewController : ItemsViewController
{
- protected readonly SelectableItemsView SelectableItemsView;
+ SelectableItemsView SelectableItemsView => (SelectableItemsView)ItemsView;
public SelectableItemsViewController(SelectableItemsView selectableItemsView, ItemsViewLayout layout)
: base(selectableItemsView, layout)
{
- SelectableItemsView = selectableItemsView;
}
// _Only_ called if the user initiates the selection change; will not be called for programmatic selection
using System;
+using CoreGraphics;
using Foundation;
using UIKit;
get => ItemsViewController as SelectableItemsViewController;
}
- public UICollectionViewDelegator(ItemsViewLayout itemsViewLayout, ItemsViewController itemsViewController)
+ public GroupableItemsViewController GroupableItemsViewController
{
- ItemsViewLayout = itemsViewLayout;
- ItemsViewController = itemsViewController;
+ get => ItemsViewController as GroupableItemsViewController;
}
- public override void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath path)
+ public UICollectionViewDelegator(ItemsViewLayout itemsViewLayout, ItemsViewController itemsViewController)
{
- ItemsViewLayout?.WillDisplayCell(collectionView, cell, path);
+ ItemsViewLayout = itemsViewLayout;
+ ItemsViewController = itemsViewController;
}
public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
{
ItemsViewController.PrepareCellForRemoval(cell);
}
+
+ public override CGSize GetReferenceSizeForHeader(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
+ {
+ if (GroupableItemsViewController == null)
+ {
+ return CGSize.Empty;
+ }
+
+ return GroupableItemsViewController.GetReferenceSizeForHeader(collectionView, layout, section);
+ }
+
+ public override CGSize GetReferenceSizeForFooter(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
+ {
+ if (GroupableItemsViewController == null)
+ {
+ return CGSize.Empty;
+ }
+
+ return GroupableItemsViewController.GetReferenceSizeForFooter(collectionView, layout, section);
+ }
}
}
\ No newline at end of file
--- /dev/null
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+ internal sealed class VerticalDefaultSupplementalView : DefaultCell
+ {
+ public static NSString ReuseId = new NSString("Xamarin.Forms.Platform.iOS.VerticalDefaultSupplementalView");
+
+ [Export("initWithFrame:")]
+ public VerticalDefaultSupplementalView(CGRect frame) : base(frame)
+ {
+ Label.Font = UIFont.PreferredHeadline;
+
+ Constraint = Label.WidthAnchor.ConstraintEqualTo(Frame.Width);
+ Constraint.Active = true;
+ }
+
+ public override void ConstrainTo(CGSize constraint)
+ {
+ Constraint.Constant = constraint.Width;
+ }
+
+ public override CGSize Measure()
+ {
+ return new CGSize(Constraint.Constant, Label.IntrinsicContentSize.Height);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using CoreGraphics;
+using Foundation;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+ public class VerticalTemplatedSupplementalView : TemplatedCell
+ {
+ public static NSString ReuseId = new NSString("Xamarin.Forms.Platform.iOS.VerticalTemplatedSupplementalView");
+
+ [Export("initWithFrame:")]
+ public VerticalTemplatedSupplementalView(CGRect frame) : base(frame)
+ {
+ }
+
+ public override CGSize Measure()
+ {
+ var measure = VisualElementRenderer.Element.Measure(ConstrainedDimension,
+ double.PositiveInfinity, MeasureFlags.IncludeMargins);
+
+ var height = VisualElementRenderer.Element.Height > 0
+ ? VisualElementRenderer.Element.Height : measure.Request.Height;
+
+ return new CGSize(ConstrainedDimension, height);
+ }
+
+ public override void ConstrainTo(CGSize constraint)
+ {
+ ConstrainedDimension = constraint.Width;
+ Layout(constraint);
+ }
+ }
+}
\ No newline at end of file
<Compile Include="CollectionView\CarouselViewRenderer.cs" />
<Compile Include="CollectionView\CollectionViewRenderer.cs" />
<Compile Include="CollectionView\EmptySource.cs" />
+ <Compile Include="CollectionView\GroupableItemsViewController.cs" />
+ <Compile Include="CollectionView\GroupableItemsViewRenderer.cs" />
+ <Compile Include="CollectionView\HorizontalDefaultSupplementalView.cs" />
+ <Compile Include="CollectionView\HorizontalTemplatedHeaderView.cs" />
<Compile Include="CollectionView\IItemsViewSource.cs" />
<Compile Include="CollectionView\ItemsSourceFactory.cs" />
<Compile Include="CollectionView\ItemsViewCell.cs" />
<Compile Include="CollectionView\DefaultCell.cs" />
<Compile Include="CollectionView\HorizontalDefaultCell.cs" />
<Compile Include="CollectionView\ItemsViewController.cs" />
+ <Compile Include="CollectionView\ObservableGroupedSource.cs" />
<Compile Include="CollectionView\ScrollToPositionExtensions.cs" />
<Compile Include="CollectionView\SelectableItemsViewController.cs" />
<Compile Include="CollectionView\SelectableItemsViewRenderer.cs" />
<Compile Include="CollectionView\SnapHelpers.cs" />
<Compile Include="CollectionView\TemplatedCell.cs" />
<Compile Include="CollectionView\HorizontalTemplatedCell.cs" />
+ <Compile Include="CollectionView\VerticalDefaultSupplementalView.cs" />
<Compile Include="CollectionView\VerticalTemplatedCell.cs" />
+ <Compile Include="CollectionView\VerticalTemplatedHeaderView.cs" />
<Compile Include="DisposeHelpers.cs" />
<Compile Include="EffectUtilities.cs" />
<Compile Include="ExportCellAttribute.cs" />