6

I'm displaying a list of SQLite objects in a ListView, but I want them to display horizontally. So instead of this:

| longitem        |
| item            |
| evenlongeritem  |
| item            |
| longeritem      |

I want this:

| longitem item   |
| evenlongeritem  |
| item longeritem |

Importantly, the items can be of varying widths, so just breaking the list into a certain number of columns would be an improvement, but not ideal. I also don't know the number of items.

Here's the code I have currently:

<ListView x:Name="inactiveList" VerticalOptions="Start" ItemTapped="PutBack">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding Name}" TextColor="Black">
                  <TextCell.ContextActions>
                         <MenuItem Command="{Binding Source={x:Reference ListPage}, Path=DeleteListItem}" CommandParameter="{Binding .}" Text="delete" />
                  </TextCell.ContextActions>
              </TextCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Code Behind:

public ListPage()
{
    InitializeComponent();

    ObservableCollection<ListItem> activeItems =
        new ObservableCollection<ListItem>(
            App.ListItemRepo.GetActiveListItems());
    activeList.ItemsSource = activeItems;
    ...

I tried just wrapping the ViewCell in a horizontal StackLayout, but I got this error:

Unhandled Exception: System.InvalidCastException: Specified cast is not valid.

I'm not sure that that error means, but I don't think it's possible to add a StackLayout inside the DataTemplate. I also can't make the ListView horizontal.

--

UPDATE 4:

I finally could make simple labels be listed horizontally, but now I'm having trouble recreating the tap and long-press actions built into the vertical ListView. Is that possible to do?

ListView.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns:local="clr-namespace:Myapp">
        <!-- ... -->
        <local:WrapLayout x:Name="inactiveList" ItemsSource="{Binding .}" Spacing="5" />

ListView.xaml.cs

using Myapp.Models;
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using SQLite;
using System.Threading.Tasks;
using System.IO;
using Xamarin.Forms;
using System.Diagnostics;
using DLToolkit.Forms.Controls;

namespace Myapp
{
    public partial class ListPage
    {
        ...
        public ListPage()
        {
            InitializeComponent();

            ObservableCollection<ListItem> inactiveItems =
                new ObservableCollection<ListItem>(
                    App.ListItemRepo.GetInactiveListItems());
            inactiveList.ItemsSource = inactiveItems;
            inactiveList.HeightRequest = 50 * inactiveItems.Count;


        }
        ...
    }

    public class WrapLayout : Layout<View>
    {

        public ObservableCollection<ListItem> ItemsSource
        {
            get { return (ObservableCollection<ListItem>)GetValue(ItemSourceProperty); }
            set { SetValue(ItemSourceProperty, value); }
        }

        public static readonly BindableProperty ItemSourceProperty =
            BindableProperty.Create
            (
                "ItemsSource",
                typeof(ObservableCollection<ListItem>),
                typeof(WrapLayout),
                propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).AddViews()
            );


        void AddViews()
        {
            Children.Clear();
            foreach (ListItem s in ItemsSource)
            {
                Button button = new Button();
                button.BackgroundColor = Color.Red;
                button.Text = s.Name;
                button.TextColor = Color.Black;
                button.Clicked = "{Binding Source={x:Reference ListPage}, Path=PutBack}";
                Children.Add(button);
            }
        }

        public static readonly BindableProperty SpacingProperty =
            BindableProperty.Create
            (
                "Spacing",
                typeof(double),
                typeof(WrapLayout),
                10.0,
                propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).OnSizeChanged()
            );

        public double Spacing
        {
            get { return (double)GetValue(SpacingProperty); }
            set { SetValue(SpacingProperty, value); }
        }

        private void OnSizeChanged()
        {
            this.ForceLayout();
        }

        protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
        {
            if (WidthRequest > 0)
                widthConstraint = Math.Min(widthConstraint, WidthRequest);
            if (HeightRequest > 0)
                heightConstraint = Math.Min(heightConstraint, HeightRequest);

            double internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint);
            double internalHeight = double.IsPositiveInfinity(heightConstraint) ? double.PositiveInfinity : Math.Max(0, heightConstraint);

            return DoHorizontalMeasure(internalWidth, internalHeight);
        }

        private SizeRequest DoHorizontalMeasure(double widthConstraint, double heightConstraint)
        {
            int rowCount = 1;

            double width = 0;
            double height = 0;
            double minWidth = 0;
            double minHeight = 0;
            double widthUsed = 0;

            foreach (var item in Children)
            {
                var size = item.Measure(widthConstraint, heightConstraint);

                height = Math.Max(height, size.Request.Height);

                var newWidth = width + size.Request.Width + Spacing;
                if (newWidth > widthConstraint)
                {
                    rowCount++;
                    widthUsed = Math.Max(width, widthUsed);
                    width = size.Request.Width;
                }
                else
                    width = newWidth;

                minHeight = Math.Max(minHeight, size.Minimum.Height);
                minWidth = Math.Max(minWidth, size.Minimum.Width);
            }

            if (rowCount > 1)
            {
                width = Math.Max(width, widthUsed);
                height = (height + Spacing) * rowCount - Spacing; // via MitchMilam 
            }

            return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight));
        }

        protected override void LayoutChildren(double x, double y, double width, double height)
        {
            double rowHeight = 0;
            double yPos = y, xPos = x;

            foreach (var child in Children.Where(c => c.IsVisible))
            {
                var request = child.Measure(width, height);

                double childWidth = request.Request.Width;
                double childHeight = request.Request.Height;
                rowHeight = Math.Max(rowHeight, childHeight);

                if (xPos + childWidth > width)
                {
                    xPos = x;
                    yPos += rowHeight + Spacing;
                    rowHeight = 0;
                }

                var region = new Rectangle(xPos, yPos, childWidth, childHeight);
                LayoutChildIntoBoundingRegion(child, region);
                xPos += region.Width + Spacing;
            }
        }
    }
}
Joe Morano
  • 1,715
  • 10
  • 50
  • 114
  • Use UniformGrid with columns set to the number you need and rows = 1 in an ItemsPanelTemplate – Kevin Cook Dec 14 '17 at 19:15
  • @KevinCook The thing is, I'm not sure how many columns I need because different items will be of different lengths. – Joe Morano Dec 14 '17 at 20:01
  • 2
    1) Use a custom layout, Xamarin has an example that matches your needs, WrapLayout : https://developer.xamarin.com/guides/xamarin-forms/user-interface/layouts/custom/ (Note: I have no idea how many items you expect in this configuration, so performance/memory(!) might be an issue). 2) Otherwise CollectionViews... – SushiHangover Dec 14 '17 at 21:13
  • @SushiHangover I'm not sure if I'm reading the guide right. Do I have to add a new class in my code behind in order to add a WrapLayout to my xaml? – Joe Morano Dec 14 '17 at 22:35
  • Yes, you are creating a new subclass of `Layout` called `WrapLayout` in your project that you will then use in your XAML (or code) – SushiHangover Dec 14 '17 at 22:38
  • @SushiHangover I feel really stupid, but I think there's something fundamental I'm missing here, that the guides don't really explain. If I just want to reorganize the view layout, why do I need to do anything in the code-behind? I thought the code-behind was just for business logic. – Joe Morano Dec 15 '17 at 02:10
  • @JoeMorano Saying "code-behind" usually means the user code that is associate to a *single* XAML file, a XAML file is just a partial class with a matching partial class written in C#/F#. So technically a new subclass of Layout is not called "code-behind", it is just code that is associated to your project, just like the C# `string` class is not "code-behind". – SushiHangover Dec 15 '17 at 02:32
  • @JoeMorano All the XAML elements you are using; `Label`, `StackLayout`, `Grid`, `Entry`, ... are written in C# code. Once **you** write a new `Layout` subclass, you can write XAML that includes it. Just like you could subclass `Label` and call it `MoranoLabel` and have it flash you favorite sports teams colors are someone types into it and then you could reference it in XAML and include it within a StackLayout.. You can also code everything in C#/F# and never use XAML... ;-) – SushiHangover Dec 15 '17 at 02:35
  • Not sure what layout you actually are looking for? Full only horizontal so people need to scroll horizontal? Or wrapped? if wrapped take a look here : https://developer.xamarin.com/samples/xamarin-forms/UserInterface/CustomLayout/WrapLayout/ – Depechie Dec 15 '17 at 10:02
  • @Depechie Sorry I should have specified, I'm looking for a vertical scroll layout. So one stacklayout wrapped in a vertical scrolllayout, and everything else is non-scrollable and inside the stacklayout, if that makes sense. But yes wraplayout does appear to solve my problem, I'm currently trying to figure out how to apply it. – Joe Morano Dec 15 '17 at 17:43
  • @JoeMorano Take a look at https://stackoverflow.com/questions/9769618/how-can-we-set-the-wrap-point-for-the-wrappanel/9770590#9770590 and the original Code Project article. That probably won't work on Xamarin without modification but it may be a starting point. – Phil Dec 29 '17 at 15:04

