6

I’m using a JComponent with HTML inside it (specifically a JLabel) to display and change some text that might need to wrap. Unfortunately, there is noticeable flicker when I change the HTML, because it’s doing two layout + paint cycles instead of just one. Is there any way to avoid painting the JLabel with the wrong layout? I’ve tried calling jframe.revalidate(); after setText, but it didn’t help.

Example code that demonstrates the flicker by changing the HTML every second. I’ve added an artificial Thread.sleep during paintComponent to simulate a large layout tree or expensive paint so that the flicker is visible in this small window. In the screenshot, the left is the correctly laid out label, while the right is an incorrect screenshot captured mid-flicker.

Left: correctly laid out panel. Right: panel during the flicker with incorrect layout

import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.WindowConstants;

public class HtmlJLabelFlicker {
    static final String text =
            "<html>This label is being set to a new value " +
            "that needs to wrap. Unfortunately, this causes " +
            "two layout + paint cycles instead of just one, " +
            "which can cause flicker if the text doesn\u2019t " +
            "change very much between refreshes. ";
    public static void createUI() {
        final JLabel label = new JLabel(text);
        final Timer timer = new Timer(1000, new ActionListener() {
            int n;
            @Override public void actionPerformed(ActionEvent e) {
                label.setText(text + n++);
            }
        });
        timer.start();
        JFrame jframe = new JFrame();
        jframe.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        jframe.addWindowListener(new WindowAdapter() {
            @Override public void windowClosed(WindowEvent e) {
                timer.stop();
            }
        });
        jframe.setSize(300, 300);
        Container content = jframe.getContentPane();
        content.add(label);
        jframe.setVisible(true);
    }

