зеркало из https://github.com/DeGsoft/maui-linux.git
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:
Родитель
4e10c0b8f3
Коммит
c0a681e852
|
@ -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" />
|
||||
|
|
Загрузка…
Ссылка в новой задаче