Sharpnado.CollectionView 2.0 is reborn with header/footer and drag and drop

Sharpnado.CollectionView 2.0 is reborn with header/footer and drag and drop
https://github.com/roubachof/Sharpnado.CollectionView

Version 2.0 breaking changes: no more HorizontalListView

The mighty Xamarin.Forms HorizontalListView has finally been renamed CollectionView \o/.

Historically, the HorizontalListView, was just uh, and horizontal list view :)
But thanks to the power of UICollectionView and RecyclerView, I quickly extended it with grid layout, list layout, drag and drop, and now with footers/headers.
At this point, the name was pretty misleading, so I had to

All references to HorizontalList has been renamed to Collection, including:

  • namespaces
  • filename
  • class names
  • HorizontalListViewLayout => CollectionViewLayout
  • ListLayout => CollectionLayout

Features

For the newcomers, the sharpnado's CollectionView is featuring:

  • Horizontal, Grid, Carousel or Vertical layout
  • Drag and Drop
  • 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

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:

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.

public class HeaderFooterGroupingPageViewModel : ANavigableViewModel
{
    public List<IDudeItem> SillyPeople
    {
        get => _sillyPeople;
        set => SetAndRaise(ref _sillyPeople, value);
    }

    private async Task<PageResult<SillyDude>> LoadSillyPeoplePageAsync(int pageNumber, int pageSize, bool isRefresh)
    {
        PageResult<SillyDude> resultPage = await _sillyDudeService.GetSillyPeoplePage(pageNumber, pageSize);

        var dudes = resultPage.Items;

        if (isRefresh)
        {
            SillyPeople = new List<IDudeItem>();
            _listSource = new List<SillyDude>();
        }

        var result = new List<IDudeItem> { 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:

 <ResourceDictionary>
    <DataTemplate x:Key="HeaderTemplate">
        <sho:DraggableViewCell x:Name="DraggableViewCell" IsDraggable="False">

            <ContentView Margin="0" BackgroundColor="{StaticResource DarkerSurface}">
                <Label
                    Style="{StaticResource TextSubhead}"
                    HorizontalOptions="Center"
                    Text="Look at my Nice Header!" />
            </ContentView>
        </sho:DraggableViewCell>
    </DataTemplate>

    <DataTemplate x:Key="FooterTemplate">
        <sho:DraggableViewCell x:Name="DraggableViewCell" IsDraggable="False">
            <StackLayout
                Padding="30,0,15,0"
                Orientation="Horizontal"
                Spacing="15">
                <ActivityIndicator
                    VerticalOptions="Center"
                    IsRunning="True"
                    Color="{StaticResource Accent}" />
                <Label
                    Style="{StaticResource TextSubhead}"
                    VerticalOptions="Center"
                    Text="Loading next dudes..." />
            </StackLayout>
        </sho:DraggableViewCell>
    </DataTemplate>

    <DataTemplate x:Key="GroupHeaderTemplate" x:DataType="viewModels:DudeGroupHeader">
        <sho:DraggableViewCell x:Name="DraggableViewCell" IsDraggable="False">
            <sho:Shadows x:Name="Shadow" Shades="{StaticResource VerticalNeumorphism}">
                <StackLayout
                    Margin="0,15,0,10"
                    Padding="0"
                    BackgroundColor="{StaticResource DarkerSurface}"
                    Orientation="Horizontal"
                    Spacing="0">

                    <Frame
                        WidthRequest="30"
                        HeightRequest="30"
                        Margin="15,0,10,0"
                        Padding="0"
                        HorizontalOptions="End"
                        VerticalOptions="Center"
                        BackgroundColor="{StaticResource Accent}"
                        CornerRadius="10"
                        HasShadow="False">
                        <Label
                            Style="{StaticResource TextTitle}"
                            HorizontalOptions="Center"
                            VerticalOptions="Center"
                            Text="{Binding StarCount}" />
                    </Frame>
                    <Label
                        Style="{StaticResource TextTitle}"
                        VerticalOptions="Center"
                        Text="Stars Dudes" />
                </StackLayout>
            </sho:Shadows>
        </sho:DraggableViewCell>
    </DataTemplate>

    <DataTemplate x:Key="DudeTemplate">
        <sho:DraggableViewCell x:Name="DraggableViewCell">
            <sho:Shadows
                x:Name="Shadow"
                CornerRadius="10"
                Shades="{StaticResource ThinDarkerNeumorphism}">
                <views:SillyListCell
                    Margin="16,13"
                    BackgroundColor="{StaticResource DarkerSurface}"
                    CornerRadius="10">
                    <views:SillyListCell.Triggers>
                        <DataTrigger
                            Binding="{Binding Source={x:Reference DraggableViewCell}, Path=IsDragAndDropping}"
                            TargetType="views:SillyListCell"
                            Value="True">
                            <Setter Property="BackgroundColor" Value="{StaticResource DarkSurface}" />
                        </DataTrigger>
                    </views:SillyListCell.Triggers>
                </views:SillyListCell>
            </sho:Shadows>
        </sho:DraggableViewCell>
    </DataTemplate>
</ResourceDictionary>

The last step is to make the correspondance between our header view models, and our headers data templates.
For that, we declare our DataTemplateSelector:

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 :

<views:HeaderFooterGroupingTemplateSelector
    x:Key="HeaderFooterGroupingTemplateSelector"
    DudeTemplate="{StaticResource DudeTemplate}"
    FooterTemplate="{sho:SizedDataTemplate 
        Template={StaticResource FooterTemplate}, Size=60}"
    GroupHeaderTemplate="{sho:SizedDataTemplate 
        Template={StaticResource GroupHeaderTemplate}, Size=75}"
    HeaderTemplate="{sho:SizedDataTemplate 
        Template={StaticResource HeaderTemplate}, Size=40}" />

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.

You can find this example in the sample project (click on "Header and Grouping Example" button).