1

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

A.A Noman
  • 5,244
  • 9
  • 24
  • 46
Ramsey
  • 33
  • 7
  • It would less likely be about graphics as it is about threading. Do you know much about threading? – Kleo G Feb 05 '18 at 10:10
  • Check out answers to [this](https://stackoverflow.com/questions/7634402/creating-a-nice-loading-animation) question. It has super simple example how to achieve this with `gif` and `ImageIcon`. – MatheM Feb 05 '18 at 10:13
  • 1
    what the attempt you have made on your issue? first show your progress on above, if anything where you got stuck, we definitely help you... – ArifMustafa Feb 05 '18 at 10:15
  • Essentially it's an arc, you just need to be able to animate the start angle and extent at different rates – MadProgrammer Feb 05 '18 at 10:25

1 Answers1

2

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

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • This is an excellent answer, I can basically use it as-is. One small, but important thing should be added, though, I've been hunting it on Linux for hours: Add a `getToolkit().sync()` call at the end of the `paintComponent()` method, otherwise you may experience serious redraw lags when the arc is small. – Tomáš Dvořák Mar 01 '21 at 19:45
  • @TomášDvořák In some 20 odd years of Java development, I've never had to use `Toolkit#sync`, however, I would recommend putting in the `Timer`s `actionPerformed` method instead – MadProgrammer Mar 01 '21 at 21:09
  • Yeah, I also wasn't even aware of it's existence until yesterday. However, when really needed, it seems to cause a lot of woes: https://stackoverflow.com/a/63458711/3220468, https://stackoverflow.com/q/4074284/3220468 . It probably only manifests itself on Linux when only a small portion of the screen is updated, so it was totally confusing to see the spinner choppy when the arc was short and then suddenly smoothing up. – Tomáš Dvořák Mar 03 '21 at 06:19