/ XAMARIN

Avatar Groups

Overlapping avatar badges are an effective way to show a collection of people, especially where space is at a premium. In this article we look at how to create this UI in Xamarin.Forms and MAUI.

Along the way, we will touch on a few other interesting concepts like:

  • Bindable Layouts
  • DataTemplate Selectors
  • MultiValue Converters

The code for this article is available over on my Github.

Let’s start at the beginning

The first thing we want is to have some data which is going be exposed via a simple Model and ViewModel.

Person Model

public class Person
{
    // whatever properties you have for a person
    // but the most important one we will use is
    // the image for the person
    public string Image { get; set; }
}

PeopleViewModel

We are going to support adding and removing people at runtime, which means we have an AddUserCommand, a RemoveUserCommand, and we expose the data through an ObservableCollection.

public class PeopleViewModel : BaseViewModel
{
    public ICommand AddUserCommand { get; private set; }
    public ICommand RemoveUserCommand { get; private set; }

    public ObservableCollection<Person> People { get; set; } = new ObservableCollection<Person>();

    public PeopleViewModel()
    {
        // setup the commands
        AddUserCommand = new Command(AddPerson);
        RemoveUserCommand = new Command(RemovePerson);
        CreateSampleData();
    }

    private void CreateSampleData()
    {
        // create 5 peeps
        for (int i = 0; i < 5; i++)
        {
            AddPerson();
        }
    }

    private void AddPerson()
    {
        // add a person using an Image from pravatar.cc
        People.Add(new Person { Image = $"https://i.pravatar.cc/64?img={People.Count+1}" });
    }

    private void RemovePerson()
    {
        if (People.Any())
        {
            People.Remove(People.Last());
        }
    }
}

TIP: There are some nice services you can use for mock random avatars. Here are a few you I’ve use for mockups and design work.

Show me the avatars

We can start out the UI simply by using a horizontal StackLayout and the BindableLayout attached extension.

BindableLayouts give you the ability to have a Layout generate its content by binding to a collection of items.

<StackLayout
    BindableLayout.ItemsSource="{Binding People}"
    HorizontalOptions="CenterAndExpand"
    Orientation="Horizontal"
    VerticalOptions="Center">

    <BindableLayout.ItemTemplate>
        <DataTemplate>
            <Frame
                Margin="-20,0,0,0"
                Padding="0"
                CornerRadius="24"
                HeightRequest="48"
                IsClippedToBounds="True"
                WidthRequest="48">
                <Image Source="{Binding Image}" />
            </Frame>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
    
</StackLayout>

There are a couple of interesting things in that markup.

  • Negative Margins - This allows us to overlap the elements, notice the Margin="-20,0,0,0".
  • Circular images - There are a number of different ways of doing this, but a simple Frame with a CornerRadius does the job.

And that’s going to give us a pretty decent looking group of overlapping avatars.

Simple Avatars

Another good option here is to use the AvatarView in the Xamarin Community Toolkit. The cool thing about the AvatarView is that it handles showing initials for avatars if it can’t load an image.

Improving the look

For our simple avatars, we can make them look a little more dynamic by giving the illusion of the avatars cutting into each other. Like this:

Cutout Avatars

We can achieve this effect in a number of ways, but an easy way would be to have a thick border (frame) around each avatar using the background color (in our case white). Also, it’s probably time to introduce a few styles for consistency and reuse as well.

...
<ResourceDictionary>
    <Style x:Key="avatarFrame" TargetType="Frame">
        <Setter Property="Margin" Value="-20,0,0,0" />
        <Setter Property="Padding" Value="0" />
        <Setter Property="BackgroundColor" Value="White" />
        <Setter Property="CornerRadius" Value="24" />
        <Setter Property="HasShadow" Value="False" />
        <Setter Property="HeightRequest" Value="48" />
        <Setter Property="VerticalOptions" Value="Start" />
        <Setter Property="WidthRequest" Value="48" />
    </Style>
    <Style x:Key="contentFrame" TargetType="Frame">
        <Setter Property="Padding" Value="0" />
        <Setter Property="BackgroundColor" Value="White" />
        <Setter Property="CornerRadius" Value="21" />
        <Setter Property="HasShadow" Value="False" />
        <Setter Property="HeightRequest" Value="42" />
        <Setter Property="VerticalOptions" Value="Center" />
        <Setter Property="HorizontalOptions" Value="Center" />
        <Setter Property="IsClippedToBounds" Value="True" />
        <Setter Property="WidthRequest" Value="42" />
    </Style>
