Search code examples
xamarin.formsgridmulti-selectswipe-gesture

Xamarin Forms Grid Boxview swipe gesture multi select


I have a simple Xamarin.Forms page with a grid layout where BoxViews are inside. I would like to be able to select these box views at the same time through the swipe gesture. How can I achieve this?

I want to build up a kind of tile map, so that boxviews are easier to select. I want to achieve this easily. Only the swiping doesn't work very well, because only "one" box view will be selected.

This is what I have so far:

XAML

    <ContentPage.Content>
        <Grid x:Name="pageGrid" RowSpacing="1" ColumnSpacing="1" VerticalOptions="Center" HorizontalOptions="Center">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
        </Grid>
    </ContentPage.Content>

Code behind:

public Page()
    {
        InitializeComponent();

        int columnIndex = 0;
        for (int rowIndex = 0; rowIndex <= 8; rowIndex++)
        {
            BoxView boxview = new BoxView { BackgroundColor = Color.White };
            SwipeGestureRecognizer swipeGestureRecognizer = new SwipeGestureRecognizer();
            swipeGestureRecognizer.Swiped += (sender, args) =>
            {
                if (boxview.BackgroundColor == Color.White)
                {
                    boxview.BackgroundColor = Color.Gray;
                }
                else if (boxview.BackgroundColor == Color.Gray)
                {
                    boxview.BackgroundColor = Color.White;
                }
            };

            swipeGestureRecognizer.Threshold = 1;
            swipeGestureRecognizer.Direction = SwipeDirection.Left | SwipeDirection.Right;
            
            TapGestureRecognizer tapGestureRecognizer = new TapGestureRecognizer();
            tapGestureRecognizer.Tapped += (sender, args) =>
            {
                if (boxview.BackgroundColor == Color.White)
                {
                    boxview.BackgroundColor = Color.Gray;
                }
                else if (boxview.BackgroundColor == Color.Gray)
                {
                    boxview.BackgroundColor = Color.White;
                }
            };
            
            boxview.GestureRecognizers.Add(swipeGestureRecognizer);
            boxview.GestureRecognizers.Add(tapGestureRecognizer);

            if (rowIndex == 4 && columnIndex == 3)
            {
                boxview.BackgroundColor = Color.Red;
            }

            pageGrid.Children.Add(boxview, columnIndex, rowIndex);

            if (rowIndex != 8) continue;
            if (columnIndex == 6) return;
            columnIndex += 1;
            rowIndex = -1;
        }
    }

Now per swipe action, only one boxview will be selected!

Layout


Solution

  • I played around with this for a little bit and came up with a few ideas which will follow, with a partial implementation at the end.

    I don't think the swipe gesture will make this work the way I think you want it to work. It looks like the Swipe gesture fires exactly one time, at the point of the finger being lifted off the screen, which to me is not the ideal experience because it means that even if you could figure out which boxes in total were swiped over (I'm not sure that could be done) they would all wait until you lifted your finger off to change color.

    The idea I just came up with was to use a pan gesture because that one fires very frequently whenever motion is detected. Both the pan and the swipe seem to have the constraint that once the event is associated with a single boxview, the event will not fire callbacks in any others, even if your finger is passing over other boxes. However, I think that can be overcome if you use the pan gesture. The pan gesture gives you the total pan deviation each time it fires, and given that the gesture callback itself will continually fire inside of the boxview where the pan started (and not in any other boxes) it means you have both an initial location and a total deviation with each pan movement. Theoretically you have all the information necessary to make this work, but you'll need to have some smart handling of the event. It may be a headache but if you can build out your grid so it's smart enough to know how each boxview relates to the others you could map out pan gesture events as they come in and make particular boxviews change color as needed.

    EDIT: At the request of the asker I built out some sample code to do this. It will be necessary to add individual tapping events back in and make it "toggle" properly rather than just select according to logic that you want. Other tweaks may also be needed but this shows proof of the concept.

        public Page()
        {
            InitializeComponent();
    
            var columnIndex = 0;
            for (var rowIndex = 0; rowIndex <= 8; rowIndex++)
            {
                var boxview = new BoxView { BackgroundColor = Color.White };
                var swipeGestureRecognizer = new PanGestureRecognizer();
    
                swipeGestureRecognizer.PanUpdated += (sender, args) =>
                {
                    var boxView = (BoxView) sender;
                    var panBaseBounds = boxView.Bounds;
                    var eventX = panBaseBounds.X + args.TotalX;
                    var eventY = panBaseBounds.Y + args.TotalY;
                    foreach (var gridChild in pageGrid.Children)
                    {
                        var testBounds = gridChild.Bounds;
                        if (testBounds.X <= eventX && eventX <= testBounds.X + testBounds.Width &&
                            testBounds.Y <= eventY && eventY <= testBounds.Y + testBounds.Height)
                        {
                            gridChild.BackgroundColor = Color.Gray;
                            break;
                        }
                    }
                };
    
                boxview.GestureRecognizers.Add(swipeGestureRecognizer);
    
                if (rowIndex == 4 && columnIndex == 3)
                {
                    boxview.BackgroundColor = Color.Red;
                }
    
                pageGrid.Children.Add(boxview, columnIndex, rowIndex);
    
                if (rowIndex != 8) continue;
                if (columnIndex == 6) return;
                columnIndex += 1;
                rowIndex = -1;
            }
        }