diff --git a/.gitignore b/.gitignore index 10f16da..f945760 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ ## files generated by popular Visual Studio add-ons. .vs/ +.idea/ # User-specific files *.suo diff --git a/CollectionView.Droid/CollectionView.Droid.csproj b/CollectionView.Droid/CollectionView.Droid.csproj index 48e892c..a348829 100644 --- a/CollectionView.Droid/CollectionView.Droid.csproj +++ b/CollectionView.Droid/CollectionView.Droid.csproj @@ -14,7 +14,6 @@ Resource Resources Assets - 1.1.2 true @@ -132,6 +131,7 @@ + @@ -142,7 +142,7 @@ - {9D6D76D0-B250-4262-B970-52DAE6764B88} + {D5C93D88-A5D6-4843-935B-CC72F073B54E} CollectionView diff --git a/CollectionView.Droid/CollectionViewRenderer.cs b/CollectionView.Droid/CollectionViewRenderer.cs index 3cf5e3e..770c3f3 100644 --- a/CollectionView.Droid/CollectionViewRenderer.cs +++ b/CollectionView.Droid/CollectionViewRenderer.cs @@ -5,6 +5,7 @@ using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using AView = Android.Views.View; +using Android.Views; namespace AiForms.Renderers.Droid { @@ -22,6 +23,7 @@ public abstract class CollectionViewRenderer : ViewRenderer TemplatedItemsView => Element; + CollectionViewScrollListener _scrollListener; SelectableSmoothScroller _scroller; ScrollToRequestedEventArgs _pendingScrollTo; @@ -44,6 +46,10 @@ protected override void Dispose(bool disposing) _scroller?.Dispose(); _scroller = null; + RecyclerView.RemoveOnScrollListener(_scrollListener); + _scrollListener?.Dispose(); + _scrollListener = null; + Adapter?.Dispose(); Adapter = null; @@ -69,12 +75,16 @@ protected override void OnElementChanged(ElementChangedEventArgs Adapter?.Dispose(); Adapter = null; } + e.OldElement.SetLoadMoreCompletionAction = null; } if (e.NewElement != null) { ((IListViewController)e.NewElement).ScrollToRequested += OnScrollToRequested; _scroller = new SelectableSmoothScroller(Context); + _scrollListener = new CollectionViewScrollListener(e.NewElement); + RecyclerView.AddOnScrollListener(_scrollListener); + e.NewElement.SetLoadMoreCompletionAction = (isEnd) => _scrollListener.IsReachedBottom = isEnd; } } diff --git a/CollectionView.Droid/CollectionViewScrollListener.cs b/CollectionView.Droid/CollectionViewScrollListener.cs new file mode 100644 index 0000000..4797116 --- /dev/null +++ b/CollectionView.Droid/CollectionViewScrollListener.cs @@ -0,0 +1,50 @@ +using System; +using AiForms.Renderers; +using Android.Support.V7.Widget; + +namespace AiForms.Renderers.Droid +{ + public class CollectionViewScrollListener:RecyclerView.OnScrollListener + { + public bool IsReachedBottom { get; set; } + + CollectionView _collectionView; + + + public CollectionViewScrollListener(CollectionView collectionView) + { + _collectionView = collectionView; + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + _collectionView = null; + } + base.Dispose(disposing); + } + + public override void OnScrolled(RecyclerView recyclerView, int dx, int dy) + { + base.OnScrolled(recyclerView, dx, dy); + + if(dx < 0 || dy < 0 || IsReachedBottom || _collectionView.LoadMoreCommand == null) + { + return; + } + + var layoutManager = recyclerView.GetLayoutManager() as LinearLayoutManager; + + var visibleItemCount = recyclerView.ChildCount; + var totalItemCount = layoutManager.ItemCount; + var firstVisibleItem = layoutManager.FindFirstVisibleItemPosition(); + + if(totalItemCount - visibleItemCount - _collectionView.LoadMoreMargin <= firstVisibleItem) + { + IsReachedBottom = true; + _collectionView.LoadMoreCommand?.Execute(null); + } + } + } +} diff --git a/CollectionView.Droid/GridCollectionViewRenderer.cs b/CollectionView.Droid/GridCollectionViewRenderer.cs index d2bd74d..5bd1252 100644 --- a/CollectionView.Droid/GridCollectionViewRenderer.cs +++ b/CollectionView.Droid/GridCollectionViewRenderer.cs @@ -66,8 +66,6 @@ protected override void Dispose(bool disposing) protected override void OnElementChanged(ElementChangedEventArgs e) { - base.OnElementChanged(e); - if (e.NewElement != null) { if (RecyclerView == null) @@ -103,6 +101,8 @@ protected override void OnElementChanged(ElementChangedEventArgs UpdatePullToRefreshEnabled(); UpdatePullToRefreshColor(); } + + base.OnElementChanged(e); } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) @@ -468,8 +468,14 @@ public override void GetItemOffsets(Android.Graphics.Rect outRect, Android.Views outRect.Right = _parentRenderer.ColumnSpacing - (spanIndex + 1) * _parentRenderer.ColumnSpacing / _spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing) } - // Group bottom spacing is applied at the last cell. - if (_parentRenderer.Element.IsGroupingEnabled && position >= _parentRenderer.Adapter.ItemCount - _spanCount) + // Disabled grouping top spacing is applied at the first row cells. + if (!_parentRenderer.Element.IsGroupingEnabled && position < _spanCount) + { + outRect.Top = _parentRenderer._firstSpacing; + } + + // Group bottom or single bottom spacing is applied at the last row cells. + if (position >= _parentRenderer.Adapter.ItemCount - _spanCount) { outRect.Bottom = _parentRenderer._lastSpacing; } diff --git a/CollectionView.Droid/HCollectionViewRenderer.cs b/CollectionView.Droid/HCollectionViewRenderer.cs index 8bf48e6..ff549ce 100644 --- a/CollectionView.Droid/HCollectionViewRenderer.cs +++ b/CollectionView.Droid/HCollectionViewRenderer.cs @@ -46,8 +46,6 @@ protected override void Dispose(bool disposing) protected override void OnElementChanged(ElementChangedEventArgs e) { - base.OnElementChanged(e); - if (e.NewElement != null) { if (Control == null) @@ -76,6 +74,8 @@ protected override void OnElementChanged(ElementChangedEventArgs UpdateSpacing(); } } + + base.OnElementChanged(e); } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) @@ -221,6 +221,18 @@ public override void GetItemOffsets(Rect outRect, Android.Views.View view, Recyc return; } + // Disabled grouping first spacing is applied at the first cell. + if (!_renderer._hCollectionView.IsGroupingEnabled && position == 0) + { + outRect.Left = _renderer._firstSpacing; + } + + // Group last or single last spacing is applied at the last cell. + if (position == _renderer.Adapter.ItemCount - 1) + { + outRect.Right = _renderer._lastSpacing; + } + if (position == 0 || _renderer.Adapter.FirstSectionItems.Contains(realPosition)) { return; diff --git a/CollectionView.iOS/CollectionView.iOS.csproj b/CollectionView.iOS/CollectionView.iOS.csproj index d4e239c..7c26e5b 100644 --- a/CollectionView.iOS/CollectionView.iOS.csproj +++ b/CollectionView.iOS/CollectionView.iOS.csproj @@ -9,7 +9,6 @@ AiForms.Renderers.iOS CollectionView.iOS Resources - 1.1.2 true @@ -86,7 +85,7 @@ - {9D6D76D0-B250-4262-B970-52DAE6764B88} + {D5C93D88-A5D6-4843-935B-CC72F073B54E} CollectionView diff --git a/CollectionView.iOS/CollectionViewRenderer.cs b/CollectionView.iOS/CollectionViewRenderer.cs index d054f6e..6daa19b 100644 --- a/CollectionView.iOS/CollectionViewRenderer.cs +++ b/CollectionView.iOS/CollectionViewRenderer.cs @@ -19,7 +19,6 @@ public class CollectionViewRenderer : ViewRenderer TemplatedItemsView => Element; - ScrollToRequestedEventArgs _requestedScroll; bool _disposed; protected override void OnElementChanged(ElementChangedEventArgs e) @@ -32,6 +31,7 @@ protected override void OnElementChanged(ElementChangedEventArgs templatedItems.CollectionChanged -= OnCollectionChanged; templatedItems.GroupedCollectionChanged -= OnGroupedCollectionChanged; e.OldElement.ScrollToRequested -= OnScrollToRequested; + e.OldElement.SetLoadMoreCompletionAction = null; } if (e.NewElement != null) @@ -42,6 +42,10 @@ protected override void OnElementChanged(ElementChangedEventArgs templatedItems.CollectionChanged += OnCollectionChanged; templatedItems.GroupedCollectionChanged += OnGroupedCollectionChanged; e.NewElement.ScrollToRequested += OnScrollToRequested; + e.NewElement.SetLoadMoreCompletionAction = (isEnd) => + { + DataSource.IsReachedBottom = isEnd; + }; UpdateBackgroundColor(); } @@ -55,7 +59,7 @@ protected override void Dispose(bool disposing) } if (disposing) - { + { ViewLayout?.Dispose(); ViewLayout = null; @@ -65,11 +69,12 @@ protected override void Dispose(bool disposing) } if (Element != null) - { + { var templatedItems = TemplatedItemsView.TemplatedItems; templatedItems.CollectionChanged -= OnCollectionChanged; templatedItems.GroupedCollectionChanged -= OnGroupedCollectionChanged; Element.ScrollToRequested -= OnScrollToRequested; + Element.SetLoadMoreCompletion = null; } } @@ -89,14 +94,12 @@ protected override void OnElementPropertyChanged(object sender, PropertyChangedE { UpdateBackgroundColor(); } - } protected virtual async void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e) { if (Superview == null) { - _requestedScroll = e; return; } diff --git a/CollectionView.iOS/CollectionViewSource.cs b/CollectionView.iOS/CollectionViewSource.cs index d94cb8d..f7c7713 100644 --- a/CollectionView.iOS/CollectionViewSource.cs +++ b/CollectionView.iOS/CollectionViewSource.cs @@ -12,16 +12,19 @@ namespace AiForms.Renderers.iOS { [Foundation.Preserve(AllMembers = true)] - public class CollectionViewSource : UICollectionViewSource, IUICollectionViewDelegateFlowLayout + public class CollectionViewSource : UICollectionViewSource, IUICollectionViewDelegateFlowLayout,IUIScrollViewDelegate { static int s_dataTemplateIncrementer = 2; // lets start at not 0 because public CGSize CellSize { get; set; } public Dictionary Counts { get; set; } + public bool IsReachedBottom { get; set; } + public float LoadMoreMargin { get; set; } protected CollectionView CollectionView; protected ITemplatedItemsView TemplatedItemsView => CollectionView; + const int DefaultItemTemplateId = 1; bool _isLongTap; bool _disposed; @@ -57,6 +60,16 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + public override void Scrolled(UIScrollView scrollView) + { + } + + protected void RaiseReachedBottom() + { + IsReachedBottom = true; + CollectionView?.LoadMoreCommand?.Execute(null); + } + public override nint NumberOfSections(UICollectionView collectionView) { if (TemplatedItemsView.TemplatedItems.Count == 0) diff --git a/CollectionView.iOS/GridCollectionViewRenderer.cs b/CollectionView.iOS/GridCollectionViewRenderer.cs index 4f75d73..8768044 100644 --- a/CollectionView.iOS/GridCollectionViewRenderer.cs +++ b/CollectionView.iOS/GridCollectionViewRenderer.cs @@ -27,8 +27,8 @@ public class GridCollectionViewRenderer : CollectionViewRenderer bool _disposed; GridCollectionView _gridCollectionView => (GridCollectionView)Element; GridCollectionViewSource _gridSource => DataSource as GridCollectionViewSource; - float _firstSpacing => _gridCollectionView.IsGroupingEnabled ? (float)_gridCollectionView.GroupFirstSpacing : 0; - float _lastSpacing => _gridCollectionView.IsGroupingEnabled ? (float)_gridCollectionView.GroupLastSpacing : 0; + float _firstSpacing => (float)_gridCollectionView.GroupFirstSpacing; + float _lastSpacing => (float)_gridCollectionView.GroupLastSpacing; bool _isRatioHeight => _gridCollectionView.ColumnHeight <= 5.0; protected override void OnElementChanged(ElementChangedEventArgs e) @@ -275,11 +275,12 @@ protected virtual void UpdateGridType() case UIInterfaceOrientation.PortraitUpsideDown: case UIInterfaceOrientation.Unknown: itemSize = GetUniformItemSize(_gridCollectionView.PortraitColumns); - + DataSource.LoadMoreMargin = Element.LoadMoreMargin / _gridCollectionView.PortraitColumns * (float)itemSize.Height; break; case UIInterfaceOrientation.LandscapeLeft: case UIInterfaceOrientation.LandscapeRight: itemSize = GetUniformItemSize(_gridCollectionView.LandscapeColumns); + DataSource.LoadMoreMargin = Element.LoadMoreMargin / _gridCollectionView.LandscapeColumns * (float)itemSize.Height; break; } ViewLayout.MinimumInteritemSpacing = (System.nfloat)_gridCollectionView.ColumnSpacing; @@ -317,6 +318,7 @@ protected virtual CGSize GetUniformItemSize(int columns) var itemWidth = Math.Floor((float)(width / (float)columns)); var itemHeight = CalcurateColumnHeight(itemWidth); + return new CGSize(itemWidth, itemHeight); } @@ -326,6 +328,7 @@ protected virtual CGSize GetAutoSpacingItemSize() var itemHeight = CalcurateColumnHeight(itemWidth); if (_gridCollectionView.SpacingType == SpacingType.Between) { + DataSource.LoadMoreMargin = Element.LoadMoreMargin / ((float)Frame.Width / itemWidth) * (float)itemHeight; return new CGSize(itemWidth, itemHeight); } @@ -348,6 +351,8 @@ protected virtual CGSize GetAutoSpacingItemSize() leftSize -= spacing; } while (true); + DataSource.LoadMoreMargin = Element.LoadMoreMargin / (float)columnCount * (float)itemHeight; + var contentWidth = itemWidth * columnCount + spacing * (columnCount - 1f); var insetSum = Frame.Width - contentWidth; diff --git a/CollectionView.iOS/GridCollectionViewSource.cs b/CollectionView.iOS/GridCollectionViewSource.cs index 5f8d752..085395b 100644 --- a/CollectionView.iOS/GridCollectionViewSource.cs +++ b/CollectionView.iOS/GridCollectionViewSource.cs @@ -40,13 +40,29 @@ public override CGSize GetSizeForItem(UICollectionView collectionView, UICollect } var column = indexPath.Row % totalColumns; - - if(column <= SurplusPixel - 1) { - // assign 1px to the cell width in order from the first cell until the surplus is gone. + + if(column >= totalColumns - SurplusPixel) { + // assign 1px to the cell width in order from the last cell until the surplus is gone. + // if assigning from the first cell, the layout is sometimes broken when items is a few. return new CGSize(CellSize.Width + 1, CellSize.Height); } return CellSize; } + + public override void Scrolled(UIScrollView scrollView) + { + base.Scrolled(scrollView); + + if (IsReachedBottom || CollectionView.LoadMoreCommand == null) + { + return; + } + + if (scrollView.ContentSize.Height <= scrollView.ContentOffset.Y + scrollView.Bounds.Height + LoadMoreMargin) + { + RaiseReachedBottom(); + } + } } } diff --git a/CollectionView.iOS/HCollectionViewRenderer.cs b/CollectionView.iOS/HCollectionViewRenderer.cs index ef7dddc..de73ff9 100644 --- a/CollectionView.iOS/HCollectionViewRenderer.cs +++ b/CollectionView.iOS/HCollectionViewRenderer.cs @@ -19,8 +19,8 @@ public class HCollectionViewRenderer : CollectionViewRenderer CGRect _previousFrame = CGRect.Empty; bool _disposed; HCollectionView _hCollectionView => Element as HCollectionView; - float _firstSpacing => _hCollectionView.IsGroupingEnabled ? (float)_hCollectionView.GroupFirstSpacing : 0; - float _lastSpacing => _hCollectionView.IsGroupingEnabled ? (float)_hCollectionView.GroupLastSpacing : 0; + float _firstSpacing => (float)_hCollectionView.GroupFirstSpacing; + float _lastSpacing => (float)_hCollectionView.GroupLastSpacing; protected override void OnElementChanged(ElementChangedEventArgs e) { @@ -158,11 +158,18 @@ protected virtual void UpdateCellSize() var height = Element.HeightRequest >= 0 ? Element.HeightRequest : Bounds.Height; DataSource.CellSize = new CGSize((float)_hCollectionView.ColumnWidth, (float)height); + DataSource.LoadMoreMargin = Element.LoadMoreMargin * (float)DataSource.CellSize.Width; } protected virtual void UpdateSpacing() { ViewLayout.MinimumLineSpacing = (System.nfloat)_hCollectionView.Spacing; + + if(_hCollectionView.IsInfinite && !_hCollectionView.IsGroupingEnabled) + { + return; + } + ViewLayout.SectionInset = new UIEdgeInsets(0, _firstSpacing, 0, _lastSpacing); } diff --git a/CollectionView.iOS/HCollectionViewSource.cs b/CollectionView.iOS/HCollectionViewSource.cs index f1e29f7..52d17b5 100644 --- a/CollectionView.iOS/HCollectionViewSource.cs +++ b/CollectionView.iOS/HCollectionViewSource.cs @@ -6,7 +6,7 @@ namespace AiForms.Renderers.iOS { [Foundation.Preserve(AllMembers = true)] - public class HCollectionViewSource : CollectionViewSource, IUIScrollViewDelegate + public class HCollectionViewSource : CollectionViewSource { HCollectionView _hCollectionView => CollectionView as HCollectionView; int _infiniteMultiple = 3; @@ -48,18 +48,28 @@ public override nint GetItemsCount(UICollectionView collectionView, nint section public override void Scrolled(UIScrollView scrollView) { - if (!_hCollectionView.IsInfinite) + base.Scrolled(scrollView); + + if (_hCollectionView.IsInfinite) { + _visibleContentWidth = scrollView.ContentSize.Width / _infiniteMultiple; + + if (scrollView.ContentOffset.X <= 0f || scrollView.ContentOffset.X > _visibleContentWidth * 2f) + { + scrollView.ContentOffset = new CGPoint(_visibleContentWidth, scrollView.ContentOffset.Y); + } return; } - _visibleContentWidth = scrollView.ContentSize.Width / _infiniteMultiple; - - if (scrollView.ContentOffset.X <= 0f || scrollView.ContentOffset.X > _visibleContentWidth * 2f) + if (IsReachedBottom || CollectionView.LoadMoreCommand == null) { - scrollView.ContentOffset = new CGPoint(_visibleContentWidth, scrollView.ContentOffset.Y); + return; } + if (scrollView.ContentSize.Width <= scrollView.ContentOffset.X + scrollView.Bounds.Width + LoadMoreMargin) + { + RaiseReachedBottom(); + } } public override CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath) diff --git a/CollectionView/CollectionView.cs b/CollectionView/CollectionView.cs index 5c1c126..d5a31dd 100644 --- a/CollectionView/CollectionView.cs +++ b/CollectionView/CollectionView.cs @@ -15,6 +15,10 @@ public class CollectionView : ListView /// Caching strategy. public CollectionView(ListViewCachingStrategy cachingStrategy):base(cachingStrategy){ ScrollController = new ScrollController(this); + SetLoadMoreCompletion = (isEnd) => + { + SetLoadMoreCompletionAction?.Invoke(isEnd); + }; } /// @@ -149,8 +153,54 @@ public IScrollController ScrollController { get { return (IScrollController)GetValue(ScrollControllerProperty); } set { SetValue(ScrollControllerProperty, value); } - } + } + + public static BindableProperty LoadMoreCommandProperty = + BindableProperty.Create( + nameof(LoadMoreCommand), + typeof(ICommand), + typeof(CollectionView), + default(ICommand), + defaultBindingMode: BindingMode.OneWay + ); + + public ICommand LoadMoreCommand + { + get { return (ICommand)GetValue(LoadMoreCommandProperty); } + set { SetValue(LoadMoreCommandProperty, value); } + } + + internal Action SetLoadMoreCompletionAction; + public static BindableProperty SetLoadMoreCompletionProperty = + BindableProperty.Create( + nameof(SetLoadMoreCompletion), + typeof(Action), + typeof(CollectionView), + default(Action), + defaultBindingMode: BindingMode.OneWayToSource + ); + + public Action SetLoadMoreCompletion + { + get { return (Action)GetValue(SetLoadMoreCompletionProperty); } + set { SetValue(SetLoadMoreCompletionProperty, value); } + } + + public static BindableProperty LoadMoreMarginProperty = + BindableProperty.Create( + nameof(LoadMoreMargin), + typeof(int), + typeof(CollectionView), + default(int), + defaultBindingMode: BindingMode.OneWay + ); + + public int LoadMoreMargin + { + get { return (int)GetValue(LoadMoreMarginProperty); } + set { SetValue(LoadMoreMarginProperty, value); } + } // kill unused properties private new object Header { get; } diff --git a/CollectionView/CollectionView.csproj b/CollectionView/CollectionView.csproj index cca615f..a849648 100644 --- a/CollectionView/CollectionView.csproj +++ b/CollectionView/CollectionView.csproj @@ -15,6 +15,7 @@ + diff --git a/CollectionView/Properties/AssemblyInfo.cs b/CollectionView/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0aa223e --- /dev/null +++ b/CollectionView/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("CollectionView.iOS")] +[assembly: InternalsVisibleTo("CollectionView.Droid")] \ No newline at end of file diff --git a/README-ja.md b/README-ja.md index 5437779..54f420a 100644 --- a/README-ja.md +++ b/README-ja.md @@ -174,6 +174,46 @@ public class SomeViewModel } ``` +### LoadMoreCommand と SetLoadMoreCompletion を使った実装例 + +最後の項目までスクロールしたら続きを読み込むような処理は LoadMoreCommand と SetLoadMoreCompletion を使って実装できます。 + +```xml + + ...omitting + +``` + +```cs +public class SomeViewModel +{ + public ObservableCollection ItemsSource { get; } = new ObservableCollection(); + public Command LoadMoreCommand { get; set; } + public Action SetLoadMoreCompletion { get; set; } + + public async Task LoadMoreCommandExecute() + { + var items = await WebApi.GetItems(10); + + if(items.Count == 0) + { + SetLoadMoreCompletion(true); // 全ての項目を読み込んだ + return; + } + + foreach(var item in items) + { + ItemsSource.Add(item); + } + + SetLoadMoreCompletion(false); // まだ全ての項目は読み込んでいない. + } +} +``` + ## 使用可能なListViewの機能 ### Bindable properties @@ -222,6 +262,30 @@ var collectionView = new HCollectionView(ListViewCachingStrategy.RetainElement); DataTemplateの要素に画像を使用する場合は、**[FFImageLoading](https://github.com/luberda-molinet/FFImageLoading)** を利用することを強く推奨します。 CollectionViewには画像を非同期で処理したりキャッシュしたりする機能がないためです。 +## 共通の Bindable Properties (GridCollectionView / HCollectionView) + +* ItemTapCommand + * アイテムがタップされた時に発火するコマンド。 +* ItemLongTapCommand + * アイテムがロングタップされた時に発火するコマンド。 +* TouchFeedbackColor + * アイテムをタッチした時に表示するエフェクト色。 +* GroupFirstSpacing + * グループ内の最初のアイテムの上(GridCollectionView)または左(HCollectionView)の間隔。 + * グループ機能を使っていない場合は最初のアイテムに適用されます。 +* GroupLastSpacing + * グループ内の最後のアイテムの下(GridCollectionView)または右(HCollectionView)の間隔。 + * グループ機能を使っていない場合は最後のアイテムに適用されます。 +* [ScrollController](#scrollcontroller) + * ViewModelなどでCollectionViewのスクロールを制御する場合に使用するオブジェクト。 +* LoadMoreCommand + * 最後のアイテムの表示が検出された時に発火するコマンド。 +* SetLoadMoreCompletion + * 何か処理をした後で LoadMoreCommand を使い続ける場合は false を、そうでない場合は true をセットします。 + * LoadMoreCommand が一度実行されると、SetLoadMoreCompletionに false をセットするまで再び LoadMoreCommand が実行されることはありません。 +* LoadMoreMargin + * LoadMoreCommandを発火させるまでの残りアイテム数。例えば3をセットすればおおよそ最後から3番目のアイテムが現れるくらいのタイミングでLoadMoreCommandが発火されます。規定値は0です。 + ## GridCollectionView Grid状に各要素を配置するListViewです。これは [WrapLayout](https://github.com/muak/AiForms.Layouts#wraplayout) に似ていますが、セルをリサイクルできるという点などで異なります。 @@ -248,24 +312,13 @@ Grid状に各要素を配置するListViewです。これは [WrapLayout](https: * グループヘッダーのセルの高さ。 * [SpacingType](#spacingtype-enumeration) * 列間の間隔の決め方を、Between と Center から選択します。GridType が AutoSpacingGrid のときのみ有効です。(Default: Between) -* GroupFirstSpacing - * グループ内の最初のアイテムの上の間隔。 -* GroupLastSpacing - * グループ内の最後のアイテムの下の間隔。 * BothSidesMargin * グループヘッダーセル以外のコンテンツ領域の左右の余白。GridType が UniformGrid のときのみ有効です。 (Default: 0) * PullToRefreshColor * PullToRefreshのインジケータに使用する色。 -* ItemTapCommand - * アイテムがタップられた時に発火するコマンド。 -* ItemLongTapCommand - * アイテムがロングタップされた時に発火するコマンド。 -* TouchFeedbackColor - * アイテムをタッチした時に表示するエフェクト色。 -* [ScrollController](#scrollcontroller) - * ViewModelなどでCollectionViewのスクロールを制御する場合に使用するオブジェクト。 * IsGroupHeaderSticky * グループヘッダーを上に固定するかどうか (iOS のみ) (default: true) +* [その他の共通プロパティ](#%E5%85%B1%E9%80%9A%E3%81%AE-Bindable-Properties-GridCollectionView--HCollectionView) ### Special Properties @@ -308,19 +361,7 @@ Grid状に各要素を配置するListViewです。これは [WrapLayout](https: > iOSの場合、コンテナ幅を十分に満たす数のセルが必要です。 > Androidの場合、完全に無限ではないので長時間スクロールすると端に到達することがあります。 - -* ItemTapCommand - * アイテムがタップられた時に発火するコマンド。 -* ItemLongTapCommand - * アイテムがロングタップされた時に発火するコマンド。 -* TouchFeedbackColor - * アイテムをタッチした時に表示するエフェクト色。 -* [ScrollController](#scrollcontroller) - * ViewModelなどでCollectionViewのスクロールを制御する場合に使用するオブジェクト。 -* GroupFirstSpacing - * グループ内の最初のアイテムの左の間隔。 -* GroupLastSpacing - * グループ内の最後のアイテムの右の間隔。 +* [その他の共通プロパティ](#%E5%85%B1%E9%80%9A%E3%81%AE-Bindable-Properties-GridCollectionView--HCollectionView) ### 行の高さについて diff --git a/README.md b/README.md index 5fc82f9..26dfbff 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,44 @@ public class SomeViewModel } ``` +### A example of how to use LoadMoreCommand and SetLoadMoreCompletion. + +```xml + + ...omitting + +``` + +```cs +public class SomeViewModel +{ + public ObservableCollection ItemsSource { get; } = new ObservableCollection(); + public Command LoadMoreCommand { get; set; } + public Action SetLoadMoreCompletion { get; set; } + + public async Task LoadMoreCommandExecute() + { + var items = await WebApi.GetItems(10); + + if(items.Count == 0) + { + SetLoadMoreCompletion(true); // All the items was loaded. + return; + } + + foreach(var item in items) + { + ItemsSource.Add(item); + } + + SetLoadMoreCompletion(false); // All the items is not still loaded. + } +} +``` + ## Available functions deriving from ListView ### Bindable properties @@ -224,6 +262,28 @@ var collectionView = new HCollectionView(ListViewCachingStrategy.RetainElement); If you use images for a data template item, using **[FFImageLoading](https://github.com/luberda-molinet/FFImageLoading)** is recommended powerfully. Because this library doesn't contain the feature of such as dealing with images asynchronously and caching. +## Common Bindable Properties (GridCollectionView / HCollectionView) + +* ItemTapCommand + * The command invoked when an item is tapped. +* ItemLongTapCommand + * The command invoked when an item is pressed longly. +* TouchFeedbackColor + * The color rendered when an item is touched. +* GroupFirstSpacing + * The spacing of the first item's top(Grid) or left(H) in a group. + * If the grouping is not enabled, it is applied to the first item. +* GroupLastSpacing + * The spacing of the last item's bottom(Grid) or right(H) in a group. + * If the grouping is not enabled, it is applied to the last item. +* LoadMoreCommand + * The command invoked when appearing the last item is detected. +* SetLoadMoreCompletion + * If it continues using the LoadMore after doing some process, set false; Otherwise set true. + * Once the LoadMoreCommand is invoked, it will not be invoked again unless the SetLoadMoreCompletion is set to false. +* LoadMoreMargin + * The number of items from the last as the threshold value to invoke the LoadMoreCommand. For example, if set this property to 3, when appearing the 3rd item from the last, the LoadMoreCommand will be invoked. This value is approximate. the default value is 0. + ## GridCollectionView This is the ListView that lays out each item in a grid pattern. Though this is similar to [WrapLayout](https://github.com/muak/AiForms.Layouts#wraplayout), is different from it in that cells can be recycled. @@ -250,24 +310,13 @@ This is the ListView that lays out each item in a grid pattern. Though this is s * The height of a group header cell. * [SpacingType](#spacingtype-enumeration) * Select the spacing type using an enumeration value either Between or Center. This is used only when GridType is AutoSpacingGrid. (Default: Between) -* GroupFirstSpacing - * The spacing of the first item's top in a group. -* GroupLastSpacing - * The spacing of the last item's bottom in a group. * BothSidesMargin * The margin of the right and left sides in the content area except for a group header cell. This is used only when GridType is UniformGrid. (Default: 0) * PullToRefreshColor * The color of the PullToRefresh indicator icon. -* ItemTapCommand - * The command invoked when an item is tapped. -* ItemLongTapCommand - * The command invoked when an item is pressed longly. -* TouchFeedbackColor - * The color rendered when an item is touched. -* [ScrollController](#scrollcontroller) - * The object for manipulating the scroll from such as ViewModel. * IsGroupHeaderSticky * Whether a group header is fixed at the top. (iOS only) (default: true) +* [The other common properties](#Common-Bindable-Properties-GridCollectionView--HCollectionView) ### Special Properties @@ -310,19 +359,7 @@ This is the ListView that lays out each item horizontally. This can make the scr > On iOS, it must be the number of cells enough to fill the container width. > On Android, it could reach each edge if keep scrolling for so long, because it is semi-infinite. - -* ItemTapCommand - * The command invoked when an item is tapped. -* ItemLongTapCommand - * The command invoked when an item is pressed longly. -* TouchFeedbackColor - * The color rendered when an item is touched. -* [ScrollController](#scrollcontroller) - * The object for manipulating the scroll from such as ViewModel. -* GroupFirstSpacing - * The spacing of the first item's left in a group. -* GroupLastSpacing - * The spacing of the last item's right in a group. +* [The other common properties](#Common-Bindable-Properties-GridCollectionView--HCollectionView) ### About Row Height diff --git a/Sample/Sample.iOS/Info.plist b/Sample/Sample.iOS/Info.plist index 724809b..785736d 100644 --- a/Sample/Sample.iOS/Info.plist +++ b/Sample/Sample.iOS/Info.plist @@ -3,9 +3,9 @@ CFBundleDisplayName - Sample + CollectionView CFBundleIdentifier - jp.kamusoft.sample + jp.kamusoft.collectionview CFBundleShortVersionString 1.0 CFBundleVersion @@ -41,7 +41,7 @@ XSAppIconAssets Resources/Images.xcassets/AppIcons.appiconset CFBundleName - Sample + collectionview CFBundleAllowMixedLocalizations diff --git a/Sample/Sample/ViewModels/CollectionViewTestViewModel.cs b/Sample/Sample/ViewModels/CollectionViewTestViewModel.cs index f62db08..951b3f9 100644 --- a/Sample/Sample/ViewModels/CollectionViewTestViewModel.cs +++ b/Sample/Sample/ViewModels/CollectionViewTestViewModel.cs @@ -29,6 +29,7 @@ public class CollectionViewTestViewModel:BindableBase, INavigatingAware public ReactiveCommand MoveCommand { get; set; } = new ReactiveCommand(); public ReactiveCommand NextCommand { get; set; } = new ReactiveCommand(); public ReactiveCommand RepeatCommand { get; set; } = new ReactiveCommand(); + public ReactiveCommand LoadMoreCommand { get; set; } = new ReactiveCommand(); public ReactivePropertySlim HeaderHeight { get; } = new ReactivePropertySlim(36); public ReactivePropertySlim Background { get; } = new ReactivePropertySlim(Color.Transparent); @@ -49,6 +50,7 @@ public class CollectionViewTestViewModel:BindableBase, INavigatingAware public ReactivePropertySlim GroupLastSpacing { get; } = new ReactivePropertySlim(0); public ReactivePropertySlim IsGroupHeaderSticky { get; } = new ReactivePropertySlim(true); public ReactivePropertySlim BothSidesMargin { get; } = new ReactivePropertySlim(0); + public Action SetEndLoadMore { get; set; } public List TestSections { get; set; } IEnumerator _testEnumerator; diff --git a/Sample/Sample/ViewModels/DemoPageViewModel.cs b/Sample/Sample/ViewModels/DemoPageViewModel.cs index 4473112..f7bb603 100644 --- a/Sample/Sample/ViewModels/DemoPageViewModel.cs +++ b/Sample/Sample/ViewModels/DemoPageViewModel.cs @@ -20,6 +20,8 @@ public class DemoPageViewModel public ReactiveProperty IsRefreshing { get; } = new ReactiveProperty(false); public AsyncReactiveCommand RefreshCommand { get; } = new AsyncReactiveCommand(); public AsyncReactiveCommand NextCommand { get; } = new AsyncReactiveCommand(); + public ReactiveCommand LoadMoreCommand { get; } = new ReactiveCommand(); + public ReactiveCommand LoadMoreHCommand { get; } = new ReactiveCommand(); public TestCollection TestList { get; set; } = new TestCollection(); @@ -38,6 +40,9 @@ public class DemoPageViewModel public ReactivePropertySlim AdditionalHeight { get; } = new ReactivePropertySlim(0); public ReactivePropertySlim IsInfinite { get; } = new ReactivePropertySlim(false); + public Action SetEndLoadMore { get;set; } + public Action SetEndLoadMoreH { get; set; } + public IScrollController ScrollController { get; set; } public IScrollController ScrollControllerH { get; set; } @@ -106,6 +111,56 @@ public DemoPageViewModel(IPageDialogService pageDialog, IToast toast) NextCommand.Subscribe(NextAction); + var loadCount = 1; + LoadMoreCommand.Subscribe(_ => + { + if( loadCount == 10 ) + { + SetEndLoadMore(true); + return; + } + var list = new List(); + for (var i = 5; i < 20; i++) + { + list.Add(new PhotoItem + { + PhotoUrl = $"https://kamusoft.jp/openimage/nativecell/{i + 1}.jpg", + Title = $"Title {i + 1}", + Category = "XXX", + }); + } + + var group = new PhotoGroup(list) { Head = $"SectionX{loadCount}" }; + ItemsSource.Add(group); + SetEndLoadMore(false); + loadCount++; + }); + + var loadHCount = 1; + LoadMoreHCommand.Subscribe(_ => + { + if(loadHCount == 10) + { + SetEndLoadMoreH(true); + return; + } + var list = new List(); + for (var i = 5; i < 20; i++) + { + list.Add(new PhotoItem + { + PhotoUrl = $"https://kamusoft.jp/openimage/nativecell/{i + 1}.jpg", + Title = $"Title {i + 1}", + Category = "XXX", + }); + } + + var group = new PhotoGroup(list) { Head = $"SectionX{loadHCount}" }; + ItemsSourceH.Add(group); + SetEndLoadMoreH(false); + loadHCount++; + }); + SetDemoItems(); } diff --git a/Sample/Sample/ViewModels/GridGroupTestIndexViewModel.cs b/Sample/Sample/ViewModels/GridGroupTestIndexViewModel.cs index e329827..77c964e 100644 --- a/Sample/Sample/ViewModels/GridGroupTestIndexViewModel.cs +++ b/Sample/Sample/ViewModels/GridGroupTestIndexViewModel.cs @@ -23,6 +23,7 @@ public GridGroupTestIndexViewModel(INavigationService navigationService) Section.Add(new PullToRefreshTest()); Section.Add(new RowSpacingAndHeightTest()); Section.Add(new ColorTest()); + Section.Add(new LoadMoreGroupTest()); TestSections.Add(Section); diff --git a/Sample/Sample/ViewModels/GridTestIndexViewModel.cs b/Sample/Sample/ViewModels/GridTestIndexViewModel.cs index 90d8c95..c89de59 100644 --- a/Sample/Sample/ViewModels/GridTestIndexViewModel.cs +++ b/Sample/Sample/ViewModels/GridTestIndexViewModel.cs @@ -22,6 +22,7 @@ public GridTestIndexViewModel(INavigationService navigationService) Section.Add(new PullToRefreshTest()); Section.Add(new RowSpacingAndHeightTest()); Section.Add(new ColorTest()); + Section.Add(new LoadMoreTest()); TestSections.Add(Section); diff --git a/Sample/Sample/ViewModels/Tests/LoadMoreGroupTest.cs b/Sample/Sample/ViewModels/Tests/LoadMoreGroupTest.cs new file mode 100644 index 0000000..c48cfa0 --- /dev/null +++ b/Sample/Sample/ViewModels/Tests/LoadMoreGroupTest.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AiForms.Renderers; + +namespace Sample.ViewModels.Tests +{ + public class LoadMoreGroupTest:TestGroup + { + IScrollController ScrollController => VM.ScrollController; + IDisposable loadMoreSub; + + + public LoadMoreGroupTest():base("LoadMore") + { + } + + public override void SetUp() + { + base.SetUp(); + loadMoreSub?.Dispose(); + } + + void CommandLoadMoreItems() + { + + for (var i = 0; i < 10; i++) + { + VM.ItemsGroupSource[2].Add(new PhotoItem + { + PhotoUrl = $"https://kamusoft.jp/openimage/nativecell/{i + 1}.jpg", + Title = $"P1 {i + 1}", + Category = "AAA", + }); + } + } + + void CommandLoadMoreGroup() + { + var list = new List(); + for (var i = 5; i < 15; i++) + { + list.Add(new PhotoItem + { + PhotoUrl = $"https://kamusoft.jp/openimage/nativecell/{i + 1}.jpg", + Title = $"P2 {i + 1}", + Category = "DDD", + }); + } + + VM.ItemsGroupSource.Add(new PhotoGroup(list) { Head = "MoreSection" }); + } + + [Test(Message = "LoadMore 10 Items And Complete LoadMore.")] + public async void LoadMoreItems() + { + loadMoreSub = VM.LoadMoreCommand.Subscribe(CommandLoadMoreItems); + + + ScrollController.ScrollToEnd(true); + await Task.Delay(500); + ScrollController.ScrollToEnd(true); + } + + [Test(Message = "LoadMoreCompletion reset And LoadMore 1 Group")] + public async void LoadMoreGroup() + { + ScrollController.ScrollToStart(true); + await Task.Delay(500); + + VM.SetEndLoadMore(false); + loadMoreSub.Dispose(); + loadMoreSub = VM.LoadMoreCommand.Subscribe(CommandLoadMoreGroup); + + ScrollController.ScrollToEnd(true); + await Task.Delay(500); + ScrollController.ScrollToEnd(true); + } + } +} diff --git a/Sample/Sample/ViewModels/Tests/LoadMoreTest.cs b/Sample/Sample/ViewModels/Tests/LoadMoreTest.cs new file mode 100644 index 0000000..f140ad3 --- /dev/null +++ b/Sample/Sample/ViewModels/Tests/LoadMoreTest.cs @@ -0,0 +1,67 @@ +using System; +using AiForms.Renderers; +using System.Threading.Tasks; + +namespace Sample.ViewModels.Tests +{ + public class LoadMoreTest:TestGroup + { + IScrollController ScrollController => VM.ScrollController; + int _pageCount = 1; + public LoadMoreTest():base("LoadMore"){} + + public override void SetUp() + { + base.SetUp(); + _pageCount = 1; + + VM.LoadMoreCommand.Subscribe(_ => + { + if(_pageCount == 3) + { + VM.SetEndLoadMore(true); + return; + } + + for (var i = 0; i < 10; i++) + { + VM.ItemsSource.Add(new PhotoItem + { + PhotoUrl = $"https://kamusoft.jp/openimage/nativecell/{i + 1}.jpg", + Title = $"P{_pageCount} {i + 1}", + Category = "AAA", + }); + } + VM.SetEndLoadMore(false); + _pageCount++; + }); + } + + [Test(Message = "LoadMore 20 Items And LoadMore Complete")] + public async void LoadMore() + { + + ScrollController.ScrollToEnd(true); + await Task.Delay(500); + ScrollController.ScrollToEnd(true); + await Task.Delay(500); + ScrollController.ScrollToEnd(true); + } + + [Test(Message = "LoadMoreCompletion reset And LoadMore 20 Items Again.")] + public async void LoadMoreAgain() + { + ScrollController.ScrollToStart(true); + await Task.Delay(500); + _pageCount = 1; + VM.SetEndLoadMore(false); + ScrollController.ScrollToEnd(true); + await Task.Delay(500); + ScrollController.ScrollToEnd(true); + await Task.Delay(500); + ScrollController.ScrollToEnd(true); + } + + + } +} diff --git a/Sample/Sample/Views/CollectionVIewTest.xaml b/Sample/Sample/Views/CollectionVIewTest.xaml index 9f036d5..f147a91 100644 --- a/Sample/Sample/Views/CollectionVIewTest.xaml +++ b/Sample/Sample/Views/CollectionVIewTest.xaml @@ -1,4 +1,4 @@ - + RecycleElement diff --git a/Sample/Sample/Views/CollectionViewGroupTest.xaml b/Sample/Sample/Views/CollectionViewGroupTest.xaml index ed2ec7c..1e77094 100644 --- a/Sample/Sample/Views/CollectionViewGroupTest.xaml +++ b/Sample/Sample/Views/CollectionViewGroupTest.xaml @@ -1,4 +1,4 @@ - + RecycleElement diff --git a/Sample/Sample/Views/DemoPage.xaml b/Sample/Sample/Views/DemoPage.xaml index 4a41ec1..fa66a02 100644 --- a/Sample/Sample/Views/DemoPage.xaml +++ b/Sample/Sample/Views/DemoPage.xaml @@ -92,6 +92,9 @@ ColumnSpacing="4" RowSpacing="4" ColumnHeight="1.0" + BothSidesMargin="6" + GroupFirstSpacing="0" + GroupLastSpacing="16" ItemTemplate="{StaticResource templateB}" ItemTapCommand="{Binding TapCommand}" ItemLongTapCommand="{Binding LongTapCommand}" diff --git a/Sample/Sample/Views/MainPage.xaml b/Sample/Sample/Views/MainPage.xaml index 30eeed1..8bcad34 100644 --- a/Sample/Sample/Views/MainPage.xaml +++ b/Sample/Sample/Views/MainPage.xaml @@ -1,4 +1,4 @@ - + + AiForms.CollectionView @@ -14,24 +14,21 @@ This has two layouts (GridCollectionView, HorizontalCollectonView). HorizontalCo -## Bug fixes - -* [iOS][GridCollectionView] When a item just exists in ItemsSource, it is arranged at center. -* some bug fixes - -## New Properties +## Changes ### GridCollectionView / HCollectionView -* GroupFirstSpacing – The spacing of the first item's top or left in a group. -* GroupLastSpacing – The spacing of the last item's bottom or right in a group. +* GroupFirstSpacing / GroupLastSpacing – Even if not using the grouping, this spacing is applied. -### GridCollectionView +## New Bindable Properties + +### GridCollectionView / HCollectionView -* BothSidesMargin – The margin of the right and left sides in the content area except for a group header cell. -* IsGroupHeaderSticky – Whether a group header is fixed at the top. (iOS only) (default: true) +* LoadMoreCommand – When detecting appearing the last item, this command is invoked. +* LoadMoreMargin – The number of items from the last as the threshold value to invoke the LoadMoreCommand. +* SetLoadMoreCompletion – If it continues using the LoadMore after doing some process, set false; Otherwise set true. - Xamarin.Forms ListView CollectionView Grid Infinite Circular Scroll UICollectionView RecyclerView FlowLayout WrapLayout HorizontalListView + Xamarin.Forms ListView CollectionView Grid Infinite Circular Scroll UICollectionView RecyclerView FlowLayout WrapLayout HorizontalListView LoadMore en-US