    public static void main(String[] args) throws Exception {
        SwingUtilities.invokeAndWait(new Runnable() {
            @Override public void run() {
                createUI();
            }
        });
    }
}
yonran
  • 18,156
  • 8
  • 72
  • 97
  • "Swing programs should override `paintComponent()` instead of overriding `paint()`."—[*Painting in AWT and Swing: The Paint Methods*](http://www.oracle.com/technetwork/java/painting-140037.html#callbacks). – trashgod Apr 26 '13 at 02:44
  • 1
    @trashgod, you’re right about best practices, but I’m only using `paint()` to simulate a bigger, slower layout. Switching from `Component` to `JComponent` doesn’t make any difference in the flickering. But I’ve edited the example to use paintComponent instead of paint as you requested. – yonran Apr 26 '13 at 03:01
  • 1
    `but I’m only using paint() to simulate a bigger, slower layout.` - unless you use a proper simulation we don't know if the problem is with your attempt to do the simulation or the problem you are attempting to describe. You also forget the `super.paintComponent(...)` which could cause problems. You should also not use Thread.sleep(). Again you don't know if this causes the problem or simulates it. I don't notice any flickering on Windows 7 using JDK7. – camickr Apr 26 '13 at 03:13
  • No flicker on Mac OS X or Ubuntu 12; also do `add`, `pack`, `setSize`, `setVisible`. – trashgod Apr 26 '13 at 03:16
  • @trashgod and @camickr, I have tried it on java 7 on OS X 10.8, Windows 7, and Ubuntu 12.04. The flicker was definitely there on all platforms, although I think it was least visible on Windows. You can adjust the `Thread.sleep` amount to, say, 100ms to be sure to see it. I’ll edit the example to have a longer simulated draw time. – yonran Apr 26 '13 at 03:27
  • Still nothing; also close the HTML. – trashgod Apr 26 '13 at 03:32
  • @trashgod, there should be only two different texts shown on the `JLabel`, changing every 1s. The third value, which is shown for 100ms, is the flicker. – yonran Apr 26 '13 at 03:44
  • 1
    I would expect a flicker if you cause the EDT to sleep for 100ms. To me its not really a flicker, just a delay in painting. – camickr Apr 26 '13 at 03:51
  • I see alternating short and long texts, but no third value. BTW, the default layout of `JFrame` is `BorderLayout`. – trashgod Apr 26 '13 at 03:51
  • @trashgod, which version of Java are you using? I’ve tried it on 1.7.0_09, 1.7.0_15, 1.7.0_17, 1.7.0_21 with the same effect. @camickr, I originally saw the problem in a larger component tree. Adding `Thread.sleep` is not going to cause extra draw calls; it only makes existing ones more visible. If you prefer, you could set a breakpoint in the RepaintManager instead of using Thread.sleep. – yonran Apr 26 '13 at 04:00
  • Adding a "artificial" delay within the EDT suggests that you have an issue with your painting routines, not Swing (per say). – MadProgrammer Apr 26 '13 at 04:30
  • Okay everyone, I’ve taken out the `Thread.sleep` call that you all didn’t like. The flicker is still quite pronounced on my Macbook Pro; I’ll check other platforms tomorrow to see how bad it is on Linux and Windows. – yonran Apr 26 '13 at 08:22
  • @yonran: On Java 6, the latest update doesn't really _flicker_, but I _can_ see the visual discontinuity as the entire label's text is replaced. I don't know a workaround except to update a smaller area. – trashgod Apr 26 '13 at 10:24
  • I can see the "flicker" in Java 1.7.0_21 in Linux x64, though it seems to occur intermittently. – VGR Apr 26 '13 at 10:35
  • The flicker happens reliably on OS X. On Windows, it happens only intermittently, and on Ubuntu with my graphics card it isn’t very visible. But if you set a breakpoint at RepaintManager.paintDirtyRegions(Map), you’ll be able to see the jitter on all platforms. Again, I assert that with different combinations of labels and layouts the flicker can be jarring on all platforms. – yonran Apr 26 '13 at 17:37

2 Answers2

7

I ran into the same issue as OP when I was trying to scrollRectToVisible after inserting a JLabel into the tree. The problem is that during layout’s getPreferredSize(), a JLabel containing HTML does not know how much horizontal space is available before it needs to wrap lines. It just returns a wide size assuming that it does not need to wrap at all. Then, during paint, the JLabel’s internal FlowView will “repair” the layout and set its actual required height, which is cached and returned at the next layout iteration.

The solution that I used is to call JLabel.paint, since there seems to be no other way to tell it to finish doing layout with the actual available width.

As an aside, other UI systems such as WPF’s MeasureOverride, JavaFX’s prefHeight with contentBias and Android’s measure give the measure pass the available space so that they don’t have this problem.

Here is the relevant code to prevent flicker:

label.setText(text + n++);
// revalidate, but do so synchronously.
Container validateRoot = label;
while (! validateRoot.isValidateRoot()) {
    Container parent = validateRoot.getParent();
    if (parent == null)
        break;
    validateRoot = parent;
}
// This first validate() call may be excluded if the width is already correct
validateRoot.validate();
NoopGraphics g = new NoopGraphics(0, 0, label.getWidth(), label.getHeight(), label.getGraphicsConfiguration(), false, false);
label.paint(g);
validateRoot.validate();
// Now you can use the measured bounds for e.g. scrollRectToVisible

NoopGraphics.java:

import java.awt.AlphaComposite;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.RenderingHints.Key;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderableImage;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.Collections;
import java.util.Map;

import javax.swing.SwingUtilities;

/**
 * A subclass of Graphics2D that returns the correct FontMetrics but does not
 * actually paint anything.
 *
 * @see <a
 *      href="http://stackoverflow.com/questions/16227877/how-to-update-a-jcomponent-with-html-without-flickering">How
 *      to update a JComponent with HTML without flickering?</a>
 */
public class NoopGraphics extends Graphics2D {
    private Font font;
    private Color color = Color.BLACK;
    private final Rectangle clip;
    private Stroke stroke;
    private Paint paint;
    private Color background;
    private AffineTransform transform = new AffineTransform();
    private final RenderingHints renderingHints = new RenderingHints(Collections.<Key,Object>emptyMap());
    private Composite composite = AlphaComposite.SrcOver;
    private boolean isAntiAliased;
    private boolean usesFractionalMetrics;
    private GraphicsConfiguration graphicsConfiguration;

    public static GraphicsConfiguration getDefaultScreenGraphicsConfiguration() {
        GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice graphicsDevice = graphicsEnvironment.getDefaultScreenDevice();
        GraphicsConfiguration graphicsConfiguration = graphicsDevice.getDefaultConfiguration();
        return graphicsConfiguration;
    }
    public NoopGraphics(int x, int y, int width, int height) {
        this(x, y, width, height, getDefaultScreenGraphicsConfiguration(), false, false);
    }
    public NoopGraphics(int x, int y, int width, int height, GraphicsConfiguration graphicsConfiguration, boolean isAntiAliased, boolean usesFractionalMetrics) {
        this.graphicsConfiguration = graphicsConfiguration;
        this.isAntiAliased = isAntiAliased;
        this.usesFractionalMetrics = usesFractionalMetrics;
        this.clip = new Rectangle(x, y, width, height);
    }
    @Override public void setXORMode(Color c1) {}
    @Override public void setPaintMode() {}
    @Override public Font getFont() {return font;}
    @Override public void setFont(Font font) {this.font=font;}
    @Override public Color getColor() {return color;}
    @Override public void setColor(Color c) {this.color=c;}
    @Override public void setClip(int x, int y, int width, int height) {
    }
    @Override public void setClip(Shape clip) {this.clip.setRect(clip.getBounds());}
    @Override public FontMetrics getFontMetrics(Font f) {
        // http://stackoverflow.com/questions/2753514/java-friendlier-way-to-get-an-instance-of-fontmetrics
        return new Canvas(graphicsConfiguration).getFontMetrics(f);
    }
    @Override public Rectangle getClipBounds() {return clip.getBounds();}
    @Override public Shape getClip() {return clip;}
    @Override public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {}
    @Override public void fillRect(int x, int y, int width, int height) {}
    @Override public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {}
    @Override public void fillOval(int x, int y, int width, int height) {}
    @Override public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {}
    @Override public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {}
    @Override public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {}
    @Override public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {}
    @Override public void drawOval(int x, int y, int width, int height) {}
    @Override public void drawLine(int x1, int y1, int x2, int y2) {}
    @Override public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) {return true;}
    @Override public boolean drawImage(Image img, int dx1,
            int dy1, int dx2, int dy2, int sx1, int sy1,
            int sx2, int sy2, ImageObserver observer) {
        return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null, observer);
    }
    @Override public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) {
        return false;
    }
    @Override public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) {
        return drawImage(img, x, y, width, height, null, observer);
    }
    @Override public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) {
        return false;
    }
    @Override public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
        return drawImage(img, x, y, null, observer);
    }
    @Override public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {}
    @Override public void dispose() {}
    @Override public Graphics create() {return this;}
    @Override public void copyArea(int x, int y, int width, int height, int dx, int dy) {}
    @Override public void clipRect(int x, int y, int width, int height) {SwingUtilities.computeIntersection(x, y, width, height, this.clip);}
    @Override public void clearRect(int x, int y, int width, int height) {}
    @Override public void translate(double tx, double ty) {getTransform().translate(tx, ty);}
    @Override public void translate(int x, int y) {translate((double)x, (double)y);}
    @Override public void transform(AffineTransform Tx) {getTransform().concatenate(Tx);}
    @Override public void shear(double shx, double shy) {getTransform().shear(shx, shy);}
    @Override public void scale(double sx, double sy) {getTransform().scale(sx, sy);}
    @Override public void setTransform(AffineTransform Tx) {this.transform = Tx;}
    @Override public void setStroke(Stroke s) {this.stroke = s;}
    @Override public void setRenderingHints(Map<?, ?> hints) {this.renderingHints.clear(); this.renderingHints.putAll(hints);}
    @Override public void setRenderingHint(Key hintKey, Object hintValue) {this.renderingHints.put(hintKey, hintValue);}
    @Override public void setPaint(Paint paint) {this.paint = paint;}
    @Override public void setComposite(Composite comp) {this.composite = comp;}
    @Override public void setBackground(Color color) {this.background = color;}
    @Override public void rotate(double theta, double x, double y) {getTransform().rotate(theta, x, y);}
    @Override public void rotate(double theta) {getTransform().rotate(theta);}
    @Override public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
        return false;
    }
    @Override public AffineTransform getTransform() {return this.transform;}
    @Override public Stroke getStroke() {return this.stroke;}
    @Override public RenderingHints getRenderingHints() {return renderingHints;}
    @Override public Object getRenderingHint(Key hintKey) {return renderingHints.get(hintKey);}
    @Override public Paint getPaint() {return this.paint;}
    @Override public FontRenderContext getFontRenderContext() {return new FontRenderContext(transform, isAntiAliased, usesFractionalMetrics);}
    @Override public GraphicsConfiguration getDeviceConfiguration() {return graphicsConfiguration;}
    @Override public Composite getComposite() {return composite;}
    @Override public Color getBackground() {return background;}
    @Override public void fill(Shape s) {}
    @Override public void drawString(AttributedCharacterIterator iterator, float x, float y) {}
    @Override public void drawString(AttributedCharacterIterator iterator, int x, int y) {drawString(iterator, (float)x, (float)y);}
    @Override public void drawString(String str, float x, float y) {drawString(new AttributedString(str).getIterator(), x, y);}
    @Override public void drawString(String str, int x, int y) {drawString(str, (float)x, (float)y);}
    @Override public void drawRenderedImage(RenderedImage img, AffineTransform xform) {}
    @Override public void drawRenderableImage(RenderableImage img, AffineTransform xform) {}
    @Override public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {}
    @Override public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) {return false;}
    @Override public void drawGlyphVector(GlyphVector g, float x, float y) {}
    @Override public void draw(Shape s) {}
    @Override public void clip(Shape s) {}
    @Override public void addRenderingHints(Map<?, ?> hints) {renderingHints.putAll(hints);}
}
yonran
  • 18,156
  • 8
  • 72
  • 97
  • 1
    it's rather slow as presented, however if you remove the first call to validateRoot.validate() everything still works, but much much faster. – Dmitry Avtonomov Dec 03 '14 at 00:43
  • 2
    Thanks @chhh. You’re right that in OP’s example, the JLabel’s available width does not change so the first validate() call is unnecessary. However, I needed it for myself because I was inserting a new node, and it needed to be validated first to get the width. Btw I also made NoopGraphics Java 6 compatible as requested. And yes, OP and I are the same person; I just ran into the exact same bug in a different place over a year later and finally found a solution :-) – yonran Dec 03 '14 at 17:13
  • I've got a strange problem with this. I've used it before with no problem, but I guess this is a special case. I have a number of labels whose text changes twice a second. Each label is within its own panel, within a JScrollPane. When I run this it starts out perfectly, then suddenly my normal drawing starts throwing exceptions. NullPointers and ArrayIndexOutOfBounds. It stops being able to draw at all. – Perry Monschau Apr 29 '16 at 19:52
0

You could try creating a new label with the HTML you want and replacing the old one.

Create and display the original label

final JLabel label = new JLabel(originalText);
Container content = jframe.getContentPane();
content.add(label);

Create the new label and replace the old one

final JLabel newLabel = new JLabel(newtext);
content.remove(label);
content.add(newLabel);
content.validate();    // This will cause the container to show the new label.