diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAbstractIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAbstractIcon.java index 8448f1288..db19bcdd3 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAbstractIcon.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAbstractIcon.java @@ -69,7 +69,13 @@ public void paintIcon( Component c, Graphics g, int x, int y ) { } } - protected abstract void paintIcon( Component c, Graphics2D g2 ); + /** + * Paint the icon at {@code [0,0]} location. + *

+ * The given graphics context is scaled. + * Use unscaled coordinates, width and height for painting. + */ + protected abstract void paintIcon( Component c, Graphics2D g ); @Override public int getIconWidth() { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java index 4d80f49b0..feff6f66e 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java @@ -21,6 +21,7 @@ import java.awt.Graphics; import java.awt.Graphics2D; import com.formdev.flatlaf.util.AnimatedIcon; +import com.formdev.flatlaf.util.AnimatedPainter; /** * Base class for animated icons that scales width and height, creates and initializes @@ -30,7 +31,7 @@ *

* This class does not store any state information (needed for animation) in its instance. * Instead a client property is set on the painted component. - * This makes it possible to use a share icon instance for multiple components. + * This makes it possible to use a shared icon instance for multiple components. * * @author Karl Tauber */ @@ -45,11 +46,34 @@ public FlatAnimatedIcon( int width, int height, Color color ) { @Override public void paintIcon( Component c, Graphics g, int x, int y ) { super.paintIcon( c, g, x, y ); - AnimatedIcon.AnimationSupport.saveIconLocation( this, c, x, y ); + AnimatedPainter.saveRepaintLocation( this, c, x, y ); } @Override protected void paintIcon( Component c, Graphics2D g ) { - AnimatedIcon.AnimationSupport.paintIcon( this, c, g, 0, 0 ); + paintWithAnimation( c, g, 0, 0, getIconWidth(), getIconHeight() ); } + + /** + * Delegates painting to {@link #paintIconAnimated(Component, Graphics2D, float[])}. + * Ignores the given bounds because {@code [x,y]} are always {@code [0,0]} and + * {@code [width,height]} are scaled, but painting code should use unscaled width + * and height because given graphics context is scaled. + * + * @since 2 + */ + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + paintIconAnimated( c, g, animatedValues ); + } + + /** + * Paint the icon at {@code 0,0} location. + *

+ * The given graphics context is scaled. + * Use unscaled coordinates, width and height for painting. + * + * @since 2 + */ + protected abstract void paintIconAnimated( Component c, Graphics2D g, float[] animatedValues ); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedBorder.java new file mode 100644 index 000000000..afb7a8e22 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedBorder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import javax.swing.JComponent; +import javax.swing.border.Border; + +/** + * Border that automatically animates painting on component value changes. + *

+ * {@link #getValues(Component)} returns the value(s) of the component. + * If the value(s) have changed, then {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * is invoked multiple times with animated value(s) (from old value(s) to new value(s)). + * If {@link #getValues(Component)} returns multiple values, then each value gets its own independent animation. + *

+ * Example for an animated border: + *

+ * private class MyAnimatedBorder
+ *     implements AnimatedBorder
+ * {
+ *     @Override
+ *     public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) {
+ *         int lh = UIScale.scale( 2 );
+ *
+ *         g.setColor( Color.blue );
+ *         g.fillRect( x, y + height - lh, Math.round( width * animatedValues[0] ), lh );
+ *     }
+ *
+ *     @Override
+ *     public float[] getValues( Component c ) {
+ *         return new float[] { c.isFocusOwner() ? 1 : 0 };
+ *     }
+ *
+ *     @Override
+ *     public Insets getBorderInsets( Component c ) {
+ *         return UIScale.scale( new Insets( 4, 4, 4, 4 ) );
+ *     }
+ *
+ *     @Override public boolean isBorderOpaque() { return false; }
+ * }
+ *
+ * // sample usage
+ * JTextField textField = new JTextField();
+ * textField.setBorder( new MyAnimatedBorder() );
+ * 
+ * + * Animation works only if the component passed to {@link #paintBorder(Component, Graphics, int, int, int, int)} + * is a instance of {@link JComponent}. + * A client property is set on the component to store the animation state. + * + * @author Karl Tauber + * @since 2 + */ +public interface AnimatedBorder + extends Border, AnimatedPainter +{ + /** + * Invokes {@link #paintWithAnimation(Component, Graphics, int, int, int, int)}. + */ + @Override + default void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + paintWithAnimation( c, g, x, y, width, height ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java index da0b0a1ae..5f351bcc1 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java @@ -18,44 +18,42 @@ import java.awt.Component; import java.awt.Graphics; +import java.awt.Graphics2D; import javax.swing.Icon; import javax.swing.JComponent; -import com.formdev.flatlaf.util.Animator.Interpolator; /** * Icon that automatically animates painting on component value changes. *

- * {@link #getValue(Component)} returns the value of the component. - * If the value changes, then {@link #paintIconAnimated(Component, Graphics, int, int, float)} - * is invoked multiple times with animated value (from old value to new value). + * {@link #getValues(Component)} returns the value(s) of the component. + * If the value(s) have changed, then {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * is invoked multiple times with animated value(s) (from old value(s) to new value(s)). + * If {@link #getValues(Component)} returns multiple values, then each value gets its own independent animation. *

* Example for an animated icon: *

- * private class AnimatedMinimalTestIcon
+ * private class MyAnimatedIcon
  *     implements AnimatedIcon
  * {
  *     @Override public int getIconWidth() { return 100; }
  *     @Override public int getIconHeight() { return 20; }
  *
  *     @Override
- *     public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) {
- *         int w = getIconWidth();
- *         int h = getIconHeight();
- *
+ *     public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) {
  *         g.setColor( Color.red );
- *         g.drawRect( x, y, w - 1, h - 1 );
- *         g.fillRect( x, y, Math.round( w * animatedValue ), h );
+ *         g.drawRect( x, y, width - 1, height - 1 );
+ *         g.fillRect( x, y, Math.round( width * animatedValues[0] ), height );
  *     }
  *
  *     @Override
- *     public float getValue( Component c ) {
- *         return ((AbstractButton)c).isSelected() ? 1 : 0;
+ *     public float[] getValues( Component c ) {
+ *         return new float[] { ((AbstractButton)c).isSelected() ? 1 : 0 };
  *     }
  * }
  *
  * // sample usage
  * JCheckBox checkBox = new JCheckBox( "test" );
- * checkBox.setIcon( new AnimatedMinimalTestIcon() );
+ * checkBox.setIcon( new MyAnimatedIcon() );
  * 
* * Animation works only if the component passed to {@link #paintIcon(Component, Graphics, int, int)} @@ -65,15 +63,28 @@ * @author Karl Tauber */ public interface AnimatedIcon - extends Icon + extends Icon, AnimatedPainter { + /** + * Invokes {@link #paintWithAnimation(Component, Graphics, int, int, int, int)}. + */ + @Override + default void paintIcon( Component c, Graphics g, int x, int y ) { + paintWithAnimation( c, g, x, y, getIconWidth(), getIconHeight() ); + } + + /** + * {@inheritDoc} + * + * @since 2 + */ @Override - public default void paintIcon( Component c, Graphics g, int x, int y ) { - AnimationSupport.paintIcon( this, c, g, x, y ); + default void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + paintIconAnimated( c, g, x, y, animatedValues[0] ); } /** - * Paints the icon for the given animated value. + * Paints the icon for the given (animated) value. * * @param c the component that this icon belongs to * @param g the graphics context @@ -82,8 +93,22 @@ public default void paintIcon( Component c, Graphics g, int x, int y ) { * @param animatedValue the animated value, which is either equal to what {@link #getValue(Component)} * returned, or somewhere between the previous value and the latest value * that {@link #getValue(Component)} returned + * + * @deprecated override {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} instead + */ + @Deprecated + default void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + } + + /** + * {@inheritDoc} + * + * @since 2 */ - void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ); + @Override + default float[] getValues( Component c ) { + return new float[] { getValue( c ) }; + } /** * Gets the value of the component. @@ -92,158 +117,36 @@ public default void paintIcon( Component c, Graphics g, int x, int y ) { * If the value changes, then this class animates from the old value to the new one. *

* For a toggle button this could be {@code 0} for off and {@code 1} for on. + * + * @deprecated override {@link #getValues(Component)} instead */ - float getValue( Component c ); - - /** - * Returns whether animation is enabled for this icon (default is {@code true}). - */ - default boolean isAnimationEnabled() { - return true; - } - - /** - * Returns the duration of the animation in milliseconds (default is 150). - */ - default int getAnimationDuration() { - return 150; - } - - /** - * Returns the resolution of the animation in milliseconds (default is 10). - * Resolution is the amount of time between timing events. - */ - default int getAnimationResolution() { - return 10; - } - - /** - * Returns the interpolator for the animation. - * Default is {@link CubicBezierEasing#STANDARD_EASING}. - */ - default Interpolator getAnimationInterpolator() { - return CubicBezierEasing.STANDARD_EASING; - } - - /** - * Returns the client property key used to store the animation support. - */ - default Object getClientPropertyKey() { - return getClass(); + @Deprecated + default float getValue( Component c ) { + return 0; } //---- class AnimationSupport --------------------------------------------- /** - * Animation support class that stores the animation state and implements the animation. + * Animation support. */ + @Deprecated class AnimationSupport { - private float startValue; - private float targetValue; - private float animatedValue; - private float fraction; - - private Animator animator; - - // last x,y coordinates of the icon needed to repaint while animating - private int x; - private int y; - + /** + * @deprecated use {@link AnimatedPainter#paintWithAnimation(Component, Graphics, int, int, int, int)} instead + */ + @Deprecated public static void paintIcon( AnimatedIcon icon, Component c, Graphics g, int x, int y ) { - if( !isAnimationEnabled( icon, c ) ) { - // paint without animation if animation is disabled or - // component is not a JComponent and therefore does not support - // client properties, which are required to keep animation state - paintIconImpl( icon, c, g, x, y, null ); - return; - } - - JComponent jc = (JComponent) c; - Object key = icon.getClientPropertyKey(); - AnimationSupport as = (AnimationSupport) jc.getClientProperty( key ); - if( as == null ) { - // painted first time --> do not animate, but remember current component value - as = new AnimationSupport(); - as.startValue = as.targetValue = as.animatedValue = icon.getValue( c ); - as.x = x; - as.y = y; - jc.putClientProperty( key, as ); - } else { - // get component value - float value = icon.getValue( c ); - - if( value != as.targetValue ) { - // value changed --> (re)start animation - - if( as.animator == null ) { - // create animator - AnimationSupport as2 = as; - as.animator = new Animator( icon.getAnimationDuration(), fraction -> { - // check whether component was removed while animation is running - if( !c.isDisplayable() ) { - as2.animator.stop(); - return; - } - - // compute animated value - as2.animatedValue = as2.startValue + ((as2.targetValue - as2.startValue) * fraction); - as2.fraction = fraction; - - // repaint icon - c.repaint( as2.x, as2.y, icon.getIconWidth(), icon.getIconHeight() ); - }, () -> { - as2.startValue = as2.animatedValue = as2.targetValue; - as2.animator = null; - } ); - } - - if( as.animator.isRunning() ) { - // if animation is still running, restart it from the current - // animated value to the new target value with reduced duration - as.animator.cancel(); - int duration2 = (int) (icon.getAnimationDuration() * as.fraction); - if( duration2 > 0 ) - as.animator.setDuration( duration2 ); - as.startValue = as.animatedValue; - } else { - // new animation - as.animator.setDuration( icon.getAnimationDuration() ); - as.animator.setResolution( icon.getAnimationResolution() ); - as.animator.setInterpolator( icon.getAnimationInterpolator() ); - - as.animatedValue = as.startValue; - } - - as.targetValue = value; - as.animator.start(); - } - - as.x = x; - as.y = y; - } - - paintIconImpl( icon, c, g, x, y, as ); - } - - private static void paintIconImpl( AnimatedIcon icon, Component c, Graphics g, int x, int y, AnimationSupport as ) { - float value = (as != null) ? as.animatedValue : icon.getValue( c ); - icon.paintIconAnimated( c, g, x, y, value ); - } - - private static boolean isAnimationEnabled( AnimatedIcon icon, Component c ) { - return Animator.useAnimation() && icon.isAnimationEnabled() && c instanceof JComponent; + AnimatedPainterSupport.paint( icon, c, g, x, y, icon.getIconWidth(), icon.getIconHeight() ); } + /** + * @deprecated use {@link AnimatedPainter#saveRepaintLocation(AnimatedPainter, Component, int, int)} instead + */ + @Deprecated public static void saveIconLocation( AnimatedIcon icon, Component c, int x, int y ) { - if( !isAnimationEnabled( icon, c ) ) - return; - - AnimationSupport as = (AnimationSupport) ((JComponent)c).getClientProperty( icon.getClientPropertyKey() ); - if( as != null ) { - as.x = x; - as.y = y; - } + AnimatedPainterSupport.saveRepaintLocation( icon, c, x, y ); } } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainter.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainter.java new file mode 100644 index 000000000..0ff83de70 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainter.java @@ -0,0 +1,177 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import javax.swing.JComponent; +import com.formdev.flatlaf.util.Animator.Interpolator; + +/** + * Painter that automatically animates painting on component value(s) changes. + *

+ * {@link #getValues(Component)} returns the value(s) of the component. + * If the value(s) have changed, then {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * is invoked multiple times with animated value(s) (from old value(s) to new value(s)). + * If {@link #getValues(Component)} returns multiple values, then each value gets its own independent animation. + *

+ * See {@link AnimatedBorder} or {@link AnimatedIcon} for examples. + *

+ * Animation works only if the component passed to {@link #paintWithAnimation(Component, Graphics, int, int, int, int)} + * is a instance of {@link JComponent}. + * A client property is set on the component to store the animation state. + * + * @author Karl Tauber + * @since 2 + */ +public interface AnimatedPainter +{ + /** + * Starts painting. + * Either invokes {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * once to paint current value(s) (see {@link #getValues(Component)}. Or if value(s) has + * changed, compared to last painting, then it starts an animation and invokes + * {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * multiple times with animated value(s) (from old value(s) to new value(s)). + * + * @param c the component that this painter belongs to + * @param g the graphics context + * @param x the x coordinate of the paint area + * @param y the y coordinate of the paint area + * @param width the width of the paint area + * @param height the height of the paint area + */ + default void paintWithAnimation( Component c, Graphics g, int x, int y, int width, int height ) { + AnimatedPainterSupport.paint( this, c, g, x, y, width, height ); + } + + /** + * Paints the given (animated) value(s). + *

+ * Invoked from {@link #paintWithAnimation(Component, Graphics, int, int, int, int)}. + * + * @param c the component that this painter belongs to + * @param g the graphics context + * @param x the x coordinate of the paint area + * @param y the y coordinate of the paint area + * @param width the width of the paint area + * @param height the height of the paint area + * @param animatedValues the animated values, which are either equal to what {@link #getValues(Component)} + * returned, or somewhere between the previous values and the latest values + * that {@link #getValues(Component)} returned + */ + void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ); + + /** + * Invoked from animator to repaint an area. + *

+ * Useful to limit the repaint region. E.g. if only the bottom border is animated. + * If more than one border side is animated (e.g. bottom and right side), then it + * makes no sense to do separate repaints because the Swing repaint manager unions + * the regions and the whole component is repainted. + *

+ * The default implementation repaints the whole given area. + */ + default void repaintDuringAnimation( Component c, int x, int y, int width, int height ) { + c.repaint( x, y, width, height ); + } + + /** + * Gets the value(s) of the component. + *

+ * This can be any value and depends on the component. + * If the value(s) changes, then this class animates from the old value(s) to the new ones. + * If multiple values are returned, then each value gets its own independent animation. + *

+ * For a toggle button this could be {@code 0} for off and {@code 1} for on. + */ + float[] getValues( Component c ); + + /** + * Returns whether animation is enabled for this painter (default is {@code true}). + */ + default boolean isAnimationEnabled() { + return true; + } + + /** + * Returns the duration of the animation in milliseconds (default is 150). + */ + default int getAnimationDuration() { + return 150; + } + + /** + * Returns the resolution of the animation in milliseconds (default is 10). + * Resolution is the amount of time between timing events. + */ + default int getAnimationResolution() { + return 10; + } + + /** + * Returns the interpolator for the animation. + * Default is {@link CubicBezierEasing#STANDARD_EASING}. + */ + default Interpolator getAnimationInterpolator() { + return CubicBezierEasing.STANDARD_EASING; + } + + /** + * Returns the duration of the animation in milliseconds (default is 150) + * for the given value index and value. + */ + default int getAnimationDuration( int valueIndex, float value ) { + return getAnimationDuration(); + } + + /** + * Returns the resolution of the animation in milliseconds (default is 10) + * for the given value index and value. + * Resolution is the amount of time between timing events. + */ + default int getAnimationResolution( int valueIndex, float value ) { + return getAnimationResolution(); + } + + /** + * Returns the interpolator for the animation + * for the given value index and value. + * Default is {@link CubicBezierEasing#STANDARD_EASING}. + */ + default Interpolator getAnimationInterpolator( int valueIndex, float value ) { + return getAnimationInterpolator(); + } + + /** + * Returns the client property key used to store the animation support. + */ + default Object getClientPropertyKey() { + return getClass(); + } + + /** + * Saves location for repainting animated area with + * {@link AnimatedPainter#repaintDuringAnimation(Component, int, int, int, int)}. + * Only needed when graphics context passed to {@link #paintWithAnimation(Component, Graphics, int, int, int, int)} + * uses transformed location. + */ + static void saveRepaintLocation( AnimatedPainter painter, Component c, int x, int y ) { + AnimatedPainterSupport.saveRepaintLocation( painter, c, x, y ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainterSupport.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainterSupport.java new file mode 100644 index 000000000..1492c31fa --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainterSupport.java @@ -0,0 +1,182 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import javax.swing.JComponent; + +/** + * Animation support class that stores the animation state and implements the animation. + * + * @author Karl Tauber + * @since 2 + */ +class AnimatedPainterSupport +{ + private int valueIndex; + private float startValue; + private float targetValue; + private float animatedValue; + private float fraction; + + private Animator animator; + + // last bounds of the paint area needed to repaint while animating + private int x; + private int y; + private int width; + private int height; + + static void paint( AnimatedPainter painter, Component c, Graphics g, + int x, int y, int width, int height ) + { + if( !isAnimationEnabled( painter, c ) ) { + // paint without animation if animation is disabled or + // component is not a JComponent and therefore does not support + // client properties, which are required to keep animation state + painter.paintAnimated( c, (Graphics2D) g, x, y, width, height, painter.getValues( c ) ); + return; + } + + // get component values + float values[] = painter.getValues( c ); + + JComponent jc = (JComponent) c; + Object key = painter.getClientPropertyKey(); + AnimatedPainterSupport[] ass = (AnimatedPainterSupport[]) jc.getClientProperty( key ); + + // check whether length of values array has changed + if( ass != null && ass.length != values.length ) { + // cancel all running animations + for( int i = 0; i < ass.length; i++ ) { + AnimatedPainterSupport as = ass[i]; + if( as.animator != null ) + as.animator.cancel(); + } + ass = null; + } + + if( ass == null ) { + ass = new AnimatedPainterSupport[values.length]; + jc.putClientProperty( key, ass ); + } + + for( int i = 0; i < ass.length; i++ ) { + AnimatedPainterSupport as = ass[i]; + float value = values[i]; + + if( as == null ) { + // painted first time --> do not animate, but remember current component value + as = new AnimatedPainterSupport(); + as.valueIndex = i; + as.startValue = as.targetValue = as.animatedValue = value; + ass[i] = as; + } else if( value != as.targetValue ) { + // value changed --> (re)start animation + + int animationDuration = painter.getAnimationDuration( as.valueIndex, value ); + + // do not animate if animation duration (for current value) is zero + if( animationDuration <= 0 ) { + if( as.animator != null ) { + as.animator.cancel(); + as.animator = null; + } + as.startValue = as.targetValue = as.animatedValue = value; + as.fraction = 0; + continue; + } + + if( as.animator == null ) { + // create animator + AnimatedPainterSupport as2 = as; + as.animator = new Animator( 1, fraction -> { + // check whether component was removed while animation is running + if( !c.isDisplayable() ) { + as2.animator.stop(); + return; + } + + // compute animated value + as2.animatedValue = as2.startValue + ((as2.targetValue - as2.startValue) * fraction); + as2.fraction = fraction; + + // repaint + painter.repaintDuringAnimation( c, as2.x, as2.y, as2.width, as2.height ); + }, () -> { + as2.startValue = as2.animatedValue = as2.targetValue; + as2.animator = null; + } ); + } + + if( as.animator.isRunning() ) { + // if animation is still running, restart it from the current + // animated value to the new target value with reduced duration + as.animator.cancel(); + int duration2 = (int) (animationDuration * as.fraction); + if( duration2 > 0 ) + as.animator.setDuration( duration2 ); + as.startValue = as.animatedValue; + } else { + // new animation + as.animator.setDuration( animationDuration ); + + as.animatedValue = as.startValue; + } + + // update animator for new value + as.animator.setResolution( painter.getAnimationResolution( as.valueIndex, value ) ); + as.animator.setInterpolator( painter.getAnimationInterpolator( as.valueIndex, value ) ); + + // start animation + as.targetValue = value; + as.animator.start(); + } + + as.x = x; + as.y = y; + as.width = width; + as.height = height; + } + + float[] animatedValues = new float[ass.length]; + for( int i = 0; i < ass.length; i++ ) + animatedValues[i] = ass[i].animatedValue; + + painter.paintAnimated( c, (Graphics2D) g, x, y, width, height, animatedValues ); + } + + private static boolean isAnimationEnabled( AnimatedPainter painter, Component c ) { + return Animator.useAnimation() && painter.isAnimationEnabled() && c instanceof JComponent; + } + + static void saveRepaintLocation( AnimatedPainter painter, Component c, int x, int y ) { + if( !isAnimationEnabled( painter, c ) ) + return; + + AnimatedPainterSupport[] ass = (AnimatedPainterSupport[]) ((JComponent)c).getClientProperty( painter.getClientPropertyKey() ); + if( ass != null ) { + for( int i = 0; i < ass.length; i++ ) { + AnimatedPainterSupport as = ass[i]; + as.x = x; + as.y = y; + } + } + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.java new file mode 100644 index 000000000..14f94ea71 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.java @@ -0,0 +1,437 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import java.awt.Color; +import java.awt.Component; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.geom.Rectangle2D; +import javax.swing.*; +import javax.swing.border.AbstractBorder; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.AnimatedBorder; +import com.formdev.flatlaf.util.ColorFunctions; +import com.formdev.flatlaf.util.HiDPIUtils; +import com.formdev.flatlaf.util.UIScale; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatAnimatedBorderTest + extends FlatTestPanel +{ + private static final Color CHART_FADE_1 = Color.blue; + private static final Color CHART_FADE_2 = Color.red; + private static final Color CHART_MATERIAL_1 = Color.green; + private static final Color CHART_MATERIAL_2 = Color.magenta; + private static final Color CHART_MATERIAL_3 = Color.pink; + private static final Color CHART_MATERIAL_4 = Color.cyan; + private static final Color CHART_MINIMAL = Color.orange; + + private static final String CHART_COLOR_KEY = "chartColor"; + + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + FlatTestFrame frame = FlatTestFrame.create( args, "FlatAnimatedBorderTest" ); + frame.showFrame( FlatAnimatedBorderTest::new ); + } ); + } + + FlatAnimatedBorderTest() { + initComponents(); + + fade1TextField.setBorder( new AnimatedFocusFadeBorder() ); + fade2TextField.setBorder( new AnimatedFocusFadeBorder() ); + + material1TextField.setBorder( new AnimatedMaterialBorder() ); + material2TextField.setBorder( new AnimatedMaterialBorder() ); + material3TextField.setBorder( new AnimatedMaterialLabeledBorder() ); + material4TextField.setBorder( new AnimatedMaterialLabeledBorder() ); + + minimalTextField.setBorder( new AnimatedMinimalTestBorder() ); + + fade1TextField.putClientProperty( CHART_COLOR_KEY, CHART_FADE_1 ); + fade2TextField.putClientProperty( CHART_COLOR_KEY, CHART_FADE_2 ); + material1TextField.putClientProperty( CHART_COLOR_KEY, CHART_MATERIAL_1 ); + material2TextField.putClientProperty( CHART_COLOR_KEY, CHART_MATERIAL_2 ); + material3TextField.putClientProperty( CHART_COLOR_KEY, CHART_MATERIAL_3 ); + material4TextField.putClientProperty( CHART_COLOR_KEY, CHART_MATERIAL_4 ); + minimalTextField.putClientProperty( CHART_COLOR_KEY, CHART_MINIMAL ); + + fade1ChartColor.setForeground( CHART_FADE_1 ); + fade2ChartColor.setForeground( CHART_FADE_2 ); + material1ChartColor.setForeground( CHART_MATERIAL_1 ); + material2ChartColor.setForeground( CHART_MATERIAL_2 ); + material3ChartColor.setForeground( CHART_MATERIAL_3 ); + material4ChartColor.setForeground( CHART_MATERIAL_4 ); + minimalChartColor.setForeground( CHART_MINIMAL ); + + material3TextField.putClientProperty( AnimatedMaterialLabeledBorder.LABEL_TEXT_KEY, "Label" ); + material4TextField.putClientProperty( AnimatedMaterialLabeledBorder.LABEL_TEXT_KEY, "Label" ); + material4TextField.setText( "Text" ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + label3 = new JLabel(); + lineChartPanel = new FlatAnimatorTest.LineChartPanel(); + fade1TextField = new JTextField(); + fade1ChartColor = new FlatAnimatorTest.JChartColor(); + fade2TextField = new JTextField(); + fade2ChartColor = new FlatAnimatorTest.JChartColor(); + label2 = new JLabel(); + material1TextField = new JTextField(); + material1ChartColor = new FlatAnimatorTest.JChartColor(); + material2TextField = new JTextField(); + material2ChartColor = new FlatAnimatorTest.JChartColor(); + material3TextField = new JTextField(); + material3ChartColor = new FlatAnimatorTest.JChartColor(); + material4TextField = new JTextField(); + material4ChartColor = new FlatAnimatorTest.JChartColor(); + label1 = new JLabel(); + minimalTextField = new JTextField(); + minimalChartColor = new FlatAnimatorTest.JChartColor(); + durationLabel = new JLabel(); + durationField = new JSpinner(); + + //======== this ======== + setLayout(new MigLayout( + "insets dialog,hidemode 3", + // columns + "[fill]" + + "[fill]para" + + "[fill]", + // rows + "[]" + + "[]" + + "[]para" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]para" + + "[]" + + "[]" + + "[grow]" + + "[]")); + + //---- label3 ---- + label3.setText("Fade:"); + add(label3, "cell 0 0"); + add(lineChartPanel, "cell 2 0 1 12"); + add(fade1TextField, "cell 0 1"); + add(fade1ChartColor, "cell 1 1"); + add(fade2TextField, "cell 0 2"); + add(fade2ChartColor, "cell 1 2"); + + //---- label2 ---- + label2.setText("Material:"); + add(label2, "cell 0 3"); + add(material1TextField, "cell 0 4"); + add(material1ChartColor, "cell 1 4"); + add(material2TextField, "cell 0 5"); + add(material2ChartColor, "cell 1 5"); + + //---- material3TextField ---- + material3TextField.putClientProperty("FlatLaf.styleClass", "large"); + add(material3TextField, "cell 0 6"); + add(material3ChartColor, "cell 1 6"); + + //---- material4TextField ---- + material4TextField.putClientProperty("FlatLaf.styleClass", "large"); + add(material4TextField, "cell 0 7"); + add(material4ChartColor, "cell 1 7"); + + //---- label1 ---- + label1.setText("Minimal:"); + add(label1, "cell 0 8"); + add(minimalTextField, "cell 0 9"); + add(minimalChartColor, "cell 1 9"); + + //---- durationLabel ---- + durationLabel.setText("Duration:"); + add(durationLabel, "cell 0 11"); + + //---- durationField ---- + durationField.setModel(new SpinnerNumberModel(200, 0, null, 50)); + add(durationField, "cell 0 11"); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel label3; + private FlatAnimatorTest.LineChartPanel lineChartPanel; + private JTextField fade1TextField; + private FlatAnimatorTest.JChartColor fade1ChartColor; + private JTextField fade2TextField; + private FlatAnimatorTest.JChartColor fade2ChartColor; + private JLabel label2; + private JTextField material1TextField; + private FlatAnimatorTest.JChartColor material1ChartColor; + private JTextField material2TextField; + private FlatAnimatorTest.JChartColor material2ChartColor; + private JTextField material3TextField; + private FlatAnimatorTest.JChartColor material3ChartColor; + private JTextField material4TextField; + private FlatAnimatorTest.JChartColor material4ChartColor; + private JLabel label1; + private JTextField minimalTextField; + private FlatAnimatorTest.JChartColor minimalChartColor; + private JLabel durationLabel; + private JSpinner durationField; + // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class AnimatedMaterialBorder --------------------------------------- + + /** + * Experimental text field border that: + * - animates focus indicator color and border width + */ + private class AnimatedFocusFadeBorder + extends AbstractBorder + implements AnimatedBorder + { + // needed because otherwise the empty paint method in superclass + // javax.swing.border.AbstractBorder would be used + @Override + public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + paintWithAnimation( c, g, x, y, width, height ); + } + + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + float animatedValue = animatedValues[0]; + FlatUIUtils.setRenderingHints( g ); + + // border width is 1 if not focused and 2 if focused + float lw = UIScale.scale( 1 + animatedValue ); + + // paint border + Color color = ColorFunctions.mix( Color.red, Color.lightGray, animatedValue ); + FlatUIUtils.paintOutlinedComponent( g, x, y, width, height, 0, 0, 0, lw, 0, + null, color, null ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.lineChart.addValue( animatedValue, chartColor ); + } + } + + @Override + public Insets getBorderInsets( Component c, Insets insets ) { + insets.top = insets.bottom = UIScale.scale( 3 ); + insets.left = insets.right = UIScale.scale( 7 ); + return insets; + } + + @Override + public float[] getValues( Component c ) { + return new float[] { FlatUIUtils.isPermanentFocusOwner( c ) ? 1 : 0 }; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } + + //---- class AnimatedMaterialBorder --------------------------------------- + + /** + * Experimental text field border that: + * - paint border only at bottom + * - animates focus indicator at bottom + */ + private class AnimatedMaterialBorder + extends AbstractBorder + implements AnimatedBorder + { + // needed because otherwise the empty paint method in superclass + // javax.swing.border.AbstractBorder would be used + @Override + public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + paintWithAnimation( c, g, x, y, width, height ); + } + + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + float animatedValue = animatedValues[0]; + FlatUIUtils.setRenderingHints( g ); + + // use paintAtScale1x() for consistent line thickness when scaled + HiDPIUtils.paintAtScale1x( g, x, y, width, height, + (g2d, x2, y2, width2, height2, scaleFactor) -> { + float lh = (float) (UIScale.scale( 1f ) * scaleFactor); + + g2d.setColor( Color.gray ); + g2d.fill( new Rectangle2D.Float( x2, y2 + height2 - lh, width2, lh ) ); + + if( animatedValue > 0 ) { + lh = (float) (UIScale.scale( 2f ) * scaleFactor); + int lw = Math.round( width2 * animatedValue ); + + g2d.setColor( Color.red ); + g2d.fill( new Rectangle2D.Float( x2 + (width2 - lw) / 2, y2 + height2 - lh, lw, lh ) ); + } + } ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.lineChart.addValue( animatedValue, chartColor ); + } + } + + @Override + public void repaintDuringAnimation( Component c, int x, int y, int width, int height ) { + // limit repaint to bottom border + int lh = UIScale.scale( 2 ); + c.repaint( x, y + height - lh, width, lh ); + } + + @Override + public Insets getBorderInsets( Component c, Insets insets ) { + insets.top = insets.bottom = UIScale.scale( 3 ); + insets.left = insets.right = UIScale.scale( 7 ); + return insets; + } + + @Override + public float[] getValues( Component c ) { + return new float[] { FlatUIUtils.isPermanentFocusOwner( c ) ? 1 : 0 }; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } + + //---- class AnimatedMaterialLabeledBorder -------------------------------- + + /** + * Experimental text field border that: + * - paints a label above the text, or in center if text field is empty + * - paint border only at bottom + * - animates focus indicator at bottom + */ + private class AnimatedMaterialLabeledBorder + extends AnimatedMaterialBorder + { + static final String LABEL_TEXT_KEY = "JTextField.labelText"; + + private static final float LABEL_FONT_SCALE = 0.75f; + + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + super.paintAnimated( c, g, x, y, width, height, animatedValues ); + + float animatedValue = animatedValues[0]; + JComponent jc = (JComponent) c; + String label = (String) jc.getClientProperty( LABEL_TEXT_KEY ); + if( label == null ) + return; + + FontMetrics fm = c.getFontMetrics( c.getFont() ); + int labelFontHeight = Math.round( fm.getHeight() * LABEL_FONT_SCALE ); + + int tx = UIScale.scale( 7 ); + int ty = y + labelFontHeight - UIScale.scale( 2 ); + float sf = LABEL_FONT_SCALE; + + if( ((JTextField)c).getDocument().getLength() == 0 ) { + // paint label in center of text field if it is empty + int ty2 = ((height - fm.getHeight()) / 2) + labelFontHeight; + ty += (ty2 - ty) * (1 - animatedValue); + sf += (1 - LABEL_FONT_SCALE) * (1 - animatedValue); + } + + Graphics2D g2 = (Graphics2D) g.create(); + try { + g2.translate( tx, ty ); + g2.scale( sf, sf ); + g2.setColor( ColorFunctions.mix( Color.red, Color.gray, animatedValue ) ); + + FlatUIUtils.drawString( jc, g2, label, 0, 0 ); + } finally { + g2.dispose(); + } + } + + @Override + public void repaintDuringAnimation( Component c, int x, int y, int width, int height ) { + c.repaint( x, y, width, height ); + } + + @Override + public Insets getBorderInsets( Component c, Insets insets ) { + super.getBorderInsets( c, insets ); + + FontMetrics fm = c.getFontMetrics( c.getFont() ); + int labelFontHeight = Math.round( fm.getHeight() * LABEL_FONT_SCALE ); + insets.top = labelFontHeight; + insets.bottom = UIScale.scale( 5 ); + return insets; + } + } + + //---- class AnimatedMinimalTestBorder ------------------------------------ + + /** + * Minimal example for an animated border. + */ + private class AnimatedMinimalTestBorder + implements AnimatedBorder + { + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + float animatedValue = animatedValues[0]; + int lh = UIScale.scale( 2 ); + + g.setColor( Color.blue ); + g.fillRect( x, y + height - lh, Math.round( width * animatedValue ), lh ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.lineChart.addValue( animatedValue, chartColor ); + } + } + + @Override + public float[] getValues( Component c ) { + return new float[] { FlatUIUtils.isPermanentFocusOwner( c ) ? 1 : 0 }; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + + @Override + public Insets getBorderInsets( Component c ) { + return UIScale.scale( new Insets( 3, 7, 3, 7 ) ); + } + + @Override + public boolean isBorderOpaque() { + return false; + } + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.jfd new file mode 100644 index 000000000..4c32e5edc --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.jfd @@ -0,0 +1,128 @@ +JFDML JFormDesigner: "7.0.5.0.382" Java: "16" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets dialog,hidemode 3" + "$columnConstraints": "[fill][fill]para[fill]" + "$rowConstraints": "[][][]para[][][][][]para[][][grow][]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label3" + "text": "Fade:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$LineChartPanel" ) { + name: "lineChartPanel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 1 12" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "fade1TextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "fade1ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "fade2TextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "fade2ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label2" + "text": "Material:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "material1TextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "material1ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "material2TextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "material2ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "material3TextField" + "$client.FlatLaf.styleClass": "large" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "material3ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "material4TextField" + "$client.FlatLaf.styleClass": "large" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "material4ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label1" + "text": "Minimal:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "minimalTextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "minimalChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 9" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "durationLabel" + "text": "Duration:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11" + } ) + add( new FormComponent( "javax.swing.JSpinner" ) { + name: "durationField" + "model": new javax.swing.SpinnerNumberModel { + minimum: 0 + stepSize: 50 + value: 200 + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 725, 325 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java index 160caca65..49e80740e 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java @@ -18,13 +18,14 @@ import java.awt.Color; import java.awt.Component; -import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; import javax.swing.*; import com.formdev.flatlaf.icons.FlatAnimatedIcon; import com.formdev.flatlaf.util.AnimatedIcon; import com.formdev.flatlaf.util.ColorFunctions; +import com.formdev.flatlaf.util.UIScale; import net.miginfocom.swing.*; /** @@ -33,6 +34,15 @@ public class FlatAnimatedIconTest extends FlatTestPanel { + private static final Color CHART_RADIO_BUTTON_1 = Color.blue; + private static final Color CHART_RADIO_BUTTON_2 = Color.red; + private static final Color CHART_RADIO_BUTTON_3 = Color.green; + private static final Color CHART_CHECK_BOX_1 = Color.magenta; + private static final Color CHART_CHECK_BOX_2 = Color.orange; + private static final Color[] CHART_SWITCH_EX = new Color[] { Color.red, Color.green, Color.blue }; + + private static final String CHART_COLOR_KEY = "chartColor"; + public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { FlatTestFrame frame = FlatTestFrame.create( args, "FlatAnimatedIconTest" ); @@ -49,16 +59,36 @@ public static void main( String[] args ) { radioButton3.setIcon( radioIcon ); checkBox1.setIcon( new AnimatedSwitchIcon() ); + checkBox3.setIcon( new AnimatedSwitchIconEx() ); checkBox2.setIcon( new AnimatedMinimalTestIcon() ); + + radioButton1.putClientProperty( CHART_COLOR_KEY, CHART_RADIO_BUTTON_1 ); + radioButton2.putClientProperty( CHART_COLOR_KEY, CHART_RADIO_BUTTON_2 ); + radioButton3.putClientProperty( CHART_COLOR_KEY, CHART_RADIO_BUTTON_3 ); + checkBox1.putClientProperty( CHART_COLOR_KEY, CHART_CHECK_BOX_1 ); + checkBox2.putClientProperty( CHART_COLOR_KEY, CHART_CHECK_BOX_2 ); + + radioButton1ChartColor.setForeground( CHART_RADIO_BUTTON_1 ); + radioButton2ChartColor.setForeground( CHART_RADIO_BUTTON_2 ); + radioButton3ChartColor.setForeground( CHART_RADIO_BUTTON_3 ); + checkBox1ChartColor.setForeground( CHART_CHECK_BOX_1 ); + checkBox2ChartColor.setForeground( CHART_CHECK_BOX_2 ); } private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents radioButton1 = new JRadioButton(); + radioButton1ChartColor = new FlatAnimatorTest.JChartColor(); + lineChartPanel = new FlatAnimatorTest.LineChartPanel(); radioButton2 = new JRadioButton(); + radioButton2ChartColor = new FlatAnimatorTest.JChartColor(); radioButton3 = new JRadioButton(); + radioButton3ChartColor = new FlatAnimatorTest.JChartColor(); checkBox1 = new JCheckBox(); + checkBox1ChartColor = new FlatAnimatorTest.JChartColor(); + checkBox3 = new JCheckBox(); checkBox2 = new JCheckBox(); + checkBox2ChartColor = new FlatAnimatorTest.JChartColor(); durationLabel = new JLabel(); durationField = new JSpinner(); @@ -66,14 +96,16 @@ private void initComponents() { setLayout(new MigLayout( "insets dialog,hidemode 3", // columns - "[]para" + - "[fill]", + "[]" + + "[fill]para" + + "[grow,fill]", // rows "[]" + "[]" + "[]para" + "[]" + "[]" + + "[]" + "[grow]" + "[]")); @@ -81,30 +113,40 @@ private void initComponents() { radioButton1.setText("radio 1"); radioButton1.setSelected(true); add(radioButton1, "cell 0 0"); + add(radioButton1ChartColor, "cell 1 0"); + add(lineChartPanel, "cell 2 0 1 7"); //---- radioButton2 ---- radioButton2.setText("radio 2"); add(radioButton2, "cell 0 1"); + add(radioButton2ChartColor, "cell 1 1"); //---- radioButton3 ---- radioButton3.setText("radio 3"); add(radioButton3, "cell 0 2"); + add(radioButton3ChartColor, "cell 1 2"); //---- checkBox1 ---- checkBox1.setText("switch"); add(checkBox1, "cell 0 3"); + add(checkBox1ChartColor, "cell 1 3"); + + //---- checkBox3 ---- + checkBox3.setText("switch ex"); + add(checkBox3, "cell 0 4"); //---- checkBox2 ---- checkBox2.setText("minimal"); - add(checkBox2, "cell 0 4"); + add(checkBox2, "cell 0 5"); + add(checkBox2ChartColor, "cell 1 5"); //---- durationLabel ---- durationLabel.setText("Duration:"); - add(durationLabel, "cell 0 6 2 1"); + add(durationLabel, "cell 0 7 3 1"); //---- durationField ---- - durationField.setModel(new SpinnerNumberModel(200, 100, null, 50)); - add(durationField, "cell 0 6 2 1"); + durationField.setModel(new SpinnerNumberModel(200, 0, null, 50)); + add(durationField, "cell 0 7 3 1"); //---- buttonGroup1 ---- ButtonGroup buttonGroup1 = new ButtonGroup(); @@ -116,10 +158,17 @@ private void initComponents() { // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JRadioButton radioButton1; + private FlatAnimatorTest.JChartColor radioButton1ChartColor; + private FlatAnimatorTest.LineChartPanel lineChartPanel; private JRadioButton radioButton2; + private FlatAnimatorTest.JChartColor radioButton2ChartColor; private JRadioButton radioButton3; + private FlatAnimatorTest.JChartColor radioButton3ChartColor; private JCheckBox checkBox1; + private FlatAnimatorTest.JChartColor checkBox1ChartColor; + private JCheckBox checkBox3; private JCheckBox checkBox2; + private FlatAnimatorTest.JChartColor checkBox2ChartColor; private JLabel durationLabel; private JSpinner durationField; // JFormDesigner - End of variables declaration //GEN-END:variables @@ -146,7 +195,8 @@ public AnimatedRadioButtonIcon() { } @Override - public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + public void paintIconAnimated( Component c, Graphics2D g, float[] animatedValues ) { + float animatedValue = animatedValues[0]; Color color = ColorFunctions.mix( onColor, offColor, animatedValue ); // border @@ -162,12 +212,17 @@ public void paintIconAnimated( Component c, Graphics g, int x, int y, float anim float dotDiameter = DOT_SIZE * animatedValue; float xy = (SIZE - dotDiameter) / 2f; g.setColor( color ); - ((Graphics2D)g).fill( new Ellipse2D.Float( xy, xy, dotDiameter, dotDiameter ) ); + g.fill( new Ellipse2D.Float( xy, xy, dotDiameter, dotDiameter ) ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.lineChart.addValue( animatedValue, chartColor ); + } } @Override - public float getValue( Component c ) { - return ((JRadioButton)c).isSelected() ? 1 : 0; + public float[] getValues( Component c ) { + return new float[] { ((JRadioButton)c).isSelected() ? 1 : 0 }; } @Override @@ -178,7 +233,7 @@ public int getAnimationDuration() { //---- class AnimatedSwitchIcon ------------------------------------------- - public class AnimatedSwitchIcon + private class AnimatedSwitchIcon extends FlatAnimatedIcon { private final Color offColor = Color.lightGray; @@ -189,22 +244,113 @@ public AnimatedSwitchIcon() { } @Override - public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + public void paintIconAnimated( Component c, Graphics2D g, float[] animatedValues ) { + float animatedValue = animatedValues[0]; Color color = ColorFunctions.mix( onColor, offColor, animatedValue ); + // paint track + g.setColor( color ); + g.fillRoundRect( 0, 0, width, height, height, height ); + + // paint thumb + int thumbSize = height - 4; + float thumbX = 2 + ((width - 4 - thumbSize) * animatedValue); + int thumbY = 2; + g.setColor( Color.white ); + g.fill( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ) ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.lineChart.addValue( animatedValue, chartColor ); + } + } + + @Override + public float[] getValues( Component c ) { + return new float[] { ((AbstractButton)c).isSelected() ? 1 : 0 }; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } + + //---- class AnimatedSwitchIconEx ----------------------------------------- + + private static final int HW = 8; + + private class AnimatedSwitchIconEx + extends FlatAnimatedIcon + { + private final Color offColor = Color.lightGray; + private final Color onColor = Color.red; + private final Color hoverColor = new Color( 0x4400cc00, true ); + private final Color pressedColor = new Color( 0x440000cc, true ); + + public AnimatedSwitchIconEx() { + super( 28 + HW, 16 + HW, null ); + } + + @Override + public void paintIconAnimated( Component c, Graphics2D g, float[] animatedValues ) { + Color color = ColorFunctions.mix( onColor, offColor, animatedValues[0] ); + + int hw2 = HW / 2; + int x = hw2; + int y = hw2; + int width = this.width - HW; + int height = this.height - HW; + + // paint track g.setColor( color ); g.fillRoundRect( x, y, width, height, height, height ); + // paint thumb int thumbSize = height - 4; - float thumbX = x + 2 + ((width - 4 - thumbSize) * animatedValue); + float thumbX = x + 2 + ((width - 4 - thumbSize) * animatedValues[0]); int thumbY = y + 2; g.setColor( Color.white ); - ((Graphics2D)g).fill( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ) ); + g.fill( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ) ); + + // paint hover + if( animatedValues[1] > 0 ) { + g.setColor( hoverColor ); + paintHoverOrPressed( g, thumbX, thumbY, thumbSize, animatedValues[1] ); + } + + // paint pressed + if( animatedValues[2] > 0 ) { + g.setColor( pressedColor ); + paintHoverOrPressed( g, thumbX, thumbY, thumbSize, animatedValues[2] ); + } + + for( int i = 0; i < animatedValues.length; i++ ) { + float animatedValue = animatedValues[i]; + if( animatedValue != 0 && animatedValue != 1 ) + lineChartPanel.lineChart.addValue( animatedValue, CHART_SWITCH_EX[i] ); + } + } + + private void paintHoverOrPressed( Graphics2D g, float thumbX, int thumbY, int thumbSize, float animatedValue ) { + float hw = (HW + 4) * animatedValue; + Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD ); + path.append( new Ellipse2D.Float( thumbX - (hw / 2), thumbY - (hw / 2), + thumbSize + hw, thumbSize + hw ), false ); + path.append( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ), false ); + g.fill( path ); } @Override - public float getValue( Component c ) { - return ((AbstractButton)c).isSelected() ? 1 : 0; + public float[] getValues( Component c ) { + AbstractButton b = (AbstractButton) c; + ButtonModel bm = b.getModel(); + + return new float[] { + b.isSelected() ? 1 : 0, + bm.isRollover() ? 1 : 0, + bm.isPressed() ? 1 : 0, + }; } @Override @@ -223,27 +369,31 @@ private class AnimatedMinimalTestIcon { @Override public int getIconWidth() { - return 100; + return UIScale.scale( 50 ); } @Override public int getIconHeight() { - return 20; + return UIScale.scale( 16 ); } @Override - public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { - int w = getIconWidth(); - int h = getIconHeight(); + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + float animatedValue = animatedValues[0]; g.setColor( Color.red ); - g.drawRect( x, y, w - 1, h - 1 ); - g.fillRect( x, y, Math.round( w * animatedValue ), h ); + g.drawRect( x, y, width - 1, height - 1 ); + g.fillRect( x, y, Math.round( width * animatedValue ), height ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.lineChart.addValue( animatedValue, chartColor ); + } } @Override - public float getValue( Component c ) { - return ((AbstractButton)c).isSelected() ? 1 : 0; + public float[] getValues( Component c ) { + return new float[] { ((AbstractButton)c).isSelected() ? 1 : 0 }; } @Override diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd index 11c55e811..62a2ae327 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd @@ -1,12 +1,12 @@ -JFDML JFormDesigner: "7.0.2.0.298" Java: "15" encoding: "UTF-8" +JFDML JFormDesigner: "7.0.5.0.382" Java: "16" encoding: "UTF-8" new FormModel { contentType: "form/swing" root: new FormRoot { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets dialog,hidemode 3" - "$columnConstraints": "[]para[fill]" - "$rowConstraints": "[][][]para[][][grow][]" + "$columnConstraints": "[][fill]para[grow,fill]" + "$rowConstraints": "[][][]para[][][][grow][]" } ) { name: "this" add( new FormComponent( "javax.swing.JRadioButton" ) { @@ -17,6 +17,16 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "radioButton1ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$LineChartPanel" ) { + name: "lineChartPanel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 1 7" + } ) add( new FormComponent( "javax.swing.JRadioButton" ) { name: "radioButton2" "text": "radio 2" @@ -24,6 +34,11 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "radioButton2ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) add( new FormComponent( "javax.swing.JRadioButton" ) { name: "radioButton3" "text": "radio 3" @@ -31,37 +46,58 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "radioButton3ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "checkBox1" "text": "switch" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "checkBox1ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "checkBox3" + "text": "switch ex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "checkBox2" "text": "minimal" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "checkBox2ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "durationLabel" "text": "Duration:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 2 1" + "value": "cell 0 7 3 1" } ) add( new FormComponent( "javax.swing.JSpinner" ) { name: "durationField" "model": new javax.swing.SpinnerNumberModel { - minimum: 100 + minimum: 0 stepSize: 50 value: 200 } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 2 1" + "value": "cell 0 7 3 1" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 415, 350 ) + "size": new java.awt.Dimension( 810, 350 ) } ) add( new FormNonVisual( "javax.swing.ButtonGroup" ) { name: "buttonGroup1" diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java index 831a132d7..11928d854 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java @@ -17,9 +17,19 @@ package com.formdev.flatlaf.testing; import java.awt.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.swing.*; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.CubicBezierEasing; +import com.formdev.flatlaf.util.HSLColor; +import com.formdev.flatlaf.util.HiDPIUtils; +import com.formdev.flatlaf.util.UIScale; +import com.formdev.flatlaf.util.Animator.Interpolator; import net.miginfocom.swing.*; /** @@ -28,8 +38,13 @@ public class FlatAnimatorTest extends FlatTestPanel { + private static final Color CHART_LINEAR = Color.blue; + private static final Color CHART_EASE_IN_OUT = Color.magenta; + private static final Color CHART_STANDARD_EASING = Color.red; + private Animator linearAnimator; private Animator easeInOutAnimator; + private Animator standardEasingAnimator; public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { @@ -40,85 +55,543 @@ public static void main( String[] args ) { FlatAnimatorTest() { initComponents(); - } - private void start() { - startLinear(); - startEaseInOut(); + linearChartColor.setForeground( CHART_LINEAR ); + easeInOutChartColor.setForeground( CHART_EASE_IN_OUT ); + standardEasingChartColor.setForeground( CHART_STANDARD_EASING ); } - private void startLinear() { - if( linearAnimator != null ) { - linearAnimator.stop(); - linearAnimator.start(); - } else { - linearAnimator = new Animator( 1000, fraction -> { - linearScrollBar.setValue( Math.round( fraction * linearScrollBar.getMaximum() ) ); - } ); - linearAnimator.start(); - } + private void start() { + linearAnimator = start( linearAnimator, null, linearScrollBar, CHART_LINEAR ); + easeInOutAnimator = start( easeInOutAnimator, CubicBezierEasing.EASE_IN_OUT, easeInOutScrollBar, CHART_EASE_IN_OUT ); + standardEasingAnimator = start( standardEasingAnimator, CubicBezierEasing.STANDARD_EASING, standardEasingScrollBar, CHART_STANDARD_EASING ); } - private void startEaseInOut() { - if( easeInOutAnimator != null ) { - easeInOutAnimator.stop(); - easeInOutAnimator.start(); + private Animator start( Animator animator, Interpolator interpolator, JScrollBar scrollBar, Color chartColor ) { + if( animator != null ) { + animator.stop(); + animator.start(); } else { - easeInOutAnimator = new Animator( 1000, fraction -> { - easeInOutScrollBar.setValue( Math.round( fraction * easeInOutScrollBar.getMaximum() ) ); + animator = new Animator( 1000, fraction -> { + scrollBar.setValue( Math.round( fraction * scrollBar.getMaximum() ) ); + lineChartPanel.lineChart.addValue( fraction, chartColor ); } ); - easeInOutAnimator.setInterpolator( CubicBezierEasing.EASE_IN_OUT ); - easeInOutAnimator.start(); + animator.setInterpolator( interpolator ); + animator.start(); } + return animator; } private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents - JLabel label1 = new JLabel(); + linearLabel = new JLabel(); + linearChartColor = new FlatAnimatorTest.JChartColor(); linearScrollBar = new JScrollBar(); - JLabel label2 = new JLabel(); + easeInOutLabel = new JLabel(); + easeInOutChartColor = new FlatAnimatorTest.JChartColor(); easeInOutScrollBar = new JScrollBar(); + standardEasingLabel = new JLabel(); + standardEasingChartColor = new FlatAnimatorTest.JChartColor(); + standardEasingScrollBar = new JScrollBar(); startButton = new JButton(); + lineChartPanel = new FlatAnimatorTest.LineChartPanel(); //======== this ======== setLayout(new MigLayout( "ltr,insets dialog,hidemode 3", // columns "[fill]" + + "[fill]" + "[grow,fill]", // rows "[]" + "[]" + - "[]")); + "[]" + + "[]para" + + "[400,grow,fill]")); - //---- label1 ---- - label1.setText("Linear:"); - add(label1, "cell 0 0"); + //---- linearLabel ---- + linearLabel.setText("Linear:"); + add(linearLabel, "cell 0 0"); + add(linearChartColor, "cell 1 0"); //---- linearScrollBar ---- linearScrollBar.setOrientation(Adjustable.HORIZONTAL); linearScrollBar.setBlockIncrement(1); - add(linearScrollBar, "cell 1 0"); + add(linearScrollBar, "cell 2 0"); - //---- label2 ---- - label2.setText("Ease in out:"); - add(label2, "cell 0 1"); + //---- easeInOutLabel ---- + easeInOutLabel.setText("Ease in out:"); + add(easeInOutLabel, "cell 0 1"); + add(easeInOutChartColor, "cell 1 1"); //---- easeInOutScrollBar ---- easeInOutScrollBar.setOrientation(Adjustable.HORIZONTAL); easeInOutScrollBar.setBlockIncrement(1); - add(easeInOutScrollBar, "cell 1 1"); + add(easeInOutScrollBar, "cell 2 1"); + + //---- standardEasingLabel ---- + standardEasingLabel.setText("Standard easing:"); + add(standardEasingLabel, "cell 0 2"); + add(standardEasingChartColor, "cell 1 2"); + + //---- standardEasingScrollBar ---- + standardEasingScrollBar.setOrientation(Adjustable.HORIZONTAL); + standardEasingScrollBar.setBlockIncrement(1); + add(standardEasingScrollBar, "cell 2 2"); //---- startButton ---- startButton.setText("Start"); startButton.addActionListener(e -> start()); - add(startButton, "cell 0 2"); + add(startButton, "cell 0 3"); + add(lineChartPanel, "cell 0 4 3 1"); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel linearLabel; + private FlatAnimatorTest.JChartColor linearChartColor; private JScrollBar linearScrollBar; + private JLabel easeInOutLabel; + private FlatAnimatorTest.JChartColor easeInOutChartColor; private JScrollBar easeInOutScrollBar; + private JLabel standardEasingLabel; + private FlatAnimatorTest.JChartColor standardEasingChartColor; + private JScrollBar standardEasingScrollBar; private JButton startButton; + private FlatAnimatorTest.LineChartPanel lineChartPanel; // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class LineChartPanel ----------------------------------------------- + + static class LineChartPanel + extends JPanel + { + LineChartPanel() { + initComponents(); + + secondsWidthSlider.setValue( lineChart.getSecondsWidth() ); + updateChartDelayedChanged(); + } + + void setSecondsWidth( int secondsWidth ) { + lineChart.setSecondsWidth( secondsWidth ); + secondsWidthSlider.setValue( secondsWidth ); + } + + private void secondsWidthChanged() { + lineChart.setSecondsWidth( secondsWidthSlider.getValue() ); + } + + private void updateChartDelayedChanged() { + lineChart.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() ); + } + + private void clearChart() { + lineChart.clear(); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + JScrollPane lineChartScrollPane = new JScrollPane(); + lineChart = new FlatAnimatorTest.LineChart(); + JLabel lineChartInfoLabel = new JLabel(); + secondsWidthSlider = new JSlider(); + updateChartDelayedCheckBox = new JCheckBox(); + JButton clearChartButton = new JButton(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets 0,hidemode 3", + // columns + "[fill]" + + "[grow,fill]", + // rows + "[400,grow,fill]" + + "[]")); + + //======== lineChartScrollPane ======== + { + lineChartScrollPane.putClientProperty("JScrollPane.smoothScrolling", false); + lineChartScrollPane.setViewportView(lineChart); + } + add(lineChartScrollPane, "cell 0 0 2 1"); + + //---- lineChartInfoLabel ---- + lineChartInfoLabel.setText("X: time (500ms per line) / Y: value (10% per line)"); + add(lineChartInfoLabel, "cell 0 1 2 1"); + + //---- secondsWidthSlider ---- + secondsWidthSlider.setMinimum(100); + secondsWidthSlider.setMaximum(2000); + secondsWidthSlider.addChangeListener(e -> secondsWidthChanged()); + add(secondsWidthSlider, "cell 0 1 2 1"); + + //---- updateChartDelayedCheckBox ---- + updateChartDelayedCheckBox.setText("Update chart delayed"); + updateChartDelayedCheckBox.setMnemonic('U'); + updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); + add(updateChartDelayedCheckBox, "cell 0 1 2 1,alignx right,growx 0"); + + //---- clearChartButton ---- + clearChartButton.setText("Clear Chart"); + clearChartButton.setMnemonic('C'); + clearChartButton.addActionListener(e -> clearChart()); + add(clearChartButton, "cell 0 1 2 1,alignx right,growx 0"); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + FlatAnimatorTest.LineChart lineChart; + private JSlider secondsWidthSlider; + private JCheckBox updateChartDelayedCheckBox; + // JFormDesigner - End of variables declaration //GEN-END:variables + } + + //---- class LineChart ---------------------------------------------------- + + static class LineChart + extends JComponent + implements Scrollable + { + private static final int NEW_SEQUENCE_TIME_LAG = 500; + private static final int NEW_SEQUENCE_GAP = 100; + + private boolean asynchron; + private int secondsWidth = 500; + + private static class Data { + final double value; + final boolean dot; + final Color chartColor; + final long time; // in milliseconds + + Data( double value, boolean dot, Color chartColor, long time ) { + this.value = value; + this.dot = dot; + this.chartColor = chartColor; + this.time = time; + } + + @Override + public String toString() { + // for debugging + return String.valueOf( value ); + } + } + + private final List syncChartData = new ArrayList<>(); + private final Map> asyncColor2dataMap = new HashMap<>(); + private final Timer repaintTime; + private Color lastUsedChartColor; + private boolean updateDelayed; + + LineChart() { + repaintTime = new Timer( 20, e -> repaintAndRevalidate() ); + repaintTime.setRepeats( false ); + } + + void enableAsynchron() { + if( !syncChartData.isEmpty() ) + throw new IllegalStateException(); + + asynchron = true; + } + + void addValue( double value, Color chartColor ) { + addValue( value, false, chartColor ); + } + + void addValue( double value, boolean dot, Color chartColor ) { + List chartData = asyncColor2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); + Data data = new Data( value, dot, chartColor, System.nanoTime() / 1000000 ); + if( asynchron ) + chartData.add( data ); + else + syncChartData.add( data ); + + lastUsedChartColor = chartColor; + + if( updateDelayed ) { + repaintTime.stop(); + repaintTime.start(); + } else + repaintAndRevalidate(); + } + + void clear() { + syncChartData.clear(); + asyncColor2dataMap.clear(); + lastUsedChartColor = null; + + repaint(); + revalidate(); + } + + void setUpdateDelayed( boolean updateDelayed ) { + this.updateDelayed = updateDelayed; + } + + int getSecondsWidth() { + return secondsWidth; + } + + void setSecondsWidth( int secondsWidth ) { + this.secondsWidth = secondsWidth; + repaint(); + revalidate(); + } + + private void repaintAndRevalidate() { + repaint(); + revalidate(); + + // scroll horizontally + if( lastUsedChartColor != null ) { + // compute chart width of last used color and start of last sequence + int[] lastSeqX = new int[1]; + int cw = chartWidth( asynchron ? asyncColor2dataMap.get( lastUsedChartColor ) : syncChartData, lastSeqX ); + + // scroll to end of last sequence (of last used color) + int lastSeqWidth = cw - lastSeqX[0]; + int width = Math.min( lastSeqWidth, getParent().getWidth() ); + int x = cw - width; + scrollRectToVisible( new Rectangle( x, 0, width, getHeight() ) ); + } + } + + @Override + protected void paintComponent( Graphics g ) { + Graphics g2 = g.create(); + try { + HiDPIUtils.paintAtScale1x( (Graphics2D) g2, this, this::paintImpl ); + } finally { + g2.dispose(); + } + } + + private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + FlatUIUtils.setRenderingHints( g ); + + int secondsWidth = (int) (this.secondsWidth * scaleFactor); + + Color lineColor = FlatUIUtils.getUIColor( "Component.borderColor", Color.lightGray ); + Color lineColor2 = FlatLaf.isLafDark() + ? new HSLColor( lineColor ).adjustTone( 30 ) + : new HSLColor( lineColor ).adjustShade( 30 ); + + g.translate( x, y ); + + // fill background + g.setColor( UIManager.getColor( "Table.background" ) ); + g.fillRect( x, y, width, height ); + + // paint horizontal lines + for( int i = 1; i < 10; i++ ) { + int hy = (height * i) / 10; + g.setColor( (i != 5) ? lineColor : lineColor2 ); + g.drawLine( 0, hy, width, hy ); + } + + // paint vertical lines + int twoHundredMillisWidth = secondsWidth / 5; + for( int i = twoHundredMillisWidth; i < width; i += twoHundredMillisWidth ) { + g.setColor( (i % secondsWidth != 0) ? lineColor : lineColor2 ); + g.drawLine( i, 0, i, height ); + } + + // paint lines + for( Map.Entry> e : asyncColor2dataMap.entrySet() ) { + List chartData = asynchron ? e.getValue() : syncChartData; + Color chartColor = e.getKey(); + paintChartData( g, chartData, chartColor, height, scaleFactor ); + } + } + + private void paintChartData( Graphics2D g, List chartData, Color chartColor, int height, double scaleFactor ) { + if( FlatLaf.isLafDark() ) + chartColor = new HSLColor( chartColor ).adjustTone( 50 ); + Color temporaryValueColor = new Color( (chartColor.getRGB() & 0xffffff) | 0x40000000, true ); + + int seqGapWidth = (int) (NEW_SEQUENCE_GAP * scaleFactor); + long seqTime = 0; + int seqX = 0; + long ptime = 0; + int px = 0; + int py = height - 1; + int cx = px; + int cy = py; + int pcount = 0; + + g.setColor( chartColor ); + + boolean first = true; + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + boolean useData = (data.chartColor == chartColor); + int dy = height - 1 - (int) ((height - 1) * data.value); + + if( data.dot ) { + int dotx = px; + if( i > 0 && data.time > ptime + NEW_SEQUENCE_TIME_LAG ) + dotx += seqGapWidth; + int o = UIScale.scale( 1 ); + int s = UIScale.scale( 3 ); + g.fillRect( dotx - o, dy - o, s, s ); + continue; + } + + if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { + if( !first && pcount == 0 ) + g.drawLine( px, py, px + (int) (4 * scaleFactor), py ); + + // start new sequence + seqTime = data.time; + seqX = !first ? px + seqGapWidth : 0; + px = seqX; + pcount = 0; + first = false; + } else { + boolean isTemporaryValue = isTemporaryValue( chartData, i ) || isTemporaryValue( chartData, i - 1 ); + if( isTemporaryValue ) + g.setColor( temporaryValueColor ); + + // line in sequence + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondsWidth)); + if( useData ) + g.drawLine( cx, cy, dx, dy ); + px = dx; + pcount++; + + if( isTemporaryValue ) + g.setColor( chartColor ); + } + + py = dy; + ptime = data.time; + + if( useData ) { + cx = px; + cy = py; + } + } + } + + /** + * One or two values between two equal values are considered "temporary", + * which means that they are the target value for the following scroll animation. + */ + private boolean isTemporaryValue( List chartData, int i ) { + if( i == 0 || i == chartData.size() - 1 ) + return false; + + Data dataBefore = chartData.get( i - 1 ); + Data dataAfter = chartData.get( i + 1 ); + + if( dataBefore.dot || dataAfter.dot ) + return false; + + double valueBefore = dataBefore.value; + double valueAfter = dataAfter.value; + + return valueBefore == valueAfter || + (i < chartData.size() - 2 && valueBefore == chartData.get( i + 2 ).value) || + (i > 1 && chartData.get( i - 2 ).value == valueAfter); + } + + private int chartWidth() { + int width = 0; + if( asynchron ) { + for( List chartData : asyncColor2dataMap.values() ) + width = Math.max( width, chartWidth( chartData, null ) ); + } else + width = Math.max( width, chartWidth( syncChartData, null ) ); + return width; + } + + private int chartWidth( List chartData, int[] lastSeqX ) { + long seqTime = 0; + int seqX = 0; + long ptime = 0; + int px = 0; + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + + if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { + // start new sequence + seqTime = data.time; + seqX = (i > 0) ? px + NEW_SEQUENCE_GAP : 0; + px = seqX; + } else { + // line in sequence + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondsWidth)); + px = dx; + } + + ptime = data.time; + } + + if( lastSeqX != null ) + lastSeqX[0] = seqX; + + return px; + } + + @Override + public Dimension getPreferredSize() { + return new Dimension( chartWidth(), 200 ); + } + + @Override + public Dimension getPreferredScrollableViewportSize() { + return new Dimension( chartWidth(), 200 ); + } + + @Override + public int getScrollableUnitIncrement( Rectangle visibleRect, int orientation, int direction ) { + return secondsWidth; + } + + @Override + public int getScrollableBlockIncrement( Rectangle visibleRect, int orientation, int direction ) { + JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass( JViewport.class, this ); + return (viewport != null) ? viewport.getWidth() : 200; + } + + @Override + public boolean getScrollableTracksViewportWidth() { + JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass( JViewport.class, this ); + return (viewport != null) ? viewport.getWidth() > chartWidth() : true; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + return true; + } + } + + //---- class JChartColor -------------------------------------------------- + + static class JChartColor + extends JComponent + { + @Override + public Dimension getPreferredSize() { + return new Dimension( UIScale.scale( 24 ), UIScale.scale( 12 ) ); + } + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + @Override + protected void paintComponent( Graphics g ) { + g.setColor( getForeground() ); + g.fillRect( 0, 0, UIScale.scale( 24 ), UIScale.scale( 12 ) ); + } + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd index bb92a1299..f8a7f3c1f 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.2.0.298" Java: "14.0.2" encoding: "UTF-8" +JFDML JFormDesigner: "7.0.5.0.382" Java: "16" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -8,16 +8,27 @@ new FormModel { } add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,insets dialog,hidemode 3" - "$columnConstraints": "[fill][grow,fill]" - "$rowConstraints": "[][][]" + "$columnConstraints": "[fill][fill][grow,fill]" + "$rowConstraints": "[][][][]para[400,grow,fill]" } ) { name: "this" add( new FormComponent( "javax.swing.JLabel" ) { - name: "label1" + name: "linearLabel" "text": "Linear:" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "linearChartColor" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) add( new FormComponent( "javax.swing.JScrollBar" ) { name: "linearScrollBar" "orientation": 0 @@ -26,14 +37,25 @@ new FormModel { "JavaCodeGenerator.variableLocal": false } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 0" + "value": "cell 2 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label2" + name: "easeInOutLabel" "text": "Ease in out:" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "easeInOutChartColor" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) add( new FormComponent( "javax.swing.JScrollBar" ) { name: "easeInOutScrollBar" "orientation": 0 @@ -42,7 +64,34 @@ new FormModel { "JavaCodeGenerator.variableLocal": false } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1" + "value": "cell 2 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "standardEasingLabel" + "text": "Standard easing:" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "standardEasingChartColor" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JScrollBar" ) { + name: "standardEasingScrollBar" + "orientation": 0 + "blockIncrement": 1 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 2" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "startButton" @@ -52,11 +101,81 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "start", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2" + "value": "cell 0 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$LineChartPanel" ) { + name: "lineChartPanel" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4 3 1" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 415, 350 ) + "size": new java.awt.Dimension( 625, 625 ) + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$columnConstraints": "[fill][grow,fill]" + "$rowConstraints": "[400,grow,fill][]" + "$layoutConstraints": "ltr,insets 0,hidemode 3" + } ) { + name: "lineChartPanelNested" + auxiliary() { + "JavaCodeGenerator.className": "LineChartPanel" + } + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "lineChartScrollPane" + "$client.JScrollPane.smoothScrolling": false + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$LineChart" ) { + name: "lineChart" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + "JavaCodeGenerator.variableModifiers": 0 + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0 2 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "lineChartInfoLabel" + "text": "X: time (500ms per line) / Y: value (10% per line)" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1 2 1" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "secondsWidthSlider" + "minimum": 100 + "maximum": 2000 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "secondsWidthChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1 2 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "updateChartDelayedCheckBox" + "text": "Update chart delayed" + "mnemonic": 85 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1 2 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "clearChartButton" + "text": "Clear Chart" + "mnemonic": 67 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "clearChart", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1 2 1,alignx right,growx 0" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 650 ) + "size": new java.awt.Dimension( 603, 325 ) } ) } }