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();
            }
        });
    }
}
4

2 回答 2

7

在将 JLabel 插入树后尝试 scrollRectToVisible 时,我遇到了与 OP 相同的问题。问题是在布局的 getPreferredSize() 期间,包含 HTML 的 JLabel 在需要换行之前不知道有多少水平空间可用。假设它根本不需要换行,它只会返回一个宽尺寸。然后,在绘制期间,JLabel 的内部 FlowView 将“修复”布局并设置其实际所需的高度,该高度被缓存并在下一次布局迭代时返回。

我使用的解决方案是调用 JLabel.paint,因为似乎没有其他方法可以告诉它以实际可用宽度完成布局。

顺便说一句,其他 UI 系统,例如WPF 的 MeasureOverrideJavaFX 的带有 contentBias 的 prefHeightAndroid 的 measure为该 measure 提供了可用空间,因此它们不存在此问题。

下面是防止闪烁的相关代码:

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);}
}
于 2014-06-02T22:31:49.810 回答
0

您可以尝试使用所需的 HTML 创建新标签并替换旧标签。

创建并显示原始标签

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

创建新标签并替换旧标签

final JLabel newLabel = new JLabel(newtext);
content.remove(label);
content.add(newLabel);
content.validate();    // This will cause the container to show the new label.
于 2013-05-03T18:40:14.433 回答