简体   繁体   English

当有数千个组件时,使 animation 更快

[英]Make the animation faster when there are thousands of components

I am trying to hide a JSplitPane with animation. By hide, I mean to setDividerLocation(0) so its left component is invisible (technically it is visible, but with zero width):我试图用 animation 隐藏一个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();
    }

}

If you run it, will see that everything seems good, and the animation runs smooth.如果你运行它,会发现一切正常,animation 运行顺利。

However, in the real application the right of the JSplitPane is a JPanel with CardLayout and each card has a lot of components.然而在实际应用中, JSplitPane右侧是一个带有CardLayoutJPanel ,每张卡片都有很多组件。

If you uncomment this line in order to simulate the number of components:如果您取消注释此行以模拟组件数量:

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

and re-run the above example, you will see that the animation no longer runs smoothly.重新运行上面的例子,你会看到animation不再流畅运行。 So, the question is, is is possible to make it smooth(-ier)?所以,问题是,是否有可能让它变得平滑(-ier)?

I have no idea how to approach a solution - if any exists.我不知道如何找到解决方案——如果有的话。

Based on my research, I registered a global ComponentListener :根据我的研究,我注册了一个全局ComponentListener

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

and saw the tons of events that are being fired.并看到了被解雇的大量事件。 So, I think the source of the problem is the tons of component events that are being fired for each component.所以,我认为问题的根源是为每个组件触发的大量组件事件。 Also, it seems that components with custom renderers (like JList - ListCellRenderer and JTable - TableCellRenderer ), component events are firing for all of the renderers.此外,似乎具有自定义渲染器的组件(如JList - ListCellRendererJTable - TableCellRenderer ),组件事件正在为所有渲染器触发。 For example, if a JList has 30 elements, 30 events (component) will be fired only for it.例如,如果JList有 30 个元素,则将仅为它触发 30 个事件(组件)。 It also seems (and that's why I mentioned it) that for CardLayout, events are taking place for the "invisible" components as well.似乎(这就是我提到它的原因)对于 CardLayout,“不可见”组件也会发生事件。

I know that 60*60 might sound crazy to you, but in a real application (mine has ~1500) as it makes sense, the painting is heavier.我知道60*60对你来说可能听起来很疯狂,但在实际应用中(我的有 ~1500)因为它有意义,绘画更重。

I know that 60*60 might sound crazy to you, but in a real application (mine has ~1500) as it makes sense, the painting is heavier.我知道 60*60 对你来说可能听起来很疯狂,但在实际应用中(我的有 ~1500)因为它有意义,绘画更重。

The layout manager is invoked every time the divider location is changed which would add a lot of overhead.每次更改分隔符位置时都会调用布局管理器,这会增加很多开销。

One solution might be to stop invoking the layout manager as the divider is animating.一种解决方案可能是在分隔线动画时停止调用布局管理器。 This can be done by overriding the doLayout() method of the right panel:这可以通过覆盖右侧面板的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();
        }
    
    }

Edit:编辑:

I was not going to include my test on swapping out the panel full of components with a panel that uses an image of components since I fell the animation is the same, but since it was suggested by someone else here is my attempt for your evaluation:我不打算包括我的测试,用一个使用组件图像的面板交换充满组件的面板,因为我发现 animation 是相同的,但由于其他人建议这是我对你的评估的尝试:

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

}

@GeorgeZ. @乔治兹。 I think the concept presented by @camickr has to do with when you actually do the layout .我认为 @camickr 提出的概念与您实际进行布局的时间有关 As an alternative to overriding doLayout , I would suggest subclassing the GridLayout to only lay out the components at the end of the animation (without overriding doLayout ).作为覆盖doLayout的替代方法,我建议子类化GridLayout以仅在 animation 的末尾布置组件(不覆盖doLayout )。 But this is the same concept as camickr 's.但这与camickr的概念相同。

Although if the contents of your components in the right panel (ie the text of the labels) remain unchanged during the animation of the divider, you can also create an Image of the right panel when the user clicks the button and display that instead of the actual panel.尽管如果右侧面板中组件的内容(即标签的文本)在分隔符的 animation 期间保持不变,您也可以在用户单击按钮时创建右侧面板的Image并显示它而不是实际面板。 This solution, I would imagine, involves:这个解决方案,我想,涉及:

  1. A CardLayout for the right panel.右侧面板的CardLayout One card has the actual rightPanel contents (ie the JLabel s).一张卡片具有实际的rightPanel内容(即JLabel s)。 The second card has only one JLabel which will be loaded with the Image (as an ImageIcon ) of the first card.第二张卡片只有一个JLabel ,它将加载第一张卡片的Image (作为ImageIcon )。
  2. As far as I know, by looking at the CardLayout 's implementation, the bounds of all the child components of the Container are set during layoutContainer method.据我所知,通过查看CardLayout的实现, Container所有子组件的边界都是在layoutContainer方法期间设置的。 That would probably mean that the labels would be layed out inspite being invisible while the second card would be shown.这可能意味着在显示第二张卡片时,尽管标签是不可见的,但标签仍会被布置。 So you should probably combine this with the subclassed GridLayout to lay out only at the end of the animation.因此,您可能应该将其与子类GridLayout结合使用,以便仅在 animation 的末尾进行布局。
  3. To draw the Image of the first card, one should first create a BufferedImage , then createGraphics on it, then call rightPanel.paint on the created Graphics2D object and finally dispose the Graphics2D object after that.要绘制第一张卡片的Image ,首先应该创建一个BufferedImage ,然后在其上创建图形,然后在创建的Graphics2D createGraphics上调用rightPanel.paint ,最后在之后处理Graphics2D object 。
  4. Create the second card such that the JLabel would be centered in it.创建第二张卡片,使JLabel居中。 To do this, you just have to provide the second card with a GridBagLayout and add only one Component in it (the JLabel ) which should be the only.为此,您只需为第二张卡片提供GridBagLayout并在其中仅添加一个ComponentJLabel ),这应该是唯一的。 GridBagLayout always centers the contents. GridBagLayout始终将内容居中。

Let me know if such a solution could be useful for you.如果这样的解决方案对您有用,请告诉我。 It might not be useful because you could maybe want to actually see the labels change their lay out profile while the animation is in progress, or you may even want the user to be able to interact with the Component s of the rightPanel while the animation is in progress.它可能没有用,因为您可能希望在 animation 进行时实际看到标签更改其布局配置文件,或者您甚至可能希望用户能够在 animation 进行时与rightPanelComponent进行交互进行中。 In both cases, taking a picture of the rightPanel and displaying it instead of the real labels while the animation takes place, should not suffice.在这两种情况下,在 animation 发生时拍摄rightPanel的照片并显示它而不是真正的标签,应该是不够的。 So it really depends, in this case, on how dynamic will be the content of the rightPanel .因此,在这种情况下,这实际上取决于rightPanel的内容的动态程度。 Please let me know in the comments.请在评论中让我知道。

If the contents are always the same for every program run, then you could probably pre-create that Image and store it.如果每个程序运行的内容始终相同,那么您可能可以预先创建该Image并存储它。 Or even, a multitude of Image s and store them and just display them one after another when the animation turns on.甚至,大量的Image并存储它们,并在 animation 打开时一个接一个地显示它们。

Similarly, if the contents are not always the same for every program run, then you could also subclass GridLayout and precalculate the bounds of each component at startup.同样,如果每次程序运行的内容并不总是相同,那么您也可以将GridLayout子类化并在启动时预先计算每个组件的边界。 Then that would make GridLayout a bit faster in laying out the components (it would be like encoding a video with the location of each object), but as I am testing it, GridLayout is already fast: it just calculates about 10 variables at the start of laying out, and then imediately passes over to setting the bounds of each Component .然后这将使GridLayout在布局组件时更快一点(这就像用每个对象的位置编码视频),但在我测试它时, GridLayout已经很快了:它只在开始时计算大约 10 个变量布局,然后立即转到设置每个Component的边界。

Edit 1:编辑 1:

And here is my attempt of my idea (with the Image ):这是我对我的想法的尝试(使用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);
        });
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM