[英]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
右侧是一个带有CardLayout
的JPanel
,每张卡片都有很多组件。
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
- ListCellRenderer
和JTable
- 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:这个解决方案,我想,涉及:
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
)。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.GridLayout
to lay out only at the end of the animation.GridLayout
结合使用,以便仅在 animation 的末尾进行布局。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 。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
并在其中仅添加一个Component
( JLabel
),这应该是唯一的。 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 进行时与
rightPanel
的Component
进行交互进行中。 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
的边界。
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.