Skip to content

Instantly share code, notes, and snippets.

@MarcAlx
Last active May 7, 2024 10:23
Show Gist options
  • Save MarcAlx/4e54edf67dc4a95d373f3d99e4a9d523 to your computer and use it in GitHub Desktop.
Save MarcAlx/4e54edf67dc4a95d373f3d99e4a9d523 to your computer and use it in GitHub Desktop.
MAUI RangeSlider
/// <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));
}
}
<!--use asis-->
<RangeSlider RangeColor="Red"
ThumbsColor="White"
OutOfRangeColor="Gray"
MinValue="0.25"
MaxValue="0.75"/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment