【问题标题】:Make the animation faster when there are thousands of components当有数千个组件时使动画更快
【发布时间】:2020-11-19 07:57:36
【问题描述】:

我试图用动画隐藏JSplitPane。隐藏,我的意思是setDividerLocation(0),所以它的左侧组件是不可见的(技术上它是可见的,但宽度为零):

public class SplitPaneTest {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLayout(new BorderLayout());

            JPanel leftPanel = new JPanel(new BorderLayout());

            leftPanel.setBorder(BorderFactory.createLineBorder(Color.green));

            JPanel rightPanel = new JPanel(new GridLayout(60, 60));
            for (int i = 0; i < 60 * 60; i++) {
//              rightPanel.add(new JLabel("s"));
            }
            rightPanel.setBorder(BorderFactory.createLineBorder(Color.red));

            JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
            frame.add(splitPane);

            JButton button = new JButton("Press me to hide");
            button.addActionListener(e -> hideWithAnimation(splitPane));
            leftPanel.add(button, BorderLayout.PAGE_START);

            frame.setMaximumSize(new Dimension(800, 800));
            frame.setSize(800, 800);
            frame.setLocationByPlatform(true);
            frame.setVisible(true);
        });
    }

    private static void hideWithAnimation(JSplitPane splitPane) {
        final Timer timer = new Timer(10, null);
        timer.addActionListener(e -> {
            splitPane.setDividerLocation(Math.max(0, splitPane.getDividerLocation() - 3));
            if (splitPane.getDividerLocation() == 0)
                timer.stop();
        });
        timer.start();
    }

}

如果你运行它,你会发现一切看起来都很好,并且动画运行流畅。

但是,在实际应用中,JSplitPane 的右侧是JPanelCardLayout,并且每张卡都有很多组件。

如果您取消注释此行以模拟组件的数量:

// rightPanel.add(new JLabel("s"));

重新运行上面的例子,你会看到动画不再流畅。所以,问题是,是否有可能让它变得平滑(-ier)?

我不知道如何解决 - 如果有的话。

根据我的研究,我注册了一个全球ComponentListener

Toolkit.getDefaultToolkit()
    .addAWTEventListener(System.out::println, AWTEvent.COMPONENT_EVENT_MASK);

并看到了正在触发的大量事件。所以,我认为问题的根源在于为每个组件触发的大量组件事件。此外,似乎具有自定义渲染器的组件(如 JList - ListCellRendererJTable - TableCellRenderer),组件事件正在为所有渲染器触发。例如,如果 JList 有 30 个元素,则仅会为它触发 30 个事件(组件)。似乎(这也是我提到它的原因)对于 CardLayout,“不可见”组件也发生了事件。

我知道60*60 对您来说可能听起来很疯狂,但在实际应用程序中(我的应用程序大约有 1500 个),因为它是有意义的,所以绘画更重。

