Basic range slider for MAUI. As there's no RangeSlider as on Xamarin: https://learn.microsoft.com/en-us/xamarin/community-toolkit/views/rangeslider
Tested on Android / iOS real device.
Basic range slider for MAUI. As there's no RangeSlider as on Xamarin: https://learn.microsoft.com/en-us/xamarin/community-toolkit/views/rangeslider
Tested on Android / iOS real device.
using System; | |
#if XAMARIN | |
using Xamarin.Forms; | |
using Xamarin.Forms.Shapes; | |
#else | |
using Microsoft.Maui.Controls.Shapes; | |
#endif | |
namespace My.NameSpace | |
{ | |
#if XAMARIN | |
//please use https://learn.microsoft.com/en-us/xamarin/community-toolkit/views/rangeslider | |
#else | |
/// <summary> | |
/// A slide 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, | |
}; | |
//To fix an android issue, user effectively slide an helper instead of thumb, | |
//needed as on android updating position of a paning object fires unaccurate values, | |
//If you don't understand change the color of the following helper. | |
#region slide helper | |
private BoxView MinThumbSlideHelper = new BoxView | |
{ | |
WidthRequest = THUMB_WIDTH*2, | |
HeightRequest = THUMB_WIDTH*2, | |
VerticalOptions = LayoutOptions.Center, | |
HorizontalOptions = LayoutOptions.Center, | |
Color = Colors.Transparent | |
}; | |
private BoxView MaxThumbSlideHelper = new BoxView | |
{ | |
WidthRequest = THUMB_WIDTH*2, | |
HeightRequest = THUMB_WIDTH*2, | |
VerticalOptions = LayoutOptions.Center, | |
HorizontalOptions = LayoutOptions.Center, | |
Color = Colors.Transparent | |
}; | |
#endregion | |
#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 | |
#region timer | |
/// <summary> | |
/// timer to ensure every slide reach complete behavior, needed as sometimes pan handler doesn't fire completed/cancel status... (observed on android) | |
/// </summary> | |
private System.Timers.Timer SlideCompleteTimer; | |
/// <summary> | |
/// start timer | |
/// </summary> | |
private void ArmTimer() | |
{ | |
this.SlideCompleteTimer.Start(); | |
} | |
/// <summary> | |
/// stop timer if needed and start it | |
/// </summary> | |
private void ReArmTimer() | |
{ | |
this.CancelTimer(); | |
this.ArmTimer(); | |
} | |
/// <summary> | |
/// cancel timer | |
/// </summary> | |
private void CancelTimer() | |
{ | |
if (this.SlideCompleteTimer?.Enabled ?? false) | |
{ | |
this.SlideCompleteTimer?.Stop(); | |
} | |
} | |
#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); | |
Grid.SetColumnSpan(this.MinThumbSlideHelper, 3); | |
Grid.SetColumnSpan(this.MaxThumbSlideHelper, 3); | |
this.Content = new Grid | |
{ | |
ColumnDefinitions = | |
{ | |
this.LeftColumn, | |
this.CenterColumn, | |
this.RightColumn | |
}, | |
Children = | |
{ | |
this.LeftTrack, | |
this.CenterTrack, | |
this.RightTrack, | |
this.MinThumb, | |
this.MaxThumb, | |
this.MinThumbSlideHelper, | |
this.MaxThumbSlideHelper | |
} | |
}; | |
this.SlideCompleteTimer = new System.Timers.Timer(250); | |
this.SlideCompleteTimer.AutoReset = false; | |
this.SlideCompleteTimer.Elapsed += (sender, args) => | |
{ | |
//when this called is reached, this means pan completion has not run properly -> ensure helper match current UI state | |
MainThread.InvokeOnMainThreadAsync(() => | |
{ | |
this.MinThumbSlideHelper.TranslationX = this.MinThumb.TranslationX; | |
this.MaxThumbSlideHelper.TranslationX = this.MaxThumb.TranslationX; | |
this.UpdateTracks(); | |
}); | |
}; | |
//handle thumb moves via pan gesture | |
double startPanXCoord = 0; | |
EventHandler<PanUpdatedEventArgs> panUpdatedHandler = (sender, args) => | |
{ | |
if (sender is View thumb) | |
{ | |
var target = thumb; | |
if(thumb == this.MinThumbSlideHelper) | |
{ | |
target = this.MinThumb; | |
} | |
else if (thumb == this.MaxThumbSlideHelper) | |
{ | |
target = this.MaxThumb; | |
} | |
if (args.StatusType == GestureStatus.Started) | |
{ | |
startPanXCoord = target.TranslationX; | |
this.ArmTimer(); | |
} | |
else if (args.StatusType == GestureStatus.Running) | |
{ | |
this.ReArmTimer(); | |
this.TranslateThumbAbs(target, startPanXCoord + args.TotalX); | |
this.UpdateMinMaxValues(); | |
} | |
else if (args.StatusType == GestureStatus.Completed ) | |
{ | |
this.CancelTimer(); | |
thumb.TranslationX = target.TranslationX; | |
} | |
else if(args.StatusType == GestureStatus.Canceled) | |
{ | |
this.CancelTimer(); | |
thumb.TranslationX = target.TranslationX; | |
} | |
} | |
}; | |
var minPanGesture = new PanGestureRecognizer(); | |
minPanGesture.PanUpdated += panUpdatedHandler; | |
var maxPanGesture = new PanGestureRecognizer(); | |
maxPanGesture.PanUpdated += panUpdatedHandler; | |
this.MinThumbSlideHelper.GestureRecognizers.Add(minPanGesture); | |
this.MaxThumbSlideHelper.GestureRecognizers.Add(maxPanGesture); | |
//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.MinThumbSlideHelper.TranslationX = this.MinThumb.TranslationX; | |
this.TranslateThumbRel(this.MaxThumb, 0.5, DEFAULT_MAX_VALUE); | |
this.MaxThumbSlideHelper.TranslationX = this.MaxThumb.TranslationX; | |
} | |
minReady = true; | |
}; | |
this.MaxThumb.SizeChanged += (sender, args) => | |
{ | |
if (minReady && !maxReady) | |
{ | |
this.TranslateThumbRel(this.MinThumb, 0.5, DEFAULT_MIN_VALUE); | |
this.MinThumbSlideHelper.TranslationX = this.MinThumb.TranslationX; | |
this.TranslateThumbRel(this.MaxThumb, 0.5, DEFAULT_MAX_VALUE); | |
this.MaxThumbSlideHelper.TranslationX = this.MaxThumb.TranslationX; | |
} | |
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}*",Math.Abs(leftSpace))); | |
this.CenterColumn.Width = (GridLength)converter.ConvertFromInvariantString(String.Format("{0}*", Math.Abs(centerSpace))); | |
this.RightColumn.Width = (GridLength)converter.ConvertFromInvariantString(String.Format("{0}*", Math.Abs(rightSpace))); | |
} | |
} | |
#endif | |
} |
<!--use asis--> | |
<RangeSlider RangeColor="Red" | |
ThumbsColor="White" | |
OutOfRangeColor="Gray" | |
MinValue="0.25" | |
MaxValue="0.75"/> |
Hi MarxAlx,
I have moved to another open source library
Thank you for your kind response.
Hi,
As I don't have an Android device right know to re-check .net 9, I can't promise you a fix anytime soon.
Can you elaborate on the problem you are facing?