Search code examples
javaswinggraphics

How to make the Youtube's rotating spinner loading screen on Java Swing


I'm making a loading screen on my swing project but I want to use the Google's rotating spinner as the loading screen. It is just the loading screen that YouTube uses while it loads the video or it can also be seen when Google Chrome(or any other browser) is loading a webpage (see screenshots). I'm not much good with the graphics class of java however I know the basics. Please suggest me a method to make the loading spinner.

Loading spinner in New Tab of Google Chrome:

enter image description here

Loading spinner in YouTube:

enter image description here


Solution

  • So, the basic idea is, the shape is an open arc. That is, has a start angle and extends a certain number of degrees to create the edge.

    Maybe have a look at Working with Geometry for some more ideas.

    The idea is then to animate both the start "angle" and the "extent" and different rates. This gets the "closing" of the circle. The trick is then the "opening" of the circle when the two ends meet, which is actually the same idea in reverse (kind of ;)). "Opening" the circle again involves recalculating the start position as the difference between the current angle and the extent and then changing the extent to be a fall circle and then letting the animation to play through again.

    Because Swing is single threaded AND NOT thread safe, it's important NOT to block the Event Dispatching Thread AND only update the state of the UI (directly or indirectly) from within the content of the Event Dispatching Thread.

    The simplest solution is to use a Swing Timer, which waits off the EDT, but generates it's notifications within the EDT.

    See How to use Swing Timers for more details.

    Example

    import java.awt.BasicStroke;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.RenderingHints;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.geom.Arc2D;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.Timer;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        ex.printStackTrace();
                    }
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private double angle;
            private double extent;
    
            private double angleDelta = -1;
            private double extentDelta = -5;
    
            private boolean flip = false;
    
            public TestPane() {
                setBackground(Color.BLACK);
                Timer timer = new Timer(10, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        angle += angleDelta;
                        extent += extentDelta;
                        if (Math.abs(extent) % 360.0 == 0) {
                            angle = angle - extent;
                            flip = !flip;
                            if (flip) {
                                extent = 360.0;
                            } else {
                                extent = 0.0;
                            }
                        }
                        repaint();
                    }
                });
                timer.start();
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(200, 200);
            }
    
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                Graphics2D g2d = (Graphics2D) g.create();
                g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
                g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
                g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
    
                Arc2D.Double arc = new Arc2D.Double(50, 50, 100, 100, angle, extent, Arc2D.OPEN);
                BasicStroke stroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND);
                g2d.setStroke(stroke);
                g2d.setColor(Color.WHITE);
                g2d.draw(arc);
                g2d.dispose();
            }
    
        }
    
    }
    

    The example also makes use of a BasicStroke to give the shape some thickness