Compare commits

...

11 Commits

Author SHA1 Message Date
Karl Tauber
4d7a6ff331 AnimatedPainter:
Some checks failed
CI / build (1.8) (push) Has been cancelled
CI / build (11) (push) Has been cancelled
CI / build (14) (push) Has been cancelled
CI / build (15) (push) Has been cancelled
CI / build (9) (push) Has been cancelled
CI / snapshot (push) Has been cancelled
CI / release (push) Has been cancelled
- renamed `getValues()` to `getAnimatableValues()`
- renamed `getClientPropertyKey()` to `getAnimationClientPropertyKey()`
- AnimatedPainterSupport improvements
2025-03-13 11:46:14 +01:00
Karl Tauber
23fc3674c9 LineChartPanel:
- support "synchron" charts
- reworked to make code easier to understand/maintain
- added some JavaBean properties to make it configurable in JFormDesigner
- fixes some bugs

removed `FlatAnimatorTest.LineChartPanel` and replaced it with `LineChartPanel`
2025-03-12 18:35:56 +01:00
Karl Tauber
34a7214dd8 LineChartPanel copied from branch smooth-scrolling, commit 44a04cca2c 2025-03-11 19:33:36 +01:00
Karl Tauber
26d7008c04 AnimatedPainter: support individual animation duration, resolution and interpolator depending on value(s) 2021-11-22 22:07:45 +01:00
Karl Tauber
3b489e8e1a AnimatedPainter: support independent animation of multiple values 2021-11-22 22:07:20 +01:00
Karl Tauber
ccbf577f46 AnimatedBorder: demo for labeled material border 2021-11-21 23:20:47 +01:00
Karl Tauber
b903f18130 FlatAnimatorTest:
- support synchronized line chart
- LineChartPanel: added slider to change horizontal scaling
- FlatAnimatedIconTest: added line chart panel
- FlatAnimatedBorderTest: added line chart panel
2021-11-21 12:27:31 +01:00
Karl Tauber
9523c89c51 FlatAnimatorTest: added standard-easing and line chart
(line chart copied from `FlatSmoothScrollingTest.LineChartPanel` in branch `smooth-scrolling` commit 331ab06b0087d3a70f7c131368b88e5e7c92102f)
2021-11-20 15:14:03 +01:00
Karl Tauber
aa6fb2fcce AnimatedPainter added (refactored from AnimatedBorder and AnimatedIcon)
(checked API compatibility of AnimatedIcon with gradle task sigtestCheck)
2021-11-20 11:13:06 +01:00
Karl Tauber
e4fa2e28ea AnimatedBorder:
- support repainting only necessary region while animating
- use AbstractBorder in test app and fixed insets
2021-11-19 18:02:54 +01:00
Karl Tauber
b2245e2246 AnimatedBorder added (for future animations) (issue #66) 2021-11-19 18:02:54 +01:00
14 changed files with 2511 additions and 235 deletions

View File

@@ -69,7 +69,13 @@ public abstract class FlatAbstractIcon
} }
} }
protected abstract void paintIcon( Component c, Graphics2D g2 ); /**
* Paint the icon at {@code [0,0]} location.
* <p>
* The given graphics context is scaled.
* Use unscaled coordinates, width and height for painting.
*/
protected abstract void paintIcon( Component c, Graphics2D g );
@Override @Override
public int getIconWidth() { public int getIconWidth() {

View File

@@ -21,6 +21,7 @@ import java.awt.Component;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import com.formdev.flatlaf.util.AnimatedIcon; 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 * Base class for animated icons that scales width and height, creates and initializes
@@ -30,7 +31,7 @@ import com.formdev.flatlaf.util.AnimatedIcon;
* <p> * <p>
* This class does not store any state information (needed for animation) in its instance. * This class does not store any state information (needed for animation) in its instance.
* Instead a client property is set on the painted component. * 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 * @author Karl Tauber
*/ */
@@ -45,11 +46,34 @@ public abstract class FlatAnimatedIcon
@Override @Override
public void paintIcon( Component c, Graphics g, int x, int y ) { public void paintIcon( Component c, Graphics g, int x, int y ) {
super.paintIcon( c, g, x, y ); super.paintIcon( c, g, x, y );
AnimatedIcon.AnimationSupport.saveIconLocation( this, c, x, y ); AnimatedPainter.saveRepaintLocation( this, c, x, y );
} }
@Override @Override
protected void paintIcon( Component c, Graphics2D g ) { 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.
* <p>
* 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 );
} }

View File

@@ -0,0 +1,83 @@
/*
* 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.
* <p>
* {@link #getAnimatableValues(Component)} returns the animatable 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 #getAnimatableValues(Component)} returns multiple values, then each value
* gets its own independent animation, which may start/end at different points in time,
* may have different duration, resolution and interpolator.
* <p>
* Example for an animated border:
* <pre>
* private class MyAnimatedBorder
* implements AnimatedBorder
* {
* &#64;Override
* public float[] getAnimatableValues( Component c ) {
* return new float[] { c.isFocusOwner() ? 1 : 0 };
* }
*
* &#64;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 );
* }
*
* &#64;Override
* public Insets getBorderInsets( Component c ) {
* return UIScale.scale( new Insets( 4, 4, 4, 4 ) );
* }
*
* &#64;Override public boolean isBorderOpaque() { return false; }
* }
*
* // sample usage
* JTextField textField = new JTextField();
* textField.setBorder( new MyAnimatedBorder() );
* </pre>
*
* 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 );
}
}

View File

@@ -18,62 +18,87 @@ package com.formdev.flatlaf.util;
import java.awt.Component; import java.awt.Component;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.Icon; import javax.swing.Icon;
import javax.swing.JComponent; import javax.swing.JComponent;
import com.formdev.flatlaf.util.Animator.Interpolator;
/** /**
* Icon that automatically animates painting on component value changes. * Icon that automatically animates painting on component value changes.
* <p> * <p>
* {@link #getValue(Component)} returns the value of the component. * {@link #getAnimatableValues(Component)} returns the animatable value(s) of the component.
* If the value changes, then {@link #paintIconAnimated(Component, Graphics, int, int, float)} * If the value(s) have changed, then {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])}
* is invoked multiple times with animated value (from old value to new value). * is invoked multiple times with animated value(s) (from old value(s) to new value(s)).
* If {@link #getAnimatableValues(Component)} returns multiple values, then each value
* gets its own independent animation, which may start/end at different points in time,
* may have different duration, resolution and interpolator.
* <p> * <p>
* Example for an animated icon: * Example for an animated icon:
* <pre> * <pre>
* private class AnimatedMinimalTestIcon * private class MyAnimatedIcon
* implements AnimatedIcon * implements AnimatedIcon
* { * {
* &#64;Override
* public float[] getAnimatableValues( Component c ) {
* return new float[] { ((AbstractButton)c).isSelected() ? 1 : 0 };
* }
*
* &#64;Override public int getIconWidth() { return 100; } * &#64;Override public int getIconWidth() { return 100; }
* &#64;Override public int getIconHeight() { return 20; } * &#64;Override public int getIconHeight() { return 20; }
* *
* &#64;Override * &#64;Override
* public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { * public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) {
* int w = getIconWidth();
* int h = getIconHeight();
*
* g.setColor( Color.red ); * g.setColor( Color.red );
* g.drawRect( x, y, w - 1, h - 1 ); * g.drawRect( x, y, width - 1, height - 1 );
* g.fillRect( x, y, Math.round( w * animatedValue ), h ); * g.fillRect( x, y, Math.round( width * animatedValues[0] ), height );
* }
*
* &#64;Override
* public float getValue( Component c ) {
* return ((AbstractButton)c).isSelected() ? 1 : 0;
* } * }
* } * }
* *
* // sample usage * // sample usage
* JCheckBox checkBox = new JCheckBox( "test" ); * JCheckBox checkBox = new JCheckBox( "test" );
* checkBox.setIcon( new AnimatedMinimalTestIcon() ); * checkBox.setIcon( new MyAnimatedIcon() );
* </pre> * </pre>
* *
* Animation works only if the component passed to {@link #paintIcon(Component, Graphics, int, int)} * Animation works only if the component passed to {@link #paintIcon(Component, Graphics, int, int)}
* is a instance of {@link JComponent}. * is an instance of {@link JComponent}.
* A client property is set on the component to store the animation state. * A client property is set on the component to store the animation state.
* *
* @author Karl Tauber * @author Karl Tauber
*/ */
public interface AnimatedIcon public interface AnimatedIcon
extends Icon extends Icon, AnimatedPainter
{ {
/**
* {@inheritDoc}
*
* @since 2
*/
@Override @Override
public default void paintIcon( Component c, Graphics g, int x, int y ) { default float[] getAnimatableValues( Component c ) {
AnimationSupport.paintIcon( this, c, g, x, y ); // for compatibility
return new float[] { getValue( c ) };
} }
/** /**
* Paints the icon for the given animated value. * 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
default void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) {
// for compatibility
paintIconAnimated( c, g, x, y, animatedValues[0] );
}
/**
* Paints the icon for the given (animated) value.
* *
* @param c the component that this icon belongs to * @param c the component that this icon belongs to
* @param g the graphics context * @param g the graphics context
@@ -82,52 +107,45 @@ public interface AnimatedIcon
* @param animatedValue the animated value, which is either equal to what {@link #getValue(Component)} * @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 * returned, or somewhere between the previous value and the latest value
* that {@link #getValue(Component)} returned * that {@link #getValue(Component)} returned
*
* @deprecated override {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} instead
*/ */
void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ); @Deprecated
default void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) {
}
/** /**
* Gets the value of the component. * Gets the animatable value of the component.
* <p> * <p>
* This can be any value and depends on the component. * This can be any value and depends on the component.
* If the value changes, then this class animates from the old value to the new one. * If the value changes, then this class animates from the old value to the new one.
* <p> * <p>
* For a toggle button this could be {@code 0} for off and {@code 1} for on. * For a toggle button this could be {@code 0} for off and {@code 1} for on.
*
* @deprecated override {@link #getAnimatableValues(Component)} instead
*/ */
float getValue( Component c ); @Deprecated
default float getValue( Component c ) {
/** return 0;
* 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). * {@inheritDoc}
*
* @since TODO
*/ */
default int getAnimationDuration() { @Override
return 150; default Object getAnimationClientPropertyKey() {
} // for compatibility
return getClientPropertyKey();
/**
* 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. * Returns the client property key used to store the animation support.
*
* @deprecated override {@link #getAnimationClientPropertyKey()} instead
*/ */
@Deprecated
default Object getClientPropertyKey() { default Object getClientPropertyKey() {
return getClass(); return getClass();
} }
@@ -135,115 +153,25 @@ public interface AnimatedIcon
//---- class AnimationSupport --------------------------------------------- //---- class AnimationSupport ---------------------------------------------
/** /**
* Animation support class that stores the animation state and implements the animation. * Animation support.
*/ */
@Deprecated
class AnimationSupport class AnimationSupport
{ {
private float startValue; /**
private float targetValue; * @deprecated use {@link AnimatedPainter#paintWithAnimation(Component, Graphics, int, int, int, int)} instead
private float animatedValue; */
private float fraction; @Deprecated
private Animator animator;
// last x,y coordinates of the icon needed to repaint while animating
private int x;
private int y;
public static void paintIcon( AnimatedIcon icon, Component c, Graphics g, int x, int y ) { public static void paintIcon( AnimatedIcon icon, Component c, Graphics g, int x, int y ) {
if( !isAnimationEnabled( icon, c ) ) { AnimatedPainterSupport.paint( icon, c, (Graphics2D) g, x, y, icon.getIconWidth(), icon.getIconHeight() );
// 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;
} }
/**
* @deprecated use {@link AnimatedPainter#saveRepaintLocation(AnimatedPainter, Component, int, int)} instead
*/
@Deprecated
public static void saveIconLocation( AnimatedIcon icon, Component c, int x, int y ) { public static void saveIconLocation( AnimatedIcon icon, Component c, int x, int y ) {
if( !isAnimationEnabled( icon, c ) ) AnimatedPainterSupport.saveRepaintLocation( icon, c, x, y );
return;
AnimationSupport as = (AnimationSupport) ((JComponent)c).getClientProperty( icon.getClientPropertyKey() );
if( as != null ) {
as.x = x;
as.y = y;
}
} }
} }
} }

View File

@@ -0,0 +1,181 @@
/*
* 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.
* <p>
* {@link #getAnimatableValues(Component)} returns the animatable 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 #getAnimatableValues(Component)} returns multiple values, then each value
* gets its own independent animation, which may start/end at different points in time,
* may have different duration, resolution and interpolator.
* <p>
* See {@link AnimatedBorder} or {@link AnimatedIcon} for examples.
* <p>
* Animation works only if the component passed to {@link #paintWithAnimation(Component, Graphics, int, int, int, int)}
* is an 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
{
/**
* Gets the animatable value(s) of the component.
* <p>
* This can be any value(s) 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.
* <p>
* For a toggle button this could be {@code 0} for off and {@code 1} for on.
* A complex check box could return values for selected, hover, pressed and focused states.
* The painter then can show independent animations for those states.
*/
float[] getAnimatableValues( Component c );
/**
* Starts painting.
* Either invokes {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])}
* once to paint current value(s) (see {@link #getAnimatableValues(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, (Graphics2D) g, x, y, width, height );
}
/**
* Paints the given (animated) value(s).
* <p>
* 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 #getAnimatableValues(Component)}
* returned, or somewhere between the previous values and the latest values
* that {@link #getAnimatableValues(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.
* <p>
* 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.
* <p>
* 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 );
}
/**
* 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 getAnimationClientPropertyKey() {
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 );
}
}

View File

@@ -0,0 +1,185 @@
/*
* 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.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 final 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;
private AnimatedPainterSupport( int valueIndex ) {
this.valueIndex = valueIndex;
}
static void paint( AnimatedPainter painter, Component c, Graphics2D g,
int x, int y, int width, int height )
{
// get animatable component values
float[] values = painter.getAnimatableValues( c );
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, g, x, y, width, height, values );
return;
}
JComponent jc = (JComponent) c;
Object key = painter.getAnimationClientPropertyKey();
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( 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, 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.getAnimationClientPropertyKey() );
if( ass != null ) {
for( int i = 0; i < ass.length; i++ ) {
AnimatedPainterSupport as = ass[i];
as.x = x;
as.y = y;
}
}
}
}

View File

@@ -0,0 +1,438 @@
/*
* 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.FlatClientProperties;
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 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" +
"[grow,fill]",
// rows
"[]" +
"[]" +
"[]para" +
"[]" +
"[]" +
"[]" +
"[]" +
"[]para" +
"[]" +
"[]" +
"[grow]" +
"[]"));
//---- label3 ----
label3.setText("Fade:");
add(label3, "cell 0 0");
add(lineChartPanel, "cell 2 0 1 12,growy");
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(FlatClientProperties.STYLE_CLASS, "large");
add(material3TextField, "cell 0 6");
add(material3ChartColor, "cell 1 6");
//---- material4TextField ----
material4TextField.putClientProperty(FlatClientProperties.STYLE_CLASS, "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 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.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "fade" );
}
}
@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[] getAnimatableValues( 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.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "material" );
}
}
@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[] getAnimatableValues( 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.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "minimal" );
}
}
@Override
public float[] getAnimatableValues( 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;
}
}
}

View File

@@ -0,0 +1,128 @@
JFDML JFormDesigner: "8.3" 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[grow,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.LineChartPanel" ) {
name: "lineChartPanel"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 2 0 1 12,growy"
} )
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, 465 )
} )
}
}

View File

@@ -18,13 +18,14 @@ package com.formdev.flatlaf.testing;
import java.awt.Color; import java.awt.Color;
import java.awt.Component; import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D; import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import javax.swing.*; import javax.swing.*;
import com.formdev.flatlaf.icons.FlatAnimatedIcon; import com.formdev.flatlaf.icons.FlatAnimatedIcon;
import com.formdev.flatlaf.util.AnimatedIcon; import com.formdev.flatlaf.util.AnimatedIcon;
import com.formdev.flatlaf.util.ColorFunctions; import com.formdev.flatlaf.util.ColorFunctions;
import com.formdev.flatlaf.util.UIScale;
import net.miginfocom.swing.*; import net.miginfocom.swing.*;
/** /**
@@ -33,6 +34,15 @@ import net.miginfocom.swing.*;
public class FlatAnimatedIconTest public class FlatAnimatedIconTest
extends FlatTestPanel 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 = { Color.red, Color.green, Color.blue };
private static final String CHART_COLOR_KEY = "chartColor";
public static void main( String[] args ) { public static void main( String[] args ) {
SwingUtilities.invokeLater( () -> { SwingUtilities.invokeLater( () -> {
FlatTestFrame frame = FlatTestFrame.create( args, "FlatAnimatedIconTest" ); FlatTestFrame frame = FlatTestFrame.create( args, "FlatAnimatedIconTest" );
@@ -49,16 +59,36 @@ public class FlatAnimatedIconTest
radioButton3.setIcon( radioIcon ); radioButton3.setIcon( radioIcon );
checkBox1.setIcon( new AnimatedSwitchIcon() ); checkBox1.setIcon( new AnimatedSwitchIcon() );
checkBox3.setIcon( new AnimatedSwitchIconEx() );
checkBox2.setIcon( new AnimatedMinimalTestIcon() ); 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() { private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
radioButton1 = new JRadioButton(); radioButton1 = new JRadioButton();
radioButton1ChartColor = new FlatAnimatorTest.JChartColor();
lineChartPanel = new LineChartPanel();
radioButton2 = new JRadioButton(); radioButton2 = new JRadioButton();
radioButton2ChartColor = new FlatAnimatorTest.JChartColor();
radioButton3 = new JRadioButton(); radioButton3 = new JRadioButton();
radioButton3ChartColor = new FlatAnimatorTest.JChartColor();
checkBox1 = new JCheckBox(); checkBox1 = new JCheckBox();
checkBox1ChartColor = new FlatAnimatorTest.JChartColor();
checkBox3 = new JCheckBox();
checkBox2 = new JCheckBox(); checkBox2 = new JCheckBox();
checkBox2ChartColor = new FlatAnimatorTest.JChartColor();
durationLabel = new JLabel(); durationLabel = new JLabel();
durationField = new JSpinner(); durationField = new JSpinner();
@@ -66,14 +96,16 @@ public class FlatAnimatedIconTest
setLayout(new MigLayout( setLayout(new MigLayout(
"insets dialog,hidemode 3", "insets dialog,hidemode 3",
// columns // columns
"[]para" + "[]" +
"[fill]", "[fill]para" +
"[grow,fill]",
// rows // rows
"[]" + "[]" +
"[]" + "[]" +
"[]para" + "[]para" +
"[]" + "[]" +
"[]" + "[]" +
"[]" +
"[grow]" + "[grow]" +
"[]")); "[]"));
@@ -81,30 +113,40 @@ public class FlatAnimatedIconTest
radioButton1.setText("radio 1"); radioButton1.setText("radio 1");
radioButton1.setSelected(true); radioButton1.setSelected(true);
add(radioButton1, "cell 0 0"); add(radioButton1, "cell 0 0");
add(radioButton1ChartColor, "cell 1 0");
add(lineChartPanel, "cell 2 0 1 8,growy");
//---- radioButton2 ---- //---- radioButton2 ----
radioButton2.setText("radio 2"); radioButton2.setText("radio 2");
add(radioButton2, "cell 0 1"); add(radioButton2, "cell 0 1");
add(radioButton2ChartColor, "cell 1 1");
//---- radioButton3 ---- //---- radioButton3 ----
radioButton3.setText("radio 3"); radioButton3.setText("radio 3");
add(radioButton3, "cell 0 2"); add(radioButton3, "cell 0 2");
add(radioButton3ChartColor, "cell 1 2");
//---- checkBox1 ---- //---- checkBox1 ----
checkBox1.setText("switch"); checkBox1.setText("switch");
add(checkBox1, "cell 0 3"); add(checkBox1, "cell 0 3");
add(checkBox1ChartColor, "cell 1 3");
//---- checkBox3 ----
checkBox3.setText("switch ex");
add(checkBox3, "cell 0 4");
//---- checkBox2 ---- //---- checkBox2 ----
checkBox2.setText("minimal"); checkBox2.setText("minimal");
add(checkBox2, "cell 0 4"); add(checkBox2, "cell 0 5");
add(checkBox2ChartColor, "cell 1 5");
//---- durationLabel ---- //---- durationLabel ----
durationLabel.setText("Duration:"); durationLabel.setText("Duration:");
add(durationLabel, "cell 0 6 2 1"); add(durationLabel, "cell 0 7 2 1");
//---- durationField ---- //---- durationField ----
durationField.setModel(new SpinnerNumberModel(200, 100, null, 50)); durationField.setModel(new SpinnerNumberModel(200, 0, null, 50));
add(durationField, "cell 0 6 2 1"); add(durationField, "cell 0 7 2 1");
//---- buttonGroup1 ---- //---- buttonGroup1 ----
ButtonGroup buttonGroup1 = new ButtonGroup(); ButtonGroup buttonGroup1 = new ButtonGroup();
@@ -116,10 +158,17 @@ public class FlatAnimatedIconTest
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
private JRadioButton radioButton1; private JRadioButton radioButton1;
private FlatAnimatorTest.JChartColor radioButton1ChartColor;
private LineChartPanel lineChartPanel;
private JRadioButton radioButton2; private JRadioButton radioButton2;
private FlatAnimatorTest.JChartColor radioButton2ChartColor;
private JRadioButton radioButton3; private JRadioButton radioButton3;
private FlatAnimatorTest.JChartColor radioButton3ChartColor;
private JCheckBox checkBox1; private JCheckBox checkBox1;
private FlatAnimatorTest.JChartColor checkBox1ChartColor;
private JCheckBox checkBox3;
private JCheckBox checkBox2; private JCheckBox checkBox2;
private FlatAnimatorTest.JChartColor checkBox2ChartColor;
private JLabel durationLabel; private JLabel durationLabel;
private JSpinner durationField; private JSpinner durationField;
// JFormDesigner - End of variables declaration //GEN-END:variables // JFormDesigner - End of variables declaration //GEN-END:variables
@@ -146,7 +195,8 @@ public class FlatAnimatedIconTest
} }
@Override @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 ); Color color = ColorFunctions.mix( onColor, offColor, animatedValue );
// border // border
@@ -162,12 +212,17 @@ public class FlatAnimatedIconTest
float dotDiameter = DOT_SIZE * animatedValue; float dotDiameter = DOT_SIZE * animatedValue;
float xy = (SIZE - dotDiameter) / 2f; float xy = (SIZE - dotDiameter) / 2f;
g.setColor( color ); 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.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "radio" );
}
} }
@Override @Override
public float getValue( Component c ) { public float[] getAnimatableValues( Component c ) {
return ((JRadioButton)c).isSelected() ? 1 : 0; return new float[] { ((JRadioButton)c).isSelected() ? 1 : 0 };
} }
@Override @Override
@@ -178,7 +233,7 @@ public class FlatAnimatedIconTest
//---- class AnimatedSwitchIcon ------------------------------------------- //---- class AnimatedSwitchIcon -------------------------------------------
public class AnimatedSwitchIcon private class AnimatedSwitchIcon
extends FlatAnimatedIcon extends FlatAnimatedIcon
{ {
private final Color offColor = Color.lightGray; private final Color offColor = Color.lightGray;
@@ -189,22 +244,113 @@ public class FlatAnimatedIconTest
} }
@Override @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 ); Color color = ColorFunctions.mix( onColor, offColor, animatedValue );
// paint track
g.setColor( color ); g.setColor( color );
g.fillRoundRect( x, y, width, height, height, height ); g.fillRoundRect( 0, 0, width, height, height, height );
// paint thumb
int thumbSize = height - 4; int thumbSize = height - 4;
float thumbX = x + 2 + ((width - 4 - thumbSize) * animatedValue); float thumbX = 2 + ((width - 4 - thumbSize) * animatedValue);
int thumbY = y + 2; int thumbY = 2;
g.setColor( Color.white ); g.setColor( Color.white );
((Graphics2D)g).fill( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ) ); g.fill( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ) );
if( animatedValue != 0 && animatedValue != 1 ) {
Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY );
lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "switch" );
}
} }
@Override @Override
public float getValue( Component c ) { public float[] getAnimatableValues( Component c ) {
return ((AbstractButton)c).isSelected() ? 1 : 0; 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) * animatedValues[0]);
int thumbY = y + 2;
g.setColor( Color.white );
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.addValue( CHART_SWITCH_EX[i], animatedValue, Integer.MIN_VALUE, "switch ex" );
}
}
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[] getAnimatableValues( 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 @Override
@@ -223,27 +369,31 @@ public class FlatAnimatedIconTest
{ {
@Override @Override
public int getIconWidth() { public int getIconWidth() {
return 100; return UIScale.scale( 50 );
} }
@Override @Override
public int getIconHeight() { public int getIconHeight() {
return 20; return UIScale.scale( 16 );
} }
@Override @Override
public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) {
int w = getIconWidth(); float animatedValue = animatedValues[0];
int h = getIconHeight();
g.setColor( Color.red ); g.setColor( Color.red );
g.drawRect( x, y, w - 1, h - 1 ); g.drawRect( x, y, width - 1, height - 1 );
g.fillRect( x, y, Math.round( w * animatedValue ), h ); g.fillRect( x, y, Math.round( width * animatedValue ), height );
if( animatedValue != 0 && animatedValue != 1 ) {
Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY );
lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "minimal" );
}
} }
@Override @Override
public float getValue( Component c ) { public float[] getAnimatableValues( Component c ) {
return ((AbstractButton)c).isSelected() ? 1 : 0; return new float[] { ((AbstractButton)c).isSelected() ? 1 : 0 };
} }
@Override @Override

View File

@@ -1,12 +1,12 @@
JFDML JFormDesigner: "7.0.2.0.298" Java: "15" encoding: "UTF-8" JFDML JFormDesigner: "8.3" encoding: "UTF-8"
new FormModel { new FormModel {
contentType: "form/swing" contentType: "form/swing"
root: new FormRoot { root: new FormRoot {
add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "insets dialog,hidemode 3" "$layoutConstraints": "insets dialog,hidemode 3"
"$columnConstraints": "[]para[fill]" "$columnConstraints": "[][fill]para[grow,fill]"
"$rowConstraints": "[][][]para[][][grow][]" "$rowConstraints": "[][][]para[][][][grow][]"
} ) { } ) {
name: "this" name: "this"
add( new FormComponent( "javax.swing.JRadioButton" ) { add( new FormComponent( "javax.swing.JRadioButton" ) {
@@ -17,6 +17,16 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 0" "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.LineChartPanel" ) {
name: "lineChartPanel"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 2 0 1 8,growy"
} )
add( new FormComponent( "javax.swing.JRadioButton" ) { add( new FormComponent( "javax.swing.JRadioButton" ) {
name: "radioButton2" name: "radioButton2"
"text": "radio 2" "text": "radio 2"
@@ -24,6 +34,11 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1" "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" ) { add( new FormComponent( "javax.swing.JRadioButton" ) {
name: "radioButton3" name: "radioButton3"
"text": "radio 3" "text": "radio 3"
@@ -31,37 +46,58 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 2" "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" ) { add( new FormComponent( "javax.swing.JCheckBox" ) {
name: "checkBox1" name: "checkBox1"
"text": "switch" "text": "switch"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 3" "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" ) { add( new FormComponent( "javax.swing.JCheckBox" ) {
name: "checkBox2" name: "checkBox2"
"text": "minimal" "text": "minimal"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, 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" ) { add( new FormComponent( "javax.swing.JLabel" ) {
name: "durationLabel" name: "durationLabel"
"text": "Duration:" "text": "Duration:"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 6 2 1" "value": "cell 0 7 2 1"
} ) } )
add( new FormComponent( "javax.swing.JSpinner" ) { add( new FormComponent( "javax.swing.JSpinner" ) {
name: "durationField" name: "durationField"
"model": new javax.swing.SpinnerNumberModel { "model": new javax.swing.SpinnerNumberModel {
minimum: 100 minimum: 0
stepSize: 50 stepSize: 50
value: 200 value: 200
} }
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 6 2 1" "value": "cell 0 7 2 1"
} ) } )
}, new FormLayoutConstraints( null ) { }, new FormLayoutConstraints( null ) {
"location": new java.awt.Point( 0, 0 ) "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" ) { add( new FormNonVisual( "javax.swing.ButtonGroup" ) {
name: "buttonGroup1" name: "buttonGroup1"

View File

@@ -20,6 +20,8 @@ import java.awt.*;
import javax.swing.*; import javax.swing.*;
import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.Animator;
import com.formdev.flatlaf.util.CubicBezierEasing; import com.formdev.flatlaf.util.CubicBezierEasing;
import com.formdev.flatlaf.util.UIScale;
import com.formdev.flatlaf.util.Animator.Interpolator;
import net.miginfocom.swing.*; import net.miginfocom.swing.*;
/** /**
@@ -28,8 +30,13 @@ import net.miginfocom.swing.*;
public class FlatAnimatorTest public class FlatAnimatorTest
extends FlatTestPanel 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 linearAnimator;
private Animator easeInOutAnimator; private Animator easeInOutAnimator;
private Animator standardEasingAnimator;
public static void main( String[] args ) { public static void main( String[] args ) {
SwingUtilities.invokeLater( () -> { SwingUtilities.invokeLater( () -> {
@@ -40,85 +47,132 @@ public class FlatAnimatorTest
FlatAnimatorTest() { FlatAnimatorTest() {
initComponents(); initComponents();
linearChartColor.setForeground( CHART_LINEAR );
easeInOutChartColor.setForeground( CHART_EASE_IN_OUT );
standardEasingChartColor.setForeground( CHART_STANDARD_EASING );
} }
private void start() { private void start() {
startLinear(); linearAnimator = start( linearAnimator, null, linearScrollBar, CHART_LINEAR );
startEaseInOut(); easeInOutAnimator = start( easeInOutAnimator, CubicBezierEasing.EASE_IN_OUT, easeInOutScrollBar, CHART_EASE_IN_OUT );
standardEasingAnimator = start( standardEasingAnimator, CubicBezierEasing.STANDARD_EASING, standardEasingScrollBar, CHART_STANDARD_EASING );
} }
private void startLinear() { private Animator start( Animator animator, Interpolator interpolator, JScrollBar scrollBar, Color chartColor ) {
if( linearAnimator != null ) { if( animator != null ) {
linearAnimator.stop(); animator.stop();
linearAnimator.start(); animator.start();
} else { } else {
linearAnimator = new Animator( 1000, fraction -> { animator = new Animator( 1000, fraction -> {
linearScrollBar.setValue( Math.round( fraction * linearScrollBar.getMaximum() ) ); scrollBar.setValue( Math.round( fraction * scrollBar.getMaximum() ) );
lineChartPanel.addValue( chartColor, fraction, Integer.MIN_VALUE, "animator" );
} ); } );
linearAnimator.start(); animator.setInterpolator( interpolator );
} animator.start();
}
private void startEaseInOut() {
if( easeInOutAnimator != null ) {
easeInOutAnimator.stop();
easeInOutAnimator.start();
} else {
easeInOutAnimator = new Animator( 1000, fraction -> {
easeInOutScrollBar.setValue( Math.round( fraction * easeInOutScrollBar.getMaximum() ) );
} );
easeInOutAnimator.setInterpolator( CubicBezierEasing.EASE_IN_OUT );
easeInOutAnimator.start();
} }
return animator;
} }
private void initComponents() { private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
JLabel label1 = new JLabel(); linearLabel = new JLabel();
linearChartColor = new FlatAnimatorTest.JChartColor();
linearScrollBar = new JScrollBar(); linearScrollBar = new JScrollBar();
JLabel label2 = new JLabel(); easeInOutLabel = new JLabel();
easeInOutChartColor = new FlatAnimatorTest.JChartColor();
easeInOutScrollBar = new JScrollBar(); easeInOutScrollBar = new JScrollBar();
standardEasingLabel = new JLabel();
standardEasingChartColor = new FlatAnimatorTest.JChartColor();
standardEasingScrollBar = new JScrollBar();
startButton = new JButton(); startButton = new JButton();
lineChartPanel = new LineChartPanel();
//======== this ======== //======== this ========
setLayout(new MigLayout( setLayout(new MigLayout(
"ltr,insets dialog,hidemode 3", "ltr,insets dialog,hidemode 3",
// columns // columns
"[fill]" + "[fill]" +
"[fill]" +
"[grow,fill]", "[grow,fill]",
// rows // rows
"[]" + "[]" +
"[]" + "[]" +
"[]")); "[]" +
"[]para" +
"[400,grow,fill]"));
//---- label1 ---- //---- linearLabel ----
label1.setText("Linear:"); linearLabel.setText("Linear:");
add(label1, "cell 0 0"); add(linearLabel, "cell 0 0");
add(linearChartColor, "cell 1 0");
//---- linearScrollBar ---- //---- linearScrollBar ----
linearScrollBar.setOrientation(Adjustable.HORIZONTAL); linearScrollBar.setOrientation(Adjustable.HORIZONTAL);
linearScrollBar.setBlockIncrement(1); linearScrollBar.setBlockIncrement(1);
add(linearScrollBar, "cell 1 0"); add(linearScrollBar, "cell 2 0");
//---- label2 ---- //---- easeInOutLabel ----
label2.setText("Ease in out:"); easeInOutLabel.setText("Ease in out:");
add(label2, "cell 0 1"); add(easeInOutLabel, "cell 0 1");
add(easeInOutChartColor, "cell 1 1");
//---- easeInOutScrollBar ---- //---- easeInOutScrollBar ----
easeInOutScrollBar.setOrientation(Adjustable.HORIZONTAL); easeInOutScrollBar.setOrientation(Adjustable.HORIZONTAL);
easeInOutScrollBar.setBlockIncrement(1); 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 ----
startButton.setText("Start"); startButton.setText("Start");
startButton.addActionListener(e -> 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 - End of component initialization //GEN-END:initComponents
} }
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
private JLabel linearLabel;
private FlatAnimatorTest.JChartColor linearChartColor;
private JScrollBar linearScrollBar; private JScrollBar linearScrollBar;
private JLabel easeInOutLabel;
private FlatAnimatorTest.JChartColor easeInOutChartColor;
private JScrollBar easeInOutScrollBar; private JScrollBar easeInOutScrollBar;
private JLabel standardEasingLabel;
private FlatAnimatorTest.JChartColor standardEasingChartColor;
private JScrollBar standardEasingScrollBar;
private JButton startButton; private JButton startButton;
private LineChartPanel lineChartPanel;
// JFormDesigner - End of variables declaration //GEN-END:variables // JFormDesigner - End of variables declaration //GEN-END:variables
//---- 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 ) );
}
}
} }

View File

@@ -1,4 +1,4 @@
JFDML JFormDesigner: "7.0.2.0.298" Java: "14.0.2" encoding: "UTF-8" JFDML JFormDesigner: "8.3" encoding: "UTF-8"
new FormModel { new FormModel {
contentType: "form/swing" contentType: "form/swing"
@@ -8,16 +8,27 @@ new FormModel {
} }
add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "ltr,insets dialog,hidemode 3" "$layoutConstraints": "ltr,insets dialog,hidemode 3"
"$columnConstraints": "[fill][grow,fill]" "$columnConstraints": "[fill][fill][grow,fill]"
"$rowConstraints": "[][][]" "$rowConstraints": "[][][][]para[400,grow,fill]"
} ) { } ) {
name: "this" name: "this"
add( new FormComponent( "javax.swing.JLabel" ) { add( new FormComponent( "javax.swing.JLabel" ) {
name: "label1" name: "linearLabel"
"text": "Linear:" "text": "Linear:"
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 0" "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" ) { add( new FormComponent( "javax.swing.JScrollBar" ) {
name: "linearScrollBar" name: "linearScrollBar"
"orientation": 0 "orientation": 0
@@ -26,14 +37,25 @@ new FormModel {
"JavaCodeGenerator.variableLocal": false "JavaCodeGenerator.variableLocal": false
} }
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 0" "value": "cell 2 0"
} ) } )
add( new FormComponent( "javax.swing.JLabel" ) { add( new FormComponent( "javax.swing.JLabel" ) {
name: "label2" name: "easeInOutLabel"
"text": "Ease in out:" "text": "Ease in out:"
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1" "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" ) { add( new FormComponent( "javax.swing.JScrollBar" ) {
name: "easeInOutScrollBar" name: "easeInOutScrollBar"
"orientation": 0 "orientation": 0
@@ -42,7 +64,34 @@ new FormModel {
"JavaCodeGenerator.variableLocal": false "JavaCodeGenerator.variableLocal": false
} }
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, 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" ) { add( new FormComponent( "javax.swing.JButton" ) {
name: "startButton" name: "startButton"
@@ -52,11 +101,19 @@ new FormModel {
} }
addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "start", false ) ) addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "start", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 2" "value": "cell 0 3"
} )
add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) {
name: "lineChartPanel"
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 4 3 1"
} ) } )
}, new FormLayoutConstraints( null ) { }, new FormLayoutConstraints( null ) {
"location": new java.awt.Point( 0, 0 ) "location": new java.awt.Point( 0, 0 )
"size": new java.awt.Dimension( 415, 350 ) "size": new java.awt.Dimension( 625, 625 )
} ) } )
} }
} }

View File

@@ -0,0 +1,883 @@
/*
/*
* Copyright 2023 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.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.HierarchyEvent;
import java.awt.event.MouseEvent;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.swing.*;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.util.HSLColor;
import com.formdev.flatlaf.util.HiDPIUtils;
import com.formdev.flatlaf.util.UIScale;
import net.miginfocom.swing.*;
/**
* @author Karl Tauber
*/
class LineChartPanel
extends JPanel
{
LineChartPanel() {
initComponents();
lineChartScrollPane.putClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, false );
oneSecondWidthChanged();
updateChartDelayedChanged();
// clear chart on startup
addHierarchyListener( e -> {
if( (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing() )
EventQueue.invokeLater( this::clearChart );
} );
// show chart tooltips immediately and forever
ToolTipManager.sharedInstance().setInitialDelay( 0 );
ToolTipManager.sharedInstance().setDismissDelay( Integer.MAX_VALUE );
}
@Override
public void addNotify() {
super.addNotify();
// allow clearing chart with Alt+C without moving focus to button
getRootPane().registerKeyboardAction(
e -> clearChart(),
KeyStroke.getKeyStroke( "alt " + (char) clearChartButton.getMnemonic() ),
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
}
public boolean isYZeroAtTop() {
return lineChart.yZeroAtTop;
}
public void setYZeroAtTop( boolean yZeroAtTop ) {
lineChart.yZeroAtTop = yZeroAtTop;
lineChart.repaint();
}
public boolean isAsynchron() {
return lineChart.asynchron;
}
public void setAsynchron( boolean asynchron ) {
lineChart.asynchron = asynchron;
lineChart.repaint();
}
public boolean isTemporaryValueDetection() {
return lineChart.temporaryValueDetection;
}
public void setTemporaryValueDetection( boolean temporaryValueDetection ) {
lineChart.temporaryValueDetection = temporaryValueDetection;
lineChart.repaint();
}
public String getLegendYValueText() {
return yValueLabel.getText();
}
public void setLegendYValueText( String s ) {
yValueLabel.setText( s );
}
public String getLegend1Text() {
return legend1Label.getText();
}
public void setLegend1Text( String s ) {
legend1Label.setText( s );
}
public String getLegend2Text() {
return legend2Label.getText();
}
public void setLegend2Text( String s ) {
legend2Label.setText( s );
}
public int getOneSecondWidth() {
return oneSecondWidthSlider.getValue();
}
public void setOneSecondWidth( int oneSecondWidth ) {
oneSecondWidthSlider.setValue( oneSecondWidth );
}
public boolean isUpdateChartDelayed() {
return updateChartDelayedCheckBox.isSelected();
}
public void setUpdateChartDelayed( boolean updateChartDelayed ) {
updateChartDelayedCheckBox.setSelected( updateChartDelayed );
updateChartDelayedChanged();
}
void addValue( Color chartColor, double value, int ivalue, String name ) {
lineChart.addValue( chartColor, value, ivalue, null, false, name );
}
void addValueWithDot( Color chartColor, double value, int ivalue, Color dotColor, String name ) {
if( dotColor == null )
dotColor = chartColor;
lineChart.addValue( chartColor, value, ivalue, dotColor, false, name );
}
void addDot( Color chartColor, double value, int ivalue, Color dotColor, String name ) {
if( dotColor == null )
dotColor = chartColor;
lineChart.addValue( chartColor, value, ivalue, dotColor, true, name );
}
void addMethodHighlight( String classAndMethod, String highlightColor ) {
lineChart.methodHighlightMap.put( classAndMethod, highlightColor );
}
private void oneSecondWidthChanged() {
int oneSecondWidth = oneSecondWidthSlider.getValue();
int msPerLineX =
oneSecondWidth <= 2000 ? 100 :
oneSecondWidth <= 4000 ? 50 :
oneSecondWidth <= 8000 ? 25 :
10;
lineChart.oneSecondWidth = oneSecondWidth;
lineChart.msPerLineX = msPerLineX;
lineChart.revalidate();
lineChart.repaint();
if( xLabelText == null )
xLabelText = xLabel.getText();
xLabel.setText( MessageFormat.format( xLabelText, msPerLineX ) );
}
private String xLabelText;
private void updateChartDelayedChanged() {
lineChart.updateDelayed = updateChartDelayedCheckBox.isSelected();
}
private void clearChart() {
lineChart.clear();
}
private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off
lineChartScrollPane = new JScrollPane();
lineChart = new LineChartPanel.LineChart();
JPanel legendPanel = new JPanel();
xLabel = new JLabel();
legend1Label = new JLabel();
JLabel yLabel = new JLabel();
yValueLabel = new JLabel();
JLabel yLabel2 = new JLabel();
JPanel hSpacer1 = new JPanel(null);
legend2Label = new JLabel();
JLabel oneSecondWidthLabel = new JLabel();
oneSecondWidthSlider = new JSlider();
updateChartDelayedCheckBox = new JCheckBox();
clearChartButton = new JButton();
//======== this ========
setLayout(new MigLayout(
"hidemode 3",
// columns
"[grow,fill]",
// rows
"[100:300,grow,fill]" +
"[]"));
//======== lineChartScrollPane ========
{
lineChartScrollPane.setViewportView(lineChart);
}
add(lineChartScrollPane, "cell 0 0");
//======== legendPanel ========
{
legendPanel.setLayout(new MigLayout(
"insets 0,hidemode 3,gapy 0",
// columns
"[fill]para" +
"[fill]",
// rows
"[]" +
"[]"));
//---- xLabel ----
xLabel.setText("X: time ({0}ms per line)");
legendPanel.add(xLabel, "cell 0 0");
legendPanel.add(legend1Label, "cell 1 0");
//---- yLabel ----
yLabel.setText("Y: ");
legendPanel.add(yLabel, "cell 0 1,gapx 0 0");
//---- yValueLabel ----
yValueLabel.setText("value");
legendPanel.add(yValueLabel, "cell 0 1,gapx 0 0");
//---- yLabel2 ----
yLabel2.setText(" (10% per line)");
legendPanel.add(yLabel2, "cell 0 1,gapx 0 0");
legendPanel.add(hSpacer1, "cell 0 1,growx");
legendPanel.add(legend2Label, "cell 1 1");
}
add(legendPanel, "cell 0 1");
//---- oneSecondWidthLabel ----
oneSecondWidthLabel.setText("Scale X:");
oneSecondWidthLabel.setDisplayedMnemonic('A');
oneSecondWidthLabel.setLabelFor(oneSecondWidthSlider);
add(oneSecondWidthLabel, "cell 0 1,alignx right,growx 0");
//---- oneSecondWidthSlider ----
oneSecondWidthSlider.setMinimum(100);
oneSecondWidthSlider.setMaximum(10000);
oneSecondWidthSlider.setSnapToTicks(true);
oneSecondWidthSlider.setMajorTickSpacing(100);
oneSecondWidthSlider.setValue(500);
oneSecondWidthSlider.addChangeListener(e -> oneSecondWidthChanged());
add(oneSecondWidthSlider, "cell 0 1,alignx right,growx 0");
//---- updateChartDelayedCheckBox ----
updateChartDelayedCheckBox.setText("Update chart delayed");
updateChartDelayedCheckBox.setMnemonic('P');
updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged());
add(updateChartDelayedCheckBox, "cell 0 1,alignx right,growx 0");
//---- clearChartButton ----
clearChartButton.setText("Clear Chart");
clearChartButton.setMnemonic('C');
clearChartButton.addActionListener(e -> clearChart());
add(clearChartButton, "cell 0 1,alignx right,growx 0");
// JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on
}
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off
private JScrollPane lineChartScrollPane;
private LineChartPanel.LineChart lineChart;
private JLabel xLabel;
private JLabel legend1Label;
private JLabel yValueLabel;
private JLabel legend2Label;
private JSlider oneSecondWidthSlider;
private JCheckBox updateChartDelayedCheckBox;
private JButton clearChartButton;
// JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on
//---- class LineChart ----------------------------------------------------
private static class LineChart
extends JComponent
implements Scrollable
{
private static final int UPDATE_DELAY_MS = 20;
private static final int NEW_SEQUENCE_TIME_LAG_MS = 500;
private static final int NEW_SEQUENCE_GAP_MS = 100;
private static final int HIT_OFFSET = 4;
private static final boolean TEST = false;
// asynchron means that chart for each color starts at x=0
private boolean asynchron;
private boolean temporaryValueDetection;
private boolean yZeroAtTop;
private int oneSecondWidth = 500;
private int msPerLineX = 100;
private final HashMap<String, String> methodHighlightMap = new HashMap<>();
private static class Data {
final double value;
final int ivalue;
final Color chartColor;
final Color dotColor;
final boolean dotOnly;
final long time; // in milliseconds
final String name;
final Exception stack;
Data( double value, int ivalue, Color chartColor, Color dotColor,
boolean dotOnly, long time, String name, Exception stack )
{
this.value = value;
this.ivalue = ivalue;
this.chartColor = chartColor;
this.dotColor = dotColor;
this.dotOnly = dotOnly;
this.time = time;
this.name = name;
this.stack = stack;
}
@Override
public String toString() {
// for debugging
return "value=" + value + ", ivalue=" + ivalue + ", dotColor=" + dotColor
+ ", dotOnly=" + dotOnly + ", time=" + time + ", name=" + name;
}
}
private final List<Data> syncChartData = new ArrayList<>();
private final Map<Color, List<Data>> asyncColor2dataMap = new HashMap<>();
private final Timer repaintTime;
private Color lastUsedChartColor;
private boolean updateDelayed;
private final List<Point> lastPoints = new ArrayList<>();
private final List<Data> lastDatas = new ArrayList<>();
private double lastSystemScaleFactor = 1;
private String lastToolTipPrinted;
LineChart() {
repaintTime = new Timer( UPDATE_DELAY_MS, e -> repaintAndRevalidate() );
repaintTime.setRepeats( false );
ToolTipManager.sharedInstance().registerComponent( this );
if( TEST )
initTestData();
}
void addValue( Color chartColor, double value, int ivalue, Color dotColor, boolean dotOnly, String name ) {
if( TEST )
return;
List<Data> chartData = asyncColor2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() );
Data data = new Data( value, ivalue, chartColor, dotColor, dotOnly, System.nanoTime() / 1_000_000, name, new Exception() );
if( asynchron )
chartData.add( data );
else
syncChartData.add( data );
lastUsedChartColor = chartColor;
if( updateDelayed ) {
repaintTime.stop();
repaintTime.start();
} else
repaintAndRevalidate();
}
void clear() {
if( TEST ) {
repaint();
return;
}
syncChartData.clear();
asyncColor2dataMap.clear();
lastUsedChartColor = null;
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::paintAt1x );
} finally {
g2.dispose();
}
}
private void paintAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
FlatUIUtils.setRenderingHints( g );
int oneSecondWidth = (int) (UIScale.scale( this.oneSecondWidth ) * scaleFactor);
int seqGapWidth = (oneSecondWidth * NEW_SEQUENCE_GAP_MS) / 1000;
int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * 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 perLineXWidth = Math.round( (oneSecondWidth / 1000f) * msPerLineX );
for( int i = 1, xv = perLineXWidth; xv < width; xv += perLineXWidth, i++ ) {
g.setColor( (i % 5 != 0) ? lineColor : lineColor2 );
g.drawLine( xv, 0, xv, height );
}
lastPoints.clear();
lastDatas.clear();
lastSystemScaleFactor = scaleFactor;
// paint lines
for( Map.Entry<Color, List<Data>> e : asyncColor2dataMap.entrySet() ) {
List<Data> chartData = asynchron ? e.getValue() : syncChartData;
Color chartColor = e.getKey();
if( FlatLaf.isLafDark() )
chartColor = new HSLColor( chartColor ).adjustTone( 50 );
Color temporaryValueColor = fade( chartColor, FlatLaf.isLafDark() ? 0.7f : 0.3f );
Color dataPointColor = fade( chartColor, FlatLaf.isLafDark() ? 0.6f : 0.2f );
// sequence start time and x coordinate
long seqStartTime = 0;
int seqStartX = 0;
// "previous" data point time and x coordinate (used for "new sequence" detection)
long ptime = Long.MIN_VALUE;
int px = 0;
// "line" data point x/y coordinates
int lx = -1;
int ly = -1;
boolean isTemporaryValue = false;
int lastTemporaryValueIndex = -1;
int size = chartData.size();
for( int i = 0; i < size; i++ ) {
Data data = chartData.get( i );
boolean useData = (data.chartColor == chartColor);
// start new sequence if there is a larger time gap to previous data point
boolean newSeq = (data.time > ptime + NEW_SEQUENCE_TIME_LAG_MS);
ptime = data.time;
if( newSeq ) {
// start new sequence
seqStartTime = data.time;
seqStartX = (i > 0) ? px + seqGapWidth : 0;
px = seqStartX;
lx = -1;
ly = -1;
isTemporaryValue = false;
}
// x/y coordinates of current data point
int dx = (int) (seqStartX + (((data.time - seqStartTime) / 1000.) * oneSecondWidth));
int dy = (int) ((height - 1) * data.value);
if( !yZeroAtTop )
dy = height - 1 - dy;
// remember x coordinate for "new sequence" detection
px = dx;
if( !useData )
continue;
// remember data point for tooltip
lastPoints.add( new Point( dx, dy ) );
lastDatas.add( data );
// paint rectangle to indicate data point
g.setColor( dataPointColor );
g.drawRect( dx - hitOffset, dy - hitOffset, hitOffset * 2, hitOffset * 2 );
// paint dot
if( data.dotColor != null ) {
int s1 = (int) Math.round( UIScale.scale( 1 ) * scaleFactor );
int s3 = (int) Math.round( UIScale.scale( 3 ) * scaleFactor );
g.setColor( data.dotColor );
g.fillRect( dx - s1, dy - s1, s3, s3 );
if( data.dotOnly )
continue;
}
// start of line?
if( lx < 0 ) {
// remember x/y coordinates for first line
lx = dx;
ly = dy;
continue;
}
if( isTemporaryValue && i > lastTemporaryValueIndex )
isTemporaryValue = false;
// draw line in sequence
g.setColor( isTemporaryValue ? temporaryValueColor : chartColor );
g.drawLine( lx, ly, dx, dy );
// remember x/y coordinates for next line
lx = dx;
ly = dy;
// check next data points for "temporary" value(s)
if( temporaryValueDetection && !isTemporaryValue ) {
// one or two values between two equal values are considered "temporary",
// which means that they are the target value for the following scroll animation
int stage = 0;
for( int j = i + 1; j < size && stage <= 2 && !isTemporaryValue; j++ ) {
Data nextData = chartData.get( j );
if( nextData.dotOnly )
continue; // ignore dots
// check whether next data point is within 10 milliseconds
if( nextData.time > data.time + 10 )
break;
if( stage >= 1 && stage <= 2 && nextData.value == data.value ) {
isTemporaryValue = true;
lastTemporaryValueIndex = j;
}
stage++;
}
}
}
}
}
private int chartWidth() {
int width = 0;
if( asynchron ) {
for( List<Data> chartData : asyncColor2dataMap.values() )
width = Math.max( width, chartWidth( chartData, null ) );
} else
width = Math.max( width, chartWidth( syncChartData, null ) );
return width;
}
private int chartWidth( List<Data> chartData, int[] lastSeqX ) {
long seqTime = 0;
int seqX = 0;
long ptime = 0;
int px = 0;
int oneSecondWidth = UIScale.scale( this.oneSecondWidth );
int seqGapWidth = (oneSecondWidth * NEW_SEQUENCE_GAP_MS) / 1000;
int size = chartData.size();
for( int i = 0; i < size; i++ ) {
Data data = chartData.get( i );
if( data.time > ptime + NEW_SEQUENCE_TIME_LAG_MS ) {
// start new sequence
seqTime = data.time;
seqX = (i > 0) ? px + seqGapWidth : 0;
px = seqX;
} else {
// line in sequence
int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth ));
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 UIScale.scale( oneSecondWidth );
}
@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;
}
@Override
public String getToolTipText( MouseEvent e ) {
int x = (int) Math.round( e.getX() * lastSystemScaleFactor );
int y = (int) Math.round( e.getY() * lastSystemScaleFactor );
int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * lastSystemScaleFactor );
StringBuilder buf = null;
int pointsCount = lastPoints.size();
for( int i = 0; i < pointsCount; i++ ) {
Point pt = lastPoints.get( i );
// check X/Y coordinates
if( x < pt.x - hitOffset || x > pt.x + hitOffset ||
y < pt.y - hitOffset || y > pt.y + hitOffset )
continue;
if( buf == null ) {
buf = new StringBuilder( 5000 );
buf.append( "<html>" );
}
Data data = lastDatas.get( i );
buf.append( "<h2>" );
if( data.dotOnly )
buf.append( "DOT: " );
buf.append( data.name );
if( data.ivalue != Integer.MIN_VALUE )
buf.append( ' ' ).append( data.ivalue );
buf.append( " (" ).append( String.format( "%.3f", data.value ) ).append( ')' );
buf.append( "</h2>" );
StackTraceElement[] stackTrace = data.stack.getStackTrace();
for( int j = 0; j < stackTrace.length; j++ ) {
StackTraceElement stackElement = stackTrace[j];
String className = stackElement.getClassName();
String methodName = stackElement.getMethodName();
String classAndMethod = className + '.' + methodName;
// ignore methods from this class
if( className.startsWith( LineChartPanel.class.getName() ) )
continue;
int repeatCount = 0;
for( int k = j + 1; k < stackTrace.length; k++ ) {
if( !stackElement.equals( stackTrace[k] ) )
break;
repeatCount++;
}
j += repeatCount;
String highlight = methodHighlightMap.get( classAndMethod );
if( highlight == null )
highlight = methodHighlightMap.get( className );
if( highlight == null )
highlight = methodHighlightMap.get( methodName );
if( highlight != null )
buf.append( "<span color=\"" ).append( highlight ).append( "\">" );
// append method
buf.append( className )
.append( ".<b>" )
.append( methodName )
.append( "</b>" );
if( highlight != null )
buf.append( "</span>" );
// append source
buf.append( " <span color=\"#888888\">" );
if( stackElement.getFileName() != null ) {
buf.append( '(' );
buf.append( stackElement.getFileName() );
if( stackElement.getLineNumber() >= 0 )
buf.append( ':' ).append( stackElement.getLineNumber() );
buf.append( ')' );
} else
buf.append( "(Unknown Source)" );
buf.append( "</span>" );
// append repeat count
if( repeatCount > 0 )
buf.append( " <b>" ).append( repeatCount + 1 ).append( "x</b>" );
buf.append( "<br>" );
// break at some methods to make stack smaller
if( classAndMethod.equals( "java.awt.event.InvocationEvent.dispatch" ) ||
classAndMethod.equals( "java.awt.Component.processMouseEvent" ) ||
classAndMethod.equals( "java.awt.Component.processMouseWheelEvent" ) ||
classAndMethod.equals( "java.awt.Component.processMouseMotionEvent" ) ||
classAndMethod.equals( "javax.swing.JComponent.processKeyBinding" ) ||
classAndMethod.equals( "javax.swing.JComponent.paintComponent" ) ||
classAndMethod.equals( "com.formdev.flatlaf.util.Animator.timingEvent" ) )
break;
}
buf.append( "..." );
}
if( buf == null )
return null;
buf.append( "<html>" );
String toolTip = buf.toString();
// print to console
if( !Objects.equals( toolTip, lastToolTipPrinted ) ) {
lastToolTipPrinted = toolTip;
System.out.println( toolTip
.replace( "<br>", "\n" )
.replace( "<h2>", "\n---- " )
.replace( "</h2>", " ----\n" )
.replaceAll( "<[^>]+>", "" ) );
}
return buf.toString();
}
private void initTestData() {
// asynchron = true;
addTestSimpleLine( Color.red, 0.0, "red" );
addTestSimpleLine( Color.green, 0.1, "green" );
addTestSimpleLine( Color.blue, 0.2, "blue" );
addTestSimpleLine( Color.magenta, 0.3, "magenta" );
addTestMiddleDotOnly( Color.red, 0.0, "red" );
addTestMiddleDotOnly( Color.green, 0.1, "green" );
addTestMiddleDotOnly( Color.blue, 0.2, "blue" );
addTestMiddleDotOnly( Color.magenta, 0.3, "magenta" );
addTestLeadingDotOnly( Color.red, 0.0, "red" );
addTestLeadingDotOnly( Color.green, 0.1, "green" );
addTestLeadingDotOnly( Color.blue, 0.2, "blue" );
addTestLeadingDotOnly( Color.magenta, 0.3, "magenta" );
addTestTrailingDotOnly( Color.red, 0.0, "red" );
addTestTrailingDotOnly( Color.green, 0.1, "green" );
addTestTrailingDotOnly( Color.blue, 0.2, "blue" );
addTestTrailingDotOnly( Color.magenta, 0.3, "magenta" );
addTestSingleData( Color.red, 0.0, "red" );
addTestSingleData( Color.green, 0.1, "green" );
addTestSingleData( Color.blue, 0.2, "blue" );
addTestSingleData( Color.magenta, 0.3, "magenta" );
temporaryValueDetection = true;
addTestWithTemporaryValues( Color.red, 0.0, "red" );
addTestWithTemporaryValues( Color.green, 0.1, "green" );
addTestWithTemporaryValues( Color.blue, 0.2, "blue" );
addTestWithTemporaryValues( Color.magenta, 0.3, "magenta" );
}
private void addTestSimpleLine( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.4, null, false, name );
testTime += 1000;
}
private void addTestMiddleDotOnly( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 20, chartColor, baseValue + 0.3, chartColor, true, name );
addTestValue( 30, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 20, chartColor, baseValue + 0.05, chartColor, true, name );
addTestValue( 30, chartColor, baseValue + 0.4, null, false, name );
testTime += 1000;
}
private void addTestLeadingDotOnly( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.05, chartColor, true, name );
addTestValue( 20, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 30, chartColor, baseValue + 0.4, null, false, name );
testTime += 1000;
}
private void addTestTrailingDotOnly( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 30, chartColor, baseValue + 0.4, null, false, name );
addTestValue( 20, chartColor, baseValue + 0.05, chartColor, true, name );
testTime += 1000;
}
private void addTestSingleData( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.15, chartColor, false, name );
testTime += 1000;
}
private void addTestWithTemporaryValues( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 5, chartColor, baseValue + 0.4, null, false, name );
addTestValue( 5, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 40, chartColor, baseValue + 0.3, null, false, name );
testTime += 1000;
}
private void addTestValue( int timeDelta, Color chartColor, double value, Color dotColor, boolean dotOnly, String name ) {
testTime += timeDelta;
List<Data> chartData = asyncColor2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() );
Data data = new Data( value, testIValue++, chartColor, dotColor, dotOnly, testTime, name, new Exception() );
if( asynchron )
chartData.add( data );
else
syncChartData.add( data );
lastUsedChartColor = chartColor;
}
private int testIValue;
private long testTime;
//TODO remove and use ColorFunctions.fade() when merging to main
private static Color fade( Color color, float amount ) {
int newAlpha = Math.round( 255 * amount );
return new Color( (color.getRGB() & 0xffffff) | (newAlpha << 24), true );
}
}
}

