Skip to content

Commit b2245e2

Browse files
committed
AnimatedBorder added (for future animations) (issue #66)
1 parent 13a6b92 commit b2245e2

File tree

3 files changed

+580
-0
lines changed

3 files changed

+580
-0
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright 2021 FormDev Software GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.formdev.flatlaf.util;
18+
19+
import java.awt.Component;
20+
import java.awt.Graphics;
21+
import javax.swing.JComponent;
22+
import javax.swing.border.Border;
23+
import com.formdev.flatlaf.util.Animator.Interpolator;
24+
25+
/**
26+
* Border that automatically animates painting on component value changes.
27+
* <p>
28+
* {@link #getValue(Component)} returns the value of the component.
29+
* If the value changes, then {@link #paintBorderAnimated(Component, Graphics, int, int, int, int, float)}
30+
* is invoked multiple times with animated value (from old value to new value).
31+
* <p>
32+
* Example for an animated border:
33+
* <pre>
34+
* private class AnimatedMinimalTestBorder
35+
* implements AnimatedBorder
36+
* {
37+
* &#64;Override
38+
* public void paintBorderAnimated( Component c, Graphics g, int x, int y, int width, int height, float animatedValue ) {
39+
* int lh = UIScale.scale( 2 );
40+
*
41+
* g.setColor( Color.blue );
42+
* g.fillRect( x, y + height - lh, Math.round( width * animatedValue ), lh );
43+
* }
44+
*
45+
* &#64;Override
46+
* public float getValue( Component c ) {
47+
* return c.isFocusOwner() ? 1 : 0;
48+
* }
49+
*
50+
* &#64;Override
51+
* public Insets getBorderInsets( Component c ) {
52+
* return UIScale.scale( new Insets( 4, 4, 4, 4 ) );
53+
* }
54+
*
55+
* &#64;Override public boolean isBorderOpaque() { return false; }
56+
* }
57+
*
58+
* // sample usage
59+
* JTextField textField = new JTextField();
60+
* textField.setBorder( new AnimatedMinimalTestBorder() );
61+
* </pre>
62+
*
63+
* Animation works only if the component passed to {@link #paintBorder(Component, Graphics, int, int, int, int)}
64+
* is a instance of {@link JComponent}.
65+
* A client property is set on the component to store the animation state.
66+
*
67+
* @author Karl Tauber
68+
*/
69+
public interface AnimatedBorder
70+
extends Border
71+
{
72+
@Override
73+
default void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) {
74+
AnimationSupport.paintBorder( this, c, g, x, y, width, height );
75+
}
76+
77+
/**
78+
* Paints the border for the given animated value.
79+
*
80+
* @param c the component that this border belongs to
81+
* @param g the graphics context
82+
* @param x the x coordinate of the border
83+
* @param y the y coordinate of the border
84+
* @param width the width coordinate of the border
85+
* @param height the height coordinate of the border
86+
* @param animatedValue the animated value, which is either equal to what {@link #getValue(Component)}
87+
* returned, or somewhere between the previous value and the latest value
88+
* that {@link #getValue(Component)} returned
89+
*/
90+
void paintBorderAnimated( Component c, Graphics g, int x, int y, int width, int height, float animatedValue );
91+
92+
/**
93+
* Gets the value of the component.
94+
* <p>
95+
* This can be any value and depends on the component.
96+
* If the value changes, then this class animates from the old value to the new one.
97+
* <p>
98+
* For a text field this could be {@code 0} for not focused and {@code 1} for focused.
99+
*/
100+
float getValue( Component c );
101+
102+
/**
103+
* Returns whether animation is enabled for this border (default is {@code true}).
104+
*/
105+
default boolean isAnimationEnabled() {
106+
return true;
107+
}
108+
109+
/**
110+
* Returns the duration of the animation in milliseconds (default is 150).
111+
*/
112+
default int getAnimationDuration() {
113+
return 150;
114+
}
115+
116+
/**
117+
* Returns the resolution of the animation in milliseconds (default is 10).
118+
* Resolution is the amount of time between timing events.
119+
*/
120+
default int getAnimationResolution() {
121+
return 10;
122+
}
123+
124+
/**
125+
* Returns the interpolator for the animation.
126+
* Default is {@link CubicBezierEasing#STANDARD_EASING}.
127+
*/
128+
default Interpolator getAnimationInterpolator() {
129+
return CubicBezierEasing.STANDARD_EASING;
130+
}
131+
132+
/**
133+
* Returns the client property key used to store the animation support.
134+
*/
135+
default Object getClientPropertyKey() {
136+
return getClass();
137+
}
138+
139+
//---- class AnimationSupport ---------------------------------------------
140+
141+
/**
142+
* Animation support class that stores the animation state and implements the animation.
143+
*/
144+
class AnimationSupport
145+
{
146+
private float startValue;
147+
private float targetValue;
148+
private float animatedValue;
149+
private float fraction;
150+
151+
private Animator animator;
152+
153+
// last bounds of the border needed to repaint while animating
154+
private int x;
155+
private int y;
156+
private int width;
157+
private int height;
158+
159+
public static void paintBorder( AnimatedBorder border, Component c, Graphics g,
160+
int x, int y, int width, int height )
161+
{
162+
if( !isAnimationEnabled( border, c ) ) {
163+
// paint without animation if animation is disabled or
164+
// component is not a JComponent and therefore does not support
165+
// client properties, which are required to keep animation state
166+
paintBorderImpl( border, c, g, x, y, width, height, null );
167+
return;
168+
}
169+
170+
JComponent jc = (JComponent) c;
171+
Object key = border.getClientPropertyKey();
172+
AnimationSupport as = (AnimationSupport) jc.getClientProperty( key );
173+
if( as == null ) {
174+
// painted first time --> do not animate, but remember current component value
175+
as = new AnimationSupport();
176+
as.startValue = as.targetValue = as.animatedValue = border.getValue( c );
177+
jc.putClientProperty( key, as );
178+
} else {
179+
// get component value
180+
float value = border.getValue( c );
181+
182+
if( value != as.targetValue ) {
183+
// value changed --> (re)start animation
184+
185+
if( as.animator == null ) {
186+
// create animator
187+
AnimationSupport as2 = as;
188+
as.animator = new Animator( border.getAnimationDuration(), fraction -> {
189+
// check whether component was removed while animation is running
190+
if( !c.isDisplayable() ) {
191+
as2.animator.stop();
192+
return;
193+
}
194+
195+
// compute animated value
196+
as2.animatedValue = as2.startValue + ((as2.targetValue - as2.startValue) * fraction);
197+
as2.fraction = fraction;
198+
199+
// repaint border
200+
c.repaint( as2.x, as2.y, as2.width, as2.height );
201+
}, () -> {
202+
as2.startValue = as2.animatedValue = as2.targetValue;
203+
as2.animator = null;
204+
} );
205+
}
206+
207+
if( as.animator.isRunning() ) {
208+
// if animation is still running, restart it from the current
209+
// animated value to the new target value with reduced duration
210+
as.animator.cancel();
211+
int duration2 = (int) (border.getAnimationDuration() * as.fraction);
212+
if( duration2 > 0 )
213+
as.animator.setDuration( duration2 );
214+
as.startValue = as.animatedValue;
215+
} else {
216+
// new animation
217+
as.animator.setDuration( border.getAnimationDuration() );
218+
as.animator.setResolution( border.getAnimationResolution() );
219+
as.animator.setInterpolator( border.getAnimationInterpolator() );
220+
221+
as.animatedValue = as.startValue;
222+
}
223+
224+
as.targetValue = value;
225+
as.animator.start();
226+
}
227+
}
228+
229+
as.x = x;
230+
as.y = y;
231+
as.width = width;
232+
as.height = height;
233+
234+
paintBorderImpl( border, c, g, x, y, width, height, as );
235+
}
236+
237+
private static void paintBorderImpl( AnimatedBorder border, Component c, Graphics g,
238+
int x, int y, int width, int height, AnimationSupport as )
239+
{
240+
float value = (as != null) ? as.animatedValue : border.getValue( c );
241+
border.paintBorderAnimated( c, g, x, y, width, height, value );
242+
}
243+
244+
private static boolean isAnimationEnabled( AnimatedBorder border, Component c ) {
245+
return Animator.useAnimation() && border.isAnimationEnabled() && c instanceof JComponent;
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)