I have been developing a mobile app in Android that required the Seekbar to have labelled intervals. However, this isn’t possible out of the box, and the only solution available is to draw a background image for the intervals. On this occasion this wasn’t enough, as the number of intervals (and their text) could change at runtime. This post covers my implementation of SeekbarWithIntervals – an extension of the Seekbar with labelled intervals – and the source code is available the bottom of this post. Here is the final result;

Seekbar with stepped intervals

The core concept is to use SeekbarWithIntervals to combine Android’s default Seekbar with another Layout control (such as LinearLayout or RelativeLayout) and dynamically create and align TextViews to represent the intervals when we need them.

The steps to create the SeekbarWithIntervals control are;

  1. Create the XML layout to be used by the SeekbarWithIntervals
  2. Load the layout when SeekbarWithIntervals is created
  3. Create the TextViews for the intervals
  4. Aligning the intervals correctly
  5. Aligning the first interval
  6. Aligning the intervals in between the first and last interval
  7. Aligning the last interval

Create the XML layout to be used by SeekbarWithIntervals

This is fairly simple, the layout consists of a RelativeLayout (to create the intervals in) and a Seekbar below it.

<RelativeLayout; xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
   
    <RelativeLayout
        android:id="@+id/intervals"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" />
 
    <SeekBar
        android:id="@+id/seekbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/intervals" />
</RelativeLayout>

Load the layout when SeekbarWithIntervals is created

We need to override the onFinishInflate() method to load our XML layout;

public class SeekbarWithIntervals extends LinearLayout {
    private RelativeLayout RelativeLayout = null;
    private SeekBar Seekbar = null;
 
    public SeekbarWithIntervals(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
    }
 
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
 
        getActivity().getLayoutInflater()
            .inflate(R.layout.seekbar_with_intervals, this);
    }
 
    private Activity getActivity() {
        return (Activity) getContext();
    }
 
    private RelativeLayout getRelativeLayout() {
        if (RelativeLayout == null) {
            RelativeLayout = (RelativeLayout) findViewById(R.id.intervals);
        }
 
        return RelativeLayout;
    }
 
    private SeekBar getSeekbar() {
        if (Seekbar == null) {
            Seekbar = (SeekBar) findViewById(R.id.seekbar);
        }
 
        return Seekbar;
    }
}

We need to add the constructor to initialise the control correctly and to stop it throwing an exception when we try and create it. We’ve also added properties and methods to get at the RelativeLayout (which will contain the intervals) and the Seekbar for use later on.

Create the TextViews for the intervals

Ideally, we want to call a method, say;

setIntervals(intervals);

and have the TextViews create themselves. So;

public void setIntervals(List<String> intervals) {
    displayIntervals(intervals);
    getSeekbar().setMax(intervals.size() - 1);
}

By setting the intervals we also know the maximum steps of the Seekbar – so we set that too. The code to create the intervals is within displayIntervals();

private void displayIntervals(List<String> intervals) {
    int idOfPreviousInterval = 0;
     
    if (getLinearLayout().getChildCount() == 0) {
        for (String interval : intervals) {
            TextView textViewInterval = createInterval(interval);
            alignTextViewToRightOfPreviousInterval(textViewInterval, idOfPreviousInterval);
             
            idOfPreviousInterval = textViewInterval.getId();
 
            getRelativeLayout().addView(textViewInterval);
        }
    }
}

Here we create a TextView for each interval and align it to the right of it’s previous interval. This is because we are creating the TextViews within a RelativeLayout, and they will align underneath each other by default otherwise.

private TextView createInterval(String interval) {
    View textBoxView = (View) LayoutInflater.from(getContext())
        .inflate(R.layout.seekbar_with_intervals_labels, null);
 
    TextView textView = (TextView) textBoxView
        .findViewById(R.id.textViewInterval);
 
    textView.setId(View.generateViewId());
    textView.setText(interval);
 
    return textView;
}
 
private void alignTextViewToRightOfPreviousInterval(TextView textView, int idOfPreviousInterval) {
    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
        LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
 
    if (idOfPreviousInterval > 0) {
        params.addRule(RelativeLayout.RIGHT_OF, idOfPreviousInterval);
    }
 
    textView.setLayoutParams(params);
}

As with the seekbar_with_intervals layout, we can also create the TextView by inflating another layout – seekbar_with_intervals_labels;

<?xml version="1.0" encoding="utf-8"?>
 
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/textViewInterval"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

Aligning the intervals correctly

After creating the TextViews for the intervals, we end up with something like this;

Seekbar with misaligned label

Obviously this isn’t what we want. We’ve aligned the intervals next to each other, and now we need to space them apart. There were many ideas and attempts before I hit a viable solution. I won’t detail the attempts here, but these are the findings;

  • After changing the layout of the intervals, the Layout needs to be refreshed.
  • Aligning the interval’s text to the centre of the TextView isn’t enough.
  • We need to take into account the width of the previous interval too.
  • Setting a width for each TextView doesn’t work.
  • The actual text-width of each interval needs to be taken into account.
  • The first and last intervals are affected by the size of the Seekbar’s thumb (the blue tag).

Let’s handle the first point;

private int WidthMeasureSpec = 0;
private int HeightMeasureSpec = 0;
 
@Override
protected synchronized void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)    {
    WidthMeasureSpec = widthMeasureSpec;
    HeightMeasureSpec = heightMeasureSpec;
 
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
 
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
     
    if (changed) {
        alignIntervals();
 
        // We've changed the intervals layout, we need to refresh.
        RelativeLayout.measure(WidthMeasureSpec, HeightMeasureSpec);
        RelativeLayout.layout(RelativeLayout.getLeft(), RelativeLayout.getTop(), 
            RelativeLayout.getRight(), RelativeLayout.getBottom());
    }
}

We need to override onLayout to change the layout of the intervals. After we align the intervals, we refresh by telling Android to measure(); and layout(); the RelativeLayout. The measure(); method takes a width and height measure specification, which we get by overriding onMeasure() and storing them in properties.

Aligning the first interval

As you can see in the image above, the location of the first interval is too far left – it needs to start at the middle of the thumb;

private void alignIntervals() {
    int widthOfSeekbarThumb = getSeekbarThumbWidth();
    int thumbOffset = widthOfSeekbarThumb / 2;
 
    alignFirstInterval(thumbOffset);
}
 
private int getSeekbarThumbWidth() {
    return getResources().getDimensionPixelOffset(R.dimen.seekbar_thumb_width);
}
 
private void alignFirstInterval(int offset) {
    TextView firstInterval = (TextView) getRelativeLayout().getChildAt(0);
    firstInterval.setPadding(offset, 0, 0, 0);
}

The location of the middle of the thumb is also half the width of the thumb. Unfortunately, I can’t find a way of getting the width of the thumb programmatically, so it’s declared as a dimens resource;

<resources>
    <dimen name="seekbar_thumb_width">25dp</dimen>
</resources>

Aligning the intervals in between the first and last interval

This is done by first working out the width we need each interval to be (excluding the first);

int widthOfSeekbar = getSeekbar().getWidth();
int firstIntervalWidth = getRelativeLayout().getChildAt(0).getWidth();
int remainingPaddableWidth = widthOfSeekbar - firstIntervalWidth - widthOfSeekbarThumb;
 
int numberOfIntervals = getSeekbar().getMax();
int maximumWidthOfEachInterval = remainingPaddableWidth / numberOfIntervals;
 
alignIntervalsInBetween(maximumWidthOfEachInterval);

Once we have this, we can align by calculating the left padding required for each interval.

N.B. We have to take into account the previous interval, as it may have more text than the current interval and will skew the padding.

private void alignIntervalsInBetween(int maximumWidthOfEachInterval) {
    int widthOfPreviousIntervalsText = 0; 
 
    // Don't align the first or last interval.
    for (int index = 1; index < (getRelativeLayout().getChildCount() - 1); index++) {
        TextView textViewInterval = (TextView) getRelativeLayout().getChildAt(index);
        int widthOfText = textViewInterval.getWidth();
 
        // This works out how much left padding is needed to center the current interval.
        int leftPadding = Math.round(maximumWidthOfEachInterval - (widthOfText / 2) - (widthOfPreviousIntervalsText / 2));
        textViewInterval.setPadding(leftPadding, 0, 0, 0);
 
        widthOfPreviousIntervalsText = widthOfText;
    }
}

Aligning the last interval

We need to align the last interval to the right, but taking into account the thumb offset for the right side too;

private void alignLastInterval(int offset, int maximumWidthOfEachInterval) {
    int lastIndex = getRelativeLayout().getChildCount() - 1;
 
    TextView lastInterval = (TextView) getRelativeLayout().getChildAt(lastIndex);
    int widthOfText = lastInterval.getWidth();
 
    int leftPadding = Math.round(maximumWidthOfEachInterval - widthOfText - offset);
    lastInterval.setPadding(leftPadding, 0, 0, 0);
}

Using SeekbarWithIntervals

Firstly, add a SeekbarWithIntervals to your layout;

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">
 
    <uk.co.informaticscentre.utils.controls.SeekbarWithIntervals
        android:id="@+id/seekbarWithIntervals"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true">
    </uk.co.informaticscentre.utils.controls.SeekbarWithIntervals>
</RelativeLayout>

Then call setIntervals();

public class MainActivity extends Activity {
    private SeekbarWithIntervals SeekbarWithIntervals = null;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        List<String> seekbarIntervals = getIntervals();
        getSeekbarWithIntervals().setIntervals(seekbarIntervals);
    }
 
    private List<String> getIntervals() {
        return new ArrayList<String>() {{
            add("1");
            add("aaa");
            add("3");
            add("bbb");
            add("5");
            add("ccc");
            add("7");
            add("ddd");
            add("9");
        }};
    }
 
    private SeekbarWithIntervals getSeekbarWithIntervals() {
        if (SeekbarWithIntervals == null) {
            SeekbarWithIntervals = (SeekbarWithIntervals) findViewById(R.id.seekbarWithIntervals);
        }
 
        return SeekbarWithIntervals;
    }
}

*If you use a custom thumb, you need to set seekbar_thumb_width in your dimens resource.


If you have any queries, comments, or feedback, please get in touch with us!