0

I understand that the Java RepaintManager will coalesce calls to repaint(), which is fine for 99% of rendering. I have one JPanel that I'd like to update on a timer (100 ms) with images to provide smooth'ish rendering like video. In practice the RepaintManager seems to steal/ignore about every other repaint() unless the mouse is being moved. I'm wondering what options I have to workaround this issue. I also looked at paintImmediately(), but it results in the same behavior as repaint(), thus not very useful. Thanks in advance for the helpful ideas!

  1. Is it possible to create and use a custom RepaintManager for a specific JPanel, and use the default for everything else?
  2. Is there a way to make the default RepaintManger determine a certain panel is "dirty" such that it would be repainted vs. ignored?

Below is some code to illustrate the implementation, you will notice (at least on my Linux testing), that the numbers will skip almost every other sequence.

    public class PanelRepaintIssue
    {
        private static final int kWIDTH = 200;
        private static final int kHEIGHT = 100;
        private static final int kNUM_IMAGES = 10;
        private static final int kREPAINT_DELAY = 250;

        private final JPanel _ImagePanel;
        private final BufferedImage[] _Images;
        private final Timer _Timer;

        private TimerTask _TimerTask;
        private int _Index;

        public PanelRepaintIssue()
        {
            _Index = 0;

            _ImagePanel = new JPanel()
            {
                @Override
                protected void paintComponent(Graphics g)
                {
                    super.paintComponent(g);

                    if (_Index < kNUM_IMAGES)
                    {
                        g.drawImage(_Images[_Index], 0, 0, null);
                    }
                }
            };
            _ImagePanel.setSize(new Dimension(kWIDTH, kHEIGHT));
            _ImagePanel.setPreferredSize(new Dimension(kWIDTH, kHEIGHT));

            _Images = new BufferedImage[kNUM_IMAGES];

            for (int i = 0; i < _Images.length; ++i)
            {
                _Images[i] = new BufferedImage(kWIDTH, kHEIGHT, BufferedImage.TYPE_INT_ARGB);
                Graphics2D t2d = _Images[i].createGraphics();
                t2d.setColor(Color.BLACK);
                t2d.fillRect(0, 0, kWIDTH, kHEIGHT);
                t2d.setColor(Color.RED);
                t2d.drawString(Integer.toString(i), kWIDTH/2, kHEIGHT/2);
                t2d.dispose();
            }

            _Timer = new Timer(this.getClass().getName());
        }

        public JPanel getPanel()
        {
            return _ImagePanel;
        }

        public void start()
        {
            if (null != _TimerTask)
            {
                _TimerTask.cancel();
                _TimerTask = null;
            }

            _TimerTask = new TimerTask()
            {
                @Override
                public void run()
                {
                    ++_Index;

                    if (_Index >= kNUM_IMAGES)
                    {
                        _Index = 0;
                    }

                    _ImagePanel.repaint();
                    // Also tried _ImagePanel.paintImmediately(0, 0, kWIDTH, kHEIGHT);
                }
            };

            _Timer.scheduleAtFixedRate(_TimerTask, 1000, kREPAINT_DELAY);
        }

        public static void main(String[] args)
        {
            PanelRepaintIssue tPanel = new PanelRepaintIssue();
            tPanel.start();
            JFrame tFrame = new JFrame("PanelRepaintIssue");
            tFrame.add(tPanel.getPanel());
            tFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            tFrame.setResizable(false);
            tFrame.pack();
            tFrame.setVisible(true);
        }
    }
  • 1
    Please read [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask) Note the part where it says: ___Help others reproduce the problem___ _if your problem is with code you've written, you should include some._ – Abra May 06 '20 at 18:34
  • I'm happy to provide some code, but thought of this as more of a fundamental of Java. Let me see if I can create something minimal that shows the issue without providing images. – Absent Link May 06 '20 at 18:43
  • Are you on linux? – matt May 07 '20 at 15:39
  • I think I had the same issue, https://stackoverflow.com/questions/61188644/prevent-swing-animation-from-jumping the only way I could get it to repaint consistently without any jumping was to call repaint at a higher rate. On osx my example was completely smooth. On linux it would start jumping. I don't think it was coalescence of repaints. – matt May 07 '20 at 15:42
  • Yes! I'm on Linux! I was just starting to wonder if it may be hardware/OS related! – Absent Link May 07 '20 at 20:02
  • I tried to add an independent timer to call repaint() more often, and it didn't make any difference as it did in matt's case. I did however find another solution mentioned https://stackoverflow.com/questions/19480076/java-animation-stutters-when-not-moving-mouse-cursor - Calling Toolkit.getDefaultToolkit().sync() after repaint() worked. – Absent Link May 13 '20 at 20:49

2 Answers2

0

I'm still not sure what the painting problem is.

I made some changes to your code.

  1. I used the SwingUtilities invokeLater method to put the creation and execution of the Swing components on the Event Dispatch Thread (EDT).

  2. I removed everything from the paintComponent method except the painting code. Nothing else should happen in the paintComponent method but painting.

Here's the code I ran.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class PanelRepaintIssue {
    private static final int kWIDTH = 200;
    private static final int kHEIGHT = 100;
    private static final int kNUM_IMAGES = 10;
    private static final int kREPAINT_DELAY = 250;

    private final JPanel _ImagePanel;
    private final BufferedImage[] _Images;
    private final Timer _Timer;

    private TimerTask _TimerTask;
    private int _Index;
    private int _TimerCnt;
    private int _PaintCnt;

    public PanelRepaintIssue() {
        _Index = 0;
        _TimerCnt = 0;
        _PaintCnt = 0;

        _ImagePanel = new JPanel() {

            private static final long serialVersionUID = 1L;

            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);

//              if (_Index < kNUM_IMAGES) {
                    g.drawImage(_Images[_Index], 0, 0, null);
                    ++_PaintCnt;
//                  System.out.println("Timer: " + _TimerCnt + 
//                          " Paint: " + _PaintCnt);
//              }
            }
        };
        _ImagePanel.setSize(new Dimension(kWIDTH, kHEIGHT));
        _ImagePanel.setPreferredSize(
                new Dimension(kWIDTH, kHEIGHT));

        _Images = new BufferedImage[kNUM_IMAGES];

        for (int i = 0; i < _Images.length; ++i) {
            _Images[i] = new BufferedImage(kWIDTH, kHEIGHT, 
                    BufferedImage.TYPE_INT_ARGB);
            Color color = new Color(128, 128, 20 * i);
            Graphics g = _Images[i].getGraphics();
            g.setColor(color);
            g.fillRect(0, 0, kWIDTH, kHEIGHT);
            g.dispose();
        }

        _Timer = new Timer(this.getClass().getName());
    }

    public JPanel getPanel() {
        return _ImagePanel;
    }

    public void start() {
        if (null != _TimerTask) {
            _TimerTask.cancel();
            _TimerTask = null;
        }

        _TimerTask = new TimerTask() {
            @Override
            public void run() {
                ++_TimerCnt;
                ++_Index;

                if (_Index >= kNUM_IMAGES) {
                    _Index = 0;
                }

                _ImagePanel.repaint();

                System.out.println("Timer: " + _TimerCnt + 
                        " Paint: " + _PaintCnt);
                // Also tried _ImagePanel.paintImmediately
                //    (0, 0, kWIDTH, kHEIGHT);
            }
        };

        _Timer.scheduleAtFixedRate(_TimerTask, 1000, 
                kREPAINT_DELAY);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                PanelRepaintIssue tPanel = 
                        new PanelRepaintIssue();
                tPanel.start();
                JFrame tFrame = new JFrame("PanelRepaintIssue");
                tFrame.add(tPanel.getPanel());
                tFrame.setDefaultCloseOperation(
                        JFrame.EXIT_ON_CLOSE);
                tFrame.setResizable(false);
                tFrame.pack();
                tFrame.setVisible(true);
            }   
        });
    }
}

Edit: Based on the new information provided in a comment instead of the original question, I reworked the code.

I didn't see a problem. Here are the last 4 lines of the println from the Timer method.

Timer: 200 Paint: 201
Timer: 201 Paint: 202
Timer: 202 Paint: 203
Timer: 203 Paint: 205

As you can see, the numbers are synchronized, except for the last line. The discrepancy was probably caused by my cancelling the program through Eclipse.

