Xamarin forms: How to add SwipeGestureRecognizer for StackLayout? - xamarin.forms

I have 15 options on my home page. Initially, I will show 9 options in UI. To view the remaining 6 icons, the user slides to the right and back to the left to see previous. I try to implement the swiping feature like below, but it is not working.
XAML
<StackLayout x:Name="firstLlayout">
<Grid>
//3 icons in horizontal
</Grid>
<Grid>
//3 icons in horizontal
</Grid>
<Grid>
//3 icons in horizontal
</Grid>
<StackLayout.GestureRecognizers>
<SwipeGestureRecognizer Direction="Right" Swiped="RightSwipe"/>
</StackLayout.GestureRecognizers>
</StackLayout>
<StackLayout IsVisible="False" x:Name="secondLayout">
<Grid>
//3 icons in horizontal
</Grid>
<Grid>
//3 icons in horizontal
</Grid>
<StackLayout.GestureRecognizers>
<SwipeGestureRecognizer Direction="Left" Swiped="LeftSwipe"/>
</StackLayout.GestureRecognizers>
</StackLayout>
XAML.CS
public void RightSwipe(object sender, EventArgs e)
{
firstLlayout.IsVisible = false;
secondLayout.IsVisible = true;
}
public void LeftSwipe(object sender, EventArgs e)
{
secondLayout.IsVisible = false;
firstLlayout.IsVisible = true;
}
When try left and right swipe nothing is happening in UI, and code execution not coming to event functions. What I am missing here?

Cause1:
Swipe action will conflict with scroll action if you put the stacklayout in a ScrollView .
Solution:
Remove the ScrollView from Root StackLayout, then the swiping will work.
Cause2: It is necessary to add a child control(like Image or Label)to StackLayout , otherwise the swipe action will never been called .
Solution: If you do want to let the content of StackLayout shows nothing in default, you can check the following code .
in code behind
using System;
using Xamarin.Forms;
namespace xxx
{
public class GestureScrollView : ScrollView
{
public event EventHandler SwipeLeft;
public event EventHandler SwipeRight;
public void OnSwipeLeft() =>
SwipeLeft?.Invoke(this, null);
public void OnSwipeRight() =>
SwipeRight?.Invoke(this, null);
}
}
in Android Project
using System;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using xxx;
using xxx.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(GestureScrollView), typeof(GestureScrollViewRenderer))]
namespace xxx.Droid
{
public class GestureScrollViewRenderer : ScrollViewRenderer
{
readonly CustomGestureListener _listener;
readonly GestureDetector _detector;
public GestureScrollViewRenderer(Context context) : base(context)
{
_listener = new CustomGestureListener();
_detector = new GestureDetector(context, _listener);
}
public override bool DispatchTouchEvent(MotionEvent e)
{
if (_detector != null)
{
_detector.OnTouchEvent(e);
base.DispatchTouchEvent(e);
return true;
}
return base.DispatchTouchEvent(e);
}
public override bool OnTouchEvent(MotionEvent ev)
{
base.OnTouchEvent(ev);
if (_detector != null)
return _detector.OnTouchEvent(ev);
return false;
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.NewElement == null)
{
_listener.OnSwipeLeft -= HandleOnSwipeLeft;
_listener.OnSwipeRight -= HandleOnSwipeRight;
}
if (e.OldElement == null)
{
_listener.OnSwipeLeft += HandleOnSwipeLeft;
_listener.OnSwipeRight += HandleOnSwipeRight;
}
}
void HandleOnSwipeLeft(object sender, EventArgs e) =>
((GestureScrollView)Element).OnSwipeLeft();
void HandleOnSwipeRight(object sender, EventArgs e) =>
((GestureScrollView)Element).OnSwipeRight();
}
public class CustomGestureListener : GestureDetector.SimpleOnGestureListener
{
static readonly int SWIPE_THRESHOLD = 100;
static readonly int SWIPE_VELOCITY_THRESHOLD = 100;
MotionEvent mLastOnDownEvent;
public event EventHandler OnSwipeLeft;
public event EventHandler OnSwipeRight;
public override bool OnDown(MotionEvent e)
{
mLastOnDownEvent = e;
return true;
}
public override bool OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
if (e1 == null)
e1 = mLastOnDownEvent;
float diffY = e2.GetY() - e1.GetY();
float diffX = e2.GetX() - e1.GetX();
if (Math.Abs(diffX) > Math.Abs(diffY))
{
if (Math.Abs(diffX) > SWIPE_THRESHOLD && Math.Abs(velocityX) > SWIPE_VELOCITY_THRESHOLD)
{
if (diffX > 0)
OnSwipeRight?.Invoke(this, null);
else
OnSwipeLeft?.Invoke(this, null);
}
}
return base.OnFling(e1, e2, velocityX, velocityY);
}
}
}
And in Xaml
Put the StackLayout in the ScrollView
<local:GestureScrollView SwipeRight="RightSwipe">
<StackLayout x:Name="firstLlayout" >
//...
</StackLayout>
</local:GestureScrollView>

