Post

Create scrollable tabs in Xamarin.Forms with CollectionView and CarouselView

Create scrollable tabs in Xamarin.Forms with CollectionView and CarouselView

When it comes to navigation patterns in mobile apps, the tabbed interface is one of the most popular options. While Xamarin.Forms has the TabbedPage (and Shell) to fulfill that need, it lacks one essential feature: scrollable tabs. After studying some of the samples floating around the web and some of the packages that provide such functionality, I tried find an easier solution.

The View

Let’s have a look at the View first. Like you may have guessed from the title, we are using a <a href="https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/collectionview/" rel="noreferrer noopener nofollow" target="_blank">CollectionView</a> for the tabs and a <a href="https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/carouselview/" rel="noreferrer noopener nofollow" target="_blank">CarouselView</a> for the Content. This combination makes it quite easy to implement tabs that cover a whole page size or smaller ones within a page.

Here’s the XAML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
 <Grid x:DataType="{x:Null}" RowSpacing="0">
    <Grid.RowDefinitions>
        <RowDefinition Height="45" />
        <RowDefinition Height="45" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <CollectionView
        x:Name="CustomTabsView"
        Grid.Row="1"
        HorizontalScrollBarVisibility="Never"
        ItemSizingStrategy="MeasureAllItems"
        ItemsSource="{Binding TabVms}"
        ItemsUpdatingScrollMode="KeepItemsInView"
        SelectedItem="{Binding CurrentTabVm, Mode=TwoWay}"
        SelectionMode="Single"
        VerticalScrollBarVisibility="Never">
        <CollectionView.ItemsLayout>
            <LinearItemsLayout Orientation="Horizontal" />
        </CollectionView.ItemsLayout>
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="local:TabViewModel">
                <Grid RowSpacing="0">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="3" />
                    </Grid.RowDefinitions>
                    <Label
                        x:Name="TitleLabel"
                        Grid.Row="0"
                        Padding="15,0"
                        FontAttributes="Bold"
                        FontSize="Small"
                        HeightRequest="50"
                        HorizontalTextAlignment="Center"
                        Text="{Binding Title}"
                        TextColor="White"
                        VerticalTextAlignment="Center" />
                    <BoxView
                        x:Name="ActiveIndicator"
                        Grid.Row="1"
                        BackgroundColor="Red"
                        IsVisible="{Binding IsSelected, Mode=TwoWay}" />
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>

    <CarouselView
        Grid.Row="2"
        CurrentItem="{Binding CurrentTabVm, Mode=TwoWay}"
        CurrentItemChanged="CarouselView_CurrentItemChanged"
        HorizontalScrollBarVisibility="Never"
        IsScrollAnimated="True"
        IsSwipeEnabled="True"
        ItemsSource="{Binding TabVms}"
        VerticalScrollBarVisibility="Never">
        <CarouselView.ItemTemplate>
            <DataTemplate x:DataType="local:TabViewModel">
                <Grid Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <Label
                        Grid.Row="0"
                        Margin="10"
                        LineBreakMode="WordWrap"
                        Text="{Binding Content}"
                        VerticalTextAlignment="Center" />
                </Grid>
            </DataTemplate>
        </CarouselView.ItemTemplate>
    </CarouselView>
</Grid>

Let me break that piece down. First, I wrapped everything in a Grid for this sample. The CollectionView of course should be horizontally scrolling but should not show any scroll bar. The tab item template is not a complex one – it is just a Label and a BoxView below it to help with indication of the selection. You are free to make the tab looking whatever you want because of the CollectionView, however.

Below that, we put a CarouselView. For this sample, I just made a simple one with a Lorem Ipsum Label in it on every item.

The ViewModels

Most of you know that I absolutely love the MVVM pattern. And this sample proves me right once again. We need just need two ViewModels to handle scrolling and synchronizing.

The first ViewModel is the TabViewModel:

Snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 public class TabViewModel : ObservableObject
{
    private string _title;
    private string _content;
    private bool _isSelected;

    public TabViewModel(string title)
    {
        this.Title = title;
        this.Content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tempor id eu nisl nunc mi ipsum faucibus vitae aliquet. Turpis egestas integer eget aliquet nibh praesent tristique magna. In fermentum posuere urna nec tincidunt. Vitae congue eu consequat ac felis donec et odio pellentesque. Augue lacus viverra vitae congue. Viverra vitae congue eu consequat. Orci nulla pellentesque dignissim enim sit amet venenatis urna. Et ultrices neque ornare aenean euismod elementum nisi. Id consectetur purus ut faucibus pulvinar. In cursus turpis massa tincidunt. Egestas pretium aenean pharetra magna. Et pharetra pharetra massa massa ultricies mi quis. Nunc sed blandit libero volutpat. Purus viverra accumsan in nisl nisi scelerisque eu ultrices vitae.";
    }

    public string Title { get => _title; set => Set(ref _title, value); }

    public string Content { get => _content; set => Set(ref _content, value); }

    public bool IsSelected { get => _isSelected; set =>Set(ref _isSelected, value); }
}

The TabViewModel in this sample just has the bare minimum, namely the Title, the Content and the IsSelectedFlag to control the Visibility of the Indicator-BoxView. Nothing dramatic so far.

The MainViewModel glues everything together, so let’s have a look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 public class MainViewModel : ObservableObject
{
    private TabViewModel _currentTabVm;

    public MainViewModel()
    {

        this.TabVms = new ObservableCollection<TabViewModel>();
        this.TabVms.Add(new TabViewModel("Short Title"));
        this.TabVms.Add(new TabViewModel("A Little Longer Title"));
        this.TabVms.Add(new TabViewModel("An Even Longer Title Than Before"));
        this.TabVms.Add(new TabViewModel("Again Short Title"));
        this.TabVms.Add(new TabViewModel("Mini Title"));
        this.TabVms.Add(new TabViewModel("Different Title"));

        this.CurrentTabVm = this.TabVms.FirstOrDefault();
    }


    public ObservableCollection<TabViewModel> TabVms { get; set; } 

    public TabViewModel CurrentTabVm 
    { 
        get => _currentTabVm;
        set
        {
            Set(ref _currentTabVm, value);
            SetSelection();
        }
    }

    private void SetSelection()
    {
        this.TabVms.ForEach(vm => vm.IsSelected = false);
        this.CurrentTabVm.IsSelected = true;
    }
}

Once again, there is nothing complex in it. We are mocking a collection of TabViewModel and handle the tab selection via Binding. After the current item got selected, we are setting the IsSelected property on it to true to show the Indicator in the CollectionView.

For this sample, I didn’t use a fully blown MVVM framework, so I am setting the BindingContext in the MainPage‘s constructor. The Binding engine in Xamarin.Forms already does almost everything to make this work.

The only thing left is to handle the positioning of the tabs if we are swiping the CarouselView. As this is purely View related, I am using the CurrentItemChanged event in code behind to center the CollectionView‘s selected item:

1
2
3
4
 private void CarouselView_CurrentItemChanged(object sender, CurrentItemChangedEventArgs e)
{
    this.CustomTabsView.ScrollTo(e.CurrentItem, null, ScrollToPosition.Center, true);
}

The result of this setup looks like this:

### Conclusion

Xamarin.Forms provides a lot of solutions out of the box. Sometimes, however, these are not enough. Luckily, we can combine some of the solutions the framework provides to create fresh solutions within our apps. This post showed one of these. The additional bonus you get with this implementation is the ability to style the tabs in whatever way you want. As always, I hope this post will be helpful for some of you.

Of course, there is also sample for this post on Github.

Until the next post, happy coding, everyone!
This post is licensed under CC BY 4.0 by the author.