3 Answers3

6

Refer to My Post. It is similar to your case.

Just need to custom Layout and manage its size and its children's arrangement.

Update:

I'm getting a "Type local:WrapLayout not found in xmlns clr-namespace:Myapp" error.

Make Class WrapLayout public , separate it from ListPage.

I'm also a little confused about how to apply data binding here

We need to add a BindableProperty named ItemSource inside wraplayout ,and add subview when the property changed.


Xmal

 <ContentPage.Content>
    <local:WrapLayout x:Name="wrap" ItemSource="{Binding .}" Spacing="5" />
</ContentPage.Content>

Code behind

List<string> list = new List<string> {
            "11111111111111111111111",
            "22222",
            "333333333333333",
            "4",
            "55555555",
            "6666666666666666666666",
            "77777",
            "8888888888",
            "99999999999999999999999999999999"
};
this.BindingContext = list;

Update: Button Event

We can define event inside WrapLayout, when we tap or long press on the button, trigger the events. And about the long press we should create custom renderers to implement it .

WrapLayout

namespace ImageWrapLayout
{
public class ButtonWithLongPressGesture : Button
{
    public EventHandler LongPressHandle;
    public EventHandler TapHandle;

    public void HandleLongPress(object sender, EventArgs e)
    {
        //Handle LongPressActivated Event
        LongPressHandle(sender, e);
    }