I like and used Lucas Zhang's answer answer-59190549 to this question however, the GestureScrollView can be changed to utilize the same GestureRecognizer's that iOS will use like so:
public class GestureScrollView : ScrollView
{
private bool isInitialized = false;
private List<SwipeGestureRecognizer> LeftSwipeRecognizers { get; } = new();
private List<SwipeGestureRecognizer> RightSwipeRecognizers { get; } = new();
public GestureScrollView() : base()
{
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
base.LayoutChildren(x, y, width, height);
//Not sure if this is the best place, but the ctor wasn't getting called.
if (!isInitialized)
{
isInitialized = true;
foreach (SwipeGestureRecognizer swipeGestureRecognizer in GestureRecognizers.Where(x => x is SwipeGestureRecognizer))
{
if (swipeGestureRecognizer.Direction.HasFlag(SwipeDirection.Left))
{
LeftSwipeRecognizers.Add(swipeGestureRecognizer);
}
if (swipeGestureRecognizer.Direction.HasFlag(SwipeDirection.Right))
{
RightSwipeRecognizers.Add(swipeGestureRecognizer);
}
}
}
}
private void ExecuteGestureCommands(List<SwipeGestureRecognizer> swipeRecognizers, SwipedEventArgs e)
{
foreach (var gesture in swipeRecognizers)
{
gesture.SendSwiped(this, e.Direction);
}
}
public void OnSwipeLeft(object sender, SwipedEventArgs e)
{
ExecuteGestureCommands(LeftSwipeRecognizers, e);
}
public void OnSwipeRight(object sender, SwipedEventArgs e)
{
ExecuteGestureCommands(RightSwipeRecognizers, e);
}
}
And the Renderer would need changes similar to these (Basically change all "EventArgs" references to "SwipedEventArgs" and pass in a new SwipedEventArgs on the event calls):
...
void HandleOnSwipeLeft(object sender, SwipedEventArgs e) => ((GestureScrollView)Element).OnSwipeLeft(sender, e);
void HandleOnSwipeRight(object sender, SwipedEventArgs e) => ((GestureScrollView)Element).OnSwipeRight(sender, e);
...
public event EventHandler<SwipedEventArgs> OnSwipeLeft;
public event EventHandler<SwipedEventArgs> OnSwipeRight;
...
if (diffX > 0)
{
OnSwipeRight?.Invoke(this, new SwipedEventArgs(null, SwipeDirection.Right));
}
else
{
OnSwipeLeft?.Invoke(this, new SwipedEventArgs(null, SwipeDirection.Left));
}
...
It's not perfect as it ignores the Up/Down directions in Android, but so does the other implementation.

Related

Xamarin Boolean Bindable Property

I am trying to make clickable Icon which will be using for Wish List, for this I have created boolean property which will return Image.
This is my code, but it does not support onClick event, Please advise to figure out this problem.
public class WishIconImg : Image, IDisposable
{
static FontImageSource unselected_source = new FontImageSource();
static FontImageSource selected_source = new FontImageSource();
public WishIconImg()
{
unselected_source.FontFamily = "FA-S";
unselected_source.Glyph = "\U000f02d5";
unselected_source.Color = Color.DarkOrange;
selected_source.FontFamily = "FA-S";
selected_source.Glyph = "\U000f02d1";
selected_source.Color = Color.DarkOrange;
OnClick += Checkbox_OnClick;
}
public static BindableProperty IsCheckedProperty = BindableProperty.Create(
nameof(IsChecked), typeof(bool), typeof(WishIconImg), defaultBindingMode: BindingMode.TwoWay,
propertyChanged: IsCheckedChanged);
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
private static void IsCheckedChanged(BindableObject bindable, object oldValue, object newValue)
{
var cb = (WishIconImg)bindable;
if (cb == null)
return;
if ((bool)newValue)
{
cb.Source = selected_source;
}
else
{
cb.Source=unselected_source ;
}
}
void Checkbox_OnClick(object sender, EventArgs e)
{
IsChecked = !IsChecked;
}
public void Dispose()
{
OnClick -= Checkbox_OnClick;
}
}
}
Xaml
<controls:WishIconImg x:Name="HeartChk" IsChecked="{Binding AddWish, Mode=TwoWay}" HeightRequest="35" WidthRequest="35" HorizontalOptions="End"/>
Even I have tried with Label property but it doesnt work
You could modify the class like following
public WishIconImg()
{
unselected_source.FontFamily = "FA-S";
unselected_source.Glyph = "\U000f02d5";
unselected_source.Color = Color.DarkOrange;
selected_source.FontFamily = "FA-S";
selected_source.Glyph = "\U000f02d1";
selected_source.Color = Color.DarkOrange;
var tapGestureRecognizer = new TapGestureRecognizer();
tapGestureRecognizer.Tapped += (s, e) => {
// handle the tap
IsChecked = !IsChecked;
};
this.GestureRecognizers.Add(tapGestureRecognizer);
}
Try adding TapGestureRecognizer for an click event
Do something like this
<StackLayout HeightRequest="35" WidthRequest="35" HorizontalOptions="End">
<controls:WishIconImg x:Name="HeartChk" IsChecked="{Binding AddWish, Mode=TwoWay}" />
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Checkbox_OnClick}" />
</StackLayout.GestureRecognizers>
</StackLayout>
In your ViewModel Bind the command for it
public System.Windows.Input.ICommand Checkbox_OnClick => new Xamarin.Forms.Command(Checkbox_OnClickTapped);
Checkbox_OnClickTapped will be your method called when your view will be clicked

