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
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 aCornerRadius
does the job.
And that’s going to give us a pretty decent looking group of overlapping 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:
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.
Let’s say we want to just show the first X number of people and then have a counter indicating how many more.
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 ofobject
- that’s because the collection can now containPeople
objects and a counter of typeint
.
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 theOnPorpertyChanged
lines of ourAddPerson
andRemovePerson
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 nowMultiBinding
to thePeople
collection and also theCount
property in the collection. Nice!
For added awesomeness we also add an EmptyViewTemplate
to show something different when there are no people.
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.
Thanks for reading, hope you found this useful, and happy coding!