Here's the revised code. I still don't see the issue.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class PanelRepaintIssue {
    private static final int kWIDTH = 300;
    private static final int kHEIGHT = 200;
    private static final int kNUM_IMAGES = 10;
    private static final int kREPAINT_DELAY = 250;

    private final JPanel _ImagePanel;
    private final BufferedImage[] _Images;
    private final Timer _Timer;

    private TimerTask _TimerTask;
    private int _Index;
    private int _TimerCnt;
    private int _PaintCnt;

    public PanelRepaintIssue() {
        _Index = 0;
        _TimerCnt = 0;
        _PaintCnt = 0;

        _ImagePanel = new JPanel() {

            private static final long serialVersionUID = 1L;

            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                g.drawImage(_Images[_Index], 0, 0, null);
                ++_PaintCnt;
            }
        };
        _ImagePanel.setSize(new Dimension(kWIDTH, kHEIGHT));
        _ImagePanel.setPreferredSize(
                new Dimension(kWIDTH, kHEIGHT));

        _Images = new BufferedImage[kNUM_IMAGES];

        int x = kWIDTH / 2;
        int y = kHEIGHT / 2;

        for (int i = 0; i < _Images.length; ++i) {
            _Images[i] = new BufferedImage(kWIDTH, kHEIGHT, 
                    BufferedImage.TYPE_INT_ARGB);
            String text = Integer.toString(i + 1);
            Graphics g = _Images[i].getGraphics();
            g.setFont(g.getFont().deriveFont(80f));
            g.setColor(Color.BLACK);
            g.drawString(text, x, y);
            g.dispose();
        }

        _Timer = new Timer(this.getClass().getName());
    }

    public JPanel getPanel() {
        return _ImagePanel;
    }

    public void start() {
        if (null != _TimerTask) {
            _TimerTask.cancel();
            _TimerTask = null;
        }

        _TimerTask = new TimerTask() {
            @Override
            public void run() {
                ++_TimerCnt;
                ++_Index;

                if (_Index >= kNUM_IMAGES) {
                    _Index = 0;
                }

                updateImagePanel();

                System.out.println("Timer: " + _TimerCnt + 
                        " Paint: " + _PaintCnt);
            }
        };

        _Timer.scheduleAtFixedRate(_TimerTask, 1000, 
                kREPAINT_DELAY);
    }

    private void updateImagePanel() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                _ImagePanel.repaint();
            }
        });
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                PanelRepaintIssue tPanel = 
                        new PanelRepaintIssue();
                tPanel.start();
                JFrame tFrame = new JFrame("PanelRepaintIssue");
                tFrame.add(tPanel.getPanel());
                tFrame.setDefaultCloseOperation(
                        JFrame.EXIT_ON_CLOSE);
                tFrame.setResizable(false);
                tFrame.pack();
                tFrame.setVisible(true);
            }   
        });
    }
}

Edited again: I ran your code while I prepared lunch. Here are the last few lines of the println output.

Timer: 4037 Paint: 4038
Timer: 4038 Paint: 4040
Timer: 4039 Paint: 4040
Timer: 4040 Paint: 4041
Timer: 4041 Paint: 4042
Timer: 4042 Paint: 4043

As you can see, not one frame of the animation was dropped. At 250 milliseconds per frame, the Swing repaint manager has no problem painting an image.

Just for fun, I decreased the kREPAINT_DELAY to 10 milliseconds per frame. That's 100 frames / second.

Here are the println lines from that test.

Timer: 1104 Paint: 1105
Timer: 1105 Paint: 1106
Timer: 1106 Paint: 1107
Timer: 1107 Paint: 1108
Timer: 1108 Paint: 1109
Timer: 1109 Paint: 1110

Again, not a single frame dropped.

The problem is probably in your code, not in the Swing repaint manager.

Gilbert Le Blanc
  • 50,182
  • 6
  • 67
  • 111
  • Thanks. I think you forgot the invokeLater() in your update. The code sample came per request of "asking a better question", but it alone doesn't show the issue that I'm having. Basically, if you were to make each image show a number (_Images[0] shows a big zero, etc.) what I am seeing is that every other image isn't displayed. Expecting to see 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - instead I see 1, 3, 5, 7, 9, repeat, unless the mouse is moved and then I see the expected. – Absent Link May 07 '20 at 14:33
0

The issue ended up being platform specific with Linux, possibly other, but only Linux was tested. The issue was corrected or avoided by using the following:

_ImagePanel.repaint();
Toolkit.getDefaultToolkit().sync();

The addition of Toolkit.getDefaultToolkit().sync() ensured that repaint() actually updated the image on the screen.