Timer Speed is increasing on picker selection change

I am creating a timer which is working on a picker selection changed.Timer is working good until the times end to the "00:00:00".If i change the selection changed in the mid of time then the speed of timer is increased and if i again change selected index than timer run much fast. My problem is that timer should run on 1 sec dealy every time.
I am sharing my code below. Thanks in advance for help.
//picker selection change
private async void GName_SelectedIndexChanged(object sender, EventArgs e)
{
lblEndTime.Text = "00:00:10";
StartTimmer();
}
public void StartTimmer()
{
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
Device.BeginInvokeOnMainThread(() =>
{
TimeSpan timeSpan = TimeSpan.Parse(lblEndTime.Text);
timeSpan -= TimeSpan.FromSeconds(1);
if (lblEndTime.Text == "00:00:00")
{
returnValue = false;
}
else
{
lblEndTime.Text = Convert.ToString(timeSpan);
}
});
return returnValue;
});
}
Use a System.Timer and stop the timer every time you change the selected index instead of creating a new one.
public partial class MainPage : ContentPage
{
Timer Timer1 { get; set; }
public MainPage()
{
InitializeComponent();
}
public void StartTimmer()
{
if (Timer1 != null)
{
Timer1.Stop();
Timer1.Dispose();
}
Timer1 = new Timer();
Timer1.Interval = 1000;
Timer1.Enabled = true;
Timer1.Start();
Timer1.Elapsed += (object sender, System.Timers.ElapsedEventArgs e) =>
{
Device.BeginInvokeOnMainThread(() =>
{
TimeSpan timeSpan = TimeSpan.Parse(lblEndTime.Text);
timeSpan -= TimeSpan.FromSeconds(1);
if (lblEndTime.Text == "00:00:00")
{
Timer1.Stop();
Timer1.Dispose();
}
else
{
lblEndTime.Text = Convert.ToString(timeSpan);
}
});
};
}
private void Button_Clicked(object sender, EventArgs e)
{
lblEndTime.Text = "00:00:10";
StartTimmer();
}
}
And in xaml:
<StackLayout>
<!-- Place new controls here -->
<Label x:Name="lblEndTime" Text="Welcome to Xamarin.Forms!"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<Button Text="Click Me" Clicked="Button_Clicked"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
</StackLayout>

How to properly redirect a Webview that contains a pdf to Google Drive