View File

@@ -0,0 +1,123 @@
JFDML JFormDesigner: "8.3" encoding: "UTF-8"
new FormModel {
contentType: "form/swing"
root: new FormRoot {
add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "hidemode 3"
"$columnConstraints": "[grow,fill]"
"$rowConstraints": "[100:300,grow,fill][]"
} ) {
name: "this"
add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
name: "lineChartScrollPane"
add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel$LineChart" ) {
name: "lineChart"
} )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 0"
} )
add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "insets 0,hidemode 3,gapy 0"
"$columnConstraints": "[fill]para[fill]"
"$rowConstraints": "[][]"
} ) {
name: "legendPanel"
auxiliary() {
"JavaCodeGenerator.variableLocal": true
}
add( new FormComponent( "javax.swing.JLabel" ) {
name: "xLabel"
"text": "X: time ({0}ms per line)"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 0"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "legend1Label"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 0"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "yLabel"
"text": "Y: "
auxiliary() {
"JavaCodeGenerator.variableLocal": true
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1,gapx 0 0"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "yValueLabel"
"text": "value"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1,gapx 0 0"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "yLabel2"
"text": " (10% per line)"
auxiliary() {
"JavaCodeGenerator.variableLocal": true
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1,gapx 0 0"
} )
add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) {
name: "hSpacer1"
auxiliary() {
"JavaCodeGenerator.variableLocal": true
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1,growx"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "legend2Label"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 1"
} )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "oneSecondWidthLabel"
"text": "Scale X:"
"displayedMnemonic": 65
"labelFor": new FormReference( "oneSecondWidthSlider" )
auxiliary() {
"JavaCodeGenerator.variableLocal": true
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1,alignx right,growx 0"
} )
add( new FormComponent( "javax.swing.JSlider" ) {
name: "oneSecondWidthSlider"
"minimum": 100
"maximum": 10000
"snapToTicks": true
"majorTickSpacing": 100
"value": 500
addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "oneSecondWidthChanged", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1,alignx right,growx 0"
} )
add( new FormComponent( "javax.swing.JCheckBox" ) {
name: "updateChartDelayedCheckBox"
"text": "Update chart delayed"
"mnemonic": 80
addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 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,alignx right,growx 0"
} )
}, new FormLayoutConstraints( null ) {
"location": new java.awt.Point( 0, 0 )
"size": new java.awt.Dimension( 880, 300 )
} )
}
}