iOS range slider for Xamarin and Xamarin.Forms

In this post I will show you how to build a range slider for Xamarin.iOS and how to write a custom renderer if you want to use it with Xamarin.Forms. If you want to use the range slider control you can write the code yourself or use the flipper forms control library, https://github.com/johankson/flipper, that can be installed from NuGet, https://www.nuget.org/packages/Flipper.Forms/. On GitHub you will also find a sample app that using the control.


iOS Range Slider

The control will be built up of six UIViews, one for the slider background, two for the indicators, two transparent views to increase the touch area for the indicators and one for the range between the indicators. The first step is to create a new class that inherits from UIView. In the constructor of the new class we are creating the views and adding them to the view. On the both indicators we’re adding a UIPanGestureRecognizer so we can detect when the users dragging the indicator.

public class RangeSlider : UIView
{
    private UIView _background, _leftIndicator, _rightIndicator, _range, _leftTouchArea, _rightTouchArena;
        private UIPanGestureRecognizer _leftIndicatorGesture, _rightIndicatorGesture;
 
     public RangeSlider()
     {
            _background = new UIView();
            _background.BackgroundColor = UIColor.LightGray;
 
            _range = new UIView();
            _range.BackgroundColor = UIColor.Blue;
 
            _leftIndicator = CreateIndicator();
            _leftIndicatorGesture = new UIPanGestureRecognizer(OnPan);
 
            _rightIndicator = CreateIndicator();
            _rightIndicatorGesture = new UIPanGestureRecognizer(OnPan);
 
            _leftTouchArea = new UIView();
            _leftTouchArea.BackgroundColor = UIColor.Clear;
            _leftTouchArea.AddGestureRecognizer(_leftIndicatorGesture);
 
            _rightTouchArena = new UIView();
            _rightTouchArena.BackgroundColor = UIColor.Clear;
            _rightTouchArena.AddGestureRecognizer(_rightIndicatorGesture);
 
            AddSubview(_background);
            AddSubview(_range);
            AddSubview(_leftIndicator);
            AddSubview(_rightIndicator);
            AddSubview(_leftTouchArea);
            AddSubview(_rightTouchArena);     
     }
 
     private UIView CreateIndicator()
     {
            var indicator = new UIView()
            {
                   BackgroundColor = UIColor.Gray
            };
 
            indicator.Layer.CornerRadius = 10;
            indicator.Layer.MasksToBounds = true;
 
            return indicator;
      }
}

Then we need to layout our views, we will do that in an override of the LayoutSubviews method. We also need to check if the views already is layouted so the layout not will be restored to the start layout if the method runs again.

private bool _layouted;
public override void LayoutSubviews()
{
      base.LayoutSubviews();
 
      if (!_layouted)
      {
            _background.Frame = new RectangleF(0, 19, (float)Frame.Width-20, 2);
            _range.Frame = new RectangleF(0, 19, (float)Frame.Width-20, 2);
            _leftIndicator.Frame = new RectangleF(0, 10, 20, 20);
            _rightIndicator.Frame = new RectangleF((float)Frame.Width - 40, 10, 20, 20);
 
            _leftTouchArea.Frame = new RectangleF(0, 0, 40, 40);
            _rightTouchArena.Frame = new RectangleF((float)Frame.Width - 60, 0, 40, 40);
 
            _layouted = true;
       }
}

In the OnPan method we want to update the position of the indicators if state of the gesture recognizer is began or changed. For this range slider we want the indicator to move in steps. To do this we need to move the indicator to next step if we have started to slide from the previous step. For that we need to know the step length in pixels and the cumulative manipulation and the delta manipulation. To calculate cumulative manipulation we need to save the position of the indicator when we starting the manipulation.