On an Xamarian.Forms app, which is trying to redirect links that contain a PDF in from a WebView to a Google Drive URL. My code works as expected on iOS but just spins on an Android.
public partial class Balance : ContentPage
{
public Balance()
{
InitializeComponent();
website.Navigated += WebView_Navigated;
website.Navigating += WebView_Navigating;
UrlWebViewSource source = new UrlWebViewSource();
source.Url = "https://www.google.com";
website.Source = source;
}
private void WebView_Navigating(object sender, WebNavigatingEventArgs e)
{
progress.IsVisible = true;
website.IsVisible = false;
}
public void WebView_Navigated(object sender, WebNavigatedEventArgs e)
{
progress.IsVisible = false;
website.IsVisible = true;
if (e.Url.Contains("pdf") && !e.Url.Contains("drive.google.com"))
{
var webview = (WebView)sender;
website.Source = "https://drive.google.com/viewerng/viewer?embedded=true&url=" + e.Url;
}
}
}
On Android the site never loads and the activity indicator just sits and spins.
According to your description, I use your code and redirect a webview, here is my code, there is no issue on android, nospins on android :
<ContentPage.Content>
<StackLayout>
<WebView
x:Name="website"
HeightRequest="1000"
Navigated="Website_Navigated"
Navigating="Website_Navigating"
WidthRequest="1000" />
</StackLayout>
</ContentPage.Content>
public partial class Page19 : ContentPage
{
public Page19 ()
{
InitializeComponent ();
UrlWebViewSource source = new UrlWebViewSource();
//source.Url = "https://www.google.com";
source.Url = "http://www.pdf995.com/samples/pdf.pdf";
website.Source = source;
}
private void Website_Navigated(object sender, WebNavigatedEventArgs e)
{
website.IsVisible = false;
}
private void Website_Navigating(object sender, WebNavigatingEventArgs e)
{
website.IsVisible = true;
if (e.Url.Contains("pdf") && !e.Url.Contains("drive.google.com"))
{
var webview = (WebView)sender;
website.Source = "https://drive.google.com/viewerng/viewer?embedded=true&url=" + e.Url;
}
}
}

Xamarin Forms Custom Stepper with 2 buttons and Entry

