Xamarin Forms Grid Boxview swipe gesture multi select - xamarin.forms

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!

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;
}
}

Related

Xamarin.Forms: Bind SwipeView to code and get events

I am quite new to xamarin.forms.
So I set up a swipeview (https://github.com/markolazic88/SwipeCardView/blob/master/docs/index.md)
But I can't seem to understand what they mean by "glueing" xaml with C#' together.
In the docs it says there are eventhandlers like this one:
void OnSwiped(object sender, SwipedCardEventArgs e)
{
switch (e.Direction)
{
case SwipeCardDirection.None:
break;
case SwipeCardDirection.Right:
break;
case SwipeCardDirection.Left:
break;
case SwipeCardDirection.Up:
break;
case SwipeCardDirection.Down:
break;
}
}
But just adding this function into the connect class doesn't work. Nothing fires. I was however able to access the swipeview like this:
public Screen_SwipeView()
{
InitializeComponent();
List<string> data = new List<string>() { "a", "b", "c" };
swipeview_swipeview.ItemsSource = data;
}
But this method isn't mentions in the docs.
How can I get the eventhandlers to fire. I feel like I am missing the bridge between the xaml component and the code.
I also tried making a class and letting it inherit from this components but that only threw errors.
You can also define the SwipeView properties (LeftItems, RightItems, TopItems, BottomItems) in your Xaml and then bind their command properties to a Command object define in your viewmodel. Example :
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Annuler"
IconImageSource="deleteitem.png"
BackgroundColor="LightPink"
Command="{Binding Source={x:Reference yourViewName}, Path=BindingContext.DeleteCommand}"
CommandParameter="{Binding .}" />
</SwipeItems>
</SwipeView.RightItems>
</SwipeView>
Easy one:
swipeview_swipeview.Dragging += OnDragging;
swipeview_swipeview.Swiped += OnSwiped;

Add element to XAML page whiles in Task.Delay()

I'm building a chatbot app with chat bubbles for incoming and outgoing messages. For the incoming messages, I've given it a Task.Delay() and now I'd like to give it an ActivityIndicator every time before the message pops up (i.e. I want to show the activity indicator whiles the message is being delayed). I've added the activity indicator to the XAML of the incoming messages control;
IncomingMessageItemControl
<ViewCell
x:Class="BluePillApp.Controls.IncomingMessageItemControl"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pancake="clr-namespace:Xamarin.Forms.PancakeView;assembly=Xamarin.Forms.PancakeView"
mc:Ignorable="d">
<Grid x:Name="Gridoo">
<pancake:PancakeView
Margin="10,10,80,10"
Padding="15"
BackgroundColor="#53ffc6"
CornerRadius="20,20,0,20"
HasShadow="False"
HorizontalOptions="StartAndExpand">
<Label
FontSize="Medium"
Text="{Binding Text}"
TextColor="#1a1a1a" />
</pancake:PancakeView>
<ActivityIndicator IsRunning="True" IsVisible="True" />
</Grid>
</ViewCell>
The problem is, in the ChatbotMessagingPage, the send button is pressed then an outgoing message is sent before getting a reply/incoming message and I've done this in MVVM like so;
ChatbotMessagingPageViewModel
//This gets the chatbots response for each message
chatbot.MainUser.ResponseReceived += async (sender, args) =>
{
await Task.Delay(1500);
Messages.Add(new ChatMessageModel() { Text = args.Response.Text, User = App.ChatBot });
};
}
#region CLASS METHODS
/// <summary>
/// This function sends a message
/// </summary>
public void Send()
{
if (!string.IsNullOrEmpty(TextToSend))
{
var msgModel = new ChatMessageModel() { Text = TextToSend, User = App.User };
//This adds a new message to the messages collection
Messages.Add(msgModel);
var result = chatbot.Evaluate(TextToSend);
result.Invoke();
//Removes the text in the Entry after message is sent
TextToSend = string.Empty;
}
}
Everytime I press the send button the ActivityIndicator comes along with the IncomingMessage, I'd like the ActivityIndicator to come first, whiles the IncomingMessage is being delayed.
I'm guessing that that view cell is the message bubble.
When you do:
Messages.Add(new ChatMessageModel() { Text = args.Response.Text, User = App.ChatBot });
Your collection is updated and your ListView or whatever hold those ViewCelss is also updated. The ActivityIndicator is part of the ViewCell so it comes at the same time as the message.
[OPTION 1] Using an additional flag
What you can do is create a flag IsBusy or IsDelay or something and bind the visibility of the ActivityIndicator and Label to it:
<Grid x:Name="Gridoo">
<pancake:PancakeView
Margin="10,10,80,10"
Padding="15"
BackgroundColor="#53ffc6"
CornerRadius="20,20,0,20"
HasShadow="False"
HorizontalOptions="StartAndExpand">
<Label
FontSize="Medium"
Text="{Binding Text}"
TextColor="#1a1a1a"
IsVisible="{Binding IsBusy, Converter={Helpers:InverseBoolConverter}}""> />
</pancake:PancakeView>
<ActivityIndicator x:Name="activityIndicator" IsRunning="True" IsVisible="{Binding IsBusy}" />
</Grid>
Note that I've used a IValueConverter to negate the value for the label. In case you're not familiar with it, check this
What's left is to add the flag in your ViewModel:
IsBusy = true; // this will make the activity indicator visible, but not the Label
// Also note that you first need to add the message
Messages.Add(new ChatMessageModel() { Text = args.Response.Text, User = App.ChatBot });
await Task.Delay(1500);
IsBusy = false; // this will hige the activity indicator visible, and make Label visible
So basically the logic is the following:
You add the message to your chat BUT the actual text is hidden when on the other hand, the activity indicator is visible.
You apply the delay
Delay ends, you change the visibility of both views.
Note that in my example I've not declared where that flag is since I'm not sure how the rest of your code looks like. It could be added to ChatMessageModel or ChatMessageViewModel since you would need a flag for each message.
[OPTION 2] in IncomingMessageItemControl.xaml.cs
A better solution could be to handle it in the code behind of your control. The issue is the same, the activity indicator and the label comes at the same time.
To fix this you can move the delay in IncomingMessageItemControl.xaml.cs.
First, you need to add x:Name to both the activity indicator and the label.
Then you could do:
private async Task ChangeVisibilityAsync()
{
Label.IsVisibe= false;
ActivityIndicator.IsVisible = true;
await Task.Delay(1500);
Label.IsVisibe = true;
ActivityIndicator.IsVisible = false;
}

