A Java Scroll Indicator with floating scrollbars which only appears during scrolling
Asked Answered
U

0

2

I need a scroll pane with scrollbars only visible during scroll events or when needed to pull. Something that looks like the scroll bars on the nowadays smartphones.

I searched a lot but found only realizations in javascript. So I tried it by myself.

Has anyone a better solution or hints?

The animation classes can be found here: TimingFrameWork

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import javax.swing.plaf.ScrollBarUI;
import javax.swing.plaf.basic.BasicScrollBarUI;

import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.interpolation.KeyFrames;
import org.jdesktop.animation.timing.interpolation.KeyTimes;
import org.jdesktop.animation.timing.interpolation.KeyValues;
import org.jdesktop.animation.timing.interpolation.PropertySetter;

/**
 * A scrollpane like component, where the scroll bars are floating over the
 * scrollable view to indicate the current scroll positions.
 * The scroll indicators appear smoothly during scroll events and disappear
 * smoothly afterwards.
 * <p>
 * The scrollbars can be dragged just as normal.</p>
 * <p>
 * The usage is similar to a classic scrollpane.</p>
 *
 * @author Jolly Littlebottom
 */
public class JScrollIndicator extends JLayeredPane {

    private static final Color THUMB_COLOR = Color.DARK_GRAY;
    private static final Color THUMB_BORDER_COLOR = Color.LIGHT_GRAY;
    private static final float MAX_ALPHA = 0.8f;
    private static final int THUMB_THICKNESS = 7;
    private static final int THUMB_MIN_SIZE = 48;
    private static final int THUMB_MARGIN = 3;
    private static final int FADE_IN_TIME = 300;
    private static final int STAY_TIME = 2000;
    private static final int FADE_OUT_TIME = 1000;


    private final JScrollPane scrollPane;
    private final ControlPanel controlPanel;