While you moving the indicator to next step when it has passed the previous you will have to check if the cumulative manipulation has passed the current step before you moving the indicator to next step.

 private void OnPan(UIPanGestureRecognizer recognizer)
        {
            if (recognizer.State == UIGestureRecognizerState.Began || recognizer.State == UIGestureRecognizerState.Changed)
            {
                var stepLength = _background.Frame.Width / ((MaxValue - MinValue) / Step);
 
                var touchPoint = recognizer.LocationInView(this);
 
                UIView indicator = null;
                UIView touchArea = null;
 
                //Is this a slide to left or right?
                if (recognizer == _leftIndicatorGesture)
                {
                    indicator = _leftIndicator;
                    touchArea = _leftTouchArea;
                }
                else if (recognizer == _rightIndicatorGesture)
                {
                    indicator = _rightIndicator;
                    touchArea = _rightTouchArena;
                }
 
                //save the start position for use when calculating cumulative manipulation
                if (recognizer.State == UIGestureRecognizerState.Began)
                {
                    _startX = (float)indicator.Center.X;
                }
 
 
                var cumulativeManipulation = touchPoint.X - _startX;
                var deltaManipulation = touchPoint.X - indicator.Center.X;
 
                //Check if the cumulative manipulation is has passed the last step
                if (deltaManipulation > 0 && cumulativeManipulation / stepLength > _lastStep ||
                    deltaManipulation < 0 && cumulativeManipulation / stepLength < _lastStep)
                {
                    if (deltaManipulation > 0)
                    {
                        _lastStep++;
                    }
                    else
                    {
                        _lastStep--;
                    }
 
                    //Calculate the new position of the indicator
                    var numberOfSteps = Math.Ceiling(deltaManipulation / stepLength);
                    var newPosition = new CGPoint(indicator.Center.X + stepLength * numberOfSteps, indicator.Center.Y);
 
                    var pixelStep = (MaxValue - MinValue) / Frame.Width;
 
                    if (touchPoint.X >= 0 && touchPoint.X <= _background.Frame.Width-10)
                    {
 
 
                        if (recognizer == _leftIndicatorGesture)
                        {
 
                            var newLeftValue = Round(MinValue + (pixelStep * newPosition.X));
 
                            if (newLeftValue >= RightValue)
                            {
                                return;
                            }
                        }
                        else if (recognizer == _rightIndicatorGesture)
                        {
                            var newRightValue = Round(MinValue + (pixelStep * newPosition.X));
 
                            if (newRightValue <= LeftValue)
                            {
                                return;
                            }
                        }
 
 
                        if (recognizer == _leftIndicatorGesture)
                        {
                            indicator.Center = newPosition;
                            touchArea.Center = newPosition;
                            var width = _rightIndicator.Center.X - _leftIndicator.Center.X;
                            _range.Frame = new CoreGraphics.CGRect(newPosition.X, _range.Frame.Y, width, _range.Frame.Height);
                        }
                        else if (recognizer == _rightIndicatorGesture)
                        {
                            indicator.Center = newPosition;
                            touchArea.Center = newPosition;
                            var width = _rightIndicator.Center.X - _leftIndicator.Center.X;
                            _range.Frame = new CoreGraphics.CGRect(_range.Frame.X, _range.Frame.Y, width, _range.Frame.Height);
                        }
 
 
 
                        LeftValue = Round(MinValue + (pixelStep * _leftIndicator.Center.X));
                        RightValue = Round(MinValue + (pixelStep * _rightIndicator.Center.X));
 
                        if (ValueChanging != null)
                        {
                            ValueChanging(this, new EventArgs());
                        }
                    }
                }
            }
            else if (recognizer.State == UIGestureRecognizerState.Ended)
            {
                if (ValueChanged != null)
                {
                    ValueChanged(this, new EventArgs());
                }
 
                _lastStep = 0;
            }
        }

I have added to events to the slider, ValueChanging and ValueChanged. ValueChanging occurs during the manipulation of the range slider and ValueChanged when the manipulation is finished. This because you maybe want to update labels with the values of the range slider during manipulation and update data based on the range slider when the manipulation is completed.

We also want to make it possible to set start values for the indicators. To do that we are creating a method called UpdateValue. We call the method from LayoutSubviews and from the setters of LeftValue and RightValue. It is important that the code not is running after that we have started using the slider. Therefore we have surrounded the call to the method with an if-statment. The code will run if a initialized variable is false, we will set it to true in the OnPan method.

private void UpdateValue(UIView indicator, UIView touchArea, double value)
{
            var percent = value / (MaxValue - MinValue);
 
            var position = (double)(_background.Frame.Width * percent);
 
            if (!double.IsNaN(position))
            {
                indicator.Center = new CGPoint(position, indicator.Center.Y);
                touchArea.Center = new CGPoint(position, indicator.Center.Y);
 
                var width = _rightIndicator.Center.X - _leftIndicator.Center.X;
                _range.Frame = new CoreGraphics.CGRect(_leftIndicator.Center.X, _range.Frame.Y, width, _range.Frame.Height);
 
                if (ValueChanged != null)
                {
                    ValueChanged(this, new EventArgs()); 
                }
            }
}

Xamarin.Forms
If you want to use the control with Xamarin.Forms you need to create a Xamarin.Forms control and a custom renderer. The Xamarin.Forms control will inherit from View and it will contains all the properties of the range slider, no logic at all.

 public class RangeSliderRenderer : ViewRenderer<Flipper.Controls.RangeSlider, iOS.Controls.RangeSlider>
    {
        protected override void OnElementChanged(ElementChangedEventArgs<RangeSlider> e)
        {
            base.OnElementChanged(e);
 
            if (e.NewElement != null)
            {
                var slider = new Controls.RangeSlider();
                slider.Frame = new CGRect((float)Frame.X, (float)Frame.Y, (float)Frame.Width, 200);
 
                slider.ValueChanging += slider_ValueChanging;
                slider.ValueChanged += slider_ValueChanged;              
 
                SetNativeControl(slider); 
            }
 
        }
 
        void slider_ValueChanged(object sender, EventArgs e)
        {
           if(Element.Command != null && Element.Command.CanExecute(null))
           {
               Element.Command.Execute(null);
           }
 
           Element.NotifyValueChanged();
        }
 
        void slider_ValueChanging(object sender, EventArgs e)
        {
            Element.LeftValue = (float)Control.LeftValue;
            Element.RightValue = (float)Control.RightValue;
        }

The complete code can be found at GitHub, https://github.com/johankson/flipper

18 thoughts on “iOS range slider for Xamarin and Xamarin.Forms

  1. RIYAZ says:

    Hi Daniel,

    Really nice post Thanks for sharing! Small request please attached a screenshot about the post’s end result so that we’ll have much clarity and feasibility about the post.

    Thanks
    RIYAZ

  2. Neha says:

    Hi Daniel,

    I tried this one is in my app. Its working good. But I am facing an issue. I want to set the LeftValue and RightValue while declaring the Range slider. This is not working for me. When my view loads. Left and Right indicator are at beginning and ending respectively.

    Please help me out with this problem.

  3. Neha says:

    Hi Daniel,

    Thanks for the reply.

    I am looking forward to your implementation of the Range slider Left/Right Value set functionality.

    Thanks
    Neha

      • Neha says:

        Hi Daniel,

        Thanks for the update. I tried it out and its do set Left & Right value but it sets wrong value somehow. When I set a value of Left like to 51, it shows 51 but sets value on indicator around 63. Now when I changed Right value for the first time after setting the values, Left indicator value automatically changed to 63.Afterwards its fine.
        Its also really hard to slide when its reach to minimum.

        Please help me out. I would really appreciate your help.

        Thanks in advance.
        Neha

  4. san says:

    I have downloaded the Flipper Forms Controls 1.0.3-Pre7.
    i have tried it in Xamarin.forms (v2.2.0.45). It works fine with iOS but in my droid project it gives me bunch of (151) error regarding resources missing:

    Resources/Resource.Designer.cs(1583,119): error CS0117: `MyProject.Droid.Resource.Styleable’ does not contain a definition for `Theme_actionOverflowMenuStyle’

    Doesn’t it support xamarin.forms 2.2?
    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *