Grouped ListView, Grouped TileView

You can create grouped ListView with GroupedList<TItem> (group items does not exists and will be automatically created) or LinearGroupedList<TItem> (group items already exists in DataSource).

Grouped ListView

public class GroupedItem
{
   public string Name;
   public bool IsGroup = false;
   public bool IsEmpty = false;
}

public class GroupedItems : GroupedList<GroupedItem>
{
   /// <summary>
   /// Get group for specified item.
   /// </summary>
   /// <param name="item">Item.</param>
   /// <returns>Group for specified item.</returns>
   protected override GroupedItem GetGroup(GroupedItem item)
   {
      var name = item.Name.Length > 0 ? item.Name[0].ToString() : string.Empty;

      foreach (var key in GroupsWithItems.Keys)
      {
         if (key.Name == name)
         {
            return key;
         }
      }

      return new GroupedItem() { Name = name, IsGroup = true, };
   }
}

public class GroupedView : ListViewCustom<GroupedListViewComponent, GroupedItem>
{
   // GroupedData used to add and remove items instead of the DataSource.
   public GroupedItems GroupedData = new GroupedItems();

   bool isGroupedViewInited;

   public override void Init()
   {
      if (isGroupedViewInited)
      {
         return;
      }

      isGroupedViewInited = true;

      base.Init();

      GroupedData.GroupComparison = (x, y) => x.Name.CompareTo(y.Name);
      GroupedData.Data = DataSource;

      CanSelect = index => !DataSource[index].IsGroup;
   }
}

Grouped TileView

using UIWidgets;
using UnityEngine;

public class GroupedTileView : ListViewCustom<GroupedListViewComponent, GroupedItem>
{
   public GroupedItems GroupedData = new GroupedItems();

   [SerializeField]
   protected GroupedListViewComponent HeaderTemplate;

   [SerializeField]
   protected GroupedListViewComponent HeaderEmptyTemplate;

   [SerializeField]
   protected GroupedListViewComponent ItemTemplate;

   [SerializeField]
   protected GroupedListViewComponent ItemEmptyTemplate;

   class Selector : IListViewTemplateSelector<GroupedListViewComponent, GroupedItem>
   {
      GroupedListViewComponent headerTemplate;

      GroupedListViewComponent headerEmptyTemplate;

      GroupedListViewComponent itemTemplate;

      GroupedListViewComponent itemEmptyTemplate;

      GroupedListViewComponent[] templates;

      public Selector(
         GroupedListViewComponent headerTemplate,
         GroupedListViewComponent headerEmptyTemplate,
         GroupedListViewComponent itemTemplate,
         GroupedListViewComponent itemEmptyTemplate)
      {
         this.headerTemplate = headerTemplate;
         this.headerEmptyTemplate = headerEmptyTemplate;
         this.itemTemplate = itemTemplate;
         this.itemEmptyTemplate = itemEmptyTemplate;

         templates = new[] { this.headerTemplate, this.headerEmptyTemplate, this.itemTemplate, this.itemEmptyTemplate, };
      }

      public GroupedListViewComponent[] AllTemplates() => templates;

      public GroupedListViewComponent Select(int index, GroupedItem item)
      {
         if (item.IsGroup)
         {
            return item.IsEmpty ? headerEmptyTemplate : headerTemplate;
         }
         else
         {
            return item.IsEmpty ? itemEmptyTemplate : itemTemplate;
         }
      }
   }

   bool isGroupedListViewInited;

   public override void Init()
   {
      if (isGroupedListViewInited)
      {
         return;
      }

      isGroupedListViewInited = true;

      TemplateSelector = new Selector(HeaderTemplate, HeaderEmptyTemplate, ItemTemplate, ItemEmptyTemplate);

      base.Init();

      GroupedData.GroupComparison = (x, y) => x.Created.CompareTo(y.Created);
      GroupedData.Data = DataSource;

      GroupedData.EmptyGroupItem = new Photo() { IsGroup = true, IsEmpty = true };
      GroupedData.EmptyItem = new Photo() { IsEmpty = true };
      GroupedData.ItemsPerBlock = ListRenderer.GetItemsPerBlock();
   }

   public override void UpdateItems()
   {
      base.UpdateItems();

      GroupedData.ItemsPerBlock = ListRenderer.GetItemsPerBlock();
   }

   public override void Resize()
   {
      base.Resize();

      GroupedData.ItemsPerBlock = ListRenderer.GetItemsPerBlock();
   }
}

Linear GroupedTileView

public class LinearGroupedTileView : ListViewCustom<GroupedListViewComponent, GroupedItem>
{
   // Real DataSource (use instead of DataSource).
   public ObservableList<GroupedItem> RealDataSource = new ObservableList<GroupedItem>();

   public LinearGroupedList<GroupedItem> GroupedData = new LinearGroupedList<GroupedItem>(x => x.IsGroup);

   [SerializeField]
   protected GroupedListViewComponent HeaderTemplate;

   [SerializeField]
   protected GroupedListViewComponent HeaderEmptyTemplate;

   [SerializeField]
   protected GroupedListViewComponent ItemTemplate;

   [SerializeField]
   protected GroupedListViewComponent ItemEmptyTemplate;

   class Selector : IListViewTemplateSelector<GroupedListViewComponent, GroupedItem>
   {
      GroupedListViewComponent headerTemplate;

      GroupedListViewComponent headerEmptyTemplate;

      GroupedListViewComponent itemTemplate;

      GroupedListViewComponent itemEmptyTemplate;

      GroupedListViewComponent[] templates;

      public Selector(
         GroupedListViewComponent headerTemplate,
         GroupedListViewComponent headerEmptyTemplate,
         GroupedListViewComponent itemTemplate,
         GroupedListViewComponent itemEmptyTemplate)
      {
         this.headerTemplate = headerTemplate;
         this.headerEmptyTemplate = headerEmptyTemplate;
         this.itemTemplate = itemTemplate;
         this.itemEmptyTemplate = itemEmptyTemplate;

         templates = new[] { this.headerTemplate, this.headerEmptyTemplate, this.itemTemplate, this.itemEmptyTemplate, };
      }

      public GroupedListViewComponent[] AllTemplates() => templates;

      public GroupedListViewComponent Select(int index, GroupedItem item)
      {
         if (item.IsGroup)
         {
            return item.IsEmpty ? headerEmptyTemplate : headerTemplate;
         }
         else
         {
            return item.IsEmpty ? itemEmptyTemplate : itemTemplate;
         }
      }
   }

   bool isGroupedListViewInited;

   public override void Init()
   {
      if (isGroupedListViewInited)
      {
         return;
      }

      isGroupedListViewInited = true;

      TemplateSelector = new Selector(HeaderTemplate, HeaderEmptyTemplate, ItemTemplate, ItemEmptyTemplate);

      base.Init();

      GroupedData.EmptyHeaderItem = new GroupedItem() { IsGroup = true, IsEmpty = true };
      GroupedData.EmptyItem = new GroupedItem() { IsEmpty = true };
      GroupedData.ItemsPerBlock = ListRenderer.GetItemsPerBlock();

      GroupedData.Input = RealDataSource;
      GroupedData.Output = DataSource;
   }

   public override void UpdateItems()
   {
      base.UpdateItems();

      GroupedData.ItemsPerBlock = ListRenderer.GetItemsPerBlock();
   }

   public override void Resize()
   {
      base.Resize();

      GroupedData.ItemsPerBlock = ListRenderer.GetItemsPerBlock();
   }
}