我希望在我的应用程序中添加滑动功能,该功能与(旧?)iPhone 上的解锁机制几乎相同(参见图片)。
我正在努力解决如何在跨平台解决方案上实现这一点。我的直接想法是使用滑块和自定义渲染器,但不确定如果用户在完成幻灯片之前放手,如何创建捕捉以启动的功能。如果有人可以协助该功能或者他们对如何实现这一目标有更好的建议,我将不胜感激。
除非和直到 - 您确实需要为每个平台提供特别原生的外观;您几乎可以使用
PanGestureRecognizer
和 AbsoluteLayout 编写自己的自定义滑块控件(无需任何自定义渲染器)。对于这种捕捉效果,您可以使用 Translation
动画 和 Cubic easing 效果。
例如,您可以定义一个控件如下;此示例控件扩展了
AbsoluteLayout
,同时允许您定义自己的代表拇指和轨迹栏的控件。它还创建了一个几乎不可见的最顶层来充当平移手势侦听器。一旦手势完成,它会检查滑动是否完成(即轨迹栏的整个宽度),然后引发 SlideCompleted
事件。
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);
}
}
使用示例:
<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>
代码隐藏
void Handle_SlideCompleted(object sender, System.EventArgs e)
{
MessageLbl.Text = "Success!!";
}
@morten-j-petersen 希望支持类似填充栏的实现;对此添加了支持。
更新了控制代码
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 用法
<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>
Android 中存在一个错误,其中手势识别器不会触发“开始”或“完成”事件!这里是链接:https://bugzilla.xamarin.com/show_bug.cgi?id=39768
因此,我实现了这个解决方法,它检查平移是否每两秒停止一次并重新启动该位置。它只在 Android 中运行计时器,就像在 iOS 中运行正常一样。代码在这里:
public class SlideToOpenView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToOpenView),
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(SlideToOpenView),
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(SlideToOpenView),
defaultValue: default(View));
public View FillBar
{
get { return (View)GetValue(FillBarProperty); }
set { SetValue(FillBarProperty, value); }
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
private bool _android = false;
public SlideToOpenView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
if (Device.RuntimePlatform == Device.Android) {
_android = true;
}
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
//Variable that stores the last state in axis X
private double _lastX = -1;
private bool _panRunning = false;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null || TrackBar == null || FillBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
Debug.WriteLine("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));
if (_panRunning == false && _android == true)
{
Device.StartTimer(TimeSpan.FromMilliseconds(2000), TimerHandle);
_panRunning = true;
}
break;
case GestureStatus.Completed:
_panRunning = false;
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),
});
//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;
}
}
//Timer handle for Android Xamarin.Forms Gesture Bug
bool TimerHandle()
{
if (_lastX == 0) {
_lastX = -1;
return false;
}
if (Thumb.TranslationX == _lastX && _lastX != -1) {
_panRunning = false;
var posX = Thumb.TranslationX;
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
// Reset translation applied during the pan
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);
_lastX = -1;
return false;
}
_lastX = Thumb.TranslationX;
return true;
}
void OnSizeChanged(object sender, EventArgs e)
{
Debug.WriteLine("OnSizeChanged");
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);
}
}
使用 xamarin 表单的自定义渲染,以便您可以定义滑块在每个平台中的外观,在 android 中 SeekBars 通常用于滑块,在 iOS UiSlider 中
https://blog.xamarin.com/customizing-xamarin-forms-controls-with-effects/
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/
此外,如果您决定使用自定义渲染,您可以使用从带有动画的 android 搜索栏派生的自己的滑块 http://www.viralandroid.com/2015/11/android-custom-seekbar-example.html
也是适用于 iOS 的自定义 UIslider
您可以在可移植类中保存通用方法,因为您已经解释了只有两种状态的行为,这也可以使用自定义开关小部件来实现
我已经把它变成了.NET MAUI的可重用控件(作为社区更容易维护),任何尝试使用它的人都可以结账
https://github.com/FreakyAli/Maui.FreakyControls/wiki/FreakySwipeButton
<freakyControls:FreakySwipeButton HeightRequest="50" SlideCompleted="Handle_SlideCompleted">
<freakyControls:FreakySwipeButton.Thumb>
<Border Padding="0" BackgroundColor="Silver">
<Image HeightRequest="40" WidthRequest="40" />
</Border>
</freakyControls:FreakySwipeButton.Thumb>
<freakyControls:FreakySwipeButton.TrackBar>
<Border Padding="0" BackgroundColor="Gray">
<Label
HorizontalTextAlignment="Center"
Text="Slide 'x' to cancel"
VerticalTextAlignment="Center" />
</Border>
</freakyControls:FreakySwipeButton.TrackBar>
<freakyControls:FreakySwipeButton.FillBar>
<VerticalStackLayout Padding="0" />
</freakyControls:FreakySwipeButton.FillBar>
</freakyControls:FreakySwipeButton>
还添加了用于滑动完成的命令。