A performant list view supporting: grid, horizontal and vertical layout, drag and drop, and reveal animations.
The Unlicense
244
stars
30
forks
source link
readme
Sharpnado.CollectionView(.Maui)
Formerly named HorizontalListView
Get it from NuGet:
| MAUI Supported platforms | XF Supported platforms |
|----------------------------|----------------------------|
| [![Nuget](https://img.shields.io/nuget/v/Sharpnado.CollectionView.Maui.svg)](https://www.nuget.org/packages/Sharpnado.CollectionView.Maui) | [![Nuget](https://img.shields.io/nuget/v/Sharpnado.CollectionView.svg)](https://www.nuget.org/packages/Sharpnado.CollectionView) |
| :heavy_check_mark: Android | :heavy_check_mark: Android |
| :heavy_check_mark: iOS | :heavy_check_mark: iOS |
![Presentation](Docs/maui_banner.png)
## Initialization
### MAUI
* In `MauiProgram.cs`:
```csharp
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp()
.UseSharpnadoCollectionView(loggerEnable: false);
}
```
### Xamarin.Forms
* On Core project in `App.xaml.cs`:
For the namespace schema to work, you need to call initializer from App.xaml.cs like this:
```csharp
public App()
{
InitializeComponent();
Sharpnado.CollectionView.Initializer.Initialize(true, false);
...
}
```
* On `iOS` add this line before `Xamarin.Forms.Forms.Init()` and `LoadApplication(new App())`.
```csharp
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
Initializer.Initialize();
global::Xamarin.Forms.Forms.Init();
LoadApplication(new App());
}
```
* On `Android` add this line before `Xamarin.Forms.Forms.Init()` and `LoadApplication(new App())`.
```csharp
public override OnCreate(Bundle savedInstanceState)
{
Initializer.Initialize();
global::Xamarin.Forms.Forms.Init();
LoadApplication(new App());
}
```
## Version 2.0 breaking changes: CollectionView
`HorizontalListView` has finally been renamed `CollectionView` \o/.
All references to `HorizontalList` has been renamed to `Collection`, including:
* namespaces
* filename
* class names
* HorizontalListViewLayout => CollectionViewLayout
* ListLayout => CollectionLayout
## Presentation
* Horizontal, Grid, Carousel or Vertical layout
* Drag and Drop feature
* Grouping with headers and footers
* Reveal custom animations
* Column count
* Infinite loading with ```Paginator``` component
* Snapping on first or middle element
* Padding and item spacing
* Handles ```NotifyCollectionChangedAction``` Add, Remove and Reset actions
* View recycling
* ```RecyclerView``` on Android
* ```UICollectionView``` on iOS
https://user-images.githubusercontent.com/596903/138864430-108dcbc2-e425-4e2e-a9e1-feb568c0a866.mp4
## Horizontal list layout
```csharp
public CollectionViewLayout CollectionLayout { get; set; } = CollectionViewLayout.Horizontal;
```
By default the layout is in ```Linear``` mode, which means you will have only one row.
You can specify the ```ItemWidth``` and ```ItemHeight```.
You can also specify ```ItemSpacing``` and ```CollectionPadding```.
```xml
...
```
You can also simply use the the `HorizontalListView` class which is a shorthand to set the layout to `Horizontal`.
```xml
```
As you can see ```TapCommand``` and ```TouchFeedbackColor``` (aka Ripple) are brought to you by the awesome effects created by mrxten (https://github.com/mrxten/XamEffects).
It's the best ripple effect plugin so far on Xamarin.Forms since it always worked for me.
With other maybe more known plugins, I had some issues on iOS.
A CollectionView with SnapStyle=Center and ItemWidth/ItemHeight set.
### ColumnCount property
You can also decide to just specify the number of column you want, the ```ColumnCount``` property, and the ```ItemWidth``` will be computed for you.
```xml
```
A CollectionView with ColumnCount=2.
### Carousel Layout
You can set ```ListLayout``` to ```Carousel```.
In this mode you can't specify ```ItemWidth``` (obviously).
If you don't specify the ```ItemHeight```, it will be automatically computed for you.
```xml
...
```
A CollectionView with ListLayout=Carousel.
## Grid Layout
If you set the ```ListLayout``` property to ```Grid```, you will have access to the same properties.
```xml
```
You can use the ```IsDragAndDropping``` property of the ```DraggableViewCell``` to change the background color with a simple ```DataTrigger```.
A GridListLayout with drag and drop enabled.
The ```ColumnCount``` property works also with the grid layout.
## Vertical Layout
You can also use Sharpnado's `CollectionView` like a regular list view.
```xml
```
A CollectionView with ListLayout=Vertical.
Of course drag and drop is also available with this layout.
## Infinite Loading
You can achieve infinite loading really easily by using the ```Paginator``` component, and bind it to the ```InfiniteListLoader``` property.
All is explained here:
https://www.sharpnado.com/paginator-platform-independent/
## Drag and drop
If you want to have both drag and drop enabled and still be able to tap the item, you need to use the ```TapCommand``` on the `CollectionView` instead of the ```xamEffects:Commands.Tap``` on the `DataTemplate` content.
It's less nice since you won't have the nice color ripple, but it will work :)
The only thing you have to do to enable drag and drop is set `EnableDragAndDrop` to `true`.
The `DragAndDropStartCommand` and `DragAndDropEndedCommand` commands will pass as argument a `DragAndDropInfo` object:
```csharp
public class DragAndDropInfo
{
public int To { get; }
public int From { get; }
public object Content { get; }
}
```
Contributor: Implemented by @jmmortega.
## DragAndDropTrigger and DragAndDropDirection
Since 1.8.2, you can now choose if you want to begin the drag and drop with a ``Pan`` gesture or a `LongPress`.
* `DragAndDropTrigger="Pan"`
* `DragAndDropTrigger="LongTap"`
You can also restrict the drag movement to a given direction:
* For the horizontal layout: `DragAndDropDirection = HorizontalOnly`
* For the vertical layout: `DragAndDropDirection = VerticalOnly`
It will give a better more precise drag experience, more precise.
### Since 1.8.1
`EnableDragAndDrop` is now a bindable property, so you can enable it at runtime.
You can now also specify a custom animation when the `EnableDragAndDrop` is set to ture:
```csharp
CollectionView.DragAndDropEnabledAnimationAsync = async (viewCell, token) =>
{
while (!token.IsCancellationRequested)
{
await viewCell.View.RotateTo(8);
await viewCell.View.RotateTo(-8);
}
await viewCell.View.RotateTo(0);
};
```
will result in:
You can decide to start the drag without long press on iOS thanks to the iOS specific property `iOSDragAndDropOnPanGesture`:
```xml
```
**Remark:** You don't have to inherit from `DraggableViewCell`, any `ViewCell` can be dragged.
### DraggableViewCell
The `DraggableViewCell` is useful for using triggers on the `IsDragAndDropping` property (changing its background color or elevation during drag and drop for example).
You can also disable the drag and drop for certain cells thanks to the `IsDraggable` property.
## Headers, groups and footers (only for linear layouts)
Since 2.0, you can assign a size to a `DataTemplate` using the `SizedDataTemplate` markup extension.
This opens the door to the implementation of header/footer/group headers.
All you have to do is to use a `DataTemplateSelector` with `SizedDataTemplate` and set the size of the given `DataTemplate`.
Let's consider the following screen:
In our example, we want, a header, a footer, but also a group header (items are grouped by silliness degree, their "star" rating).
So we will be using inheritance on the view model side to achieve that:
```csharp
namespace DragAndDropSample.ViewModels
{
public interface IDudeItem
{
}
public class DudeHeader : IDudeItem
{
}
public class DudeFooter : IDudeItem
{
}
public class DudeGroupHeader : IDudeItem
{
public int StarCount { get; set; }
public string Text => $"{StarCount} Stars";
}
public class SillyDudeVmo : IDudeItem
{
public SillyDudeVmo(SillyDude dude, ICommand tapCommand)
{
if (dude != null)
{
Id = dude.Id;
Name = dude.Name;
FullName = dude.FullName;
Role = dude.Role;
Description = dude.Description;
ImageUrl = dude.ImageUrl;
SillinessDegree = dude.SillinessDegree;
SourceUrl = dude.SourceUrl;
}
TapCommand = tapCommand;
}
public bool IsMovable { get; protected set; } = true;
public ICommand TapCommand { get; set; }
public int Id { get; }
public string Name { get; }
public string FullName { get; }
public string Role { get; }
public string Description { get; }
public string ImageUrl { get; }
public double SillinessDegree { get; }
public string SourceUrl { get; }
public override string ToString()
{
return $"{FullName} silly degree: {SillinessDegree}";
}
}
}
```
Then after sorting our collection by rating, we will bind our `CollectionView` to the SillyPeople list.
```csharp
public class HeaderFooterGroupingPageViewModel : ANavigableViewModel
{
public List SillyPeople
{
get => _sillyPeople;
set => SetAndRaise(ref _sillyPeople, value);
}
private async Task> LoadSillyPeoplePageAsync(int pageNumber, int pageSize, bool isRefresh)
{
PageResult resultPage = await _sillyDudeService.GetSillyPeoplePage(pageNumber, pageSize);
var dudes = resultPage.Items;
if (isRefresh)
{
SillyPeople = new List();
_listSource = new List();
}
var result = new List { new DudeHeader() };
_listSource.AddRange(dudes);
foreach (var group in _listSource.OrderByDescending(d => d.SillinessDegree)
.GroupBy((dude) => dude.SillinessDegree))
{
result.Add(new DudeGroupHeader { StarCount = group.Key});
result.AddRange(group.Select(dude => new SillyDudeVmo(dude, TapCommand)));
}
result.Add(new DudeFooter());
SillyPeople = result;
}
}
```
Thanks god for Linq!
You can see how easy it is to order and create our header view models.
Now let's switch to the XAML world!
We create a template for each of our header types:
```xml
```
The last step is to make the correspondance between our header view models, and our headers data templates.
For that, we declare our `DataTemplateSelector`:
```csharp
public class HeaderFooterGroupingTemplateSelector: DataTemplateSelector
{
public SizedDataTemplate HeaderTemplate { get; set; }
public SizedDataTemplate FooterTemplate { get; set; }
public SizedDataTemplate GroupHeaderTemplate { get; set; }
public DataTemplate DudeTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
switch (item)
{
case DudeHeader header:
return HeaderTemplate;
case DudeFooter footer:
return FooterTemplate;
case DudeGroupHeader groupHeader:
return GroupHeaderTemplate;
default:
return DudeTemplate;
}
}
}
```
You can see that all the headers (all the data template with an associated size in fact) need to be a `SizedDataTemplate`.
Then we just assign a fixed size to each template when we declare our `DataTemplateSelector` :
```xml
```
We don't have to assign a size to our item template (here the silly dude), it will pick the `ItemWidth` (for an horizontal layout) or `ItemHeight` (for a vertical one) size.
https://user-images.githubusercontent.com/596903/138863695-1a3c426f-6d3c-4096-a743-20392fe9db3c.mp4
You can find this example in the sample project (click on "Header and Grouping Example" button).
## Reveal animations
Contributor: original idea from @jmmortega.
You can set custom animations on cells that will be triggered when a cell appears for the first time.
*Properties for reveal animations*
```csharp
public Func PreRevealAnimationAsync { get; set; }
public Func RevealAnimationAsync { get; set; }
public Func PostRevealAnimationAsync { get; set; }
```
In the following example I flip the cell on the vertical axis and fade them for grid and linear layout. And flip the cell on the horizontal axis for vertical layout.
*GridPage.xaml.cs*
```csharp
public partial class GridPage : ContentPage
{
public GridPage()
{
InitializeComponent();
CollectionView.PreRevealAnimationAsync = async (viewCell) =>
{
viewCell.View.Opacity = 0;
if (CollectionView.CollectionLayout == CollectionViewLayout.Vertical)
{
viewCell.View.RotationX = 90;
}
else
{
viewCell.View.RotationY = -90;
}
};
CollectionView.RevealAnimationAsync = async (viewCell) =>
{
await viewCell.View.FadeTo(1);
if (CollectionView.CollectionLayout == CollectionViewLayout.Vertical)
{
await viewCell.View.RotateXTo(0);
}
else
{
await viewCell.View.RotateYTo(0);
}
};
}
}
```
## Others properties
### Properties available with both layout mode
```csharp
public static readonly BindableProperty ListLayoutProperty = BindableProperty.Create(
nameof(ListLayout),
typeof(CollectionViewLayout),
typeof(CollectionView),
CollectionViewLayout.Linear,
propertyChanged: OnListLayoutChanged,
propertyChanging: OnListLayoutChanging);
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(
nameof(ItemsSource),
typeof(IEnumerable),
typeof(CollectionView),
default(IEnumerable