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.
FYI
I tested it today on a recently updated app to .net 9, I'm not facing any issue with panning. Maybe something has been fixed by MAUI since.
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?