    /**
     * Creates a <code>JScrollIndicator</code> that displays the contents of the
     * specified component, where both horizontal and vertical scrollbars appear
     * whenever the component's contents are larger than the view and scrolling
     * in underway or the mouse is over the scrollbar position.
     *
     * @see #setViewportView
     * @param view the component to display in the scrollpane's viewport
     */
    public JScrollIndicator(final JComponent view) {
        this(view, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 
            JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    }

    /**
     * Creates a <code>JScrollIndicator</code> that displays the view component
     * in a viewport whose view position can be controlled with a pair of
     * scrollbars.
     * The scrollbar policies specify when the scrollbars are displayed,
     * For example, if <code>vsbPolicy</code> is
     * <code>JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED</code>
     * then the vertical scrollbar only appears if the view doesn't fit
     * vertically. The available policy settings are listed at
     * {@link #JScrollPane.setVerticalScrollBarPolicy} and
     * {@link #JScrollPane.setHorizontalScrollBarPolicy}.
     *
     * @param view the component to display in the scrollpanes viewport
     * @param vsbPolicy an integer that specifies the vertical scrollbar policy
     * @param hsbPolicy an integer that specifies the horizontal scrollbar policy
     */
    public JScrollIndicator(final JComponent view, int vsbPolicy, int hsbPolicy) {

        scrollPane = new JScrollPane(view, vsbPolicy, hsbPolicy);
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        add(scrollPane, JLayeredPane.DEFAULT_LAYER);

        controlPanel = new ControlPanel(scrollPane);
        add(controlPanel, JLayeredPane.PALETTE_LAYER);

        addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(ComponentEvent e) {
                // listen to changes of JLayeredPane size
                scrollPane.setSize(getSize());
                scrollPane.getViewport().revalidate();
                controlPanel.setSize(getSize());
                controlPanel.revalidate();
            }
        });
    }

    /**
     * Returns the scroll pane used by this scroll indicator.
     * Use carefully (e.g. to set unit increments) because not all changes have an
     * effect. You have to write listeners in this cases (e.g. for changing the
     * scrollbar policy)
     * 
     * @return
     */
    public JScrollPane getScrollPane() {
        return scrollPane;
    }

    private class ControlPanel extends JPanel {

        private final JMyScrollBar vScrollBar;
        private final JMyScrollBar hScrollBar;

        private ControlPanel(JScrollPane scrollPane) {

            setLayout(new BorderLayout());
            setOpaque(false);

            vScrollBar = new JMyScrollBar(JScrollBar.VERTICAL);
            scrollPane.setVerticalScrollBar(vScrollBar);
            scrollPane.remove(vScrollBar);
            if (scrollPane.getVerticalScrollBarPolicy() != JScrollPane.VERTICAL_SCROLLBAR_NEVER) {
                add(vScrollBar, BorderLayout.EAST);
            }

            hScrollBar = new JMyScrollBar(JScrollBar.HORIZONTAL);
            scrollPane.setHorizontalScrollBar(hScrollBar);
            scrollPane.remove(hScrollBar);
            if (scrollPane.getHorizontalScrollBarPolicy() != JScrollPane.HORIZONTAL_SCROLLBAR_NEVER) {
                add(hScrollBar, BorderLayout.SOUTH);
            }
        }
    }

    private class JMyScrollBar extends JScrollBar {

        protected final MyScrollBarUI scrollUI;

        public JMyScrollBar(int direction) {
            super(direction);

            scrollUI = new MyScrollBarUI(this);
            super.setUI(scrollUI);

            int size = THUMB_THICKNESS + THUMB_MARGIN;
            setPreferredSize(new Dimension(size, size));

            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseEntered(MouseEvent e) {
                    scrollUI.setVisible(true, true);
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    scrollUI.setVisible(false, false);
                }
            });

            addAdjustmentListener(new AdjustmentListener() {
                @Override
                public void adjustmentValueChanged(AdjustmentEvent e) {
                    scrollUI.setVisible(true, false);
                }
            });
        }

        @Override
        public void setUI(ScrollBarUI ui) {
        }

        @Override
        public void updateUI() {
        }

        @Override
        public void paint(Graphics g) {
            scrollUI.paintThumb(g, this); // just the thumb
        }

        @Override
        public void repaint(Rectangle r) {
            JScrollIndicator scrollIndicator = JScrollIndicator.this;
            Rectangle rect = SwingUtilities.convertRectangle(this, r, scrollIndicator);
            rect.grow(1, 1);
            // ensure for a translucent thumb, that the view is first painted
            scrollIndicator.repaint(rect);
        }
    }

    public class MyScrollBarUI extends BasicScrollBarUI {

        private JMyScrollBar scrollBar;
        private float alpha = 0.0f;
        private FadeAnimation fadeAnimation;

        private MyScrollBarUI(JMyScrollBar scrollBar) {
            this.scrollBar = scrollBar;
            fadeAnimation = new FadeAnimation(this);
        }

        @Override
        protected void installComponents() {
            incrButton = new JButton();
            decrButton = new JButton();
            if (scrollBar.getOrientation() == JScrollBar.HORIZONTAL) {
                int size = THUMB_THICKNESS + THUMB_MARGIN; // let lower right corner empty
                incrButton.setPreferredSize(new Dimension(size, size));
            }
            else {
                incrButton.setPreferredSize(new Dimension(THUMB_MARGIN, THUMB_MARGIN));
            }
            decrButton.setPreferredSize(new Dimension(THUMB_MARGIN, THUMB_MARGIN));
        }

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

            // ensure the minimum size of the thumb
            int w = minimumThumbSize.width;
            int h = minimumThumbSize.height;
            if (scrollBar.getOrientation() == JScrollBar.VERTICAL) {
                h = Math.max(h, Math.min(maximumThumbSize.height, THUMB_MIN_SIZE));
            }
            else {
                w = Math.max(w, Math.min(maximumThumbSize.width, THUMB_MIN_SIZE));
            }
            minimumThumbSize = new Dimension(w, h);
        }

        private void paintThumb(Graphics g, JComponent c) {

            if (alpha == 0.0f) {
                return; // don't paint anything
            }

            g.setColor(getAlphaColor(THUMB_COLOR));
            int radius = THUMB_THICKNESS >>> 1; // half width

            Rectangle thumbBounds = getThumbBounds();

            int x = thumbBounds.x;
            int y = thumbBounds.y;
            int w = thumbBounds.width;
            int h = thumbBounds.height;

            if (scrollBar.getOrientation() == JScrollBar.VERTICAL) {
                w -= THUMB_MARGIN;
            }
            else {
                h -= THUMB_MARGIN;
            }

            g.fillRoundRect(x, y, w, h, radius, radius);
            g.setColor(getAlphaColor(THUMB_BORDER_COLOR));
            g.drawRoundRect(x, y, w, h, radius, radius);
        }

        private Color getAlphaColor(Color color) {
            if (alpha == 1.0f) {
                return color;
            }
            int rgb = color.getRGB() & 0xFFFFFF; // color without alpha values
            rgb |= ((int)(alpha*255)) << 24; // add alpha value
            return new Color(rgb, true);
        }

        public void setAlpha(float alpha) {
            this.alpha = alpha;
            scrollBar.repaint(getThumbBounds());
        }

        public void setVisible(boolean visible, boolean mouseOver) {
            if (visible) {
                fadeAnimation.fadeIn(mouseOver);
            }
            else {
                fadeAnimation.fadeOut();
            }
            scrollBar.repaint(getThumbBounds());
        }
    }

    private class FadeAnimation {

        private final MyScrollBarUI scrollUI;
        private Animator fadeAnimator;
        private Timer fadeOutTimer;
        private boolean isFadeIn;

        private FadeAnimation(MyScrollBarUI scrollUI) {
            this.scrollUI = scrollUI;
        }

        public synchronized void fadeIn(boolean mouseOver) {

            if (mouseOver) {
                cancelTimer();
            }

            if (!isFadeIn) {
                isFadeIn = true;
                cancelAnimationAndTimer();

                fadeAnimator = PropertySetter.createAnimator(
                    FADE_IN_TIME, scrollUI, "alpha",
                    new KeyFrames(KeyValues.create(scrollUI.alpha, MAX_ALPHA),
                    new KeyTimes(0.0f, 1.0f)));
                fadeAnimator.start();

                if (!mouseOver) {
                    fadeOutTimer = new Timer(FADE_IN_TIME + STAY_TIME,
                        new ActionListener() {
                            @Override
                            public void actionPerformed(ActionEvent e) {
                                FadeAnimation.this.fadeOut();
                            }
                        });
                    fadeOutTimer.start();
                }
            }
        }

        public synchronized void fadeOut() {
            cancelAnimationAndTimer();
            if (isFadeIn) {
                isFadeIn = false;
                fadeAnimator = PropertySetter.createAnimator(
                    FADE_OUT_TIME, scrollUI, "alpha",
                    new KeyFrames(KeyValues.create(scrollUI.alpha, 0.0f),
                    new KeyTimes(0.0f, 1.0f)));
                fadeAnimator.start();
            }
        }

        private void cancelAnimationAndTimer() {
            if (fadeAnimator != null && fadeAnimator.isRunning()) {
                fadeAnimator.cancel();
                fadeAnimator = null;
            }
            cancelTimer();
        }

        private void cancelTimer() {
            if (fadeOutTimer != null) {
                fadeOutTimer.stop();
                fadeOutTimer = null;
            }
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                String text = "";
                for (int i = 0; i < 100; i++) {
                    for (int j = 0; j < 100; j++) {
                        text += i + " - " + j;
                    }
                    text += "\n";
                }
                JTextArea area = new JTextArea(text);
                JScrollIndicator scrollIndicator = new JScrollIndicator(area);
                frame.getContentPane().add(scrollIndicator);
                frame.setBounds(100, 100, 200, 300);
                frame.setVisible(true);
            }
        });
    }
}
Underlay answered 5/1, 2012 at 10:17 Comment(1)
It's a very nice solution. However, I found two issue with it: when used with a JTable the scrollbar includes the header and the right scroll bar doesn't go all the way down, and the resizing of column headers disappers.Sarazen

© 2022 - 2024 — McMap. All rights reserved.