.NET MAUI  

Horizontal List of Overlapping Images in MAUI [GamesCatalog] - Part 14

Previous part: Asynchronously Grouping Totals in MAUI [GamesCatalog] - Part 13

Step 1. In MainVM.cs, we’ll create the 3 lists that will hold the images.

    private ObservableCollection<UIHorizontalColViewGroupedImage> lastFiveIGDBIdsByUpdatedAtWant = [],
        lastFiveIGDBIdsByUpdatedAtPlaying = [], lastFiveIGDBIdsByUpdatedAtPlayed = [];

    public ObservableCollection<UIHorizontalColViewGroupedImage> LastFiveIGDBIdsByUpdatedAtWant
    {
        get => lastFiveIGDBIdsByUpdatedAtWant;
        set { if (value != lastFiveIGDBIdsByUpdatedAtWant) SetProperty(ref (lastFiveIGDBIdsByUpdatedAtWant), value); }
    }

    public ObservableCollection<UIHorizontalColViewGroupedImage> LastFiveIGDBIdsByUpdatedAtPlaying
    {
        get => lastFiveIGDBIdsByUpdatedAtPlaying;
        set { if (value != lastFiveIGDBIdsByUpdatedAtPlaying) SetProperty(ref (lastFiveIGDBIdsByUpdatedAtPlaying), value); }
    }

    public ObservableCollection<UIHorizontalColViewGroupedImage> LastFiveIGDBIdsByUpdatedAtPlayed
    {
        get => lastFiveIGDBIdsByUpdatedAtPlayed;
        set { if (value != lastFiveIGDBIdsByUpdatedAtPlayed) SetProperty(ref (lastFiveIGDBIdsByUpdatedAtPlayed), value); }
    }

Step 1.1. Let’s create the model for the image list.

namespace GamesCatalog.Models
{
    public record UIHorizontalColViewGroupedImage(string Url, Thickness Margin);
}

Step 2. We’ll create arrays to manage the modification of the lists.

    private string?[] CacheLastFiveIGDBIdsByUpdatedAtWant = [], CacheLastFiveIGDBIdsByUpdatedAtPlaying = [], CacheLastFiveIGDBIdsByUpdatedAtPlayed = [];

Step 3. Let’s create a function that builds the image lists for the screen.

    public void BuildColView(List<TotalGroupedByStatus> totalsGroupedByStatus, GameStatus gameStatus, ref ObservableCollection<UIHorizontalColViewGroupedImage> lastFiveIGDBIdsByUpdatedAt, ref string?[] lastFiveIGDBIdsByUpdatedAtCache)
    {
        TotalGroupedByStatus? totalsGrouped = totalsGroupedByStatus.FirstOrDefault(x => x.Status == gameStatus);

        switch (gameStatus)
        {
            case GameStatus.Want:
                QtdWant = totalsGrouped?.Total ?? 0;
                break;
            case GameStatus.Playing:
                QtdPlaying = totalsGrouped?.Total ?? 0;
                break;
            case GameStatus.Played:
                QtdPlayed = totalsGrouped?.Total ?? 0;
                break;
        }

        string?[] _lastFiveIGDBIdsByUpdatedAt = totalsGrouped?.LastFiveIGDBIdsByUpdatedAt ?? [];

        if (_lastFiveIGDBIdsByUpdatedAt is null or [])
        {
            MainVM.ClearColView(ref lastFiveIGDBIdsByUpdatedAt);

            lastFiveIGDBIdsByUpdatedAtCache = [];

            return;
        }

        if (!lastFiveIGDBIdsByUpdatedAtCache.SequenceEqual(_lastFiveIGDBIdsByUpdatedAt))
        {
            MainVM.ClearColView(ref lastFiveIGDBIdsByUpdatedAt);

            bool isFisrt = true;

            foreach (var item in _lastFiveIGDBIdsByUpdatedAt)
            {
                if (isFisrt)
                {
                    lastFiveIGDBIdsByUpdatedAt.Add(new UIHorizontalColViewGroupedImage(Path.Combine(GameService.ImagesPath, $"{item}.jpg"), new Thickness(0)));
                    isFisrt = false;
                }
                else
                    lastFiveIGDBIdsByUpdatedAt.Add(new UIHorizontalColViewGroupedImage(Path.Combine(GameService.ImagesPath, $"{item}.jpg"), new Thickness(-45, 0, 0, 0)));
            }
        }

        lastFiveIGDBIdsByUpdatedAtCache = _lastFiveIGDBIdsByUpdatedAt;
    }

This function separates the status group, sets the total, and manages the creation of the list, which will be updated if there are any changes in the array of IDs for the last 5 games. The list contains 5 partially overlapping images.

