|
/// <summary> |
|
/// A Slider to select two values between 0.0 and 1.0 |
|
/// |
|
/// For Xamarin please use: https://learn.microsoft.com/en-us/xamarin/community-toolkit/views/rangeslider |
|
/// </summary> |
|
public class RangeSlider : ContentView |
|
{ |
|
#region const |
|
private readonly static int TRACK_HEIGHT = 4; |
|
private readonly static int THUMB_WIDTH = 20; |
|
private readonly static int THUMB_RADIUS = THUMB_WIDTH / 2; |
|
private readonly static double DEFAULT_MIN_VALUE = 0.25; |
|
private readonly static double DEFAULT_MAX_VALUE = 0.75; |
|
private readonly static Color DEFAULT_RANGE_COLOR = Colors.LightBlue; |
|
private readonly static Color DEFAULT_OUT_OF_RANGE_COLOR = Colors.LightGray; |
|
private readonly static Color DEFAULT_THUMB_COLOR = Colors.White; |
|
#endregion |
|
|
|
/// <summary> |
|
/// Store column in varaible to ease width adjustements |
|
/// </summary> |
|
#region track columns |
|
private ColumnDefinition LeftColumn = new ColumnDefinition { Width = GridLength.Star }; |
|
private ColumnDefinition CenterColumn = new ColumnDefinition { Width = GridLength.Star }; |
|
private ColumnDefinition RightColumn = new ColumnDefinition { Width = GridLength.Star }; |
|
#endregion |
|
|
|
#region UI |
|
/// <summary> |
|
/// To colorize everything outsid of range (low) |
|
/// </summary> |
|
private Grid LeftTrack = new Grid |
|
{ |
|
HeightRequest = TRACK_HEIGHT, |
|
HorizontalOptions = LayoutOptions.FillAndExpand, |
|
VerticalOptions = LayoutOptions.Center, |
|
BackgroundColor = DEFAULT_OUT_OF_RANGE_COLOR |
|
}; |
|
|
|
/// <summary> |
|
/// To colorize everything in range |
|
/// </summary> |
|
private Grid CenterTrack = new Grid |
|
{ |
|
HeightRequest = TRACK_HEIGHT, |
|
HorizontalOptions = LayoutOptions.FillAndExpand, |
|
VerticalOptions = LayoutOptions.Center, |
|
BackgroundColor = DEFAULT_RANGE_COLOR |
|
}; |
|
|
|
/// <summary> |
|
/// To colorize everything outsid of range (up) |
|
/// </summary> |
|
private Grid RightTrack = new Grid |
|
{ |
|
HeightRequest = TRACK_HEIGHT, |
|
HorizontalOptions = LayoutOptions.FillAndExpand, |
|
VerticalOptions = LayoutOptions.Center, |
|
BackgroundColor = DEFAULT_OUT_OF_RANGE_COLOR |
|
}; |
|
|
|
/// <summary> |
|
/// Thumb for min value |
|
/// </summary> |
|
private Frame MinThumb = new Frame |
|
{ |
|
CornerRadius = 10, |
|
BackgroundColor = DEFAULT_THUMB_COLOR, |
|
VerticalOptions = LayoutOptions.Center, |
|
HorizontalOptions = LayoutOptions.Center, |
|
BorderColor = DEFAULT_RANGE_COLOR, |
|
WidthRequest = THUMB_WIDTH, |
|
HeightRequest = THUMB_WIDTH, |
|
}; |
|
|
|
/// <summary> |
|
/// Thumb for max value |
|
/// </summary> |
|
private Frame MaxThumb = new Frame |
|
{ |
|
CornerRadius = 10, |
|
BackgroundColor = DEFAULT_THUMB_COLOR, |
|
VerticalOptions = LayoutOptions.Center, |
|
HorizontalOptions = LayoutOptions.Center, |
|
BorderColor = DEFAULT_RANGE_COLOR, |
|
WidthRequest = THUMB_WIDTH, |
|
HeightRequest = THUMB_WIDTH, |
|
}; |
|
#endregion |
|
|
|
#region bindable properties |
|
|
|
#region MinValue |
|
/// <summary> |
|
/// Identifies the <see cref="MinValueProperty"/> bindable property. |
|
/// </summary> |
|
public static readonly BindableProperty MinValueProperty = |
|
BindableProperty.Create(nameof(MinValue), |
|
typeof(double), |
|
typeof(RangeSlider), |
|
DEFAULT_MIN_VALUE, |
|
BindingMode.TwoWay); |
|
/// <summary> |
|
/// MinValue between 0 and 1 |
|
/// </summary> |
|
/// <seealso cref="MinValueProperty"/> |
|
public double MinValue |
|
{ |
|
get => (double)GetValue(MinValueProperty); |
|
set |
|
{ |
|
this.TranslateThumbRel(this.MinThumb, this.MinValue, value); |
|
SetValue(MinValueProperty, value); |
|
} |
|
} |
|
#endregion |
|
|
|
#region MaxValue |
|
/// <summary> |
|
/// Identifies the <see cref="MaxValueProperty"/> bindable property. |
|
/// </summary> |
|
public static readonly BindableProperty MaxValueProperty = |
|
BindableProperty.Create(nameof(MaxValue), |
|
typeof(double), |
|
typeof(RangeSlider), |
|
DEFAULT_MAX_VALUE, |
|
BindingMode.TwoWay); |
|
/// <summary> |
|
/// MaxValue between 0 and 1 |
|
/// </summary> |
|
/// <seealso cref="MaxValueProperty"/> |
|
public double MaxValue |
|
{ |
|
get => (double)GetValue(MaxValueProperty); |
|
set |
|
{ |
|
this.TranslateThumbRel(this.MaxThumb, this.MaxValue, value); |
|
SetValue(MaxValueProperty, value); |
|
} |
|
} |
|
#endregion |
|
|
|
#region RangeColor |
|
/// <summary> |
|
/// Identifies the <see cref="RangeColorProperty"/> bindable property. |
|
/// </summary> |
|
public static readonly BindableProperty RangeColorProperty = |
|
BindableProperty.Create(nameof(RangeColor), |
|
typeof(Color), |
|
typeof(RangeSlider), |
|
DEFAULT_RANGE_COLOR, |
|
BindingMode.TwoWay, |
|
propertyChanged: (bindable, oldValue, newValue) => |
|
{ |
|
if (bindable is RangeSlider slider && newValue is Color color) |
|
{ |
|
slider.CenterTrack.BackgroundColor = color; |
|
slider.MinThumb.BorderColor = color; |
|
slider.MaxThumb.BorderColor = color; |
|
} |
|
}); |
|
/// <summary> |
|
/// Color of range |
|
/// </summary> |
|
/// <seealso cref="RangeColorProperty"/> |
|
public Color RangeColor |
|
{ |
|
get => (Color)GetValue(RangeColorProperty); |
|
set => SetValue(RangeColorProperty, value); |
|
} |
|
#endregion |
|
|
|
#region ThumbsColor |
|
/// <summary> |
|
/// Identifies the <see cref="ThumbsColorProperty"/> bindable property. |
|
/// </summary> |
|
public static readonly BindableProperty ThumbsColorProperty = |
|
BindableProperty.Create(nameof(ThumbsColor), |
|
typeof(Color), |
|
typeof(RangeSlider), |
|
DEFAULT_THUMB_COLOR, |
|
BindingMode.TwoWay, |
|
propertyChanged: (bindable, oldValue, newValue) => |
|
{ |
|
if (bindable is RangeSlider slider && newValue is Color color) |
|
{ |
|
slider.MinThumb.BackgroundColor = color; |
|
slider.MaxThumb.BackgroundColor = color; |
|
} |
|
}); |
|
/// <summary> |
|
/// Color of thumb |
|
/// </summary> |
|
/// <seealso cref="ThumbsColorProperty"/> |
|
public Color ThumbsColor |
|
{ |
|
get => (Color)GetValue(ThumbsColorProperty); |
|
set => SetValue(ThumbsColorProperty, value); |
|
} |
|
#endregion |
|
|
|
#region OutOfRangeColor |
|
/// <summary> |
|
/// Identifies the <see cref="OutOfRangeColorProperty"/> bindable property. |
|
/// </summary> |
|
public static readonly BindableProperty OutOfRangeColorProperty = |
|
BindableProperty.Create(nameof(OutOfRangeColor), |
|
typeof(Color), |
|
typeof(RangeSlider), |
|
DEFAULT_OUT_OF_RANGE_COLOR, |
|
BindingMode.TwoWay, |
|
propertyChanged: (bindable, oldValue, newValue) => |
|
{ |
|
if (bindable is RangeSlider slider && newValue is Color color) |
|
{ |
|
slider.LeftTrack.BackgroundColor = color; |
|
slider.RightTrack.BackgroundColor = color; |
|
} |
|
}); |
|
/// <summary> |
|
/// Color for area outside of range |
|
/// </summary> |
|
/// <seealso cref="OutOfRangeColorProperty"/> |
|
public Color OutOfRangeColor |
|
{ |
|
get => (Color)GetValue(OutOfRangeColorProperty); |
|
set => SetValue(OutOfRangeColorProperty, value); |
|
} |
|
#endregion |
|
|
|
#endregion |
|
|
|
#region engine properties |
|
/// <summary> |
|
/// Max thumb real position |
|
/// </summary> |
|
private double EffectiveMinThumbX |
|
{ |
|
get |
|
{ |
|
return this.MinThumb.X + this.MinThumb.TranslationX; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Min thumb real position |
|
/// </summary> |
|
private double EffectiveMaxThumbX |
|
{ |
|
get |
|
{ |
|
return this.MaxThumb.X + this.MaxThumb.TranslationX; |
|
} |
|
} |
|
#endregion |
|
|
|
#region utils |
|
/// <summary> |
|
/// to ease gridlenth init from strings |
|
/// </summary> |
|
private GridLengthTypeConverter converter = new GridLengthTypeConverter(); |
|
#endregion |
|
|
|
public RangeSlider() |
|
{ |
|
Grid.SetColumn(this.LeftTrack, 0); |
|
Grid.SetColumn(this.CenterTrack, 1); |
|
Grid.SetColumn(this.RightTrack, 2); |
|
Grid.SetColumnSpan(this.MinThumb, 3); |
|
Grid.SetColumnSpan(this.MaxThumb, 3); |
|
|
|
this.Content = new Grid |
|
{ |
|
ColumnDefinitions = |
|
{ |
|
this.LeftColumn, |
|
this.CenterColumn, |
|
this.RightColumn |
|
}, |
|
Children = |
|
{ |
|
this.LeftTrack, |
|
this.CenterTrack, |
|
this.RightTrack, |
|
this.MinThumb, |
|
this.MaxThumb, |
|
} |
|
}; |
|
|
|
//handle thumb moves via pan gesture |
|
double startPanXCoord = 0; |
|
var panGesture = new PanGestureRecognizer(); |
|
panGesture.PanUpdated += (sender, args) => |
|
{ |
|
if (sender is View thumb) |
|
{ |
|
if (args.StatusType == GestureStatus.Started) |
|
{ |
|
startPanXCoord = thumb.TranslationX; |
|
} |
|
else if (args.StatusType == GestureStatus.Running) |
|
{ |
|
this.TranslateThumbAbs(thumb, startPanXCoord + args.TotalX); |
|
this.UpdateMinMaxValues(); |
|
} |
|
} |
|
}; |
|
this.MinThumb.GestureRecognizers.Add(panGesture); |
|
this.MaxThumb.GestureRecognizers.Add(panGesture); |
|
|
|
//set min values |
|
var minReady = false; |
|
var maxReady = false; |
|
this.MinThumb.SizeChanged += (sender, args) => |
|
{ |
|
if (maxReady && !minReady) |
|
{ |
|
this.TranslateThumbRel(this.MinThumb, 0.5, DEFAULT_MIN_VALUE); |
|
this.TranslateThumbRel(this.MaxThumb, 0.5, DEFAULT_MAX_VALUE); |
|
} |
|
minReady = true; |
|
}; |
|
|
|
this.MaxThumb.SizeChanged += (sender, args) => |
|
{ |
|
if (minReady && !maxReady) |
|
{ |
|
this.TranslateThumbRel(this.MinThumb, 0.5, DEFAULT_MIN_VALUE); |
|
this.TranslateThumbRel(this.MaxThumb, 0.5, DEFAULT_MAX_VALUE); |
|
} |
|
maxReady = true; |
|
}; |
|
} |
|
|
|
/// <summary> |
|
/// Update Min and Max values |
|
/// </summary> |
|
private void UpdateMinMaxValues() |
|
{ |
|
this.SetValue(MinValueProperty, (this.EffectiveMinThumbX + THUMB_RADIUS) / this.Width); |
|
this.SetValue(MaxValueProperty, (this.EffectiveMaxThumbX + THUMB_RADIUS) / this.Width); |
|
} |
|
|
|
/// <summary> |
|
/// Translate thumb using relatives values (old and new) (between 0 and 1) |
|
/// </summary> |
|
private void TranslateThumbRel(View thumb, double oldValue, double newValue) |
|
{ |
|
var relativeDelta = newValue - Math.Max(0, Math.Min(1, oldValue)); |
|
var absoluteDelta = relativeDelta * this.Width; |
|
this.TranslateThumbAbs(thumb, absoluteDelta); |
|
} |
|
|
|
/// <summary> |
|
/// Translate thumb using an absolute X value (in pixels) |
|
/// </summary> |
|
private void TranslateThumbAbs(View thumb, double deltaX) |
|
{ |
|
var wishedX = thumb.X + deltaX; |
|
var otherThumbX = 0.0; |
|
var minBoundX = -THUMB_RADIUS; |
|
var maxBoundX = this.Width - THUMB_RADIUS; |
|
var newX = 0.0; |
|
if (thumb == this.MinThumb) |
|
{ |
|
otherThumbX = -THUMB_WIDTH + this.EffectiveMaxThumbX;//THUMB_WIDTH to avoid overlapping |
|
newX = Math.Max(minBoundX, Math.Min(Math.Min(wishedX, otherThumbX), maxBoundX)); |
|
} |
|
else if (thumb == this.MaxThumb) |
|
{ |
|
otherThumbX = THUMB_WIDTH + this.EffectiveMinThumbX;//THUMB_WIDTH to avoid overlapping |
|
newX = Math.Min(maxBoundX, Math.Max(Math.Max(wishedX, otherThumbX), minBoundX)); |
|
} |
|
thumb.TranslationX = newX - thumb.X; |
|
this.UpdateTracks(); |
|
} |
|
|
|
/// <summary> |
|
/// Updage background tracks according to values |
|
/// </summary> |
|
private void UpdateTracks() |
|
{ |
|
var leftSpace = (int)Math.Round(this.MinValue * 100, 0); |
|
var centerSpace = (int)Math.Round((this.MaxValue - this.MinValue) * 100); |
|
var rightSpace = (int)Math.Round((1 - this.MaxValue) * 100); |
|
this.LeftColumn.Width = (GridLength)converter.ConvertFromInvariantString(String.Format("{0}*", leftSpace)); |
|
this.CenterColumn.Width = (GridLength)converter.ConvertFromInvariantString(String.Format("{0}*", centerSpace)); |
|
this.RightColumn.Width = (GridLength)converter.ConvertFromInvariantString(String.Format("{0}*", rightSpace)); |
|
} |
|
} |