Xamarin Diaries – Stepped Slider

Welcome to a new Xamarin Day! Today we want to share with you some of the last components we have built and that we call SteppedSlider.

As the name suggests, it derives from a regular Slider and adds some interesting functionalities:

  • Multiple steps
  • Any string list as input values
  • Responsive

And the main reason why it is worth to take a look to this component is because it joins some solutions to common problems.

Here is how SteppedSlider looks like:

As you can see, its progression can be some arbitrary values and they don’t necessarily need to be sequential anymore.

Let’s take our hands into the code!

Stepped Slider

We created the SteppedSlider in the form of a ContentView. This is because we didn’t want to complicate it at this moment by creating a derived class.

SteppedSlider.xaml

<ContentView.Content>
       <StackLayout>
           <StackLayout x:Name="stack" HorizontalOptions="FillAndExpand" Orientation="Horizontal" Spacing="0" IsEnabled="{Binding IsEnabled}"/>
           <Slider x:Name="slider" HorizontalOptions="FillAndExpand" ValueChanged="Slider_ValueChanged" IsEnabled="{Binding IsEnabled}"/>
       </StackLayout>
</ContentView.Content>

As you can see, xaml structure is very simple, just the regular slider and a stacklayout above it where we will add the labels for the steps.

Slider Operation

The operation of the stepped slider is based on two basis:

1.

slider.Maximum = Values.Count - 1;
slider.Minimum = 0;

We will split the slider in so many integer values as values are in the input list.

2.

Slider_ValueChanged will do the magic just by simply rounding the slider current value to the closest integer. It creates in the UI the feeling of step as it only allows slider to go from one integer to the following or previous one:

0 > 0.5 < 1 > 1.5 < 2 …

void Slider_ValueChanged(object sender, Xamarin.Forms.ValueChangedEventArgs e)
{
           slider.Value = Math.Round(e.NewValue);
           Value = Values[(int)slider.Value];
}

We return the value from the input values list that corresponds to the position selected in the slider.

Labels representation

We need to position the labels for the different slider positions so that they exactly fit the steps to create in the user the feeling he or she is really “switching” between the different positions.

To do that we created the following function:

void BuildSlider()
{
   if (Values != null)
   {
       stack.Children.Clear();       

       slider.Maximum = Values.Count - 1;
       slider.Minimum = 0;

       if (Values.Contains(Value))
       {
           slider.Value = Values.IndexOf(Value);
       }
       else
       {
           slider.Value = 0;
       }

       var textService = DependencyService.Get<IText>();

       var ballSize = slider.Height;
       var labelWidth = (slider.Width - ballSize) / (Values.Count - 1);

       for (var i = 0; i < Values.Count; i++)
       {
           var textWidth = textService.calculateWidth(Values[i].ToString());
           var margin = (ballSize / 2) - (textWidth / 2);
           margin = margin > 0 ? margin : 0;

           var label = new Label
           {
               Text = Values[i].ToString(),
               WidthRequest = i == Values.Count - 1 ? ballSize - margin : labelWidth - margin,
               HorizontalTextAlignment = i == Values.Count - 1 ? TextAlignment.End : TextAlignment.Start,
               Margin = i == Values.Count - 1 ? new Thickness(0, 0, margin, 0) : new Thickness(margin, 0, 0, 0),
               LineBreakMode = LineBreakMode.NoWrap
           };
           label.BindingContext = this;
           label.SetBinding(Label.TextColorProperty, "IsEnabled", BindingMode.Default, new BoolToColorConverter(), null);
           stack.Children.Add(label);
       }
   }
}

Considerations:

  • TextService is a native providers that allows us to know the exact width for the words to be represented inside the labels. I first read about it in this forum: https://forums.xamarin.com/discussion/67545/how-to-calculate-or-measure-width-of-a-string
  • Ball size is equal to the slider height as it is the biggest part of the slider.
  • Label width then will be slider’s width minus the ball size (as it surpasses the slider line at both sides with the half of its body) divided by the number of steps to be (values minus one)
  • We use the textservice to calculate the margin to add to the text to align it to the center of the ball.

All these calculations have to be done when the slider has already been rendered as we need height and width for the following measures. To ensure we build the slider in that moment we could do two things:

  • Overwrite OnAppearing event so that the whole page is created before rendering the labels. We discarded this options because generated a dirty flickering effect on the UI.
  • Use slider’s propertychanged event to capture exactly the moment when height and width are set. The second condition is there to capture the rotation moment. In the first initialization, width takes place before height so we need to capture height property change. But when the device is rotated, height does not occur as the height of the ball is the same in both modes, just width changes.
public SteppedSlider()
{
   InitializeComponent();

   slider.BindingContext = this;

   slider.PropertyChanged += (sender, e) => {
       var slider = (Slider)sender;
       if (e.PropertyName == "Height")
       {
           BuildSlider();
       }
       else if (e.PropertyName == "Width" && slider.Height > 0)
       {
           //Screen rotation
           BuildSlider();
       }
   };
}

Bindings

For this component we identified three flows of data needed:

  • Values for the steps of the slider
  • Current value
  • IsEnabled

Two first flows will be created as bindable properties, allowing us to bind properties on the view model.

public static readonly BindableProperty ValuesProperty = BindableProperty.Create("Values", typeof(List<string>), typeof(SteppedSlider), null, BindingMode.Default, null, null, null, null, null);

public static readonly BindableProperty ValueProperty = BindableProperty.Create("Value", typeof(string), typeof(SteppedSlider), string.Empty, BindingMode.TwoWay, null, null, null, null, null);

public List<string> Values
{
   get { return (List<string>)base.GetValue(SteppedSlider.ValuesProperty); }
   set { base.SetValue(SteppedSlider.ValuesProperty, value); }
}

public string Value
{
   get { return (string)base.GetValue(SteppedSlider.ValueProperty); }
   set { base.SetValue(SteppedSlider.ValueProperty, value); }
}

The main aspect to consider for BindableProperties is BindingMode. OneWay or TwoWay. In this case, Values is only incoming data (OneWay) while Value will be used both for initial value and selected value (TwoWay).

IsEnabled property from StackLayout and Slider will be bind to the ContentView’s IsEnabled property.

The magic here is to tell Slider, StackLayout and Labels inside it that their BindingContext is ContentView, so that they can look for IsEnabled changes in the right place.

<ContentView.Content>
   <StackLayout>
       <StackLayout x:Name="stack" ... IsEnabled="{Binding IsEnabled}"/>
       <Slider x:Name="slider" … IsEnabled="{Binding IsEnabled}"/>
   </StackLayout>
</ContentView.Content>
public SteppedSlider()
{
   InitializeComponent();

   slider.BindingContext = this;

   ...
}
void BuildSlider()
{
       ...

       for (var i = 0; i < Values.Count; i++)
       {
           ...

           var label = new Label
           {
               ...
           };
           label.BindingContext = this;
           label.SetBinding(Label.TextColorProperty, "IsEnabled", BindingMode.Default, new BoolToColorConverter(), null);
           stack.Children.Add(label);
       }
   }
}

Summary

It can be easier to create some new components in Xamarin.Forms that better fits our requirements just by applying some good practices and understanding how Lifecycle and Binding works.

In this case we created and splitted slider and explained how we can use binding properties, bindingcontext and lifecycle events to take control of everything.

Related Articles

Xamarin Diaries – Offline by default

Xamarin Diaries – Working with Maps

Cross-platform development with Xamarin

About Daniel Hompanera

I'm SW Architect and Team Leader at Solid Gear. Passionated about technology and methodology, what actually makes me happy is to work with People day by day and everything that can make things get better. I don't have favourite platform or technology, although I've been working with .NET technologies for all these almost 10 years and I feel an special appeal to it. I've worked Agile, not so Agile and Fragile environments but if I could choose, of course I would choose Solid Gear's Agile Method with no doubts

Share on LinkedInTweet about this on TwitterShare on FacebookShare on Google+Buffer this page

Leave a Comment

By completing the form you agree to the Privacy Policy