Step 4. Let’s create a function that clears the list.

   private static void ClearColView(ref ObservableCollection<UIHorizontalColViewGroupedImage> lastFiveIGDBIdsByUpdatedAt)
   {
       while (lastFiveIGDBIdsByUpdatedAt.Count > 0)
           lastFiveIGDBIdsByUpdatedAt.RemoveAt(lastFiveIGDBIdsByUpdatedAt.Count - 1);
   }

Step 5. We’ll modify the SetTotalsGroupedByStatus() function to handle the lists asynchronously.

    public async Task SetTotalsGroupedByStatus()
    {
        await SetTotalsGroupedByStatusSemaphore.WaitAsync();

        try
        {
            List<TotalGroupedByStatus>? totalsGroupedByStatus = await gameService.GetTotalsGroupedByStatus();

            if (totalsGroupedByStatus is not null && totalsGroupedByStatus.Count > 0)
            {
                List<Task> tasks = [
                    Task.Run(() => BuildColView(totalsGroupedByStatus, GameStatus.Want, ref lastFiveIGDBIdsByUpdatedAtWant, ref CacheLastFiveIGDBIdsByUpdatedAtWant)),
                    Task.Run(() => BuildColView(totalsGroupedByStatus, GameStatus.Playing, ref lastFiveIGDBIdsByUpdatedAtPlaying, ref CacheLastFiveIGDBIdsByUpdatedAtPlaying)),
                    Task.Run(() => BuildColView(totalsGroupedByStatus, GameStatus.Played, ref lastFiveIGDBIdsByUpdatedAtPlayed, ref CacheLastFiveIGDBIdsByUpdatedAtPlayed))
                ];

                await Task.WhenAll(tasks);
            }
            else
            {
                QtdPlayed = QtdPlaying = QtdWant = 0;

                MainVM.ClearColView(ref lastFiveIGDBIdsByUpdatedAtWant);
                MainVM.ClearColView(ref lastFiveIGDBIdsByUpdatedAtPlaying);
                MainVM.ClearColView(ref lastFiveIGDBIdsByUpdatedAtPlayed);
            }
        }
        catch (Exception ex) { throw ex; }
        finally
        {
            SetTotalsGroupedByStatusSemaphore.Release();
        }
    }

The three lists will be processed in parallel.

Attached is the full code for the MainVM.cs class.

Step 6. Let’s add a CollectionView with horizontal scrolling to display the images.

CollectionView

Code

    <CollectionView
        Margin="0"
        FlowDirection="RightToLeft"
        HorizontalOptions="Start"
        ItemsLayout="HorizontalList"
        ItemsSource="{Binding LastFiveIGDBIdsByUpdatedAtWant}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="model:UIHorizontalColViewGroupedImage">
                <AbsoluteLayout
                    Margin="0"
                    Padding="0"
                    IsClippedToBounds="False">
                    <Image
                        Margin="{Binding Margin}"
                        Aspect="Fill"
                        BackgroundColor="LightGray"
                        HeightRequest="136"
                        Source="{Binding Url}"
                        WidthRequest="100" />
                </AbsoluteLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>

Step 6.1. In the other two grids, add the CollectionView components for the "to play" and "played" statuses.

Played

Code

      <CollectionView
          Margin="0"
          FlowDirection="RightToLeft"
          HorizontalOptions="Start"
          ItemsLayout="HorizontalList"
          ItemsSource="{Binding LastFiveIGDBIdsByUpdatedAtPlaying}">
          <CollectionView.ItemTemplate>
              <DataTemplate x:DataType="model:UIHorizontalColViewGroupedImage">
                  <AbsoluteLayout
                      Margin="0"
                      Padding="0"
                      IsClippedToBounds="False">
                      <Image
                          Margin="{Binding Margin}"
                          Aspect="Fill"
                          BackgroundColor="LightGray"
                          HeightRequest="136"
                          Source="{Binding Url}"
                          WidthRequest="100" />
                  </AbsoluteLayout>
              </DataTemplate>
          </CollectionView.ItemTemplate>
      </CollectionView>

Code

Code

    <CollectionView
        Margin="0"
        FlowDirection="RightToLeft"
        HorizontalOptions="Start"
        ItemsLayout="HorizontalList"
        ItemsSource="{Binding LastFiveIGDBIdsByUpdatedAtPlayed}"
        ItemsUpdatingScrollMode="KeepScrollOffset">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="model:UIHorizontalColViewGroupedImage">
                <AbsoluteLayout
                    Margin="0"
                    Padding="0"
                    IsClippedToBounds="False">
                    <Image
                        Margin="{Binding Margin}"
                        Aspect="Fill"
                        BackgroundColor="LightGray"
                        HeightRequest="136"
                        Source="{Binding Url}"
                        WidthRequest="100" />
                </AbsoluteLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>

Step 7. Now we have our lists showing the last 5 games for each status on the screen.

Output

In the next step, we will create the listing screen for our games saved in the local database, organized by status.

Code on git: GamesCatalog