From d079741f9485512d7f9945814c6eca63c5e76207 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 9 Sep 2025 18:54:11 +0200 Subject: [PATCH] Extras: FlatAnimatedLafChange: made transition smoother: - use a single component in layered pane to paint new and old UI snapshots (previously used two components) - the snapshot layer component is now opaque, which avoids that window component hierarchy is involved when painting snapshots - snapshots are now painted immediately, which should result in a smoother transition - changed animation resolution from 30ms to 16ms --- CHANGELOG.md | 1 + .../flatlaf/extras/FlatAnimatedLafChange.java | 144 ++++++++++-------- 2 files changed, 84 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86cb7812..970a1f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ FlatLaf Change Log - If using `FlatLaf.registerCustomDefaultsSource( "com.myapp.themes" )` and named Java modules, it is no longer necessary to add `opens com.myapp.themes;` to `module-info.java`. (issue #1026) +- Extras: Made animated theme change (class `FlatAnimatedLafChange`) smoother. #### Fixed bugs diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java index a7160d88..f2ab0686 100644 --- a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java +++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java @@ -53,15 +53,13 @@ public class FlatAnimatedLafChange public static int duration = 160; /** - * The resolution of the animation in milliseconds. Default is 30 ms. + * The resolution of the animation in milliseconds. Default is 16 ms. */ - public static int resolution = 30; + public static int resolution = 16; private static Animator animator; - private static final Map oldUIsnapshots = new WeakHashMap<>(); - private static final Map newUIsnapshots = new WeakHashMap<>(); + private static final Map snapshots = new WeakHashMap<>(); private static float alpha; - private static boolean inShowSnapshot; /** * Create a snapshot of the old UI and shows it on top of the UI. @@ -78,63 +76,52 @@ public class FlatAnimatedLafChange alpha = 1; // show snapshot of old UI - showSnapshot( true, oldUIsnapshots ); + showSnapshot( true ); } - private static void showSnapshot( boolean old, Map map ) { - inShowSnapshot = true; - + private static void showSnapshot( boolean old ) { // create snapshots for all shown windows Window[] windows = Window.getWindows(); for( Window window : windows ) { if( !(window instanceof RootPaneContainer) || !window.isShowing() ) continue; + JLayeredPane layeredPane = ((RootPaneContainer)window).getLayeredPane(); + // create snapshot image // (using volatile image to have correct sub-pixel text rendering on Java 9+) - VolatileImage snapshot = window.createVolatileImage( window.getWidth(), window.getHeight() ); - if( snapshot == null ) + VolatileImage snapshotImage = layeredPane.createVolatileImage( layeredPane.getWidth(), layeredPane.getHeight() ); + if( snapshotImage == null ) continue; // paint window to snapshot image - JLayeredPane layeredPane = ((RootPaneContainer)window).getLayeredPane(); - layeredPane.paint( snapshot.getGraphics() ); + layeredPane.paint( snapshotImage.getGraphics() ); - // create snapshot layer, which is added to layered pane and paints - // snapshot with animated alpha - JComponent snapshotLayer = new JComponent() { - @Override - public void paint( Graphics g ) { - if( inShowSnapshot || snapshot.contentsLost() ) - return; - - if( old ) - ((Graphics2D)g).setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, alpha ) ); - g.drawImage( snapshot, 0, 0, null ); - } - - @Override - public void removeNotify() { - super.removeNotify(); - - // release system resources used by volatile image - snapshot.flush(); - } - }; - if( !old ) + if( old ) { + // create snapshot layer, which is added to layered pane and paints + // snapshot with animated alpha + SnapshotLayer snapshotLayer = new SnapshotLayer(); snapshotLayer.setOpaque( true ); - snapshotLayer.setSize( layeredPane.getSize() ); + snapshotLayer.setSize( layeredPane.getSize() ); + snapshotLayer.oldSnapshotImage = snapshotImage; - // add image layer to layered pane - layeredPane.add( snapshotLayer, Integer.valueOf( JLayeredPane.DRAG_LAYER + (old ? 2 : 1) ) ); - map.put( layeredPane, snapshotLayer ); + snapshots.put( layeredPane, snapshotLayer ); + } else { + SnapshotLayer snapshotLayer = snapshots.get( layeredPane ); + if( snapshotLayer == null ) { + snapshotImage.flush(); + continue; + } - // let FlatRootPaneUI know that animated Laf change is in progress - if( old ) + snapshotLayer.newSnapshotImage = snapshotImage; + + // add snapshot layer to layered pane + layeredPane.add( snapshotLayer, Integer.valueOf( JLayeredPane.DRAG_LAYER + 1 ) ); + + // let FlatRootPaneUI know that animated Laf change is in progress layeredPane.getRootPane().putClientProperty( "FlatLaf.internal.animatedLafChange", true ); + } } - - inShowSnapshot = false; } /** @@ -146,23 +133,22 @@ public class FlatAnimatedLafChange if( !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) ) return; - if( oldUIsnapshots.isEmpty() ) + if( snapshots.isEmpty() ) return; // show snapshot of new UI - showSnapshot( false, newUIsnapshots ); + showSnapshot( false ); // create animator animator = new Animator( duration, fraction -> { - if( fraction < 0.1 || fraction > 0.9 ) - return; // ignore initial and last events - alpha = 1f - fraction; // repaint snapshots - for( Map.Entry e : oldUIsnapshots.entrySet() ) { - if( e.getKey().isShowing() ) - e.getValue().repaint(); + for( Map.Entry e : snapshots.entrySet() ) { + if( e.getKey().isShowing() ) { + SnapshotLayer snapshotLayer = e.getValue(); + snapshotLayer.paintImmediately( 0, 0, snapshotLayer.getWidth(),snapshotLayer.getHeight() ); + } } Toolkit.getDefaultToolkit().sync(); @@ -176,18 +162,18 @@ public class FlatAnimatedLafChange } private static void hideSnapshot() { - hideSnapshot( oldUIsnapshots ); - hideSnapshot( newUIsnapshots ); - } - - private static void hideSnapshot( Map map ) { // remove snapshots - for( Map.Entry e : map.entrySet() ) { - e.getKey().remove( e.getValue() ); - e.getKey().repaint(); + for( Map.Entry e : snapshots.entrySet() ) { + JLayeredPane layeredPane = e.getKey(); + SnapshotLayer snapshotLayer = e.getValue(); + + layeredPane.remove( snapshotLayer ); + layeredPane.repaint(); + + snapshotLayer.flushSnapshotImages(); // run Runnable that FlatRootPaneUI put into client properties - JRootPane rootPane = e.getKey().getRootPane(); + JRootPane rootPane = layeredPane.getRootPane(); rootPane.putClientProperty( "FlatLaf.internal.animatedLafChange", null ); Runnable r = (Runnable) rootPane.getClientProperty( "FlatLaf.internal.animatedLafChange.runWhenFinished" ); if( r != null ) { @@ -196,7 +182,7 @@ public class FlatAnimatedLafChange } } - map.clear(); + snapshots.clear(); } /** @@ -208,4 +194,40 @@ public class FlatAnimatedLafChange else hideSnapshot(); } + + //---- class SnapshotLayer ------------------------------------------------ + + private static class SnapshotLayer + extends JComponent + { + VolatileImage oldSnapshotImage; + VolatileImage newSnapshotImage; + + @Override + public void paint( Graphics g ) { + if( oldSnapshotImage.contentsLost() || + newSnapshotImage == null || newSnapshotImage.contentsLost() ) + return; + + // draw new UI snapshot + g.drawImage( newSnapshotImage, 0, 0, null ); + + // draw old UI snapshot + ((Graphics2D)g).setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, alpha ) ); + g.drawImage( oldSnapshotImage, 0, 0, null ); + } + + @Override + public void removeNotify() { + super.removeNotify(); + flushSnapshotImages(); + } + + void flushSnapshotImages() { + // release system resources used by volatile image + oldSnapshotImage.flush(); + if( newSnapshotImage != null ) + newSnapshotImage.flush(); + } + } }