Сделайте анимацию быстрее, когда есть тысячи компонентов

Я пытаюсь скрыть 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 находится JPanel с CardLayout, и каждая карта имеет множество компонентов.

Если вы раскомментируете эту строку, чтобы смоделировать количество компонентов:

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

и повторно запустите приведенный выше пример, вы увидите, что анимация больше не работает плавно. Итак, вопрос в том, можно ли сделать его гладким(-ier)?

Я понятия не имею, как подойти к решению - если оно существует.

Основываясь на своих исследованиях, я зарегистрировал глобальный ComponentListener:

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

и увидел тонны событий, которые увольняются. Итак, я думаю, что источником проблемы является множество событий компонентов, которые запускаются для каждого компонента. Кроме того, кажется, что компоненты с пользовательскими средствами визуализации (такими как JList - ListCellRenderer и JTable - TableCellRenderer) вызывают события компонентов для всех средств визуализации. Например, если JList имеет 30 элементов, 30 событий (компонентов) будут запущены только для него. Также кажется (и именно поэтому я упомянул об этом), что для CardLayout события происходят и для невидимых компонентов.

Я знаю, что 60*60 может показаться вам сумасшедшим, но в реальном приложении (у меня ~ 1500), как и следовало ожидать, рисование тяжелее.


person George Z.    schedule 29.07.2020    source источник


Ответы (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();
    }

}
person camickr    schedule 29.07.2020
comment
Это не работает. Я не знаю, есть ли какие-либо улучшения, но для меня это то же самое. Я тоже думал о чем-то подобном (каким-то образом блокируя рисование), но не испортит ли это всю идею анимации? - person George Z.; 29.07.2020

@ДжорджЗ. Я думаю, что концепция, представленная @camickr, связана с тем, когда вы на самом деле делаете макет. В качестве альтернативы переопределению doLayout я бы предложил создать подкласс GridLayout, чтобы размещать компоненты только в конце анимации (без переопределения doLayout). Но это та же концепция, что и у camickr.

Хотя если содержимое ваших компонентов на правой панели (т. е. текст меток) остается неизменным во время анимации разделителя, вы также можете создать Image правой панели, когда пользователь нажимает кнопку кнопку и отображать ее вместо фактической панели. Я полагаю, что это решение включает в себя:

  1. CardLayout для правой панели. Одна карта имеет фактическое содержимое rightPanel (т.е. JLabels). Вторая карта имеет только один JLabel, который будет загружен с Image (как ImageIcon) первой карты.
  2. Насколько я знаю, глядя на реализацию CardLayout, границы всех дочерних компонентов Container устанавливаются во время метода layoutContainer. Это, вероятно, означало бы, что метки будут размещены, несмотря на то, что они невидимы, пока будет отображаться вторая карта. Поэтому вам, вероятно, следует объединить это с подклассом GridLayout, чтобы выкладывать только в конце анимации.
  3. Чтобы нарисовать Image первой карты, нужно сначала создать BufferedImage, затем createGraphics на ней, затем вызвать rightPanel.paint на созданном объекте Graphics2D и, наконец, после этого избавиться от объекта Graphics2D.
  4. Создайте вторую карту так, чтобы JLabel был в ее центре. Для этого вам просто нужно предоставить вторую карту с GridBagLayout и добавить в нее только один Component (JLabel), который должен быть единственным. GridBagLayout всегда центрирует содержимое.

Дайте мне знать, если такое решение может быть полезно для вас. Это может быть бесполезно, потому что вы, возможно, захотите увидеть, как метки изменяют свой профиль макета во время анимации, или вы даже можете захотеть, чтобы пользователь мог взаимодействовать с Components из rightPanel во время анимации. прогресс. В обоих случаях недостаточно сфотографировать rightPanel и отобразить его вместо настоящих меток во время анимации. Так что в данном случае это действительно зависит от того, насколько динамичным будет содержимое файла rightPanel. Пожалуйста, дайте мне знать в комментариях.

Если содержимое всегда одинаково для каждого запуска программы, то вы, вероятно, могли бы предварительно создать этот Image и сохранить его. Или даже множество Image и хранить их и просто отображать их один за другим, когда включается анимация.

Точно так же, если содержимое не всегда одинаково для каждого запуска программы, вы также можете создать подкласс 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);
        });
    }
}
person gthanop    schedule 29.07.2020
comment
Взаимодействие с правой панелью во время анимации не обязательно. С другой стороны (я уже упоминал об этом в разделе комментариев ответа @camickr), я думаю, что весь смысл анимации таких вещей в том, чтобы сделать пользовательский интерфейс более привлекательным. Замораживание макета правой панели приводит к противоположному результату. 1+, потому что я мог бы использовать эту идею где-нибудь еще. Кроме того, я не думаю, что когда-либо думал об образе целиком... - person George Z.; 29.07.2020
comment
@ДжорджЗ. Спасибо. Вы не рассматривали возможность покраски на заказ? Я имею в виду, как рисовать текст меток самостоятельно, переопределяя paintComponent? Внутри paintComponent вы, вероятно, могли бы разделить ширину Component (за вычетом ширины текста всех столбцов) на количество столбцов, чтобы увидеть, какой промежуток должен пройти между ними. Это опять же зависит от того, насколько динамичным является контент. - person gthanop; 29.07.2020
comment
@GeorgeZ., Re: предложение 3. Я реализовал это предложение о замене компонента в правой части разделенной панели на JLabel, содержащий BufferedImage компонентов. Просто отредактировал мой ответ, чтобы показать попытку. Я думаю, что анимация та же, но результат хуже, так как вы по-прежнему не видите анимацию компонентов, но видите границу справа. Я никогда не видел полностью плавной анимации при выполнении чего-либо в Swing. Иногда механизм repaint() просто зависает. - person camickr; 30.07.2020