I recently worked on an open-source Java Swing application that included filtering features to help the user refine a list of results. These features displayed controls to filter on several attributes, such as category, extension, and size. In particular, the size control used a dual-thumb slider to allow the user to select a range of sizes to display. A demo screenshot illustrates this.
In order to create the range slider, we needed a new Swing component because the existing JSlider component only displays a single thumb to adjust the slider value. Interestingly, even though JSlider only allows you to update a single value, its data model actually provides support for two values; these define an internal range within the minimum and maximum bounds.
The internal range is defined by the slider value and an “extent,” which is the length of the inner range that begins at the slider value. In DefaultBoundedRangeModel, the default “extent” is zero and not normally used for anything. We can add it to the slider value to define an upper value of a selected range.
public void setUpperValue(int value) {
// Compute new extent.
int lowerValue = getValue();
// Set extent to set upper value.int
newExtent = Math.min(Math.max(0, value - lowerValue),
getMaximum() - lowerValue);
setExtent(newExtent);
}
The new dual-thumb slider component takes advantage of the built-in definition for DefaultBoundedRangeModel, and is implemented with just two classes: RangeSlider and RangeSliderUI. These are included in the demo source code.
RangeSlider extends JSlider to install a UI delegate and provide value change methods. The methods set the lower and upper values in the selected range. We define the existing slider value as the lower value, and the upper value as the lower value plus the extent. To access these values, we override some methods, and add a couple of our own.
- We add setUpperValue(int) to calculate the extent based on the current lower value. We apply limits to ensure that the upper value is bounded by the lower value and the maximum.
- We override setValue(int) to calculate the extent to ensure that the upper value does not change. We apply limits to ensure that the lower value stays bounded by the minimum and the upper value.
- We override updateUI() to install a custom UI delegate to render the two thumbs.
RangeSliderUI extends BasicSliderUI to paint the two thumbs and handle user events. This provides a customized rendering of the two thumbs and handles user events to update their values. This is somewhat involved so we will only highlight the primary aspects of this code.
The main tasks are: (1) calculating the thumb sizes and locations, (2) painting the thumbs, and (3) handling mouse and keyboard input events on the thumbs.
Step 1: Thumb sizes and locations
To calculate the thumb sizes and locations, we need to override several UI delegate methods.
- We override installUI(JComponent) to initialize a new upperThumbRect attribute, which we use to store the size and position of the upper thumb.
- We override calculateThumbSize() to update the size of the upper thumb, which is defined to be the same as the lower thumb.
- We override calculateThumbLocation() to update the location of the upper thumb, which is based on the upper value stored in the slider component. The thumb is always positioned to be centered over its value on the track. The logic is essentially copied from the superclass method and applied to upperThumbRect.
- We override getThumbSize() to return the dimensions of the thumb. It is possible to use custom images for the thumbs—any shape will do.
Step 2: Paint
To paint the slider component, we need to override the default implementations of various paint methods in the UI delegate, and add a few of our own.
- We override paintThumb(Graphics) to do nothing. This is called by the superclass paint() method to draw the single thumb, and we want to replace this with our own logic for two thumbs.
- We override paint(Graphics, JComponent) to draw the two thumbs as needed. We define an upperThumbSelected indicator to keep track of which thumb was last selected, so we can draw the unselected thumb first, and draw the selected one on top of it.
- We add the paintLowerThumb(Graphics) and paintLowerThumb(Graphics) methods. The two methods are similar – they draw a thumb in the appropriate rectangular region. (The sample code uses a colored shape for the thumb.)
- We override paintTrack(Graphics) to draw the slider track, and highlight the selected range.
Step 3: Handle input
To handle mouse drag events on the thumbs, we define a RangeTrackListener inner class that extends the TrackListener class in BasicSliderUI. We also override createTrackListener(JSlider) to return a new instance of the class. In RangeTrackListener, we implement several methods.
- We override mousePressed(MouseEvent) to detect which thumb is being pressed.
- We override mouseDragged(MouseEvent) to adjust the appropriate slider value as the thumb is dragged.
- We override mouseReleased(MouseEvent) to reset indicators.
- We add moveLowerThumb() and moveUpperThumb() to update the slider values and repaint the thumbs.
Finally, to handle keyboard events on the thumbs, such as the arrow keys, we override a pair of methods used to move the slider by increments.
- We override scrollByBlock(int) to move the selected thumb by a block increment. This is called when the Page Up or Page Down key is pressed.
- We override scrollByUnit(int) to move the selected thumb by a unit increment. This is called when one of the arrow keys is pressed.
Summary
The dual-thumb slider component allows the user to select a range by adjusting its lower and upper bounds. And by implementing our own UI delegate, we can adjust the appearance of the component to suit our needs. This can be very useful when working with visual designers who want to create a unique vision.
The Range Slider Demo source code is available at http://github.com/ernieyu/Swing-range-slider.

