Horizontal List View in Xamarin Forms

I know we have a nuget package for this stuff. I’ve never used it before, and I need this control in my current project, but then I found out that this nuget package is not compatible with the new .Net Standard 2.0 so I was not able to install it to my project. I was forced to look for another solution and thanks God, I found this awesome blog post about Carousel Layout by Chris Riesgo. That post is about Carousel, is not exactly about horizontal list view, but with little modification there and there, I got the horizontal list view I need. If you want to know more about Carousel Layout, you can go to that link, because I will just explain the adjustments I made.

Horizontal List View

This custom class is pretty similar with Chris Riesgo’s Carousel class. Basically, what you do in this class is putting a horizontal stack layout inside horizontal scroll view.  I then added two more bindable properties, which are ItemWithRequest and ItemHeightRequest , because unlike carousel, horizontal list view won’t take the whole width and height of the screen. I wanted a flexibility when  I set the size of list view’s item.

I also changed the user interaction method. This is actually the crucial part of my modification, because in carousel, whatever item show in screen is the SelectedItem, but horizontal list view shows more than one item on the screen. So, to handle ItemSelected  event, I remove the tapped event from the horizontal list view and then added TapGestureRecognizer to each list view’s item. This change enable me to detect which item have been tapped and update the value of SelectedItem to that item.

Below this is the final code of the custom Horizontal ListView class. And because we only use casual scrolling, we don’t need custom renderer in Android and iOS’s project.

public class HorizontalListView : ScrollView
{

    readonly StackLayout _stack;

    int _selectedIndex;

    TapGestureRecognizer tapGestureRecognizer;

    public HorizontalListView()
    {
        Orientation = ScrollOrientation.Horizontal;

        _stack = new StackLayout
        {
            Orientation = StackOrientation.Horizontal,
            Spacing = 0,

        };

        Content = _stack;

        tapGestureRecognizer = new TapGestureRecognizer();
        tapGestureRecognizer.Tapped += (s, e) => {
            var bindableObject = s as BindableObject;
            SelectedItem = bindableObject.BindingContext;
            UpdateSelectedIndex();
        };
    }

    public new IList Children
    {
        get => _stack.Children;
    }

    private bool _layingOutChildren;
    protected override void LayoutChildren(double x, double y, double width, double height)
    {
        base.LayoutChildren(x, y, ItemWidthRequest, ItemHeightRequest);
        if (_layingOutChildren) return;

        _layingOutChildren = true;
        foreach (var child in Children) child.WidthRequest = ItemWidthRequest;
        _layingOutChildren = false;
    }

    public static readonly BindableProperty SelectedIndexProperty =
        BindableProperty.Create(nameof(SelectedIndex), typeof(int), typeof(HorizontalListView), 0, BindingMode.TwoWay,
            propertyChanged: async (bindable, oldValue, newValue) =>
            {
                await ((HorizontalListView)bindable).UpdateSelectedItem();
            }
        );

    public int SelectedIndex
    {
        get
        {
            return (int)GetValue(SelectedIndexProperty);
        }
        set
        {
            SetValue(SelectedIndexProperty, value);
        }
    }

    async Task UpdateSelectedItem()
    {
        await Task.Delay(300);
        SelectedItem = SelectedIndex > -1 ? Children[SelectedIndex].BindingContext : null;
    }

    public static readonly BindableProperty ItemsSourceProperty =
        BindableProperty.Create( nameof(ItemsSource), typeof(IList), typeof(HorizontalListView), null,
            propertyChanging: (bindableObject, oldValue, newValue) =>
            {
                ((HorizontalListView)bindableObject).ItemsSourceChanging();
            },
            propertyChanged: (bindableObject, oldValue, newValue) =>
            {
                ((HorizontalListView)bindableObject).ItemsSourceChanged();
            }
        );

    public IList ItemsSource
    {
        get
        {
            return (IList)GetValue(ItemsSourceProperty);
        }
        set
        {
            SetValue(ItemsSourceProperty, value);
        }
    }

    void ItemsSourceChanging()
    {
        if (ItemsSource == null) return;
        _selectedIndex = ItemsSource.IndexOf(SelectedItem);
    }

