From 90a282ed60c6366e2a8ff8a90f8512abf64e51ff Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Sat, 10 Aug 2019 18:00:56 -0600 Subject: [PATCH] Move all the header/footer adjustment to IItemsViewSource Fixes #7121 Fixes #7102 --- .../Issue7102.cs | 50 +++++++++++++++++++ ...rin.Forms.Controls.Issues.Shared.projitems | 3 +- Xamarin.Forms.Controls/CoreGallery.cs | 2 +- .../CollectionModifier.cs | 6 +-- .../FooterOnlyString.xaml | 13 +++++ .../FooterOnlyString.xaml.cs | 25 ++++++++++ .../HeaderFooterGallery.cs | 1 + .../ObservableCodeCollectionViewGallery.cs | 3 +- .../Xamarin.Forms.Controls.csproj | 3 ++ .../CollectionView/EmptySource.cs | 25 +++++++++- .../CollectionView/IItemsViewSource.cs | 6 +++ .../CollectionView/ItemsViewAdapter.cs | 31 +++++------- .../CollectionView/ListSource.cs | 41 +++++++++++++-- .../CollectionView/ObservableItemsSource.cs | 46 +++++++++++++++-- 14 files changed, 221 insertions(+), 34 deletions(-) create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue7102.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/FooterOnlyString.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/FooterOnlyString.xaml.cs diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue7102.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue7102.cs new file mode 100644 index 00000000000..8177ae4b218 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue7102.cs @@ -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 + } +} diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index f0800b81bc4..2aa7d6c59b8 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -25,7 +25,8 @@ - + + diff --git a/Xamarin.Forms.Controls/CoreGallery.cs b/Xamarin.Forms.Controls/CoreGallery.cs index 4e02033306b..51f86dcffab 100644 --- a/Xamarin.Forms.Controls/CoreGallery.cs +++ b/Xamarin.Forms.Controls/CoreGallery.cs @@ -285,7 +285,7 @@ public override string ToString() new GalleryPageFactory(() => new MemoryLeakGallery(), "Memory Leak"), new GalleryPageFactory(() => new Issues.A11yTabIndex(), "Accessibility TabIndex"), new GalleryPageFactory(() => new FontImageSourceGallery(), "Font ImageSource"), - new GalleryPageFactory(() => new CollectionViewGallery(), "CollectionView Gallery"), + new GalleryPageFactory(() => new CollectionViewGallery(), "_CollectionView Gallery"), new GalleryPageFactory(() => new CollectionViewCoreGalleryPage(), "CollectionView Core Gallery"), new GalleryPageFactory(() => new Issues.PerformanceGallery(), "Performance"), new GalleryPageFactory(() => new EntryReturnTypeGalleryPage(), "Entry ReturnType "), diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/CollectionModifier.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/CollectionModifier.cs index fa81807bfaa..e5b05c789d8 100644 --- a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/CollectionModifier.cs +++ b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/CollectionModifier.cs @@ -17,10 +17,10 @@ protected CollectionModifier(CollectionView cv, string buttonText) 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); diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/FooterOnlyString.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/FooterOnlyString.xaml new file mode 100644 index 00000000000..08286e4afda --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/FooterOnlyString.xaml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/FooterOnlyString.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/FooterOnlyString.xaml.cs new file mode 100644 index 00000000000..c31cfcf4e3b --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/FooterOnlyString.xaml.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/HeaderFooterGallery.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/HeaderFooterGallery.cs index 12e848c51e2..ee2a11b94de 100644 --- a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/HeaderFooterGallery.cs +++ b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/HeaderFooterGalleries/HeaderFooterGallery.cs @@ -20,6 +20,7 @@ public HeaderFooterGallery() 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), } } }; diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ObservableCodeCollectionViewGallery.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ObservableCodeCollectionViewGallery.cs index 5fac776cfd6..00517e763a0 100644 --- a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ObservableCodeCollectionViewGallery.cs +++ b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ObservableCodeCollectionViewGallery.cs @@ -25,7 +25,8 @@ public ObservableCodeCollectionViewGallery(ItemsLayoutOrientation orientation = 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); diff --git a/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj b/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj index 794a66254ba..32dc1a70ab6 100644 --- a/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj +++ b/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj @@ -47,6 +47,9 @@ MSBuild:UpdateDesignTimeXaml + + MSBuild:UpdateDesignTimeXaml + MSBuild:UpdateDesignTimeXaml diff --git a/Xamarin.Forms.Platform.Android/CollectionView/EmptySource.cs b/Xamarin.Forms.Platform.Android/CollectionView/EmptySource.cs index 0fe83c973cc..742080fbf8a 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/EmptySource.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/EmptySource.cs @@ -6,11 +6,34 @@ sealed internal class EmptySource : IItemsViewSource { public int Count => 0; + public bool HasHeader { get; set; } + public bool HasFooter { get; set; } + public object this[int index] => throw new IndexOutOfRangeException("IItemsViewSource is empty"); 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; } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/CollectionView/IItemsViewSource.cs b/Xamarin.Forms.Platform.Android/CollectionView/IItemsViewSource.cs index 3cf0ef75f9d..cc5f85717f9 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/IItemsViewSource.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/IItemsViewSource.cs @@ -6,5 +6,11 @@ internal interface IItemsViewSource : IDisposable { int Count { get; } object this[int index] { get; } + + bool HasHeader { get; set; } + bool HasFooter { get; set; } + + bool IsHeader(int index); + bool IsFooter(int index); } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs b/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs index ceeb85db02d..2122ae186e7 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/ItemsViewAdapter.cs @@ -17,8 +17,6 @@ public class ItemsViewAdapter : RecyclerView.Adapter Size? _size; bool _usingItemTemplate = false; - int _headerOffset = 0; - bool _hasFooter; internal ItemsViewAdapter(ItemsView itemsView, Func createItemContentView = null) { @@ -27,29 +25,26 @@ internal ItemsViewAdapter(ItemsView itemsView, Func new ItemContentView(context); } } - private void ItemsViewPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs property) + protected virtual void ItemsViewPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs property) { if (property.Is(ItemsView.HeaderProperty)) { - UpdateHeaderOffset(); - } - else if (property.Is(ItemsView.ItemTemplateProperty)) - { - UpdateUsingItemTemplate(); + UpdateHasHeader(); } else if (property.Is(ItemsView.ItemTemplateProperty)) { @@ -93,7 +88,7 @@ public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int positi return; } - var itemsSourcePosition = position - _headerOffset; + var itemsSourcePosition = position; switch (holder) { @@ -147,7 +142,7 @@ public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int 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) { @@ -192,7 +187,7 @@ public virtual int GetPositionForItem(object item) { if (ItemsSource[n] == item) { - return n + _headerOffset; + return n; } } @@ -204,24 +199,24 @@ void UpdateUsingItemTemplate() _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) diff --git a/Xamarin.Forms.Platform.Android/CollectionView/ListSource.cs b/Xamarin.Forms.Platform.Android/CollectionView/ListSource.cs index d829a9ad457..d33e5644bd8 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/ListSource.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/ListSource.cs @@ -3,28 +3,61 @@ namespace Xamarin.Forms.Platform.Android { - sealed class ListSource : List, IItemsViewSource + sealed class ListSource : IItemsViewSource { + private List _internal; + public ListSource() { } - public ListSource(IEnumerable enumerable) : base(enumerable) + public ListSource(IEnumerable enumerable) { - + _internal = new List(enumerable); } public ListSource(IEnumerable enumerable) { foreach (object item in enumerable) { - Add(item); + _internal.Add(item); } } + public int Count => _internal.Count + (HasHeader ? 1 : 0) + (HasFooter ? 1 : 0); + public object this[int index] => _internal[AdjustIndexRequest(index)]; + + public bool HasHeader { get; set; } + public bool HasFooter { get; set; } + public void Dispose() { } + + public bool IsFooter(int index) + { + if (!HasFooter) + { + return false; + } + + if (HasHeader) + { + return index == Count + 1; + } + + return index == Count; + } + + public bool IsHeader(int index) + { + return HasHeader && index == 0; + } + + int AdjustIndexRequest(int index) + { + return index - (HasHeader ? 1 : 0); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/CollectionView/ObservableItemsSource.cs b/Xamarin.Forms.Platform.Android/CollectionView/ObservableItemsSource.cs index 3c43c54f31a..89577939686 100644 --- a/Xamarin.Forms.Platform.Android/CollectionView/ObservableItemsSource.cs +++ b/Xamarin.Forms.Platform.Android/CollectionView/ObservableItemsSource.cs @@ -19,15 +19,37 @@ public ObservableItemsSource(IList itemSource, RecyclerView.Adapter adapter) ((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[AdjustIndexRequest(index)]; - 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) + { + if (!HasFooter) + { + return false; + } + + if (HasHeader) + { + return index == _itemsSource.Count + 1; + } + + return index == _itemsSource.Count; + } + + public bool IsHeader(int index) + { + return HasHeader && index == 0; + } + protected virtual void Dispose(bool disposing) { if (!_disposed) @@ -41,6 +63,16 @@ protected virtual void Dispose(bool disposing) } } + int AdjustIndexRequest(int index) + { + return index - (HasHeader ? 1 : 0); + } + + int AdjustNotifyIndex(int index) + { + return index + (HasHeader ? 1 : 0); + } + void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) @@ -72,18 +104,19 @@ void Move(NotifyCollectionChangedEventArgs args) if (count == 1) { // For a single item, we can use NotifyItemMoved and get the animation - _adapter.NotifyItemMoved(args.OldStartingIndex, args.NewStartingIndex); + _adapter.NotifyItemMoved(AdjustNotifyIndex(args.OldStartingIndex), AdjustNotifyIndex(args.NewStartingIndex)); return; } - var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex); - var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count; + var start = AdjustNotifyIndex(Math.Min(args.OldStartingIndex, args.NewStartingIndex)); + var end = AdjustNotifyIndex(Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count); _adapter.NotifyItemRangeChanged(start, end); } void Add(NotifyCollectionChangedEventArgs args) { var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); + startIndex = AdjustNotifyIndex(startIndex); var count = args.NewItems.Count; if (count == 1) @@ -107,6 +140,8 @@ void Remove(NotifyCollectionChangedEventArgs args) return; } + startIndex = AdjustNotifyIndex(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; @@ -122,6 +157,7 @@ void Remove(NotifyCollectionChangedEventArgs args) void Replace(NotifyCollectionChangedEventArgs args) { var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); + startIndex = AdjustNotifyIndex(startIndex); var newCount = args.NewItems.Count; if (newCount == args.OldItems.Count)