Я использую графику с двойной буферизацией в своем подклассе JLayer для реализации простой анимации смахивания в приложении Java Swing. Он отлично работает на старых дисплеях, но когда я запускаю его на дисплее Retina, экран теряет удвоенное разрешение при запуске анимации и возвращает его по окончании. Я не уверен, как поддерживать более высокое разрешение во время анимации.
Мой анимационный метод изначально выглядел так:
private void animate() {
Timer timer = new Timer(frameMillis, null);
final ActionListener actionListener = (evt) -> { /* omitted for brevity */ };
timer.addActionListener(actionListener);
int imageType = BufferedImage.TYPE_INT_ARGB;
upcomingScreen = new BufferedImage(liveComponent.getWidth(), liveComponent.getHeight(), imageType);
Graphics2D graphics2D = (Graphics2D) upcomingScreen.getGraphics();
liveComponent.paint(graphics2D); // liveComponent is a JComponent
graphics2D.dispose();
timer.start();
}
Пробовал удвоить размер изображения, не помогло.
upcomingScreen = new BufferedImage(liveComponent.getWidth()*2, liveComponent.getHeight()*2, imageType);
Чтобы отразить эти изменения, я изменил свой код рисования в LayerUI, удвоив xLimit
, width
, height
:
public void paint(final Graphics g, final JComponent c) {
if (isAnimating) {
int xLimit = (c.getWidth()*2 * frame) / maxFrames;
int width = c.getWidth()*2;
int height = c.getHeight()*2;
g.drawImage(uScreen, 0, 0, xLimit, height, 0, 0, xLimit, height, c);
g.drawImage(pScreen, xLimit, 0, width, height, xLimit, 0, width, height, c);
} else {
super.paint(g, c);
}
}
Это не помогает. Он рисует то же самое с этим последним изменением или без него, что не имеет смысла.
Вот класс, который иллюстрирует проблему:
/**
* <p>Created by IntelliJ IDEA.
* <p>Date: 5/2/20
* <p>Time: 10:25 AM
*
* @author Miguel Mu\u00f1oz
*/
@SuppressWarnings({"HardcodedLineSeparator", "StringConcatenation", "HardCodedStringLiteral", "DuplicatedCode"})
public final class SwipeViewTest extends JPanel {
public static final String text1 = "Demo of Swipe View.\n\nThe swipe button will toggle between two pages of text. It has a built-in " +
"special effect, which is a swipe. When you hit the swipe button, it should flip between two pages of text. This worked fine on " +
"the older displays, but for some reason, on a Retina display, the text briefly switches to low resolution as the swipe proceeds, " +
"then switches back once it has finished. This code is written for retina displays. I don't know if it will work for the older, " +
"low resolution displays.\n\nYou can watch it swipe by hitting the space bar or by clicking the swipe button.";
public static final String text2 = "Demo of Swipe View.\n\nThis is the second page of the swipe-text demo. The change in resolution is " +
"most easily noticed when watching the line at the top, which doesn't change as the swipe is performed.";
private final SwipeView<TestView> swipeView;
private final TestView testView;
public static void main(String[] args) {
JFrame frame = new JFrame("SwipeView demo");
SwipeViewTest comp = new SwipeViewTest();
comp.install();
frame.add(comp);
frame.setLocationByPlatform(true);
frame.pack();
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
private boolean page1 = true;
private SwipeViewTest() {
super(new BorderLayout());
testView = new TestView();
swipeView = SwipeView.wrap(testView, 1000);
add(BorderLayout.CENTER, swipeView.getLayer());
}
private void install() {
JButton jButton = new JButton("Swipe");
jButton.addActionListener(this::doSwipe);
add(jButton, BorderLayout.PAGE_END);
AncestorListener ancestorListener = new AncestorListener() {
@Override
public void ancestorAdded(final AncestorEvent event) {
JComponent button = event.getComponent();
button.requestFocus();
button.removeAncestorListener(this); // execute only once.
}
@Override public void ancestorRemoved(final AncestorEvent event) { }
@Override public void ancestorMoved(final AncestorEvent event) { }
};
jButton.addAncestorListener(ancestorListener);
}
private void doSwipe(ActionEvent ignored) {
swipeView.swipeLeft(this::flipPage);
}
private void flipPage() {
page1 = !page1;
if (page1) {
testView.setText(text1);
} else {
testView.setText(text2);
}
}
private static class TestView extends JPanel {
private final JTextArea textArea;
TestView() {
super(new BorderLayout());
textArea = new JTextArea(20, 40);
JScrollPane scrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.setEditable(false);
textArea.setText(text1);
add(scrollPane, BorderLayout.CENTER);
}
private void setText(String text) {
textArea.setText(text);
}
}
/**
* SwipeView adds a swipe special effect to a Component. This draws a swipe-right or swipe-left effect on a chosen
* action. It also optionally supports a repeated action when the mouse is held down.
* <p>
* This class is very specific right now, but I hope to generalize it for other special effects later.
* <p>Created by IntelliJ IDEA.
* <p>Date: 4/4/18
* <p>Time: 12:38 AM
*
* @author Miguel Mu\u00f1oz
*/
@SuppressWarnings("MagicNumber")
public static final class SwipeView<C extends JComponent> extends LayerUI<C> {
public static <J extends JComponent> SwipeView<J> wrap(J view, int durationMillis) {
JLayer<J> jLayer = new JLayer<>(view);
final SwipeView<J> ui = new SwipeView<>(view, jLayer, durationMillis);
jLayer.setUI(ui);
return ui;
}
private final C liveComponent;
private Image priorScreen = null;
private Image upcomingScreen = null;
private final JLayer<C> layer;
private boolean isAnimating = false;
private SwipeDirection swipeDirection = SwipeDirection.SWIPE_RIGHT;
private final int maxFrames;
// Calculated:
@SuppressWarnings("FieldCanBeLocal")
private final int frameMillis;
private int frame = 0;
private final long startTime = System.currentTimeMillis();
private SwipeView(C view, JLayer<C> theLayer, int animationDurationMillis) {
super();
liveComponent = view;
layer = theLayer;
maxFrames = (30 * animationDurationMillis) / 1000;
frameMillis = animationDurationMillis / maxFrames;
}
public JLayer<C> getLayer() { return layer; }
/**
* Perform the specified operation with a swipe-right special effect. This is often used in an ActionListener:
* <pre>
* first.addActionListener((e) -> swipeView.swipeRight(recordModel::goFirst));
* </pre>
* Here, the Action listener will perform a Swipe-right after executing the goFirst() method of recordModel.
*
* @param operation The operation
*/
@SuppressWarnings("WeakerAccess")
public void swipeRight(Runnable operation) {
swipe(operation, SwipeDirection.SWIPE_RIGHT);
}
/**
* Perform the specified operation with a swipe-left special effect. This is often used in an ActionListener:
* <pre>
* first.addActionListener((e) -> swipeView.swipeLeft(recordModel::goFirst));
* </pre>
* Here, the Action listener will perform a Swipe-Left after executing the goFirst() method of recordModel.
*
* @param operation The operation
*/
@SuppressWarnings("WeakerAccess")
public void swipeLeft(Runnable operation) {
swipe(operation, SwipeDirection.SWIPE_LEFT);
}
private void swipe(Runnable operation, SwipeDirection swipeDirection) {
prepareToAnimate(swipeDirection);
operation.run();
animate();
}
// @SuppressWarnings({"HardCodedStringLiteral", "HardcodedFileSeparator"})
@Override
public void paint(final Graphics g, final JComponent c) {
if (isAnimating) {
int xLimit = (c.getWidth() * 2 * frame) / maxFrames;
if (swipeDirection == SwipeDirection.SWIPE_LEFT) {
xLimit = (c.getWidth() * 2) - xLimit;
}
int width = c.getWidth() * 2;
int height = c.getHeight() * 2;
// //noinspection UseOfSystemOutOrSystemErr
// System.out.printf("Dimensions: Frame: %d/%d (at %d) xLimit: %4d (%4d x %4d) (from %4d x %4d) Animating: %b%n",
// frame, maxFrames, System.currentTimeMillis() - startTime, xLimit, width, height, c.getWidth(), c.getHeight(), isAnimating);
assert upcomingScreen != null;
assert priorScreen != null;
Image pScreen = Objects.requireNonNull(priorScreen);
Image uScreen = Objects.requireNonNull(upcomingScreen);
if (swipeDirection == SwipeDirection.SWIPE_RIGHT) {
g.drawImage(uScreen, 0, 0, xLimit, height, 0, 0, xLimit, height, c);
g.drawImage(pScreen, xLimit, 0, width, height, xLimit, 0, width, height, c);
} else {
g.drawImage(uScreen, xLimit, 0, width, height, xLimit, 0, width, height, c);
g.drawImage(pScreen, 0, 0, xLimit, height, 0, 0, xLimit, height, c);
}
} else {
super.paint(g, c);
}
}
private void prepareToAnimate(SwipeDirection swipeDirection) {
this.swipeDirection = swipeDirection;
isAnimating = true;
frame = 0;
// Save current state
priorScreen = new BufferedImage(liveComponent.getWidth() * 2, liveComponent.getHeight() * 2, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = (Graphics2D) priorScreen.getGraphics();
liveComponent.paint(graphics2D);
graphics2D.dispose();
}
private void animate() {
Timer timer = new Timer(frameMillis, null);
final ActionListener actionListener = (evt) -> {
frame++;
layer.repaint();
if (frame == maxFrames) {
frame = 0;
isAnimating = false;
timer.stop(); // Investigate: Am I leaking timers?
}
};
timer.addActionListener(actionListener);
upcomingScreen = new BufferedImage(liveComponent.getWidth() * 2, liveComponent.getHeight() * 2, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = (Graphics2D) upcomingScreen.getGraphics();
liveComponent.paint(graphics2D);
graphics2D.dispose();
timer.start();
}
}
public static enum SwipeDirection {
@SuppressWarnings("JavaDoc") SWIPE_RIGHT,
@SuppressWarnings("JavaDoc") SWIPE_LEFT
}
}