Xamarin Forms slide button

帅比萌擦擦* 提交于 2019-11-30 10:39:06

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


Update : 08/30

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>

using custom renders for xamarin forms so that you could define how the slider should look in each platform, In android SeekBars are commonly used for sliders and in iOS UiSlider

https://blog.xamarin.com/customizing-xamarin-forms-controls-with-effects/

https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/

also since if you have decided to use custom render, you can use your own slider derived from android seek bar with animations http://www.viralandroid.com/2015/11/android-custom-seekbar-example.html

also a custom UIslider for iOS

you can hold up your generic methods in a portable class , as you have explained the behavior which only have two states this might also be achievable using a custom switch widget

There is a bug in Android in which the Gesture Recognizer does not trigger the Started or Completed event! Here the link: https://bugzilla.xamarin.com/show_bug.cgi?id=39768

So, I implemented this workaround which checks if the pan is stopped every two seconds and restarts the position. It only runs the timer in Android as in iOS runs ok. Here the code:

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


    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!