    public void HandleTap(object sender, EventArgs e)
    {
        //Handle Tap Event
        TapHandle(sender, e);
    }
}


public class WrapLayout : Layout<View>
{

    public List<string> ItemSource
    {
        get { return (List<string>)GetValue(ItemSourceProperty); }
        set { SetValue(ItemSourceProperty, value); }
    }

    public static readonly BindableProperty ItemSourceProperty =
        BindableProperty.Create
        (
            "ItemSource",
            typeof(List<string>),
            typeof(WrapLayout),
            propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).AddViews()
        );


    void AddViews()
    {
        Children.Clear();
        foreach (string s in ItemSource)
        {
            ButtonWithLongPressGesture button = new ButtonWithLongPressGesture();
            button.BackgroundColor = Color.Red;
            button.Text = s;
            button.TextColor = Color.Black;
            Children.Add(button);

            button.TapHandle += WrapLayoutTapHandle;
            button.LongPressHandle = WrapLayoutLongPressHandle;
        }
    }


    public EventHandler WrapLayoutLongPressHandle;
    public EventHandler WrapLayoutTapHandle;




    public static readonly BindableProperty SpacingProperty =
        BindableProperty.Create
        (
            "Spacing",
            typeof(double),
            typeof(WrapLayout),
            10.0,
            propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).OnSizeChanged()
        );

    public double Spacing
    {
        get { return (double)GetValue(SpacingProperty); }
        set { SetValue(SpacingProperty, value); }
    }

    private void OnSizeChanged()
    {
        this.ForceLayout();
    }

    protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
    {
        if (WidthRequest > 0)
            widthConstraint = Math.Min(widthConstraint, WidthRequest);
        if (HeightRequest > 0)
            heightConstraint = Math.Min(heightConstraint, HeightRequest);

        double internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint);
        double internalHeight = double.IsPositiveInfinity(heightConstraint) ? double.PositiveInfinity : Math.Max(0, heightConstraint);

        return DoHorizontalMeasure(internalWidth, internalHeight);
    }

    private SizeRequest DoHorizontalMeasure(double widthConstraint, double heightConstraint)
    {
        int rowCount = 1;

        double width = 0;
        double height = 0;
        double minWidth = 0;
        double minHeight = 0;
        double widthUsed = 0;

        foreach (var item in Children)
        {
            var size = item.Measure(widthConstraint, heightConstraint);

            height = Math.Max(height, size.Request.Height);

            var newWidth = width + size.Request.Width + Spacing;
            if (newWidth > widthConstraint)
            {
                rowCount++;
                widthUsed = Math.Max(width, widthUsed);
                width = size.Request.Width;
            }
            else
                width = newWidth;

            minHeight = Math.Max(minHeight, size.Minimum.Height);
            minWidth = Math.Max(minWidth, size.Minimum.Width);
        }

        if (rowCount > 1)
        {
            width = Math.Max(width, widthUsed);
            height = (height + Spacing) * rowCount - Spacing; // via MitchMilam 
        }

        return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight));
    }

    protected override void LayoutChildren(double x, double y, double width, double height)
    {
        double rowHeight = 0;
        double yPos = y, xPos = x;

        foreach (var child in Children.Where(c => c.IsVisible))
        {
            var request = child.Measure(width, height);

            double childWidth = request.Request.Width;
            double childHeight = request.Request.Height;
            rowHeight = Math.Max(rowHeight, childHeight);

            if (xPos + childWidth > width)
            {
                xPos = x;
                yPos += rowHeight + Spacing;
                rowHeight = 0;
            }

            var region = new Rectangle(xPos, yPos, childWidth, childHeight);
            LayoutChildIntoBoundingRegion(child, region);
            xPos += region.Width + Spacing;
        }
    }
}
}

Custom Renderers for LongPress

[assembly: ExportRenderer(typeof(ButtonWithLongPressGesture), typeof(LongPressGestureRecognizerButtonRenderer))]
namespace ImageWrapLayout.iOS
{
class LongPressGestureRecognizerButtonRenderer : ButtonRenderer
{
    ButtonWithLongPressGesture view;

    public LongPressGestureRecognizerButtonRenderer()
    {
        this.AddGestureRecognizer(new UILongPressGestureRecognizer((longPress) => {
            if (longPress.State == UIGestureRecognizerState.Began)
            {
                view.HandleLongPress(view, new EventArgs());
            }
        }));
    }

    protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
    {
        base.OnElementChanged(e);

        if (e.NewElement != null)
            view = e.NewElement as ButtonWithLongPressGesture;

        //if(Control == null)
        //{
        UIButton but = Control as UIButton;
            but.TouchUpInside += (sender, e1) => {
                view.HandleTap(view, new EventArgs());
            };
        //}
    }
}
}

Usage (In Page.cs)

 inactiveList.WrapLayoutLongPressHandle += (sender, e) =>
 {
 };

 inactiveList.WrapLayoutTapHandle += (sender, e) =>
 {
 };
ColeX
  • 14,062
  • 5
  • 43
  • 240
  • I tried your code, but I'm getting a "Type local:WrapLayout not found in xmlns clr-namespace:Myapp" error. I'm also a little confused about how to apply data binding here. Would I just put the `DataTemplate` inside the `WrapLayout` and then give the `WrapLayout` an `ItemsSource`? – Joe Morano Dec 29 '17 at 02:40
  • @JoeMorano check my update again ,for the reason why you got the error. – ColeX Dec 29 '17 at 07:36
  • I fixed that error, but now when I try to add data binding, I get this error: "Exception: System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array. occurred" I updated my code. Do you know what the error means? – Joe Morano Dec 30 '17 at 03:08
  • @JoeMorano I see You set itemsource inside wraplayout .And also bind with data in xmal .You just need to set in xmal .refer to my example code – ColeX Dec 30 '17 at 03:24
  • I think with your code the wraplayout doesn't get any data. I want to display an ObservableCollection of ListItems by calling `new ObservableCollection(App.ListItemRepo.GetInactiveListItems());`. Where should I call the `GetInactiveListItems` method? – Joe Morano Dec 30 '17 at 03:30
  • @JoeMorano in.the initial construct of the page – ColeX Dec 30 '17 at 03:33
  • You mean inside `public partial class ListPage {...}`? – Joe Morano Dec 30 '17 at 03:35
  • @JoeMorano yes,It is – ColeX Dec 30 '17 at 03:35
  • So I should put something like `public partial class ListPage {inactiveList.ItemsSource = new ObservableCollection(App.ListItemRepo.GetInactiveListItems());}`? – Joe Morano Dec 30 '17 at 03:37
  • You can put this inside the method ListPage(){} ,And set nothing on itemsource in xmal – ColeX Dec 30 '17 at 03:40
  • When I do that, I get this error: "Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object. occurred". I'll update my code in my question. – Joe Morano Dec 30 '17 at 03:51
  • @JoeMorano remove the itemsource set in xmal – ColeX Dec 30 '17 at 04:13
  • Ah, there we go! Would you happen to know if it's possible to make the labels buttons instead, and call a method if they're tapped or long-pressed? – Joe Morano Dec 30 '17 at 04:50
  • @JoeMorano tap for what? – ColeX Dec 30 '17 at 04:52
  • In the previous ListView I had, if an item was tapped, it called a `PutBack` method, passing through the ListItem as parameters. If an item was long-pressed, a "delete" button appeared on the toolbar, and if that was pressed, it called a `Delete` method, passing through the ListItem as parameters. – Joe Morano Dec 30 '17 at 04:54