【问题讨论】:

    标签: java swing awt edt componentlistener


    【解决方案1】:

    @GeorgeZ。我认为@camickr 提出的概念与当你实际进行布局时有关。作为覆盖doLayout 的替代方案,我建议将GridLayout 子类化以仅在动画结束时布置组件(不覆盖doLayout)。但这与 camickr 的概念相同。

    虽然if你的组件在右侧面板的内容(即标签的文本)在分隔线动画的过程中保持不变,你也可以创建右侧面板的Image当用户单击按钮并显示该按钮而不是实际面板时。我想,这个解决方案包括:

    1. CardLayout 用于右侧面板。一张卡片具有实际的rightPanel 内容(即JLabels)。第二张卡只有一个JLabel,它将加载第一张卡的Image(作为ImageIcon)。
    2. 据我所知,通过查看CardLayout 的实现,Container所有子组件的边界是在layoutContainer 方法期间设置的。这可能意味着标签会被布置,尽管第二张卡片会被显示,但它们是不可见的。因此,您可能应该将其与子类 GridLayout 结合起来,以便仅在动画结束时进行布局。
    3. 要绘制第一张卡片的Image,首先应该创建一个BufferedImage,然后在其上创建createGraphics,然后在创建的Graphics2D对象上调用rightPanel.paint,最后处置Graphics2D对象之后。
    4. 创建第二张卡片,使JLabel 居中。为此,您只需为第二张卡提供GridBagLayout 并在其中仅添加一个ComponentJLabel),这应该是唯一的。 GridBagLayout 始终将内容居中。

    让我知道这样的解决方案是否对您有用。它可能没有用,因为您可能希望在动画进行时实际看到标签更改其布局配置文件,或者您甚至可能希望用户能够与Components 的rightPanel 交互在动画进行时。在这两种情况下,拍摄rightPanel 的照片并在动画发生时显示它而不是真实标签是不够的。因此,在这种情况下,这真的取决于rightPanel 的内容的动态程度。请在 cmets 中告诉我。

    如果每个程序运行的内容始终相同,那么您可能可以预先创建 Image 并存储它。甚至,大量的Images 并将它们存储起来,然后在动画打开时一个接一个地显示。

    同样,如果每个程序运行的内容并不总是相同,那么您也可以继承GridLayout 并在启动时预先计算每个组件的边界。然后这将使GridLayout 在布局组件时更快一点(这就像用每个对象的位置编码视频),但是当我正在测试它时,GridLayout 已经很快了:它只计算了大约 10布局开始时的变量,然后立即传递到设置每个 Component 的边界。

    编辑 1:

    这是我对我的想法的尝试(使用Image):

    import java.awt.BorderLayout;
    import java.awt.CardLayout;
    import java.awt.Color;
    import java.awt.Container;
    import java.awt.Graphics2D;
    import java.awt.GraphicsConfiguration;
    import java.awt.GraphicsDevice;
    import java.awt.GraphicsEnvironment;
    import java.awt.GridBagLayout;
    import java.awt.GridLayout;
    import java.awt.Transparency;
    import java.awt.image.BufferedImage;
    import java.util.Objects;
    import java.util.function.Consumer;
    import java.util.function.IntBinaryOperator;
    import javax.swing.BorderFactory;
    import javax.swing.ImageIcon;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JSplitPane;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    
    public class SplitPaneTest {
        
        //Just a Timer which plays the animation of the split pane's divider going from side to side...
        public static class SplitPaneAnimationTimer extends Timer {
            private final JSplitPane splitPane;
            private int speed, newDivLoc;
            private IntBinaryOperator directionf;
            private Consumer<SplitPaneAnimationTimer> onFinish;
    
            public SplitPaneAnimationTimer(final int delay, final JSplitPane splitPane) {
                super(delay, null);
                this.splitPane = Objects.requireNonNull(splitPane);
                super.setRepeats(true);
                super.setCoalesce(false);
                super.addActionListener(e -> {
                    splitPane.setDividerLocation(directionf.applyAsInt(newDivLoc, splitPane.getDividerLocation() + speed));
                    if (newDivLoc == splitPane.getDividerLocation()) {
                        stop();
                        if (onFinish != null)
                            onFinish.accept(this);
                    }
                });
                speed = 0;
                newDivLoc = 0;
                directionf = null;
                onFinish = null;
            }
            
            public int getSpeed() {
                return speed;
            }
            
            public JSplitPane getSplitPane() {
                return splitPane;
            }
            
            public void play(final int newDividerLocation, final int speed, final IntBinaryOperator directionf, final Consumer<SplitPaneAnimationTimer> onFinish) {
                if (newDividerLocation != splitPane.getDividerLocation() && Math.signum(speed) != Math.signum(newDividerLocation - splitPane.getDividerLocation()))
                    throw new IllegalArgumentException("Speed needs to be in the direction towards the newDividerLocation (from the current position).");
                this.directionf = Objects.requireNonNull(directionf);
                newDivLoc = newDividerLocation;
                this.speed = speed;
                this.onFinish = onFinish;
                restart();
            }
        }
        
        //Just a GridLayout subclassed to only allow laying out the components only if it is enabled.
        public static class ToggleGridLayout extends GridLayout {
            private boolean enabled;
            
            public ToggleGridLayout(final int rows, final int cols) {
                super(rows, cols);
                enabled = true;
            }
            
            @Override
            public void layoutContainer(final Container parent) {
                if (enabled)
                    super.layoutContainer(parent);
            }
            
            public void setEnabled(final boolean enabled) {
                this.enabled = enabled;
            }
        }
        
        //How to create a BufferedImage (instead of using the constructor):
        private static BufferedImage createBufferedImage(final int width, final int height, final boolean transparent) {
            final GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
            final GraphicsDevice gdev = genv.getDefaultScreenDevice();
            final GraphicsConfiguration gcnf = gdev.getDefaultConfiguration();
            return transparent
                   ? gcnf.createCompatibleImage(width, height, Transparency.TRANSLUCENT)
                   : gcnf.createCompatibleImage(width, height);
        }
        
        //This is the right panel... It is composed by two cards: one for the labels and one for the image.
        public static class RightPanel extends JPanel {
            private static final String CARD_IMAGE = "IMAGE",
                                        CARD_LABELS = "LABELS";
            
            private final JPanel labels, imagePanel; //The two cards.
            private final JLabel imageLabel; //The label in the second card.
            private final int speed; //The speed to animate the motion of the divider.
            private final SplitPaneAnimationTimer spat; //The Timer which animates the motion of the divider.
            private String currentCard; //Which card are we currently showing?...
            
            public RightPanel(final JSplitPane splitPane, final int delay, final int speed, final int rows, final int cols) {
                super(new CardLayout());
                super.setBorder(BorderFactory.createLineBorder(Color.red));
                
                spat = new SplitPaneAnimationTimer(delay, splitPane);
                this.speed = Math.abs(speed); //We only need a positive (absolute) value.
                
                //Label and panel of second card:
                imageLabel = new JLabel();
                imageLabel.setHorizontalAlignment(JLabel.CENTER);
                imageLabel.setVerticalAlignment(JLabel.CENTER);
                imagePanel = new JPanel(new GridBagLayout());
                imagePanel.add(imageLabel);
                
                //First card:
                labels = new JPanel(new ToggleGridLayout(rows, cols));
                for (int i = 0; i < rows * cols; ++i)
                    labels.add(new JLabel("|"));
                
                //Adding cards...
                final CardLayout clay = (CardLayout) super.getLayout();
                super.add(imagePanel, CARD_IMAGE);
                super.add(labels, CARD_LABELS);
                clay.show(this, currentCard = CARD_LABELS);
            }
            
            //Will flip the cards.
            private void flip() {
                final CardLayout clay = (CardLayout) getLayout();
                final ToggleGridLayout labelsLayout = (ToggleGridLayout) labels.getLayout();
                if (CARD_LABELS.equals(currentCard)) { //If we are showing the labels:
                    
                    //Disable the laying out...
                    labelsLayout.setEnabled(false);
                    
                    //Take a picture of the current panel state:
                    final BufferedImage pic = createBufferedImage(labels.getWidth(), labels.getHeight(), true);
                    final Graphics2D g2d = pic.createGraphics();
                    labels.paint(g2d);
                    g2d.dispose();
                    imageLabel.setIcon(new ImageIcon(pic));
                    imagePanel.revalidate();
                    imagePanel.repaint();
                    
                    //Flip the cards:
                    clay.show(this, currentCard = CARD_IMAGE);
                }
                else { //Else if we are showing the image:
                    
                    //Enable the laying out...
                    labelsLayout.setEnabled(true);
                    
                    //Revalidate and repaint so as to utilize the laying out of the labels...
                    labels.revalidate();
                    labels.repaint();
                    
                    //Flip the cards:
                    clay.show(this, currentCard = CARD_LABELS);
                }
            }
            
            //Called when we need to animate fully left motion (ie until reaching left side):
            public void goLeft() {
                final JSplitPane splitPane = spat.getSplitPane();
                final int currDivLoc = splitPane.getDividerLocation(),
                          minDivLoc = splitPane.getMinimumDividerLocation();
                if (CARD_LABELS.equals(currentCard) && currDivLoc > minDivLoc) { //If the animation is stopped:
                    flip(); //Show the image label.
                    spat.play(minDivLoc, -speed, Math::max, ignore -> flip()); //Start the animation to the left.
                }
            }
            
            //Called when we need to animate fully right motion (ie until reaching right side):
            public void goRight() {
                final JSplitPane splitPane = spat.getSplitPane();
                final int currDivLoc = splitPane.getDividerLocation(),
                          maxDivLoc = splitPane.getMaximumDividerLocation();
                if (CARD_LABELS.equals(currentCard) && currDivLoc < maxDivLoc) { //If the animation is stopped:
                    flip(); //Show the image label.
                    spat.play(maxDivLoc, speed, Math::min, ignore -> flip()); //Start the animation to the right.
                }
            }
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> {
                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
    
                JPanel leftPanel = new JPanel(new BorderLayout());
    
                leftPanel.setBorder(BorderFactory.createLineBorder(Color.green));
    
                int rows, cols;
                
                rows = cols = 60;
    
                JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
                
                final RightPanel rightPanel = new RightPanel(splitPane, 10, 3, rows, cols);
                
                splitPane.setLeftComponent(leftPanel);
                splitPane.setRightComponent(rightPanel);
                
                JButton left = new JButton("Go left"),
                        right = new JButton("Go right");
                
                left.addActionListener(e -> rightPanel.goLeft());
                right.addActionListener(e -> rightPanel.goRight());
                
                final JPanel buttons = new JPanel(new GridLayout(1, 0));
                buttons.add(left);
                buttons.add(right);
                
                frame.add(splitPane, BorderLayout.CENTER);
                frame.add(buttons, BorderLayout.PAGE_START);
                frame.setSize(1000, 800);
                frame.setMaximumSize(frame.getSize());
                frame.setLocationByPlatform(true);
                frame.setVisible(true);
                
                splitPane.setDividerLocation(0.5);
            });
        }
    }
    

    【讨论】:

    • 在动画发生时与右侧面板交互不是强制性的。另一方面,(我已经在@camickr 的回答评论部分提到了它),我认为动画这样的东西的全部意义在于让 UI 更花哨。 “冻结”右侧面板的布局实现了相反的效果。 1+,因为我可能会在其他地方使用这个想法。另外,我不认为我会想到整个图像的事情......
    • @GeorgeZ。谢谢。你考虑过定制绘画吗?我的意思是喜欢自己通过覆盖paintComponent 来绘制标签的文本?在paintComponent 内部,您可能可以将Component 的宽度(减去所有列文本的宽度)除以列数,以查看它们之间必须有多少间隙。不过,这又取决于内容的动态程度。
    • @GeorgeZ.,回复:建议 3。我确实实现了用包含组件的 BufferedImage 的 JLabel 替换拆分面板右侧的组件的建议。 Jjust编辑了我的答案以显示尝试。我认为动画是一样的,但结果更糟,因为你仍然看不到组件的动画,但你确实看到了右边的边框。在 Swing 中做任何事情时,我从未见过完全流畅的动画。有时 repaint() 机制会陷入困境。
    【解决方案2】:

    我知道 60*60 对你来说可能听起来很疯狂,但在实际应用中(我的应用程序有 ~1500),因为它是有道理的,所以绘画更重。

    每次更改分隔线位置时都会调用布局管理器,这会增加很多开销。

    一种解决方案可能是在分隔线动画时停止调用布局管理器。这可以通过覆盖右侧面板的doLayout() 方法来完成:

        import java.awt.*;
        import java.awt.event.*;
        import javax.swing.*;
        
        public class SplitPaneTest2 {
        
            public static boolean doLayout = true;
        
            public static void main(String[] args) {
                SwingUtilities.invokeLater(() -> {
                    JFrame frame = new JFrame();
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.setLayout(new BorderLayout());
        
                    JPanel leftPanel = new JPanel(new BorderLayout());
        
                    leftPanel.setBorder(BorderFactory.createLineBorder(Color.green));
        
                    JPanel rightPanel = new JPanel(new GridLayout(60, 60))
                    {
                        @Override
                        public void doLayout()
                        {
                            if (SplitPaneTest2.doLayout)
                                super.doLayout();
                        }
                    };
                    for (int i = 0; i < 60 * 60; i++) {
                      rightPanel.add(new JLabel("s"));
                    }
                    rightPanel.setBorder(BorderFactory.createLineBorder(Color.red));
        
                    JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
                    frame.add(splitPane);
        
                    JButton button = new JButton("Press me to hide");
                    button.addActionListener(e -> hideWithAnimation(splitPane));
                    leftPanel.add(button, BorderLayout.PAGE_START);
        
                    frame.setMaximumSize(new Dimension(800, 800));
                    frame.setSize(800, 800);
                    frame.setLocationByPlatform(true);
                    frame.setVisible(true);
                });
            }
        
            private static void hideWithAnimation(JSplitPane splitPane) {
                SplitPaneTest2.doLayout = false;
                final Timer timer = new Timer(10, null);
                timer.addActionListener(e -> {
                    splitPane.setDividerLocation(Math.max(0, splitPane.getDividerLocation() - 3));
                    if (splitPane.getDividerLocation() == 0)
                    {
                        timer.stop();
                        SplitPaneTest2.doLayout = true;
                        splitPane.getRightComponent().revalidate();
                    }
                });
                timer.start();
            }
        
        }
    

    编辑:

    我不打算将我的测试包括用一个使用组件图像的面板换掉充满组件的面板,因为我觉得动画是一样的,但是因为它是由其他人建议的,所以这是我的尝试您的评价:

    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import java.awt.image.*;
    
    public class SplitPaneTest2 {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> {
                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
    
                JPanel leftPanel = new JPanel(new BorderLayout());
    
                leftPanel.setBorder(BorderFactory.createLineBorder(Color.green));
    
                JPanel rightPanel = new JPanel(new GridLayout(60, 60));
                for (int i = 0; i < 60 * 60; i++) {
                  rightPanel.add(new JLabel("s"));
                }
                rightPanel.setBorder(BorderFactory.createLineBorder(Color.red));
    
                JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
                frame.add(splitPane);
    
                JButton button = new JButton("Press me to hide");
                button.addActionListener(e -> hideWithAnimation(splitPane));
                leftPanel.add(button, BorderLayout.PAGE_START);
    
                frame.setMaximumSize(new Dimension(800, 800));
                frame.setSize(800, 800);
                frame.setLocationByPlatform(true);
                frame.setVisible(true);
            });
        }
    
        private static void hideWithAnimation(JSplitPane splitPane) {
    
            Component right = splitPane.getRightComponent();
            Dimension size = right.getSize();
    
            BufferedImage bi = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g = bi.createGraphics();
            right.paint( g );
            g.dispose();
    
            JLabel label = new JLabel( new ImageIcon( bi ) );
            label.setHorizontalAlignment(JLabel.LEFT);
    
            splitPane.setRightComponent( label );
            splitPane.setDividerLocation( splitPane.getDividerLocation() );
    
            final Timer timer = new Timer(10, null);
            timer.addActionListener(e -> {
    
                splitPane.setDividerLocation(Math.max(0, splitPane.getDividerLocation() - 3));
                if (splitPane.getDividerLocation() == 0)
                {
                    timer.stop();
                    splitPane.setRightComponent( right );
                }
            });
            timer.start();
        }
    
    }
    

    【讨论】:

    • 它没有用。我不知道它是否有任何改进,但对我来说是一样的。我也想过类似的事情(以某种方式阻止绘画),但不会破坏整个“动画创意”吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-18
    • 2012-02-27
    • 2011-09-04
    • 1970-01-01
    相关资源
    最近更新 更多