</ResourceDictionary>

...

<StackLayout
    BindableLayout.ItemsSource="{Binding People}"
    HorizontalOptions="CenterAndExpand"
    Orientation="Horizontal"
    VerticalOptions="Center">

    <BindableLayout.ItemTemplate>
        <DataTemplate>
            <Frame Style="{StaticResource avatarFrame}">
                <Frame Style="{StaticResource contentFrame}">
                    <Image Source="{Binding Image}" />
                </Frame>
            </Frame>
        </DataTemplate>
    </BindableLayout.ItemTemplate>

</StackLayout>

Adding a counter

If we have learnt anything during the last year or two of global events, it’s that too many people in one place can be a problem. The same goes for our avatar collection. At the moment, we are showing all the avatars, which is a bit of a problem if you have lots of people in your list.

Too Many Avatars

Let’s say we want to just show the first X number of people and then have a counter indicating how many more.

Avatars with Counter

A simple approach (although not everyone’s favorite) is to create another property on the ViewModel that takes the first X people and include a counter as the last element.

public List<object> PeopleCount
{
    get
    {
        var numberToShow = 5;
        List<object> returnList = new List<object>();

        // add the number of people we are after
        returnList.AddRange(People.Take(numberToShow));

        // if we have more people than we want to show, add a count
        if (People.Count > numberToShow)
            returnList.Add(People.Count - numberToShow);

        return returnList;
} 

Note:

  • Notice the new PeopleCount Property is now a collection of object - that’s because the collection can now contain People objects and a counter of type int.

If the number of people can change at runtime, it is also necessary to manually raise property changes in your ViewModel to tell the UI to update when you add or remove items from the People Collection.

private void AddPerson()
{
    // add a person using an Image from pravatar.cc
    People.Add(new Person { Image = $"https://i.pravatar.cc/64?img={People.Count + 1}" });
    OnPropertyChanged(nameof(PeopleCount));
}

private void RemovePerson()
{
    if (People.Any())
    {
        People.Remove(People.Last());
        OnPropertyChanged(nameof(PeopleCount));
    }
}

Visualising different data types

A DataTemplateSelector is a nifty way of being able to select a visualisation for an item based on the data being displayed. In our case, we will create a DataTemplateSelector to show either a person or a counter template based on the type of the item.

public class PersonListDataTemplateSelector : DataTemplateSelector
{
    public DataTemplate PersonTemplate { get; set; }
    public DataTemplate CounterTemplate { get; set; }

    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        if (item is Models.Person)
            return PersonTemplate;
        else
            return CounterTemplate;
    }
}

Now we add the DataTemplateSelector to your page along with DataTemplates for each type

<DataTemplate x:Key="personTemplate">
    <Frame Style="{StaticResource avatarFrame}">
        <Frame Style="{StaticResource contentFrame}">
            <Image Source="{Binding Image}" />
        </Frame>
    </Frame>
</DataTemplate>

<DataTemplate x:Key="counterTemplate">
    <Frame Style="{StaticResource avatarFrame}">
        <Frame BackgroundColor="LightGray" Style="{StaticResource contentFrame}">
            <Label
                FontSize="16"
                HorizontalOptions="Center"
                Text="{Binding ., StringFormat='+{0}'}"
                TextColor="Black"
                VerticalOptions="Center" />
        </Frame>
    </Frame>
</DataTemplate>

<templates:PersonListDataTemplateSelector
    x:Key="personDataTemplateSelector"
    CounterTemplate="{StaticResource counterTemplate}"
    PersonTemplate="{StaticResource personTemplate}" />

Lastly we can update the our StackLayout to be bound to the new Property on the ViewModel and the DataTemplateSelector.

<StackLayout
    BindableLayout.ItemTemplateSelector="{StaticResource personDataTemplateSelector}"
    BindableLayout.ItemsSource="{Binding PeopleCount}"
    HorizontalOptions="CenterAndExpand"
    Orientation="Horizontal"
    VerticalOptions="Center" />

Using MultiValue Converters

What we have right now works fine, but we have to keep our PeopleCount collection in sync by raising OnPropertyChanged whenever the main Person collection changes. A lot of people aren’t going to like that, so we can try an alternative approach of using a converter to do the work for us.

Unfortunately a single Converter isn’t going to do the job because we need our UI to update if either the collection changes entirely, or if the count in the collection changes. This is where a MultiValueConverter can help us.

MultiBinding provides the ability to attach a collection of Binding objects to a single binding target property. They are created with the MultiBinding class, which evaluates all of its Binding objects, and returns a single value through a IMultiValueConverter instance provided by your application. In addition, MultiBinding reevaluates all of its Binding objects when any of the bound data changes.

First thing, let’s create a PeopleMultiBind converter:

public class PeopleMultiBind : IMultiValueConverter
{
    public int NumberToShow { get; set; } = 5;

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        foreach (var value in values)
            {
                if (value is IEnumerable<Person> enumerable)
                {
                    // get the first X number of people
                    List<object> returnList = new List<object>();
                    returnList.AddRange(enumerable.Take(NumberToShow));

                    // if there are even more people - add a counter element
                    if (enumerable.Count() > NumberToShow)
                    {
                        returnList.Add(enumerable.Count() - NumberToShow);
                    }

                    return returnList;
                }
                else
                    return null;
            }
        return null;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

It’s basically doing the same thing as our property earlier, where we take the first X objects (in our case we have a property NumberToShow) and then if we have more, we and an int to the end of the collection.

We can now delete that PeopleCount property on our ViewModel and the OnPorpertyChanged lines of our AddPerson and RemovePerson commands, because our MultiValueConverter is going to take care of it all

Now we tie it all together by referencing the converter in our ResourceDictionary

    <converters:PeopleMultiBind x:Key="peopleMulti" NumberToShow="5" />

And then update our Binding to use the multi-bind, as such:

<StackLayout
    BindableLayout.ItemTemplateSelector="{StaticResource personDataTemplateSelector}"
    HorizontalOptions="CenterAndExpand"
    Orientation="Horizontal"
    VerticalOptions="Center">

    <BindableLayout.ItemsSource>
        <MultiBinding Converter="{StaticResource peopleMulti}">
            <Binding Path="People" />
            <Binding Path="People.Count" />
        </MultiBinding>
    </BindableLayout.ItemsSource>

    <BindableLayout.EmptyViewTemplate>
        <DataTemplate>
            <Label Text="Nobody" />
        </DataTemplate>
    </BindableLayout.EmptyViewTemplate>
</StackLayout>

Notice how the ItemsSource indicates is now MultiBinding to the People collection and also the Count property in the collection. Nice!

For added awesomeness we also add an EmptyViewTemplate to show something different when there are no people.

Avatars Changing

Wrap-up

Well that’s it, we have looked at creating an overlapping avatar group using a variety of techniques. Mind you, if you are not expecting the collection of avatars to change at runtime you could probably do away with the second half of this blog post - but it’s a fun experiment anyway.

The code for this article is available over on my Github.

Thanks for reading, hope you found this useful, and happy coding!

kphillpotts

Kym Phillpotts

Geek, Parent, Human, Senior Content Developer at Microsoft. Co-curator of http://weeklyxamarin.com and twitch streaming live coding at http://twitch.tv/kymphillpotts.

Read More