I implemented this CustomStepper:
using System;
using Xamarin.Forms;
namespace AppXamarin
{
public class CustomStepper : StackLayout
{
Button PlusBtn;
Button MinusBtn;
Entry Entry;
public static readonly BindableProperty TextProperty =
BindableProperty.Create(
propertyName: "Text",
returnType: typeof(int),
declaringType: typeof(CustomStepper),
defaultValue: 0,
defaultBindingMode: BindingMode.TwoWay);
public int Text
{
get { return (int)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public CustomStepper()
{
PlusBtn = new Button { WidthRequest = 30, HeightRequest = 30 };
MinusBtn = new Button { WidthRequest = 30, HeightRequest = 30 };
PlusBtn.Image = "exp20181029Artboard51";
MinusBtn.Image = "exp20181029Artboard52";
switch (Device.RuntimePlatform)
{
case Device.UWP:
case Device.Android:
{
PlusBtn.BackgroundColor = Color.Transparent;
MinusBtn.BackgroundColor = Color.Transparent;
break;
}
case Device.iOS:
{
PlusBtn.BackgroundColor = Color.Transparent;
MinusBtn.BackgroundColor = Color.Transparent;
break;
}
}
Orientation = StackOrientation.Horizontal;
PlusBtn.Clicked += PlusBtn_Clicked;
MinusBtn.Clicked += MinusBtn_Clicked;
Entry = new Entry { PlaceholderColor = Color.Gray, Keyboard = Keyboard.Numeric, WidthRequest = 30, BackgroundColor = Color.Transparent, FontSize = 15 };
Entry.Keyboard = Keyboard.Numeric;
Entry.Behaviors.Add(new NumericValidationBehavior());
Entry.SetBinding(Entry.TextProperty, new Binding(nameof(Text), BindingMode.TwoWay, source: this));
Entry.HorizontalTextAlignment = TextAlignment.Center;
Entry.TextChanged += Entry_TextChanged;
Children.Add(MinusBtn);
Children.Add(Entry);
Children.Add(PlusBtn);
}
private void Entry_TextChanged(object sender, TextChangedEventArgs e)
{
if (!string.IsNullOrEmpty(e.NewTextValue) && e.NewTextValue != ".")
this.Text = int.Parse(e.NewTextValue);
}
private void MinusBtn_Clicked(object sender, EventArgs e)
{
if (Text > 0)
Text--;
}
private void PlusBtn_Clicked(object sender, EventArgs e)
{
Text++;
}
}
}
When placing normally in the page I can access it and take the text property and use it in my Xaml.cs code. But in my case, I'm placing it inside a listview and as you know in listview the items are bindable I can't access it directly. In the regular stepper when it is placed in the listview we can use the "ValueChanged" method and can easily get the value by using e.NewValue in the "ValueChanged" method in the Xaml.cs file. Is there a way that I can add something to the CustomStepper class that can help me access the Text property and uses it in the Xaml.cs file? Thanks in advance
You can create a property for EventHandlers. In this case, you would use the event modifier on the property to tell the program that the property is triggering an event. For example:
private EventHandler onValueChangedEvent = null;
public event EventHandler OnValueChanged
{
add
{
onValueChangedEvent = null;
onValueChangedEvent = value;
}
remove
{
// Will show a warning. You can ignore it.
onValueChangedEvent = null;
}
}
private void Entry_TextChanged(object sender, TextChangedEventArgs e)
{
if (!string.IsNullOrEmpty(e.NewTextValue) && e.NewTextValue != ".")
this.Text = int.Parse(e.NewTextValue);
onValueChangedEvent?.Invoke(this, e);
}
You would then bind/assign an event handler in your xaml.cs code to the OnValueChanged property, which will get triggered when the value changes.

Xamarin.Forms Slider.Clicked event?

I'm making an mp3 player in Xamarin.Forms, in which the slider should both show time expired and allow jumping to locations in the track.
As far as I can tell, only the ValueChanged event is available, with the unfortunate sideeffect that every time my timer updates the slider value, I also trigger my player.SeekTo method, causing broken playback.
Is there any way to specifically pick up touch events on a slider?
If not, does anyone have any suggestions on how to make this work?
In case it's relevant, here's my code for the MainPage:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace MuZor
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
PlayButton.Clicked += PlayButton_Clicked;
PlaylistsButton.Clicked += PlaylistsButton_Clicked;
RandomButton.Clicked += RandomButton_Clicked;
RepeatButton.Clicked += RepeatButton_Clicked;
ChoiceButton.Clicked += ChoiceButton_Clicked;
PreviousButton.Clicked += PreviousButton_Clicked;
NextButton.Clicked += NextButton_Clicked;
TimeSlider.ValueChanged += TimeSlider_ValueChanged;
MessagingCenter.Subscribe<Interfaces.IAudioPlayer>(App.player, "PlayerPrepared", (args) =>
{
System.Diagnostics.Debug.WriteLine("Message received");
PlayerPrepared();
});
}
private void TimeSlider_ValueChanged(object sender, ValueChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("Slider value changed, value = " + e.NewValue + "(int = " + (int)e.NewValue);
//App.player.SeekTo((int)e.NewValue);
}
private void NextButton_Clicked(object sender, EventArgs e)
{
if (!App.settings.RandomOn && App.settings.CurrentTrack < App.playlist.Count - 1)
{
Play((int)App.settings.CurrentTrack + 1);
}
}
private void PreviousButton_Clicked(object sender, EventArgs e)
{
if (!App.settings.RandomOn && App.settings.CurrentTrack > 0)
{
Play((int)App.settings.CurrentTrack - 1);
}
}
private void ChoiceButton_Clicked(object sender, EventArgs e)
{
}
private void RepeatButton_Clicked(object sender, EventArgs e)
{
}
private void RandomButton_Clicked(object sender, EventArgs e)
{
}
private void PlaylistsButton_Clicked(object sender, EventArgs e)
{
}
private void PlayButton_Clicked(object sender, EventArgs e)
{
if (App.settings.IsPaused || App.player.IsPlaying())
{
App.player.PauseResume();
if (App.settings.IsPaused)
{
UnPause();
}
else
{
Pause();
}
}
else
{
int trackToplay = App.settings.CurrentTrack != null ? (int)App.settings.CurrentTrack : 0;
Play(trackToplay);
}
}
private void Play(int currentTrack)
{
HelperClasses.SettingsHelper.SaveCurrentTrack(currentTrack);
App.player.LoadAndPlay(App.playlist[currentTrack].Path);
}
private void Pause()
{
HelperClasses.SettingsHelper.SavePausedState(true);
Device.BeginInvokeOnMainThread(() =>
{
PlayButton.Text = "Play";
});
}
private void UnPause()
{
HelperClasses.SettingsHelper.SavePausedState(false);
Device.BeginInvokeOnMainThread(() =>
{
PlayButton.Text = "Pause";
});
StartTimer();
}
private void StartTimer()
{
double position;
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
{
position = App.player.GetCurrentPosition();
TimeSpan runTime = TimeSpan.FromMilliseconds(position);
Device.BeginInvokeOnMainThread(() =>
{
TimeLabel.Text = runTime.ToString(#"mm\:ss");
TimeSlider.Value = position;
});
if (App.player.IsPlaying())
return true;
else
return false;
});
}
private void PlayerPrepared()
{
var totalDurationInMS = App.player.GetDuration();
TimeSlider.Maximum = totalDurationInMS;
TimeSlider.Minimum = 0;
TimeSpan totalDuration = TimeSpan.FromMilliseconds(totalDurationInMS);
Device.BeginInvokeOnMainThread(() =>
{
RemainingTimeLabel.Text = totalDuration.ToString(#"mm\:ss");
TimeLabel.Text = "00:00";
});
UnPause();
}
}
}
I think I've found a workaround. Since the valuechanged event contains both old and new values, I'll only fire SeekTo if the difference is negative or bigger than 2.

Resources