    void ItemsSourceChanged()
    {
        _stack.Children.Clear();
        foreach (var item in ItemsSource)
        {
            var view = (View)ItemTemplate.CreateContent();
            var bindableObject = view as BindableObject;
            if (bindableObject != null)
                bindableObject.BindingContext = item;
            _stack.Children.Add(view);
            view.GestureRecognizers.Add(tapGestureRecognizer);
        }

        if (_selectedIndex >= 0) SelectedIndex = _selectedIndex;
    }

    public DataTemplate ItemTemplate
    {
        get;
        set;
    }

    public static readonly BindableProperty SelectedItemProperty =
        BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(HorizontalListView), null, BindingMode.TwoWay,
            propertyChanged: (bindable, oldValue, newValue) =>
            {
                ((HorizontalListView)bindable).UpdateSelectedIndex();
            }
        );

    public object SelectedItem
    {
        get
        {
            return GetValue(SelectedItemProperty);
        }
        set
        {
            SetValue(SelectedItemProperty, value);
        }
    }

    void UpdateSelectedIndex()
    {
        if (SelectedItem == BindingContext) return;

        SelectedIndex = Children
            .Select(c => c.BindingContext)
            .ToList()
            .IndexOf(SelectedItem);
    }

    public static readonly BindableProperty ItemWidthRequestProperty =
        BindableProperty.Create(nameof(ItemWidthRequest), typeof(Int32), typeof(HorizontalListView), 100, BindingMode.TwoWay);

    public Int32 ItemWidthRequest
    {
        set { SetValue(ItemWidthRequestProperty, value); }
        get { return (Int32)GetValue(ItemWidthRequestProperty); }
    }

    public static readonly BindableProperty ItemHeightRequestProperty =
        BindableProperty.Create(nameof(ItemHeightRequest), typeof(Int32), typeof(HorizontalListView), 100, BindingMode.TwoWay);

    public Int32 ItemHeightRequest
    {
        set { SetValue(ItemHeightRequestProperty, value); }
        get { return (Int32)GetValue(ItemHeightRequestProperty); }
    }

}

Model and ViewModel

To see what this horizontal list view looks like in action, I created a simple example. First, let’s go to the Model class. in this class, I created a model class with two properties on it.

public class HorizontalItem
{
    public string Title { get; set; }
    public string Icon { get; set; }
}

And then we move to the important part, the view model. I mentioned above that whenever user tap on an item, the SelectedItem property changes it’s value. So, what we need to do in view model is catch that triggered event. On this example, I only send message to the view class to show alert base on the item I click.

public class HorizontalViewModel : BaseViewModel
{
    private ObservableCollection horizontalItems;
    public ObservableCollection HorizontalItems
    {
        get => horizontalItems;
        set => SetProperty(ref horizontalItems, value);
    }

    private HorizontalItem selectedItem;
    public HorizontalItem SelectedItem
    {
        get => selectedItem;
        set
        {
            SetProperty(ref selectedItem, value);
            ItemSelected();
        }
    }

    public HorizontalViewModel()
    {
        HorizontalItems = new ObservableCollection()
        {
            new HorizontalItem() { Title = "One", Icon = "one.png" },
            new HorizontalItem() { Title = "Two", Icon = "two.png" },
            new HorizontalItem() { Title = "Three", Icon = "three.png" },
            new HorizontalItem() { Title = "Four", Icon = "four.png" },
            new HorizontalItem() { Title = "Five", Icon = "five.png" },
            new HorizontalItem() { Title = "Six", Icon = "six.png" },
            new HorizontalItem() { Title = "Seven", Icon = "seven.png" },
            new HorizontalItem() { Title = "Eight", Icon = "eight.png" },
            new HorizontalItem() { Title = "Nine", Icon = "nine.png" },
            new HorizontalItem() { Title = "Ten", Icon = "ten.png" },
        };
    }

    private void ItemSelected()
    {
        MessagingCenter.Send(this, "ItemSelected", SelectedItem);
    }
}

When you run the program, it will look like this.

Sample Code is available in my Github repo

 

Credit:

  • All Number Icon from FlatIcon by Roundicon
  • Xamarin.Forms Carousel View Receipe by Chris Riesgo (blog)