Equivalent of ItemAppearing in xamarin UWP

I have a ListView bind to list of BitmapImage.
I want to get the Index of current image in focus when I scroll thru this list.
But, I notice that ItemAppearing property is not there in UWP but it is there in Xamarin Forms.
How can I get the index of the current item in view?
Thanks!
<ScrollViewer Grid.Row="0" ZoomMode="{x:Bind ZoomMode, Mode=OneWay}" HorizontalScrollMode="Enabled" HorizontalScrollBarVisibility="Auto">
<ListView HorizontalAlignment="Center" ItemsSource="{x:Bind ImagePages, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="BitmapImage">
<Image Source="{x:Bind }" Margin="0 2" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListView>
</ScrollViewer>
For starters, the ItemAppearing property is not the behavior you are looking for. The ItemAppearing event for the ListView in Xamarin Forms is fired when the list item is rendered. For a small list this event will be fired for all items immediately. The equivalent event in UWP is ListView.ChoosingItemContainer event which like the ItemAppearing event, unless the ListView is virtualized is fired for all items in the list. Even for a large virtualized list, it is fired for several pages of items.
This is not what you want. As I understand it, you want to know the image that is visible at the top of the list view when the list is scrolled. Here is how to do that.
First of all. Get rid of the ScrollViewer. The ListView already has a ScrollViewer inside of it.
<ListView x:Name="listViewImage" Grid.Row="0" HorizontalAlignment="Center" ItemsSource="{x:Bind ImagePages, Mode=OneWay}"
Loaded="listViewImage_Loaded">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="BitmapImage">
<Image Source="{x:Bind }" Margin="0 2"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListView>
Note that I have named the ListView and I have added a Loaded event handler. In this handler, find the ScrollViewer inside the ListView and attach a handler to the ViewChanged event.
private void listViewImage_Loaded(object sender, RoutedEventArgs e)
{
Border b = VisualTreeHelper.GetChild(listViewImage, 0) as Border;
ScrollViewer sv = b.Child as ScrollViewer;
sv.ViewChanged += Sv_ViewChanged;
}
In the view changed handler, find the first visible ListViewItem and get its index in the collection. This is what you want.
private void Sv_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
ScrollViewer sv = sender as ScrollViewer;
GeneralTransform gt = sv.TransformToVisual(this);
Point p = gt.TransformPoint(new Point(0, 0));
List<UIElement> list = new List<UIElement>(VisualTreeHelper.FindElementsInHostCoordinates(p, sv));
ListViewItem item = list.OfType<ListViewItem>().FirstOrDefault();
if(item != null)
{
int index = listViewImage.IndexFromContainer(item);
Debug.WriteLine("Visible item at top of list is " + index);
}
}

