Xamarin.Forms Slider: dynamically changing Minimum and Maximum value on Android won’t update indicator position

“Something’s wrong with the Slider in Xamarin.Forms…”, I heard somebody say. “…changing the min and max value does not correctly update the position of the knob on Android”, they continued. A bug in Xamarin.Forms? Who knows? It was enough to get me interested, so I started digging and indeed, there is a problem. So let’s fix it, shall we?

The issue

Xamarin.Forms Slider supports setting a minimum value, a maximum value and a current value. This means if we have a slider going from 0 to 100 and the value is set to 50, the knob (or indicator will be displayed exactly in the middle:

0 |----------X----------| 100

If either the minimum or the maximum changes, the value of the indicator will remain unchanged but it should appear in a different location; even if the actual width of the slider is unchanged. If we increase the range of the slider from above from 100 to 200 and keep the value at 50, this is what we expect to see:

0 |-----X---------------| 200

While this is working just fine as long as we set the min and max values before using the slider, dynamically updating them won’t reposition the indicator correctly. If you’re asking now, why the heck this would matter, think of a UI where there is one slider that controls a value and another slider that controls the possible range of values. The second slider would dynamically update the min and max properties (which are by the way even data bindable). It works great on iOS where the native UISlider has got direct support for minimum and maximum values but on Android it’s a different story – or should I say: an oversight?

Only a problem on Android

The native counterpart for a slider on Android is a SeekBar – it also does support setting a minimum and maximum value but there are three small problems:

  • The only thing Xamarin.Android exposes is a “Max” property. It seems like binding the native “setMin()” method to a “Min” property was forgotten.
  • Xamarin.Forms SliderRenderer is using hardcoded values of 0 (for minimum – what else, it cannot set it! 🤔) and 1000 (for maximum) and the requested range is mapped to the range of 0..1000 when getting or setting the current value.
  • Android in general doesn’t seem to correctly update the indicator position when dynamically changing the min or max value – at least that’s what I found during my researches.

The solution

We must force Android to redraw the slider if either one of the values is changing. This can be done with a custom renderer or with an effect. I’ll post the code of the effect here and leave it to you to convert it to a custom renderer if required. In both cases the trick is to update the native SeekBar’s “Progress” property which reflects the current value. Given the findings in the original renderer, this is what I came up with:

_seekBar.Progress = (int)((_slider.Value - _slider.Minimum) / (_slider.Maximum - _slider.Minimum) * 1000.0);

In the code above “_slider” is the Xamarin.Forms Slider element. Here’s the complete effect which can be added to any slider that requires dynamic min/max value updates:

using System.ComponentModel;
using Android.Widget;
using SLiderTtest.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly:ResolutionGroupName("cSharx")]
[assembly:ExportEffect(typeof(AndroidMinMaxListenerEffect), "AndroidMinMaxListenerEffect")]
     
namespace SLiderTtest.Droid
{
    public class AndroidMinMaxListenerEffect : PlatformEffect
    {
        SeekBar _seekBar;
        Slider _slider;

        protected override void OnAttached()
        {
            _seekBar = Control as SeekBar;
            if (_seekBar == null)
            {
                return;
            }

            _slider = Element as Slider;
            if (_slider == null)
            {
                return;
            }
        }

        protected override void OnDetached()
        {
            _seekBar = null;
            _slider = null;
        }

        protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
        {
            // Sanity checks.
            if (_seekBar == null || _slider == null)
            {
                return;
            }

            base.OnElementPropertyChanged(args);
            if (args.PropertyName == Slider.MaximumProperty.PropertyName || args.PropertyName == Slider.MinimumProperty.PropertyName)
            {
                // Force an upate of the native control. Inspired by code found at:
                // https://github.com/xamarin/Xamarin.Forms/blob/65f4e078ceccba4917f207862e2d51dbe99d1277/Xamarin.Forms.Platform.Android/Renderers/SliderRenderer.cs#L35
                _seekBar.Progress = (int)((_slider.Value - _slider.Minimum) / (_slider.Maximum - _slider.Minimum) * 1000.0);
            }
        }
    }
}

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *