Daniel Hindrikes
Developer and architect with focus on mobile- and cloud solutions!
Developer and architect with focus on mobile- and cloud solutions!
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.
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
{
protected override void OnElementChanged(ElementChangedEventArgs 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