How to detect list view scrolling direction in xamarin forms

I have tried to detect the scrolling direction of the list view. My requirement is need to implement different functionality while list view scrolling up and scrolling down. Please suggest any idea for detecting list view scrolling direction. I have tried below syntax in my list view.
Sample code:
<StackLayout>
<Label x:Name="Direction" />
<ListView ItemsSource="{Binding Items}" HasUnevenRows = "true" ItemAppearing="Handle_ItemAppearing" IsPullToRefreshEnabled = "true">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text = "{Binding}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
I don't think you can do it by default, you can only act on a item that is appearing or disappearing. So, you either need to work with that by creating some code which gets the index of (dis)appearing items and and see if the indexes are getting higher or lower to determine whether someone is scrolling up or down. Or you need to hook up a custom renderer, but I'm not sure the native controls have anything to detect this either.
I've whipped up a very basic example for you, you can find the full code here.
Basically hook into the event available, keep track of the last index in a class variable and compare it to the current index of the item that is appearing.
private void Handle_ItemAppearing (object sender, Xamarin.Forms.ItemVisibilityEventArgs e)
{
var currentIdx = Items.IndexOf ((string)e.Item);
if (currentIdx > _lastItemAppearedIdx)
Direction.Text = "Up";
else
Direction.Text = "Down";
_lastItemAppearedIdx = Items.IndexOf ((string)e.Item);
}
In this code I simply show it in a Label, but of course you can create some enum to return or fire an event or something to make the code some more reusable. Here is the code in action:
Recently came through this problem and fixed it this way:
<StackLayout>
<Label x:Name="Direction" />
<ListView ItemsSource="{Binding Items}" HasUnevenRows = "true" ItemAppearing="Handle_ItemAppearing" ItemDisappearing="Handle_ItemDisappearing" IsPullToRefreshEnabled = "true">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text = "{Binding}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
string ScrollingDirection;
int visibleTabIndex;
int disappearingTabIndex;
public async void Handle_ItemAppearing(object sender, ItemVisibilityEventArgs e)
{
var visibleTab = e.Item;
visibleTabIndex = MyItemsList.IndexOf(visibleTab);
if (disappearingTabIndex > visibleTabIndex) ScrollingDirection = "DOWN";
else ScrollingDirection = "UP";
}
public async void Handle_ItemDisappearing(object sender, ItemVisibilityEventArgs e)
{
var invisibleTab = e.Item as TicketsList;
disappearingTabIndex = tvm.Tickets.IndexOf(invisibleTab);
}

Updating SilverLight lists when Bound in-memory collection gets a new member

