I'm looking to add a swipe function in my app that is pretty much identical to the unlock mechanic on (old?) iPhones (See pictures).
I am struggling with how this could be achieved on a crossplatform solution. My immediate thought would be to use a slider and a custom renderer but unsure how to create the function of snapping to start if the user lets go before finishing the slide. Would appreciate if anyone could either assist with that function or if they have a better suggestion on how achieve this.
Unless and until - you really need a particularly native look for every platform; you can pretty much write your own custom slider control using PanGestureRecognizer
, and AbsoluteLayout (without any need for custom-renderers). For that snapping effect you can use Translation
animation with Cubic easing effect.
For example, you can define a control as following; this sample control extends AbsoluteLayout
while allowing you to define your own controls representing thumb and track-bar. It also creates an almost invisible top-most layer to act as pan-gesture listener. Once, gesture is completed, it checks to see if slide for complete (i.e entire width of track-bar) - and then raises SlideCompleted
event.
public class SlideToActView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToActView),
defaultValue: default(View), propertyChanged: OnThumbChanged);
public View Thumb
{
get { return (View)GetValue(ThumbProperty); }
set { SetValue(ThumbProperty, value); }
}
private static void OnThumbChanged(BindableObject bindable, object oldValue, object newValue)
{
((SlideToActView)bindable).OnThumbChangedImpl((View)oldValue, (View)newValue);
}
protected virtual void OnThumbChangedImpl(View oldValue, View newValue)
{
OnSizeChanged(this, EventArgs.Empty);
}
public static readonly BindableProperty TrackBarProperty =
BindableProperty.Create(
"TrackBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View), propertyChanged: OnTrackBarChanged);
public View TrackBar
{
get { return (View)GetValue(TrackBarProperty); }
set { SetValue(TrackBarProperty, value); }
}
private static void OnTrackBarChanged(BindableObject bindable, object oldValue, object newValue)
{
((SlideToActView)bindable).OnTrackBarChangedImpl((View)oldValue, (View)newValue);
}
protected virtual void OnTrackBarChangedImpl(View oldValue, View newValue)
{
OnSizeChanged(this, EventArgs.Empty);
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
public SlideToActView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null | TrackBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
await TrackBar.FadeTo(_fadeEffect, _animLength);
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
var x = Math.Max(0, e.TotalX);
if (x > (Width - Thumb.Width))
x = (Width - Thumb.Width);
if (e.TotalX < Thumb.TranslationX)
return;
Thumb.TranslationX = x;
break;
case GestureStatus.Completed:
var posX = Thumb.TranslationX;
// Reset translation applied during the pan (snap effect)
await TrackBar.FadeTo(1, _animLength);
await Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn);
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
break;
}
}
void OnSizeChanged(object sender, EventArgs e)
{
if (Width == 0 || Height == 0)
return;
if (Thumb == null || TrackBar == null)
return;
Children.Clear();
SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
Children.Add(TrackBar);
SetLayoutFlags(Thumb, AbsoluteLayoutFlags.None);
SetLayoutBounds(Thumb, new Rectangle(0, 0, this.Width/5, this.Height));
Children.Add(Thumb);
SetLayoutFlags(_gestureListener, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(_gestureListener, new Rectangle(0, 0, 1, 1));
Children.Add(_gestureListener);
}
}
Sample usage:
<StackLayout Margin="40">
<local:SlideToActView HeightRequest="50" SlideCompleted="Handle_SlideCompleted">
<local:SlideToActView.Thumb>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Silver" Padding="0">
<Image Source="icon.png" HorizontalOptions="Center" VerticalOptions="Center" HeightRequest="40" WidthRequest="40" />
</Frame>
</local:SlideToActView.Thumb>
<local:SlideToActView.TrackBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Gray" Padding="0">
<Label Text="Slide 'x' to cancel" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
</Frame>
</local:SlideToActView.TrackBar>
</local:SlideToActView>
<Label x:Name="MessageLbl" FontAttributes="Bold" TextColor="Green" />
</StackLayout>
Code-Behind
void Handle_SlideCompleted(object sender, System.EventArgs e)
{
MessageLbl.Text = "Success!!";
}
As @morten-j-petersen wanted support for a fill-bar like implementation; added support for that.
Updated control code
public class SlideToActView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View Thumb
{
get { return (View)GetValue(ThumbProperty); }
set { SetValue(ThumbProperty, value); }
}
public static readonly BindableProperty TrackBarProperty =
BindableProperty.Create(
"TrackBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View TrackBar
{
get { return (View)GetValue(TrackBarProperty); }
set { SetValue(TrackBarProperty, value); }
}
public static readonly BindableProperty FillBarProperty =
BindableProperty.Create(
"FillBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View FillBar
{
get { return (View)GetValue(FillBarProperty); }
set { SetValue(FillBarProperty, value); }
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
public SlideToActView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null || TrackBar == null || FillBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
await TrackBar.FadeTo(_fadeEffect, _animLength);
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
var x = Math.Max(0, e.TotalX);
if (x > (Width - Thumb.Width))
x = (Width - Thumb.Width);
//Uncomment this if you want only forward dragging.
//if (e.TotalX < Thumb.TranslationX)
// return;
Thumb.TranslationX = x;
SetLayoutBounds(FillBar, new Rectangle(0, 0, x + Thumb.Width / 2, this.Height));
break;
case GestureStatus.Completed:
var posX = Thumb.TranslationX;
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
// Reset translation applied during the pan
await Task.WhenAll(new Task[]{
TrackBar.FadeTo(1, _animLength),
Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn),
});
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
break;
}
}
void OnSizeChanged(object sender, EventArgs e)
{
if (Width == 0 || Height == 0)
return;
if (Thumb == null || TrackBar == null || FillBar == null)
return;
Children.Clear();
SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
Children.Add(TrackBar);
SetLayoutFlags(FillBar, AbsoluteLayoutFlags.None);
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
Children.Add(FillBar);
SetLayoutFlags(Thumb, AbsoluteLayoutFlags.None);
SetLayoutBounds(Thumb, new Rectangle(0, 0, this.Width/5, this.Height));
Children.Add(Thumb);
SetLayoutFlags(_gestureListener, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(_gestureListener, new Rectangle(0, 0, 1, 1));
Children.Add(_gestureListener);
}
}
XAML Usage
<StackLayout Margin="40">
<local:SlideToActView HeightRequest="50" SlideCompleted="Handle_SlideCompleted">
<local:SlideToActView.Thumb>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Silver" Padding="0">
<Image Source="icon.png" HorizontalOptions="Center" VerticalOptions="Center" HeightRequest="40" WidthRequest="40" />
</Frame>
</local:SlideToActView.Thumb>
<local:SlideToActView.TrackBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Gray" Padding="0">
<Label Text="Slide 'x' to cancel" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
</Frame>
</local:SlideToActView.TrackBar>
<local:SlideToActView.FillBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Red" Padding="0" />
</local:SlideToActView.FillBar>
</local:SlideToActView>
<Label x:Name="MessageLbl" FontAttributes="Bold" TextColor="Green" />
</StackLayout>