Creating a HUD style slider

August 23, 2009

hud_slider_without_tickshud_slider_with_ticks
Someone recently requested I add a HUD style slider to the widget set, which I quickly added and is now available in the latest Mac Widgets for Java developer build. Creating a custom SliderUI delegate is pretty easy, as BasicSliderUI has all the right hooks. You can specify the thumb size, tick size, track bounds, thumb location and much more by overriding corresponding methods. This is a refreshing change from the complicated process of creating a custom scroll bar UI delegate (which I talked about in detail in part 1, part 2, and part 3 of the “Skinning a scroll bar” series).

As I’ve noted in previous HUD style component posts (here and here), you may notice a bit of redundant code between the posts. I’ve done this to keep the blog entries independent, but the actual Mac Widgets for Java code uses a utility class to do most of the HUD painting.

Finally, for this widget to look “correct”, it must be added to a HUD style window, which I talked about here.

public class HudSliderUI extends BasicSliderUI {

    private static final int SLIDER_KNOB_WIDTH = 11;
    private static final int SLIDER_KNOB_HEIGHT_NO_TICKS = 11;
    private static final int SLIDER_KNOB_HEIGHT_WITH_TICKS = 13;
    private static final int TRACK_HEIGHT = 4;

    private static final Color TRACK_BACKGROUND_COLOR = new Color(143, 147, 144, 100);
    private static final Color TRACK_BORDER_COLOR = new Color(255, 255, 255, 200);

    private static final Color TOP_SLIDER_KNOB_COLOR = new Color(0x555555);
    private static final Color BOTTOM_SLIDER_KNOB_COLOR = new Color(0x393939);
    private static final Color TOP_SLIDER_KNOB_PRESSED_COLOR = new Color(0xb0b2b6);
    private static final Color BOTTOM_SLIDER_KNOB_PRESSED_COLOR = new Color(0x86888b);

    public static final Color BORDER_COLOR = new Color(0xc5c8cf);

    private static final Color LIGHT_SHADOW_COLOR = new Color(0, 0, 0, 145);
    private static final Color DARK_SHADOW_COLOR = new Color(0, 0, 0, 50);

    private static final ShapeProvider NO_TICKS_SHAPE_PROVIDER =
            createCircularSliderKnobShapeProvider();

    private static final ShapeProvider TICKS_SHAPE_PROVIDER =
            createPointedSliderKnobShapeProvider();

    public HudSliderUI(JSlider b) {
        super(b);
    }

    @Override
    protected void installDefaults(JSlider slider) {
        super.installDefaults(slider);
        slider.setOpaque(false);
    }

    @Override
    protected Dimension getThumbSize() {
        int sliderKnobHeight = slider.getPaintTicks()
                ? SLIDER_KNOB_HEIGHT_WITH_TICKS : SLIDER_KNOB_HEIGHT_NO_TICKS;
        return new Dimension(SLIDER_KNOB_WIDTH, sliderKnobHeight);
    }

    @Override
    public void paintThumb(Graphics graphics) {
        Paint paint = createSliderKnobButtonPaint(isDragging(), thumbRect.height);
        ShapeProvider shapeProvider = slider.getPaintTicks()
                ? TICKS_SHAPE_PROVIDER : NO_TICKS_SHAPE_PROVIDER;
        paintHudControlBackground((Graphics2D) graphics, thumbRect, shapeProvider,
                paint);
    }

    @Override
    public void paintTrack(Graphics graphics) {
        Graphics2D graphics2d = (Graphics2D) graphics;
        graphics2d.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        double trackY = slider.getHeight()/2.0 - TRACK_HEIGHT/2.0;
        RoundRectangle2D track = new RoundRectangle2D.Double(
                0, trackY, slider.getWidth()-1, TRACK_HEIGHT - 1, 4, 2);

        graphics.setColor(TRACK_BACKGROUND_COLOR);
        graphics2d.fill(track);
        graphics2d.setColor(TRACK_BORDER_COLOR);
        graphics2d.draw(track);
    }

    @Override
    protected int getTickLength() {
        return 5;
    }

    @Override
    protected void calculateThumbLocation() {
        super.calculateThumbLocation();

        // if this is a horizontal style slider and we're drawing a pointy style thumb
        // then shift the thumb down three pixels.
        if ( slider.getOrientation() == JSlider.HORIZONTAL
                && slider.getPaintTicks()) {
            thumbRect.y += 3;
        } else  {
            // TODO handle vertical slider.
        }
    }

    @Override
    protected void calculateTickRect() {
        super.calculateTickRect();

        // if this is a horizontal style slider, shift the ticks down one pixel so that
        // they aren't right up against the track.
        if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
            tickRect.y += 1;
        } else  {
            // TODO handle vertical slider.
        }
    }

    @Override
    protected void paintMajorTickForHorizSlider(Graphics g, Rectangle tickBounds, int x) {
        g.setColor(Color.WHITE);
        super.paintMajorTickForHorizSlider(g, tickBounds, x);
    }

    @Override
    public void setThumbLocation(int x, int y) {
        super.setThumbLocation(x, y);
        // repaint the whole slider -- it's easier than trying to figure out
        // whats dirty, especially since the thumb will be drawn outside of the
        // thumbRect (the shadow part).
        slider.repaint();
    }

    @Override
    public void paintFocus(Graphics g) {
        // don't paint focus.
    }

    private static Paint createSliderKnobButtonPaint(boolean isPressed, int height) {
        // grab the top and bottom gradient colors based on the pressed state.
        Color topColor = isPressed
                ? TOP_SLIDER_KNOB_PRESSED_COLOR : TOP_SLIDER_KNOB_COLOR;
        Color bottomColor = isPressed
                ? BOTTOM_SLIDER_KNOB_PRESSED_COLOR : BOTTOM_SLIDER_KNOB_COLOR;
        // compenstate for the two pixel shadow drawn below the slider thumb.
        int bottomY = height - 2;
        return new GradientPaint(0, 0, topColor, 0, bottomY, bottomColor);
    }

    /**
     * Creates a simple circle.
     */
    private static ShapeProvider createCircularSliderKnobShapeProvider() {
        return new ShapeProvider() {
            public Shape createShape(double x, double y, double width, double height) {
                return new Ellipse2D.Double(x, y, width, height);
            }
        };
    }

    /**
     * Cerates a pointy slider thumb shape that looks roughly like this:
     *     +----+
     *    /      \
     *    +      +
     *    |      |
     *    +      +
     *      \  /
     *       \/
     */
    private static ShapeProvider createPointedSliderKnobShapeProvider() {
        return new ShapeProvider() {
            public Shape createShape(double x, double y, double width, double height) {
                float xFloat = (float) x;
                float yFloat = (float) y;
                float widthFloat = (float) width;
                float heightFloat = (float) height;

                // draw the thumb shape based on the given height and width.
                GeneralPath path = new GeneralPath();
                // move in two pixels so that we can curve down to the next point.
                path.moveTo(xFloat + 2.0f, yFloat);
                // curve down to the second point.
                path.curveTo(xFloat + 0.25f, yFloat + 0.25f, xFloat - 0.25f,
                        yFloat + 2.0f, xFloat, yFloat + 2.0f);
                // move straight down to the next point.
                path.lineTo(xFloat, yFloat + heightFloat/1.60f);
                // move down and right to form the left half of the pointy section.
                path.lineTo(xFloat + widthFloat/2, yFloat + heightFloat);
                // move up and right to form the right half of the pointy section.
                path.lineTo(xFloat + widthFloat, yFloat + heightFloat/1.60f);
                // move straight up to the point to curve from.
                path.lineTo(xFloat + widthFloat, yFloat + 2.0f);
                // curve up and right to the top of the thumb.
                path.curveTo(xFloat + widthFloat - 0.25f, yFloat + 2.0f,
                        xFloat + widthFloat - 0.25f, yFloat + 0.25f,
                        xFloat + widthFloat - 2.0f, yFloat);
                path.closePath();

                return path;
            }
        };
    }

    /**
     * Paints a HUD style background in the given shape. This includes a drop shadow
     * which will be drawn under the shape to be painted. The shadow will be draw
     * outside the given bounds.
     * @param graphics the {@code Graphics2D} context to draw in.
     * @param bounds the bounds to paint in.
     * @param shapeProvider the delegate to request the {@link Shape} from.
     * @param paint the {@link Paint} to use to fill the {@code Shape}.
     */
    public static void paintHudControlBackground(
            Graphics2D graphics, Rectangle bounds, ShapeProvider shapeProvider,
            Paint paint) {

        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        int x = bounds.x;
        int y = bounds.y;
        int width = bounds.width;
        int height = bounds.height;

        // paint the first (further away) part of the drop shadow.
        graphics.setColor(LIGHT_SHADOW_COLOR);
        graphics.draw(shapeProvider.createShape(x, y, width - 1, height));
        // paint the second (closer) part of the drop shadow.
        graphics.setColor(DARK_SHADOW_COLOR);
        graphics.draw(shapeProvider.createShape(x, y, width - 1, height + 1));

        // fill the HUD shape.
        graphics.setPaint(paint);
        graphics.fill(shapeProvider.createShape(x, y + 1, width, height - 1));
        // stroke the HUD shape.
        graphics.setColor(BORDER_COLOR);
        graphics.draw(shapeProvider.createShape(x, y, width - 1, height - 1));
    }

    /**
     * An interface for specifying a shape to paint and draw a drop shadown under.
     */
    public interface ShapeProvider {
        Shape createShape(double x, double y, double width, double height);
    }
}

4 Responses to “Creating a HUD style slider”

  1. Tristan Seifert Says:

    Will there be a HUD UI for a JTable and JScrollBar / JScrollPane?

  2. Ken Says:

    Hi Tristan,

    I’ll start working on those next.

    -Ken


  3. […] Orr put out a HUD-style slider for his Mac Widgets for Java project. It is a very nice looking, minimalist component that goes well with all the other components he […]


Leave a comment