I'm trying to pupulate one of three listboxes from a (fourth) source list box. The source has a list of school Subjects which are classified as elementary, middle or high school subjects. The source listbox is a list of checkboxes. The user clicks on the checkbox and one of the other three are intended to get a copy of the Subject object from the source list. I've got the thing wired up and successfully hit a CheckBox_Changed method. I can successfully locate the Subject instance from the source list and add it to the target list's Source array.
What I can't do is show the update on the Silverlight control that the target array is bound to.
Any ideas?
Thanks
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
var cb = (CheckBox)sender;
var children = ((Grid)cb.Parent).Children;
// cb has a sibling TextBlock item that has the index of the item in
// the list of subjects
var ch2 = children[1] as TextBlock;
var subjectIndexStr = ch2.Text;
var myWorkingSubject = workingSubjectList[int.Parse(subjectIndexStr)];
switch (myWorkingSubject.SubjectLevelId)
{
// updates to the elementarySubjects, middleSubjects and highSubjects
// don't get reflected in the lists that use them as a resource.
case (int)SubjectLevels.Elementary:
elementarySubjects.Add(myWorkingSubject);
break;
case (int)SubjectLevels.Middle:
middleSubjects.Add(myWorkingSubject);
break;
case (int)SubjectLevels.High:
highSubjects.Add(myWorkingSubject);
break;
default: break;
}
}
// this is how the target classes are declared.
public class SubjectsElementary : ObservableCollection<WorkingSubject>
{
}
public class SubjectsMiddle : ObservableCollection<WorkingSubject>
{
}
public class SubjectsHigh : ObservableCollection<WorkingSubject>
{
}
Here are snippets from the .xaml file
<TutorRouterSvc:WorkingSubjectList x:Key="subjects" />
<TutorRouterSvc:SubjectsElementary x:Key="elementarySubjects" />
<TutorRouterSvc:SubjectsMiddle x:Key="middleSubjects" />
<TutorRouterSvc:SubjectsHigh x:Key="highSubjects" />
<ListBox x:Name="subjectList" ItemsSource="{Binding Mode=OneWay, Source={StaticResource subjects}}">
<ListBox.Resources>
</ListBox.Resources>
<ListBox.ItemTemplate>
<StaticResource ResourceKey="DataSubjectsTemplate1"/>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ListBox Margin="0,0,8,0" x:Name="elementarySubjectList"
ItemsSource="{Binding Mode=OneWay, Source={StaticResource elementarySubjects}}"
Background="#FFE75151" Grid.Row="0">
<ListBox.ItemTemplate>
<StaticResource ResourceKey="DataSubjectsTemplate1"/>
</ListBox.ItemTemplate>
</ListBox>
<ListBox Margin="0,0,8,0" x:Name="middleSubjectList"
ItemsSource="{Binding Mode=OneWay, Source={StaticResource middleSubjects}}"
Background="#FFE75151" Grid.Row="1">
<ListBox.ItemTemplate>
<StaticResource ResourceKey="DataSubjectsTemplate1"/>
</ListBox.ItemTemplate>
</ListBox>
<ListBox Margin="0,0,8,0" x:Name="highSubjectList"
ItemsSource="{Binding Mode=OneWay, Source={StaticResource highSubjects}}"
Background="#FFE75151" Grid.Row="1">
<ListBox.ItemTemplate>
<StaticResource ResourceKey="DataSubjectsTemplate1"/>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
I'm not quite sure, but this may be fixable by doing the changes inside a Dispatch.BeginInvoke().
You could refactor the switch statement to a new method called UpdateListBox, then call it:
Dispatcher.BeginInvoke(() => UpdateListBox(myWorkingSubject.SubjectLevelId))
Maybe this is happening because the XAML is Newing up a new instance of your objects, which it's databinding to.
Try adding this to the cosntructor on your Page.xaml.cs (or where ever the control is located);
_subjects = Resources["subjects"] as WorkingSubjectsList;
_elementarySubjects = Resources["elementarySubjects"] as SubjectsElementary;
etc...
Maybe that will help. I've implemented the same concept by binding listboxes to Observable collections on several occassions and haven't experienced what you're encountering.
I do have a couple of suggestions:
have you tried this on your check changed event?
workingsubject _item = workingSubjectList[subjectsList.selectedindex];
switch (_item.SubjectLevel) //I'm assuming this property as you have the ID and it looks to be an enumeration
{
case Elementary:
elementarySubjects.Add(_item):
break;
case Middle:
middleSubjects.Add(_item):
break;
case High:
highSubjects.Add(_item):
break;
case default:
throw new Exception("Unrecognized Subject Level");
}
hth.

Resources