2

You could use this: https://github.com/daniel-luberda/DLToolkit.Forms.Controls/tree/master/FlowListView

It's used just the same as ListView but has column support

enter image description here

Daniel Luberda
  • 7,374
  • 1
  • 32
  • 40
  • Does this support uneven columns? – Joe Morano Dec 25 '17 at 05:10
  • 1
    I tried implementing this, but the list isn't showing up at all. – Joe Morano Dec 26 '17 at 04:41
  • @JoeMorano I have this issue, as well. It also doesn't help that `FlowListView` defines several properties with names similar to the native `ListView` _in addition to the properties already there_. Talk about confusing! –  Mar 20 '18 at 18:04
1

You technically can do it. All VisualElements have a Rotation BindableProperty, so set rotation to 270.

public static readonly BindableProperty RotationProperty;
public static readonly BindableProperty RotationXProperty;
public static readonly BindableProperty RotationYProperty;

This code is from Visual Element Class. Also refer the sample below.

<ListView x:Name="MessagesListView" Rotation="270" ItemsSource="{Binding Items}" RowHeight="40">
  <ListView.ItemTemplate>
    <DataTemplate>
      <ViewCell>
        <ViewCell.View>
          <StackLayout>
            <!--mylayouthere-->
          </StackLayout>
        </ViewCell.View>
      </ViewCell>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>