Implement CollectionView grouping on Android (#7199)

* Move all the header/footer adjustment to IItemsViewSource
fixes #7121
fixes #7102
partially implements #3172
fixes #7243

* Fix selection bugs introduced by header/footer on Android

* Implement grouping for CollectionView on Android

* Enable grouping tests for Android

* Naming and comment cleanup

* Update Xamarin.Forms.Platform.Android/CollectionView/ListSource.cs

Co-Authored-By: Gerald Versluis <gerald.versluis@microsoft.com>

* Update Xamarin.Forms.Platform.Android/CollectionView/ObservableGroupedSource.cs
This commit is contained in:
E.Z. Hart 2019-08-27 19:29:07 -06:00 коммит произвёл Samantha Houts
Родитель 4e10c0b8f3
Коммит c0a681e852
46 изменённых файлов: 1206 добавлений и 178 удалений

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

@ -29,7 +29,7 @@ namespace Xamarin.Forms.Controls.Issues
#endif
}
#if UITEST && __IOS__ // Grouping is not implemented on Android yet
#if UITEST
[Test]
public void RemoveSelectedItem()
{
@ -88,8 +88,6 @@ namespace Xamarin.Forms.Controls.Issues
RunningApp.WaitForElement("MoveGroup");
RunningApp.Tap("MoveGroup");
}
#endif
}
}

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

@ -0,0 +1,50 @@
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.Github, 7102, "[Bug] CollectionView Header cause delay to adding items.",
PlatformAffected.Android)]
public class Issue7102 : TestNavigationPage
{
protected override void Init()
{
#if APP
FlagTestHelpers.SetCollectionViewTestFlag();
PushAsync(new GalleryPages.CollectionViewGalleries.ObservableCodeCollectionViewGallery(grid: false));
#endif
}
#if UITEST
[Test]
public void HeaderDoesNotBreakIndexes()
{
RunningApp.WaitForElement("entryInsert");
RunningApp.Tap("entryInsert");
RunningApp.ClearText();
RunningApp.EnterText("1");
RunningApp.Tap("Insert");
// If the bug is still present, then there will be
// two "Item: 0" items instead of the newly inserted item
// Or the header will have disappeared
RunningApp.WaitForElement("Inserted");
RunningApp.WaitForElement("This is the header");
}
#endif
}
}

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

@ -34,6 +34,7 @@
<Compile Include="$(MSBuildThisFileDirectory)NestedCollectionViews.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ShellGestures.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ShellBackButtonBehavior.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue7102.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ShellInsets.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CollectionViewGrouping.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue5412.cs" />

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

@ -17,10 +17,10 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
HorizontalOptions = LayoutOptions.Fill
};
var button = new Button { Text = buttonText, AutomationId = $"btn{buttonText}" };
var label = new Label { Text = LabelText, VerticalTextAlignment = TextAlignment.Center };
var button = new Button { Text = buttonText, AutomationId = $"btn{buttonText}", HeightRequest = 20, FontSize = 10 };
var label = new Label { Text = LabelText, VerticalTextAlignment = TextAlignment.Center, FontSize = 10 };
Entry = new Entry { Keyboard = Keyboard.Numeric, Text = InitialEntryText, WidthRequest = 100, AutomationId = $"entry{buttonText}" };
Entry = new Entry { Keyboard = Keyboard.Numeric, Text = InitialEntryText, WidthRequest = 100, FontSize = 10, AutomationId = $"entry{buttonText}" };
layout.Children.Add(label);
layout.Children.Add(Entry);

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

@ -3,7 +3,7 @@
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 x:Name="CollectionView" IsGrouped="True" Header="This is a header" Footer="Hey, a footer.">
<CollectionView.ItemTemplate>
<DataTemplate>

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

@ -11,12 +11,12 @@ using Xamarin.Forms.Xaml;
namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
{
[XamlCompilation(XamlCompilationOptions.Compile)]
[Preserve (AllMembers = true)]
[Preserve(AllMembers = true)]
public partial class BasicGrouping : ContentPage
{
public BasicGrouping ()
public BasicGrouping()
{
InitializeComponent ();
InitializeComponent();
CollectionView.ItemsSource = new SuperTeams();
}

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

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.GridGrouping">
<ContentPage.Content>
<CollectionView x:Name="CollectionView" IsGrouped="True" Header="This is a header" Footer="This is a footer.">
<CollectionView.ItemsLayout>
<GridItemsLayout Span="2" Orientation="Vertical"></GridItemsLayout>
</CollectionView.ItemsLayout>
<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>

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

@ -0,0 +1,14 @@
using Xamarin.Forms.Xaml;
namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class GridGrouping : ContentPage
{
public GridGrouping()
{
InitializeComponent();
CollectionView.ItemsSource = new SuperTeams();
}
}
}

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

@ -32,6 +32,8 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGa
new SwitchGrouping(), Navigation),
GalleryBuilder.NavButton("Grouping, Observable", () =>
new ObservableGrouping(), Navigation),
GalleryBuilder.NavButton("Grouping, Grid", () =>
new GridGrouping(), Navigation),
}
}
};

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

@ -10,7 +10,7 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGa
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.HeightRequestProperty, Value = 30 });
buttonStyle.Setters.Add(new Setter() { Property = Button.FontSizeProperty, Value = 10 });
var layout = new Grid
@ -34,6 +34,8 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGa
var collectionView = new CollectionView
{
Header = "This is a header",
Footer = "Hey, I'm a footer. Look at me!",
ItemTemplate = ItemTemplate(),
GroupFooterTemplate = GroupFooterTemplate(),
GroupHeaderTemplate = GroupHeaderTemplate(),
@ -102,7 +104,15 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGa
Style = buttonStyle };
groupRemover.Clicked += (obj, args) => {
itemsSource?.Remove(itemsSource[0]);
groupRemover.Text = $"Remove {itemsSource[0].Name}";
if (itemsSource.Count > 0)
{
groupRemover.Text = $"Remove {itemsSource[0].Name}";
}
else
{
groupRemover.Text = "";
groupRemover.IsEnabled = false;
}
mover.Text = $"Move Selected To {itemsSource[0].Name}";
};

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

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.HeaderFooterGalleries.FooterOnlyString">
<ContentPage.Content>
<CollectionView x:Name="CollectionView" Footer="This is a footer">
</CollectionView>
</ContentPage.Content>
</ContentPage>

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

@ -0,0 +1,25 @@
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.HeaderFooterGalleries
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class FooterOnlyString : ContentPage
{
readonly DemoFilteredItemSource _demoFilteredItemSource = new DemoFilteredItemSource(20);
public FooterOnlyString()
{
InitializeComponent();
CollectionView.ItemTemplate = ExampleTemplates.PhotoTemplate();
CollectionView.ItemsSource = _demoFilteredItemSource.Items;
}
}
}

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

@ -20,6 +20,7 @@
GalleryBuilder.NavButton("Header/Footer (Forms View)", () => new HeaderFooterView(), Navigation),
GalleryBuilder.NavButton("Header/Footer (Template)", () => new HeaderFooterTemplate(), Navigation),
GalleryBuilder.NavButton("Header/Footer (Grid)", () => new HeaderFooterGrid(), Navigation),
GalleryBuilder.NavButton("Footer Only (String)", () => new FooterOnlyString(), Navigation),
}
}
};

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

@ -25,7 +25,8 @@
var itemTemplate = ExampleTemplates.PhotoTemplate();
var collectionView = new CollectionView {ItemsLayout = itemsLayout, ItemTemplate = itemTemplate, AutomationId = "collectionview" };
var collectionView = new CollectionView {ItemsLayout = itemsLayout, ItemTemplate = itemTemplate,
AutomationId = "collectionview", Header = "This is the header" };
var generator = new ItemsSourceGenerator(collectionView, initialItems, ItemsSourceType.ObservableCollection);

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

@ -17,7 +17,7 @@
<Button AutomationId="DirectUpdate" HeightRequest="35" FontSize="10" Text="Clear CV selection and add Items 0 and 3" Clicked="DirectUpdateClicked" />
<CollectionView x:Name="CollectionView" ItemsSource="{Binding Items}"
<CollectionView x:Name="CollectionView" ItemsSource="{Binding Items}" Header="This is the header"
SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}">
<CollectionView.ItemTemplate>
<DataTemplate>

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

@ -15,7 +15,7 @@
<Label x:Name="SelectedItemsCommand" Grid.Row="2"/>
<CollectionView x:Name="CollectionView" Grid.Row="3" />
<CollectionView x:Name="CollectionView" Grid.Row="3" Header="This is the header" />
</Grid>
</ContentPage.Content>

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

@ -7,7 +7,7 @@
<Label Text="The CollectionView below should have several items already selected."/>
<CollectionView x:Name="CollectionView" Grid.Row="3">
<CollectionView x:Name="CollectionView" Grid.Row="3" Header="This is the header">
<CollectionView.ItemsLayout>
<GridItemsLayout Span="4" Orientation="Vertical"></GridItemsLayout>
</CollectionView.ItemsLayout>

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

@ -10,7 +10,7 @@
<Label x:Name="Result" Text="Pending..."></Label>
<CollectionView ItemsSource="{Binding Items}"
<CollectionView ItemsSource="{Binding Items}" Header="This is the header"
SelectionMode="Single"
SelectionChangedCommandParameter="{Binding SelectedItem,Source={x:Reference MyCollectionView}}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"

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

@ -41,7 +41,7 @@
<Label x:Name="SelectedItemsCommand" Grid.Row="3" HeightRequest="50" Margin="2" />
<CollectionView x:Name="CollectionView" Grid.Row="4">
<CollectionView x:Name="CollectionView" Grid.Row="4" Header="This is the header">
<CollectionView.ItemsLayout>
<GridItemsLayout Span="3" Orientation="Vertical"></GridItemsLayout>
</CollectionView.ItemsLayout>

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

@ -50,11 +50,17 @@
<EmbeddedResource Update="GalleryPages\BindableLayoutGalleryPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\GridGrouping.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="GalleryPages\CollectionViewGalleries\HeaderFooterGalleries\FooterOnlyString.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="GalleryPages\CharacterSpacingGallery.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="GalleryPages\CollectionViewGalleries\EmptyViewGalleries\EmptyViewSwapGallery.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="GalleryPages\CollectionViewGalleries\NestedGalleries\NestedCollectionViewGallery.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>

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

@ -0,0 +1,55 @@
using Android.Support.V7.Widget;
namespace Xamarin.Forms.Platform.Android
{
// Passes change notifications directly through to a RecyclerView.Adapter
internal class AdapterNotifier : ICollectionChangedNotifier
{
readonly RecyclerView.Adapter _adapter;
public AdapterNotifier(RecyclerView.Adapter adapter)
{
_adapter = adapter;
}
public void NotifyDataSetChanged()
{
_adapter.NotifyDataSetChanged();
}
public void NotifyItemChanged(IItemsViewSource source, int startIndex)
{
_adapter.NotifyItemChanged(startIndex);
}
public void NotifyItemInserted(IItemsViewSource source, int startIndex)
{
_adapter.NotifyItemInserted(startIndex);
}
public void NotifyItemMoved(IItemsViewSource source, int fromPosition, int toPosition)
{
_adapter.NotifyItemMoved(fromPosition, toPosition);
}
public void NotifyItemRangeChanged(IItemsViewSource source, int start, int end)
{
_adapter.NotifyItemRangeChanged(start, end);
}
public void NotifyItemRangeInserted(IItemsViewSource source, int startIndex, int count)
{
_adapter.NotifyItemRangeInserted(startIndex, count);
}
public void NotifyItemRangeRemoved(IItemsViewSource source, int startIndex, int count)
{
_adapter.NotifyItemRangeRemoved(startIndex, count);
}
public void NotifyItemRemoved(IItemsViewSource source, int startIndex)
{
_adapter.NotifyItemRemoved(startIndex);
}
}
}

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

@ -4,7 +4,7 @@ using Android.Content;
namespace Xamarin.Forms.Platform.Android
{
public class CarouselViewRenderer : ItemsViewRenderer
public class CarouselViewRenderer : ItemsViewRenderer<ItemsView, ItemsViewAdapter<ItemsView, IItemsViewSource>, IItemsViewSource>
{
// TODO hartez 2018/08/29 17:13:17 Does this need to override SelectLayout so it ignores grids? (Yes, and so it can warn on unknown layouts)
@ -23,7 +23,7 @@ namespace Xamarin.Forms.Platform.Android
// But for the Carousel, we want it to create the items to fit the width/height of the viewport
// So we give it an alternate delegate for creating the views
ItemsViewAdapter = new ItemsViewAdapter(ItemsView,
ItemsViewAdapter = new ItemsViewAdapter<ItemsView, IItemsViewSource>(ItemsView,
(view, context) => new SizedItemContentView(context, () => Width, () => Height));
SwapAdapter(ItemsViewAdapter, false);

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

@ -1,9 +1,8 @@
using Android.Content;
using Android.Graphics;
namespace Xamarin.Forms.Platform.Android
{
public class CollectionViewRenderer : SelectableItemsViewRenderer
public class CollectionViewRenderer : GroupableItemsViewRenderer<GroupableItemsView, GroupableItemsViewAdapter<GroupableItemsView, IGroupableItemsViewSource>, IGroupableItemsViewSource>
{
public CollectionViewRenderer(Context context) : base(context)
{

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

@ -6,11 +6,42 @@ namespace Xamarin.Forms.Platform.Android
{
public int Count => 0;
public object this[int index] => throw new IndexOutOfRangeException("IItemsViewSource is empty");
public bool HasHeader { get; set; }
public bool HasFooter { get; set; }
public void Dispose()
{
}
public bool IsHeader(int index)
{
return HasHeader && index == 0;
}
public bool IsFooter(int index)
{
if (!HasFooter)
{
return false;
}
if (HasHeader)
{
return index == 1;
}
return index == 0;
}
public int GetPosition(object item)
{
return -1;
}
public object GetItem(int position)
{
throw new IndexOutOfRangeException("IItemsViewSource is empty");
}
}
}

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

@ -101,7 +101,7 @@ namespace Xamarin.Forms.Platform.Android
}
var itemContentView = new SizedItemContentView(parent.Context, () => parent.Width, () => parent.Height);
return new TemplatedItemViewHolder(itemContentView, template);
return new TemplatedItemViewHolder(itemContentView, template, isSelectionEnabled: false);
}
public override int GetItemViewType(int position)

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

@ -17,7 +17,8 @@ namespace Xamarin.Forms.Platform.Android
{
var itemViewType = _recyclerView.GetAdapter().GetItemViewType(position);
if (itemViewType == ItemViewType.Header || itemViewType == ItemViewType.Footer)
if (itemViewType == ItemViewType.Header || itemViewType == ItemViewType.Footer
|| itemViewType == ItemViewType.GroupHeader || itemViewType == ItemViewType.GroupFooter)
{
return _gridItemsLayout.Span;
}

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

@ -0,0 +1,67 @@
using System;
using Android.Content;
using Android.Support.V7.Widget;
using Android.Views;
namespace Xamarin.Forms.Platform.Android
{
public class GroupableItemsViewAdapter<TItemsView, TItemsViewSource> : SelectableItemsViewAdapter<TItemsView, TItemsViewSource>
where TItemsView : GroupableItemsView
where TItemsViewSource : IGroupableItemsViewSource
{
internal GroupableItemsViewAdapter(TItemsView groupableItemsView,
Func<View, Context, ItemContentView> createView = null) : base(groupableItemsView, createView)
{
}
protected override TItemsViewSource CreateItemsSource()
{
return (TItemsViewSource)ItemsSourceFactory.Create(ItemsView, this);
}
public override int GetItemViewType(int position)
{
if (ItemsSource.IsGroupHeader(position))
{
return ItemViewType.GroupHeader;
}
if (ItemsSource.IsGroupFooter(position))
{
return ItemViewType.GroupFooter;
}
return base.GetItemViewType(position);
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
var context = parent.Context;
if (viewType == ItemViewType.GroupHeader)
{
var itemContentView = new ItemContentView(context);
return new TemplatedItemViewHolder(itemContentView, ItemsView.GroupHeaderTemplate, isSelectionEnabled: false);
}
if (viewType == ItemViewType.GroupFooter)
{
var itemContentView = new ItemContentView(context);
return new TemplatedItemViewHolder(itemContentView, ItemsView.GroupFooterTemplate, isSelectionEnabled: false);
}
return base.OnCreateViewHolder(parent, viewType);
}
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
if (holder is TemplatedItemViewHolder templatedItemViewHolder &&
(ItemsSource.IsGroupFooter(position) || ItemsSource.IsGroupHeader(position)))
{
BindTemplatedItemViewHolder(templatedItemViewHolder, ItemsSource.GetItem(position));
}
base.OnBindViewHolder(holder, position);
}
}
}

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

@ -0,0 +1,32 @@
using System;
using System.ComponentModel;
using Android.Content;
namespace Xamarin.Forms.Platform.Android
{
public class GroupableItemsViewRenderer<TItemsView, TAdapter, TItemsViewSource> : SelectableItemsViewRenderer<TItemsView, TAdapter, TItemsViewSource>
where TItemsView : GroupableItemsView
where TAdapter : GroupableItemsViewAdapter<TItemsView, TItemsViewSource>
where TItemsViewSource : IGroupableItemsViewSource
{
public GroupableItemsViewRenderer(Context context) : base(context)
{
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs changedProperty)
{
base.OnElementPropertyChanged(sender, changedProperty);
if (changedProperty.IsOneOf(GroupableItemsView.IsGroupedProperty,
GroupableItemsView.GroupFooterTemplateProperty, GroupableItemsView.GroupHeaderTemplateProperty))
{
UpdateItemsSource();
}
}
protected override TAdapter CreateAdapter()
{
return (TAdapter)new GroupableItemsViewAdapter<TItemsView, TItemsViewSource>(ItemsView);
}
}
}

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

@ -0,0 +1,15 @@
namespace Xamarin.Forms.Platform.Android
{
// Lets observable items sources notify observers about dataset changes
internal interface ICollectionChangedNotifier
{
void NotifyDataSetChanged();
void NotifyItemChanged(IItemsViewSource source, int startIndex);
void NotifyItemInserted(IItemsViewSource source, int startIndex);
void NotifyItemMoved(IItemsViewSource source, int fromPosition, int toPosition);
void NotifyItemRangeChanged(IItemsViewSource source, int start, int end);
void NotifyItemRangeInserted(IItemsViewSource source, int startIndex, int count);
void NotifyItemRangeRemoved(IItemsViewSource source, int startIndex, int count);
void NotifyItemRemoved(IItemsViewSource source, int startIndex);
}
}

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

@ -2,9 +2,23 @@ using System;
namespace Xamarin.Forms.Platform.Android
{
internal interface IItemsViewSource : IDisposable
public interface IItemsViewSource : IDisposable
{
int Count { get; }
object this[int index] { get; }
int GetPosition(object item);
object GetItem(int position);
bool HasHeader { get; set; }
bool HasFooter { get; set; }
bool IsHeader(int position);
bool IsFooter(int position);
}
public interface IGroupableItemsViewSource : IItemsViewSource
{
bool IsGroupHeader(int position);
bool IsGroupFooter(int position);
}
}

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

@ -4,7 +4,7 @@ using Android.Views;
namespace Xamarin.Forms.Platform.Android
{
internal class ItemContentView : ViewGroup
public class ItemContentView : ViewGroup
{
protected IVisualElementRenderer Content;
Size? _size;

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

@ -6,5 +6,7 @@
public const int TemplatedItem = 42;
public const int Header = 43;
public const int Footer = 44;
public const int GroupHeader = 45;
public const int GroupFooter = 46;
}
}

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

@ -7,7 +7,7 @@ namespace Xamarin.Forms.Platform.Android
{
internal static class ItemsSourceFactory
{
public static IItemsViewSource Create(IEnumerable itemsSource, RecyclerView.Adapter adapter)
public static IItemsViewSource Create(IEnumerable itemsSource, ICollectionChangedNotifier notifier)
{
if (itemsSource == null)
{
@ -16,14 +16,33 @@ namespace Xamarin.Forms.Platform.Android
switch (itemsSource)
{
// TODO hartez ObservableItemSource should be taking an INotifyCollectionChanged in its constructor
case IList _ when itemsSource is INotifyCollectionChanged:
return new ObservableItemsSource(itemsSource as IList, adapter);
return new ObservableItemsSource(itemsSource as IList, notifier);
case IEnumerable<object> generic:
return new ListSource(generic);
}
return new ListSource(itemsSource);
}
public static IItemsViewSource Create(IEnumerable itemsSource, RecyclerView.Adapter adapter)
{
return Create(itemsSource, new AdapterNotifier(adapter));
}
public static IItemsViewSource Create(ItemsView itemsView, RecyclerView.Adapter adapter)
{
return Create(itemsView.ItemsSource, adapter);
}
public static IGroupableItemsViewSource Create(GroupableItemsView itemsView, RecyclerView.Adapter adapter)
{
if (itemsView.IsGrouped)
{
return new ObservableGroupedSource(itemsView, new AdapterNotifier(adapter));
}
return new UngroupedItemsSource(Create(itemsView.ItemsSource, adapter));
}
}
}

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

@ -7,33 +7,34 @@ using ViewGroup = Android.Views.ViewGroup;
namespace Xamarin.Forms.Platform.Android
{
public class ItemsViewAdapter : RecyclerView.Adapter
public class ItemsViewAdapter<TItemsView, TItemsViewSource> : RecyclerView.Adapter
where TItemsView : ItemsView
where TItemsViewSource : IItemsViewSource
{
protected readonly ItemsView ItemsView;
protected readonly TItemsView ItemsView;
readonly Func<View, Context, ItemContentView> _createItemContentView;
internal readonly IItemsViewSource ItemsSource;
internal readonly TItemsViewSource ItemsSource;
bool _disposed;
Size? _size;
bool _usingItemTemplate = false;
int _headerOffset = 0;
bool _hasFooter;
internal ItemsViewAdapter(ItemsView itemsView, Func<View, Context, ItemContentView> createItemContentView = null)
internal ItemsViewAdapter(TItemsView itemsView, Func<View, Context, ItemContentView> createItemContentView = null)
{
Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewAdapter));
Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewAdapter<TItemsView, TItemsViewSource>));
ItemsView = itemsView ?? throw new ArgumentNullException(nameof(itemsView));
UpdateUsingItemTemplate();
UpdateHeaderOffset();
UpdateHasFooter();
ItemsView.PropertyChanged += ItemsViewPropertyChanged;
_createItemContentView = createItemContentView;
ItemsSource = ItemsSourceFactory.Create(itemsView.ItemsSource, this);
ItemsSource = CreateItemsSource();
UpdateHasHeader();
UpdateHasFooter();
if (_createItemContentView == null)
{
@ -41,21 +42,22 @@ namespace Xamarin.Forms.Platform.Android
}
}
private void ItemsViewPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs property)
protected virtual TItemsViewSource CreateItemsSource()
{
if (property.Is(ItemsView.HeaderProperty))
return (TItemsViewSource)ItemsSourceFactory.Create(ItemsView, this);
}
protected virtual void ItemsViewPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs property)
{
if (property.Is(Xamarin.Forms.ItemsView.HeaderProperty))
{
UpdateHeaderOffset();
UpdateHasHeader();
}
else if (property.Is(ItemsView.ItemTemplateProperty))
else if (property.Is(Xamarin.Forms.ItemsView.ItemTemplateProperty))
{
UpdateUsingItemTemplate();
}
else if (property.Is(ItemsView.ItemTemplateProperty))
{
UpdateUsingItemTemplate();
}
else if (property.Is(ItemsView.FooterProperty))
else if (property.Is(Xamarin.Forms.ItemsView.FooterProperty))
{
UpdateHasFooter();
}
@ -93,36 +95,17 @@ namespace Xamarin.Forms.Platform.Android
return;
}
var itemsSourcePosition = position - _headerOffset;
switch (holder)
{
case TextViewHolder textViewHolder:
textViewHolder.TextView.Text = ItemsSource[itemsSourcePosition].ToString();
textViewHolder.TextView.Text = ItemsSource.GetItem(position).ToString();
break;
case TemplatedItemViewHolder templatedItemViewHolder:
BindTemplatedItemViewHolder(templatedItemViewHolder, ItemsSource[itemsSourcePosition]);
BindTemplatedItemViewHolder(templatedItemViewHolder, ItemsSource.GetItem(position));
break;
}
}
void BindTemplatedItemViewHolder(TemplatedItemViewHolder templatedItemViewHolder, object context)
{
if (ItemsView.ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem)
{
templatedItemViewHolder.Bind(context, ItemsView, SetStaticSize, _size);
}
else
{
templatedItemViewHolder.Bind(context, ItemsView);
}
}
void SetStaticSize(Size size)
{
_size = size;
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
var context = parent.Context;
@ -147,7 +130,7 @@ namespace Xamarin.Forms.Platform.Android
return new TemplatedItemViewHolder(itemContentView, ItemsView.ItemTemplate);
}
public override int ItemCount => ItemsSource.Count + _headerOffset + (_hasFooter ? 1 : 0);
public override int ItemCount => ItemsSource.Count;
public override int GetItemViewType(int position)
{
@ -188,15 +171,24 @@ namespace Xamarin.Forms.Platform.Android
public virtual int GetPositionForItem(object item)
{
for (int n = 0; n < ItemsSource.Count; n++)
{
if (ItemsSource[n] == item)
{
return n + _headerOffset;
}
}
return ItemsSource.GetPosition(item);
}
return -1;
protected void BindTemplatedItemViewHolder(TemplatedItemViewHolder templatedItemViewHolder, object context)
{
if (ItemsView.ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem)
{
templatedItemViewHolder.Bind(context, ItemsView, SetStaticSize, _size);
}
else
{
templatedItemViewHolder.Bind(context, ItemsView);
}
}
void SetStaticSize(Size size)
{
_size = size;
}
void UpdateUsingItemTemplate()
@ -204,32 +196,32 @@ namespace Xamarin.Forms.Platform.Android
_usingItemTemplate = ItemsView.ItemTemplate != null;
}
void UpdateHeaderOffset()
void UpdateHasHeader()
{
_headerOffset = ItemsView.Header == null ? 0 : 1;
ItemsSource.HasHeader = ItemsView.Header != null;
}
void UpdateHasFooter()
{
_hasFooter = ItemsView.Footer != null;
ItemsSource.HasFooter = ItemsView.Footer != null;
}
bool IsHeader(int position)
{
return _headerOffset > 0 && position == 0;
return ItemsSource.IsHeader(position);
}
bool IsFooter(int position)
{
return _hasFooter && position > ItemsSource.Count;
return ItemsSource.IsFooter(position);
}
RecyclerView.ViewHolder CreateHeaderFooterViewHolder(object content, DataTemplate template, Context context)
protected RecyclerView.ViewHolder CreateHeaderFooterViewHolder(object content, DataTemplate template, Context context)
{
if (template != null)
{
var footerContentView = new ItemContentView(context);
return new TemplatedItemViewHolder(footerContentView, template);
return new TemplatedItemViewHolder(footerContentView, template, isSelectionEnabled: false);
}
if (content is View formsView)

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

@ -11,22 +11,25 @@ using AViewCompat = Android.Support.V4.View.ViewCompat;
namespace Xamarin.Forms.Platform.Android
{
public class ItemsViewRenderer : RecyclerView, IVisualElementRenderer, IEffectControlProvider
public class ItemsViewRenderer<TItemsView, TAdapter, TItemsViewSource> : RecyclerView, IVisualElementRenderer, IEffectControlProvider
where TItemsView : ItemsView
where TAdapter : ItemsViewAdapter<TItemsView, TItemsViewSource>
where TItemsViewSource : IItemsViewSource
{
readonly AutomationPropertiesProvider _automationPropertiesProvider;
readonly EffectControlProvider _effectControlProvider;
protected ItemsViewAdapter ItemsViewAdapter;
protected TAdapter ItemsViewAdapter;
int? _defaultLabelFor;
bool _disposed;
protected ItemsView ItemsView;
protected TItemsView ItemsView;
IItemsLayout _layout;
SnapManager _snapManager;
ScrollHelper _scrollHelper;
RecyclerViewScrollListener _recyclerViewScrollListener;
RecyclerViewScrollListener<TItemsView, TItemsViewSource> _recyclerViewScrollListener;
EmptyViewAdapter _emptyViewAdapter;
readonly DataChangeObserver _emptyCollectionObserver;
@ -39,7 +42,7 @@ namespace Xamarin.Forms.Platform.Android
public ItemsViewRenderer(Context context) : base(new ContextThemeWrapper(context, Resource.Style.collectionViewStyle))
{
Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewRenderer));
Xamarin.Forms.CollectionView.VerifyCollectionViewFlagEnabled(nameof(ItemsViewRenderer<TItemsView, TAdapter, TItemsViewSource>));
_automationPropertiesProvider = new AutomationPropertiesProvider(this);
_effectControlProvider = new EffectControlProvider(this);
@ -97,7 +100,7 @@ namespace Xamarin.Forms.Platform.Android
}
var oldElement = ItemsView;
var newElement = (ItemsView)element;
var newElement = (TItemsView)element;
TearDownOldElement(oldElement);
SetUpNewElement(newElement);
@ -203,7 +206,7 @@ namespace Xamarin.Forms.Platform.Android
// TODO hartez 2018/10/24 10:41:55 If the ItemTemplate changes from set to null, we need to make sure to clear the recyclerview pool
if (changedProperty.Is(ItemsView.ItemsSourceProperty))
if (changedProperty.Is(Xamarin.Forms.ItemsView.ItemsSourceProperty))
{
UpdateItemsSource();
}
@ -215,23 +218,24 @@ namespace Xamarin.Forms.Platform.Android
{
UpdateFlowDirection();
}
else if (changedProperty.IsOneOf(ItemsView.EmptyViewProperty, ItemsView.EmptyViewTemplateProperty))
else if (changedProperty.IsOneOf(Xamarin.Forms.ItemsView.EmptyViewProperty,
Xamarin.Forms.ItemsView.EmptyViewTemplateProperty))
{
UpdateEmptyView();
}
else if (changedProperty.Is(ItemsView.ItemSizingStrategyProperty))
else if (changedProperty.Is(Xamarin.Forms.ItemsView.ItemSizingStrategyProperty))
{
UpdateAdapter();
}
else if (changedProperty.Is(ItemsView.HorizontalScrollBarVisibilityProperty))
else if (changedProperty.Is(Xamarin.Forms.ItemsView.HorizontalScrollBarVisibilityProperty))
{
UpdateHorizontalScrollBarVisibility();
}
else if (changedProperty.Is(ItemsView.VerticalScrollBarVisibilityProperty))
else if (changedProperty.Is(Xamarin.Forms.ItemsView.VerticalScrollBarVisibilityProperty))
{
UpdateVerticalScrollBarVisibility();
}
else if (changedProperty.Is(ItemsView.ItemsUpdatingScrollModeProperty))
else if (changedProperty.Is(Xamarin.Forms.ItemsView.ItemsUpdatingScrollModeProperty))
{
UpdateItemsUpdatingScrollMode();
}
@ -257,9 +261,9 @@ namespace Xamarin.Forms.Platform.Android
UpdateEmptyView();
}
protected virtual ItemsViewAdapter CreateAdapter()
protected virtual TAdapter CreateAdapter()
{
return new ItemsViewAdapter(ItemsView);
return (TAdapter)new ItemsViewAdapter<TItemsView, TItemsViewSource>(ItemsView);
}
void UpdateAdapter()
@ -274,7 +278,7 @@ namespace Xamarin.Forms.Platform.Android
oldItemViewAdapter?.Dispose();
}
protected virtual void SetUpNewElement(ItemsView newElement)
protected virtual void SetUpNewElement(TItemsView newElement)
{
if (newElement == null)
{
@ -316,7 +320,7 @@ namespace Xamarin.Forms.Platform.Android
// Listen for ScrollTo requests
ItemsView.ScrollToRequested += ScrollToRequested;
_recyclerViewScrollListener = new RecyclerViewScrollListener(ItemsView, ItemsViewAdapter);
_recyclerViewScrollListener = new RecyclerViewScrollListener<TItemsView, TItemsViewSource>(ItemsView, ItemsViewAdapter);
AddOnScrollListener(_recyclerViewScrollListener);
}

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

@ -1,30 +1,130 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Xamarin.Forms.Platform.Android
{
sealed class ListSource : List<object>, IItemsViewSource
sealed class ListSource : IItemsViewSource, IList
{
IList _itemsSource;
public ListSource()
{
}
public ListSource(IEnumerable<object> enumerable) : base(enumerable)
public ListSource(IEnumerable<object> enumerable)
{
_itemsSource = new List<object>(enumerable);
}
public ListSource(IEnumerable enumerable)
{
foreach (object item in enumerable)
{
Add(item);
_itemsSource.Add(item);
}
}
public int Count => _itemsSource.Count + (HasHeader ? 1 : 0) + (HasFooter ? 1 : 0);
public bool HasHeader { get; set; }
public bool HasFooter { get; set; }
public bool IsReadOnly => _itemsSource.IsReadOnly;
public bool IsFixedSize => _itemsSource.IsFixedSize;
public object SyncRoot => _itemsSource.SyncRoot;
public bool IsSynchronized => _itemsSource.IsSynchronized;
object IList.this[int index] { get => _itemsSource[index]; set => _itemsSource[index] = value; }
public void Dispose()
{
}
public bool IsFooter(int index)
{
return HasFooter && index == Count - 1;
}
public bool IsHeader(int index)
{
return HasHeader && index == 0;
}
public int GetPosition(object item)
{
for (int n = 0; n < _itemsSource.Count; n++)
{
if (_itemsSource[n] == item)
{
return AdjustPosition(n);
}
}
return -1;
}
public object GetItem(int position)
{
return _itemsSource[AdjustIndexRequest(position)];
}
int AdjustIndexRequest(int index)
{
return index - (HasHeader ? 1 : 0);
}
int AdjustPosition(int index)
{
return index + (HasHeader ? 1 : 0);
}
public int Add(object value)
{
return _itemsSource.Add(value);
}
public bool Contains(object value)
{
return _itemsSource.Contains(value);
}
public void Clear()
{
_itemsSource.Clear();
}
public int IndexOf(object value)
{
return _itemsSource.IndexOf(value);
}
public void Insert(int index, object value)
{
_itemsSource.Insert(index, value);
}
public void Remove(object value)
{
_itemsSource.Remove(value);
}
public void RemoveAt(int index)
{
_itemsSource.RemoveAt(index);
}
public void CopyTo(Array array, int index)
{
_itemsSource.CopyTo(array, index);
}
public IEnumerator GetEnumerator()
{
return _itemsSource.GetEnumerator();
}
}
}

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

@ -0,0 +1,429 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace Xamarin.Forms.Platform.Android
{
internal class ObservableGroupedSource : IGroupableItemsViewSource, ICollectionChangedNotifier
{
readonly ICollectionChangedNotifier _notifier;
readonly IList _groupSource;
List<IItemsViewSource> _groups = new List<IItemsViewSource>();
bool _disposed;
bool _hasGroupHeaders;
bool _hasGroupFooters;
public int Count
{
get
{
var groupContents = 0;
for (int n = 0; n < _groups.Count; n++)
{
groupContents += _groups[n].Count;
}
return (HasHeader ? 1 : 0)
+ (HasFooter ? 1 : 0)
+ groupContents;
}
}
public bool HasHeader { get; set; }
public bool HasFooter { get; set; }
public ObservableGroupedSource(GroupableItemsView groupableItemsView, ICollectionChangedNotifier adapter)
{
var groupSource = groupableItemsView.ItemsSource;
_notifier = adapter;
_groupSource = groupSource as IList ?? new ListSource(groupSource);
if (_groupSource is INotifyCollectionChanged incc)
{
incc.CollectionChanged += CollectionChanged;
}
_hasGroupFooters = groupableItemsView.GroupFooterTemplate != null;
_hasGroupHeaders = groupableItemsView.GroupHeaderTemplate != null;
UpdateGroupTracking();
}
public void Dispose()
{
Dispose(true);
}
public bool IsFooter(int position)
{
if (!HasFooter)
{
return false;
}
return position == Count - 1;
}
public bool IsHeader(int position)
{
return HasHeader && position == 0;
}
public bool IsGroupHeader(int position)
{
if (IsFooter(position) || IsHeader(position))
{
return false;
}
var (group, inGroup) = GetGroupAndIndex(position);
return _groups[group].IsHeader(inGroup);
}
public bool IsGroupFooter(int position)
{
if (IsFooter(position) || IsHeader(position))
{
return false;
}
var (group, inGroup) = GetGroupAndIndex(position);
return _groups[group].IsFooter(inGroup);
}
public int GetPosition(object item)
{
int previousGroupsOffset = 0;
for (int groupIndex = 0; groupIndex < _groupSource.Count; groupIndex++)
{
if (_groupSource[groupIndex] == item)
{
return AdjustPositionForHeader(groupIndex);
}
var group = _groups[groupIndex];
var inGroup = group.GetPosition(item);
if (inGroup > -1)
{
return AdjustPositionForHeader(previousGroupsOffset + inGroup);
}
previousGroupsOffset += group.Count;
}
return -1;
}
public object GetItem(int position)
{
var (group, inGroup) = GetGroupAndIndex(position);
if (IsGroupFooter(position) || IsGroupHeader(position))
{
// This is looping to find the group/index twice, need to make it less inefficient
return _groupSource[group];
}
return _groups[group].GetItem(inGroup);
}
// The ICollectionChangedNotifier methods are called by child observable items sources (i.e., the groups)
// This class can then translate their local changes into global positions for upstream notification
// (e.g., to the actual RecyclerView.Adapter, so that it can notify the RecyclerView and handle animating
// the changes)
public void NotifyDataSetChanged()
{
Reload();
}
public void NotifyItemChanged(IItemsViewSource group, int localIndex)
{
localIndex = GetAbsolutePosition(group, localIndex);
_notifier.NotifyItemChanged(this, localIndex);
}
public void NotifyItemInserted(IItemsViewSource group, int localIndex)
{
localIndex = GetAbsolutePosition(group, localIndex);
_notifier.NotifyItemInserted(this, localIndex);
}
public void NotifyItemMoved(IItemsViewSource group, int localFromIndex, int localToIndex)
{
localFromIndex = GetAbsolutePosition(group, localFromIndex);
localToIndex = GetAbsolutePosition(group, localToIndex);
_notifier.NotifyItemMoved(this, localFromIndex, localToIndex);
}
public void NotifyItemRangeChanged(IItemsViewSource group, int localStartIndex, int localEndIndex)
{
localStartIndex = GetAbsolutePosition(group, localStartIndex);
localEndIndex = GetAbsolutePosition(group, localEndIndex);
_notifier.NotifyItemRangeChanged(this, localStartIndex, localEndIndex);
}
public void NotifyItemRangeInserted(IItemsViewSource group, int localIndex, int count)
{
localIndex = GetAbsolutePosition(group, localIndex);
_notifier.NotifyItemRangeInserted(this, localIndex, count);
}
public void NotifyItemRangeRemoved(IItemsViewSource group, int localIndex, int count)
{
localIndex = GetAbsolutePosition(group, localIndex);
_notifier.NotifyItemRangeRemoved(this, localIndex, count);
}
public void NotifyItemRemoved(IItemsViewSource group, int localIndex)
{
localIndex = GetAbsolutePosition(group, localIndex);
_notifier.NotifyItemRemoved(this, localIndex);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
ClearGroupTracking();
if(_groupSource is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged -= CollectionChanged;
}
}
}
void UpdateGroupTracking()
{
ClearGroupTracking();
for (int n = 0; n < _groupSource.Count; n++)
{
var source = ItemsSourceFactory.Create(_groupSource[n] as IEnumerable, this);
source.HasFooter = _hasGroupFooters;
source.HasHeader = _hasGroupHeaders;
_groups.Add(source);
}
}
void ClearGroupTracking()
{
for (int n = _groups.Count - 1; n >= 0; n--)
{
_groups[n].Dispose();
_groups.RemoveAt(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()
{
UpdateGroupTracking();
_notifier.NotifyDataSetChanged();
}
void Add(NotifyCollectionChangedEventArgs args)
{
var groupIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
var groupCount = args.NewItems.Count;
UpdateGroupTracking();
// Determine the absolute starting position and the number of items in the groups being added
var absolutePosition = GetAbsolutePosition(_groups[groupIndex], 0);
var itemCount = CountItemsInGroups(groupIndex, groupCount);
if (itemCount == 1)
{
_notifier.NotifyItemInserted(this, absolutePosition);
return;
}
_notifier.NotifyItemRangeInserted(this, absolutePosition, itemCount);
}
void Remove(NotifyCollectionChangedEventArgs args)
{
var groupIndex = args.OldStartingIndex;
if (groupIndex < 0)
{
// INCC implementation isn't giving us enough information to know where the removed groups was in the
// collection. So the best we can do is a full reload.
Reload();
return;
}
// If we have a start index, we can be more clever about removing the group(s) (and get the nifty animations)
var groupCount = args.OldItems.Count;
var absolutePosition = GetAbsolutePosition(_groups[groupIndex], 0);
// Figure out how many items are in the groups we're removing
var itemCount = CountItemsInGroups(groupIndex, groupCount);
if (itemCount == 1)
{
_notifier.NotifyItemRemoved(this, absolutePosition);
UpdateGroupTracking();
return;
}
_notifier.NotifyItemRangeRemoved(this, absolutePosition, itemCount);
UpdateGroupTracking();
}
void Replace(NotifyCollectionChangedEventArgs args)
{
var groupCount = args.NewItems.Count;
if (groupCount != args.OldItems.Count)
{
// The original and replacement sets are of unequal size; this means that most everything currently in
// view will have to be updated. So just reload the whole thing.
Reload();
return;
}
var newStartIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
var oldStartIndex = args.OldStartingIndex > -1 ? args.OldStartingIndex : _groupSource.IndexOf(args.OldItems[0]);
var newItemCount = CountItemsInGroups(newStartIndex, groupCount);
var oldItemCount = CountItemsInGroups(oldStartIndex, groupCount);
if (newItemCount != oldItemCount)
{
// The original and replacement sets are of unequal size; this means that most everything currently in
// view will have to be updated. So just reload the whole thing.
Reload();
return;
}
// We are replacing one set of items with a set of equal size; we can do a simple item or range notification
var firstGroupIndex = Math.Min(newStartIndex, oldStartIndex);
var absolutePosition = GetAbsolutePosition(_groups[firstGroupIndex], 0);
if (newItemCount == 1)
{
_notifier.NotifyItemChanged(this, absolutePosition);
UpdateGroupTracking();
}
else
{
_notifier.NotifyItemRangeChanged(this, absolutePosition, newItemCount * 2);
UpdateGroupTracking();
}
}
void Move(NotifyCollectionChangedEventArgs args)
{
var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + args.NewItems.Count;
var itemCount = CountItemsInGroups(start, end - start);
var absolutePosition = GetAbsolutePosition(_groups[start], 0);
_notifier.NotifyItemRangeChanged(this, absolutePosition, itemCount);
UpdateGroupTracking();
}
int GetAbsolutePosition(IItemsViewSource group, int indexInGroup)
{
var groupIndex = _groups.IndexOf(group);
var runningIndex = 0;
for (int n = 0; n < groupIndex; n++)
{
runningIndex += _groups[n].Count;
}
return AdjustPositionForHeader(runningIndex + indexInGroup);
}
(int, int) GetGroupAndIndex(int absolutePosition)
{
absolutePosition = AdjustIndexForHeader(absolutePosition);
var group = 0;
var localIndex = 0;
while (absolutePosition > 0)
{
localIndex += 1;
if (localIndex == _groups[group].Count)
{
group += 1;
localIndex = 0;
}
absolutePosition -= 1;
}
return (group, localIndex);
}
int AdjustIndexForHeader(int index)
{
return index - (HasHeader ? 1 : 0);
}
int AdjustPositionForHeader(int position)
{
return position + (HasHeader ? 1 : 0);
}
int CountItemsInGroups(int groupStartIndex, int groupCount)
{
var itemCount = 0;
for (int n = 0; n < groupCount; n++)
{
itemCount += _groups[groupStartIndex + n].Count;
}
return itemCount;
}
}
}

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

@ -1,44 +1,84 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using Android.Support.V7.Widget;
namespace Xamarin.Forms.Platform.Android
{
internal class ObservableItemsSource : IItemsViewSource
{
readonly RecyclerView.Adapter _adapter;
readonly IList _itemsSource;
readonly ICollectionChangedNotifier _notifier;
bool _disposed;
public ObservableItemsSource(IList itemSource, RecyclerView.Adapter adapter)
public ObservableItemsSource(IList itemSource, ICollectionChangedNotifier notifier)
{
_itemsSource = itemSource;
_adapter = adapter;
_notifier = notifier;
_notifier = notifier;
((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged;
}
public int Count => _itemsSource.Count;
public int Count => _itemsSource.Count + (HasHeader ? 1 : 0) + (HasFooter ? 1 : 0);
public object this[int index] => _itemsSource[index];
public bool HasHeader { get; set; }
public bool HasFooter { get; set; }
public void Dispose()
{
Dispose(true);
}
public bool IsFooter(int index)
{
return HasFooter && index == Count - 1;
}
public bool IsHeader(int index)
{
return HasHeader && index == 0;
}
public int GetPosition(object item)
{
for (int n = 0; n < _itemsSource.Count; n++)
{
if (_itemsSource[n] == item)
{
return AdjustPositionForHeader(n);
}
}
return -1;
}
public object GetItem(int position)
{
return _itemsSource[AdjustIndexForHeader(position)];
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
if (_disposed)
{
if (disposing)
{
((INotifyCollectionChanged)_itemsSource).CollectionChanged -= CollectionChanged;
}
_disposed = true;
return;
}
_disposed = true;
if (disposing)
{
((INotifyCollectionChanged)_itemsSource).CollectionChanged -= CollectionChanged;
}
}
int AdjustIndexForHeader(int index)
{
return index - (HasHeader ? 1 : 0);
}
int AdjustPositionForHeader(int position)
{
return position + (HasHeader ? 1 : 0);
}
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
@ -58,7 +98,7 @@ namespace Xamarin.Forms.Platform.Android
Move(args);
break;
case NotifyCollectionChangedAction.Reset:
_adapter.NotifyDataSetChanged();
_notifier.NotifyDataSetChanged();
break;
default:
throw new ArgumentOutOfRangeException();
@ -72,27 +112,28 @@ namespace Xamarin.Forms.Platform.Android
if (count == 1)
{
// For a single item, we can use NotifyItemMoved and get the animation
_adapter.NotifyItemMoved(args.OldStartingIndex, args.NewStartingIndex);
_notifier.NotifyItemMoved(this, AdjustPositionForHeader(args.OldStartingIndex), AdjustPositionForHeader(args.NewStartingIndex));
return;
}
var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
_adapter.NotifyItemRangeChanged(start, end);
var start = AdjustPositionForHeader(Math.Min(args.OldStartingIndex, args.NewStartingIndex));
var end = AdjustPositionForHeader(Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count);
_notifier.NotifyItemRangeChanged(this, start, end);
}
void Add(NotifyCollectionChangedEventArgs args)
{
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
startIndex = AdjustPositionForHeader(startIndex);
var count = args.NewItems.Count;
if (count == 1)
{
_adapter.NotifyItemInserted(startIndex);
_notifier.NotifyItemInserted(this, startIndex);
return;
}
_adapter.NotifyItemRangeInserted(startIndex, count);
_notifier.NotifyItemRangeInserted(this, startIndex, count);
}
void Remove(NotifyCollectionChangedEventArgs args)
@ -103,25 +144,28 @@ namespace Xamarin.Forms.Platform.Android
{
// 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 NotifyDataSetChanged()
_adapter.NotifyDataSetChanged();
_notifier.NotifyDataSetChanged();
return;
}
startIndex = AdjustPositionForHeader(startIndex);
// 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;
if (count == 1)
{
_adapter.NotifyItemRemoved(startIndex);
_notifier.NotifyItemRemoved(this, startIndex);
return;
}
_adapter.NotifyItemRangeRemoved(startIndex, count);
_notifier.NotifyItemRangeRemoved(this, startIndex, count);
}
void Replace(NotifyCollectionChangedEventArgs args)
{
var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
startIndex = AdjustPositionForHeader(startIndex);
var newCount = args.NewItems.Count;
if (newCount == args.OldItems.Count)
@ -130,11 +174,11 @@ namespace Xamarin.Forms.Platform.Android
// notification to the adapter
if (newCount == 1)
{
_adapter.NotifyItemChanged(startIndex);
_notifier.NotifyItemChanged(this, startIndex);
}
else
{
_adapter.NotifyItemRangeChanged(startIndex, newCount);
_notifier.NotifyItemRangeChanged(this, startIndex, newCount);
}
return;
@ -142,7 +186,7 @@ namespace Xamarin.Forms.Platform.Android
// 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 NotifyDataSetChanged and let the RecyclerView update everything
_adapter.NotifyDataSetChanged();
_notifier.NotifyDataSetChanged();
}
}
}

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

@ -5,14 +5,16 @@ using Android.Support.V7.Widget;
namespace Xamarin.Forms.Platform.Android.CollectionView
{
public class RecyclerViewScrollListener : RecyclerView.OnScrollListener
public class RecyclerViewScrollListener<TItemsView, TItemsViewSource> : RecyclerView.OnScrollListener
where TItemsView : ItemsView
where TItemsViewSource : IItemsViewSource
{
bool _disposed;
int _horizontalOffset, _verticalOffset;
ItemsView _itemsView;
ItemsViewAdapter _itemsViewAdapter;
TItemsView _itemsView;
ItemsViewAdapter<TItemsView, TItemsViewSource> _itemsViewAdapter;
public RecyclerViewScrollListener(ItemsView itemsView, ItemsViewAdapter itemsViewAdapter)
public RecyclerViewScrollListener(TItemsView itemsView, ItemsViewAdapter<TItemsView, TItemsViewSource> itemsViewAdapter)
{
_itemsView = itemsView;
_itemsViewAdapter = itemsViewAdapter;

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

@ -6,15 +6,15 @@ using Object = Java.Lang.Object;
namespace Xamarin.Forms.Platform.Android
{
public class SelectableItemsViewAdapter : ItemsViewAdapter
public class SelectableItemsViewAdapter<TItemsView, TItemsSource> : ItemsViewAdapter<TItemsView, TItemsSource>
where TItemsView : SelectableItemsView
where TItemsSource : IItemsViewSource
{
protected readonly SelectableItemsView SelectableItemsView;
List<SelectableViewHolder> _currentViewHolders = new List<SelectableViewHolder>();
internal SelectableItemsViewAdapter(SelectableItemsView selectableItemsView,
internal SelectableItemsViewAdapter(TItemsView selectableItemsView,
Func<View, Context, ItemContentView> createView = null) : base(selectableItemsView, createView)
{
SelectableItemsView = selectableItemsView;
}
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
@ -77,13 +77,13 @@ namespace Xamarin.Forms.Platform.Android
int[] GetSelectedPositions()
{
switch (SelectableItemsView.SelectionMode)
switch (ItemsView.SelectionMode)
{
case SelectionMode.None:
return new int[0];
case SelectionMode.Single:
var selectedItem = SelectableItemsView.SelectedItem;
var selectedItem = ItemsView.SelectedItem;
if (selectedItem == null)
{
return new int[0];
@ -92,7 +92,7 @@ namespace Xamarin.Forms.Platform.Android
return new int[1] { GetPositionForItem(selectedItem) };
case SelectionMode.Multiple:
var selectedItems = SelectableItemsView.SelectedItems;
var selectedItems = ItemsView.SelectedItems;
var result = new int[selectedItems.Count];
for (int n = 0; n < result.Length; n++)
@ -127,7 +127,7 @@ namespace Xamarin.Forms.Platform.Android
void UpdateFormsSelection(int adapterPosition)
{
var mode = SelectableItemsView.SelectionMode;
var mode = ItemsView.SelectionMode;
switch (mode)
{
@ -135,11 +135,11 @@ namespace Xamarin.Forms.Platform.Android
// Selection's not even on, so there's nothing to do here
return;
case SelectionMode.Single:
SelectableItemsView.SelectedItem = ItemsSource[adapterPosition];
ItemsView.SelectedItem = ItemsSource.GetItem(adapterPosition);
return;
case SelectionMode.Multiple:
var item = ItemsSource[adapterPosition];
var selectedItems = SelectableItemsView.SelectedItems;
var item = ItemsSource.GetItem(adapterPosition);
var selectedItems = ItemsView.SelectedItems;
if (selectedItems.Contains(item))
{

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

@ -4,11 +4,11 @@ using Android.Content;
namespace Xamarin.Forms.Platform.Android
{
public class SelectableItemsViewRenderer : ItemsViewRenderer
public class SelectableItemsViewRenderer<TItemsView, TAdapter, TItemsViewSource> : ItemsViewRenderer<TItemsView, TAdapter, TItemsViewSource>
where TItemsView : SelectableItemsView
where TAdapter : SelectableItemsViewAdapter<TItemsView, TItemsViewSource>
where TItemsViewSource : IItemsViewSource
{
SelectableItemsView SelectableItemsView => (SelectableItemsView)ItemsView;
SelectableItemsViewAdapter SelectableItemsViewAdapter => (SelectableItemsViewAdapter)ItemsViewAdapter;
public SelectableItemsViewRenderer(Context context) : base(context)
{
}
@ -25,28 +25,23 @@ namespace Xamarin.Forms.Platform.Android
}
}
protected override void SetUpNewElement(ItemsView newElement)
protected override void SetUpNewElement(TItemsView newElement)
{
if (newElement != null && !(newElement is SelectableItemsView))
{
throw new ArgumentException($"{nameof(newElement)} must be of type {typeof(SelectableItemsView).Name}");
}
base.SetUpNewElement(newElement);
UpdateNativeSelection();
}
protected override ItemsViewAdapter CreateAdapter()
protected override TAdapter CreateAdapter()
{
return new SelectableItemsViewAdapter(SelectableItemsView);
return (TAdapter)new SelectableItemsViewAdapter<TItemsView, TItemsViewSource>(ItemsView);
}
void UpdateNativeSelection()
{
var mode = SelectableItemsView.SelectionMode;
var mode = ItemsView.SelectionMode;
SelectableItemsViewAdapter.ClearNativeSelection();
ItemsViewAdapter.ClearNativeSelection();
switch (mode)
{
@ -54,16 +49,16 @@ namespace Xamarin.Forms.Platform.Android
return;
case SelectionMode.Single:
var selectedItem = SelectableItemsView.SelectedItem;
SelectableItemsViewAdapter.MarkNativeSelection(selectedItem);
var selectedItem = ItemsView.SelectedItem;
ItemsViewAdapter.MarkNativeSelection(selectedItem);
return;
case SelectionMode.Multiple:
var selectedItems = SelectableItemsView.SelectedItems;
var selectedItems = ItemsView.SelectedItems;
foreach(var item in selectedItems)
{
SelectableItemsViewAdapter.MarkNativeSelection(item);
ItemsViewAdapter.MarkNativeSelection(item);
}
return;
}

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

@ -7,15 +7,17 @@ using Android.Util;
namespace Xamarin.Forms.Platform.Android
{
internal abstract class SelectableViewHolder : RecyclerView.ViewHolder, global::Android.Views.View.IOnClickListener
public abstract class SelectableViewHolder : RecyclerView.ViewHolder, global::Android.Views.View.IOnClickListener
{
bool _isSelected;
Drawable _selectedDrawable;
Drawable _selectableItemDrawable;
readonly bool _isSelectionEnabled;
protected SelectableViewHolder(global::Android.Views.View itemView) : base(itemView)
protected SelectableViewHolder(global::Android.Views.View itemView, bool isSelectionEnabled = true) : base(itemView)
{
itemView.SetOnClickListener(this);
_isSelectionEnabled = isSelectionEnabled;
}
public bool IsSelected
@ -37,7 +39,10 @@ namespace Xamarin.Forms.Platform.Android
public void OnClick(global::Android.Views.View view)
{
OnViewHolderClicked(AdapterPosition);
if (_isSelectionEnabled)
{
OnViewHolderClicked(AdapterPosition);
}
}
public event EventHandler<int> Clicked;

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

@ -3,7 +3,7 @@ using Xamarin.Forms.Internals;
namespace Xamarin.Forms.Platform.Android
{
internal class TemplatedItemViewHolder : SelectableViewHolder
public class TemplatedItemViewHolder : SelectableViewHolder
{
readonly ItemContentView _itemContentView;
readonly DataTemplate _template;
@ -11,7 +11,8 @@ namespace Xamarin.Forms.Platform.Android
public View View { get; private set; }
public TemplatedItemViewHolder(ItemContentView itemContentView, DataTemplate template) : base(itemContentView)
public TemplatedItemViewHolder(ItemContentView itemContentView, DataTemplate template,
bool isSelectionEnabled = true) : base(itemContentView, isSelectionEnabled)
{
_itemContentView = itemContentView;
_template = template;

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

@ -6,7 +6,7 @@ namespace Xamarin.Forms.Platform.Android
{
public TextView TextView { get; }
public TextViewHolder(TextView itemView) : base(itemView)
public TextViewHolder(TextView itemView, bool isSelectionEnabled = true) : base(itemView, isSelectionEnabled)
{
TextView = itemView;
TextView.Clickable = true;

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

@ -0,0 +1,52 @@
namespace Xamarin.Forms.Platform.Android
{
internal class UngroupedItemsSource : IGroupableItemsViewSource
{
readonly IItemsViewSource _source;
public UngroupedItemsSource(IItemsViewSource source)
{
_source = source;
}
public int Count => _source.Count;
public bool HasHeader { get => _source.HasHeader; set => _source.HasHeader = value; }
public bool HasFooter { get => _source.HasFooter; set => _source.HasFooter = value; }
public void Dispose()
{
_source.Dispose();
}
public object GetItem(int position)
{
return _source.GetItem(position);
}
public int GetPosition(object item)
{
return _source.GetPosition(item);
}
public bool IsFooter(int position)
{
return _source.IsFooter(position);
}
public bool IsGroupFooter(int position)
{
return false;
}
public bool IsGroupHeader(int position)
{
return false;
}
public bool IsHeader(int position)
{
return _source.IsHeader(position);
}
}
}

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

@ -65,10 +65,15 @@
<Compile Include="AppCompat\ILifeCycleState.cs" />
<Compile Include="AppCompat\ShellFragmentContainer.cs" />
<Compile Include="BorderBackgroundManager.cs" />
<Compile Include="CollectionView\AdapterNotifier.cs" />
<Compile Include="CollectionView\CarouselViewRenderer.cs" />
<Compile Include="CollectionView\CenterSnapHelper.cs" />
<Compile Include="CollectionView\DataChangeObserver.cs" />
<Compile Include="CollectionView\EmptySource.cs" />
<Compile Include="CollectionView\GroupableItemsViewAdapter.cs" />
<Compile Include="CollectionView\GroupableItemsViewRenderer.cs" />
<Compile Include="CollectionView\ICollectionChangedNotifier.cs" />
<Compile Include="CollectionView\ObservableGroupedSource.cs" />
<Compile Include="CollectionView\RecyclerViewScrollListener.cs" />
<Compile Include="CollectionView\GridLayoutSpanSizeLookup.cs" />
<Compile Include="CollectionView\NongreedySnapHelper.cs" />
@ -99,6 +104,7 @@
<Compile Include="CollectionView\TemplatedItemViewHolder.cs" />
<Compile Include="CollectionView\TextViewHolder.cs" />
<Compile Include="CollectionView\ItemViewType.cs" />
<Compile Include="CollectionView\UngroupedItemsSource.cs" />
<Compile Include="Elevation.cs" />
<Compile Include="Extensions\DrawableExtensions.cs" />
<Compile Include="Extensions\EntryRendererExtensions.cs" />