vito
10 months ago
8 changed files with 461 additions and 103 deletions
@ -0,0 +1,112 @@ |
|||||||
|
package com.fine.theme.light.ui; |
||||||
|
|
||||||
|
import com.fine.theme.icon.LazyIcon; |
||||||
|
import com.fine.theme.utils.AnimatedPainter; |
||||||
|
import com.fine.theme.utils.FineUIScale; |
||||||
|
import com.formdev.flatlaf.ui.FlatUIUtils; |
||||||
|
import com.fr.design.foldablepane.HeaderPane; |
||||||
|
|
||||||
|
import javax.swing.Icon; |
||||||
|
import javax.swing.JComponent; |
||||||
|
import javax.swing.UIManager; |
||||||
|
import javax.swing.plaf.ComponentUI; |
||||||
|
import javax.swing.plaf.PanelUI; |
||||||
|
import java.awt.AlphaComposite; |
||||||
|
import java.awt.Component; |
||||||
|
import java.awt.Font; |
||||||
|
import java.awt.FontMetrics; |
||||||
|
import java.awt.Graphics; |
||||||
|
import java.awt.Graphics2D; |
||||||
|
import java.awt.Insets; |
||||||
|
import java.awt.geom.AffineTransform; |
||||||
|
|
||||||
|
/** |
||||||
|
* 标题组件UI |
||||||
|
* |
||||||
|
* @author vito |
||||||
|
* @since 11.0 |
||||||
|
* Created on 2024/1/29 |
||||||
|
*/ |
||||||
|
public class FineHeaderPaneUI extends PanelUI implements AnimatedPainter { |
||||||
|
|
||||||
|
private Icon triangleRight; |
||||||
|
|
||||||
|
private static final int ICON_FIX = -2; |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建UI |
||||||
|
* |
||||||
|
* @param c 组件 |
||||||
|
* @return ComponentUI |
||||||
|
*/ |
||||||
|
public static ComponentUI createUI(JComponent c) { |
||||||
|
return new FineHeaderPaneUI(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void installUI(JComponent c) { |
||||||
|
super.installUI(c); |
||||||
|
triangleRight = new LazyIcon("triangle_right"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void uninstallUI(JComponent c) { |
||||||
|
super.uninstallUI(c); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void paint(Graphics g, JComponent c) { |
||||||
|
paintWithAnimation(c, g, c.getX(), c.getY(), c.getWidth(), c.getHeight()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void paintAnimated(Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues) { |
||||||
|
Graphics2D g2d = (Graphics2D) g.create(); |
||||||
|
HeaderPane headerPane = (HeaderPane) c; |
||||||
|
g2d.setColor(c.getBackground()); |
||||||
|
if (headerPane.isPressed()) { |
||||||
|
g2d.setColor(UIManager.getColor("Button.pressedBackground")); |
||||||
|
} |
||||||
|
Insets insets = headerPane.getInsets(); |
||||||
|
g2d.fillRect(0, insets.top / 2, |
||||||
|
headerPane.getWidth(), headerPane.getHeight() - (insets.top + insets.bottom) / 2); |
||||||
|
|
||||||
|
int iconY = (headerPane.getHeight() - triangleRight.getIconHeight()) / 2; |
||||||
|
|
||||||
|
|
||||||
|
// 动起来,旋转角度(弧度),动画帧绘制开始
|
||||||
|
double rotationAngle = Math.toRadians(90 * animatedValues[0]); |
||||||
|
|
||||||
|
// 设置旋转中心
|
||||||
|
AffineTransform oldTransform = g2d.getTransform(); |
||||||
|
// 折叠面板需要icon显示左边缘对齐,fix一下
|
||||||
|
g2d.rotate(rotationAngle, FineUIScale.scale(ICON_FIX) + triangleRight.getIconWidth() / 2.0, |
||||||
|
iconY + triangleRight.getIconHeight() / 2.0); |
||||||
|
// 绘制旋转后的正方形
|
||||||
|
triangleRight.paintIcon(c, g2d, FineUIScale.scale(ICON_FIX), iconY); |
||||||
|
// 恢复原始的变换
|
||||||
|
g2d.setTransform(oldTransform); |
||||||
|
// ----- 动画帧绘制结束 -----
|
||||||
|
|
||||||
|
g2d.setFont(c.getFont().deriveFont(Font.BOLD)); |
||||||
|
g2d.setPaint(c.getForeground()); |
||||||
|
g2d.setComposite(AlphaComposite.SrcOver.derive(c.getForeground().getAlpha() / 255f)); |
||||||
|
|
||||||
|
FontMetrics metrics = g2d.getFontMetrics(); |
||||||
|
int ascent = metrics.getAscent(); |
||||||
|
int descent = metrics.getDescent(); |
||||||
|
|
||||||
|
float titleX = triangleRight.getIconWidth() + FineUIScale.scale(ICON_FIX) |
||||||
|
+ FineUIScale.scale(UIManager.getInt("ExpandablePane.HeaderPane.hGap")); |
||||||
|
float titleY = (headerPane.getHeight() - (ascent + descent)) / 2.0f + ascent; |
||||||
|
FlatUIUtils.setRenderingHints(g2d); |
||||||
|
g2d.drawString(headerPane.getTitle(), titleX, titleY); |
||||||
|
g2d.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public float[] getValues(Component c) { |
||||||
|
HeaderPane headerPane = (HeaderPane) c; |
||||||
|
return new float[]{headerPane.isShow() ? 1 : 0}; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,131 @@ |
|||||||
|
package com.fine.theme.utils; |
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.Animator.Interpolator; |
||||||
|
import com.formdev.flatlaf.util.CubicBezierEasing; |
||||||
|
|
||||||
|
import java.awt.Component; |
||||||
|
import java.awt.Graphics; |
||||||
|
import java.awt.Graphics2D; |
||||||
|
|
||||||
|
/** |
||||||
|
* Painter 可在组件值更改时自动对绘画进行动画处理。 |
||||||
|
* getValues(Component) 返回组件的值。如果值已更改,则 paintAnimated(Component, Graphics2D, int, int, int, int, float[]) |
||||||
|
* 使用动画值(从旧值到新值)多次调用。如果 getValues(Component) 返回多个值,则每个值都有自己独立的动画。 |
||||||
|
* 仅当传递给 paintWithAnimation(Component, Graphics, int, int, int, int) 的组件是 的 JComponent实例时,动画才有效。 |
||||||
|
* 在组件上设置客户端属性以存储动画状态。 |
||||||
|
* |
||||||
|
* @author vito |
||||||
|
* @since 11.0 |
||||||
|
* Created on 2024/1/29 |
||||||
|
*/ |
||||||
|
public interface AnimatedPainter { |
||||||
|
/** |
||||||
|
* 开始绘画。调用 paintAnimated(Component, Graphics2D, int, int, int, int, float[]) 一次来绘制当前值 |
||||||
|
* 或者如果值与上次绘制相比发生了变化,则它会启动动画并使用动画值(从旧值到新值)多次调用 |
||||||
|
* paintAnimated(Component, Graphics2D, int, int, int, int, float[]) 。 |
||||||
|
* |
||||||
|
* @param c 动画所属组件 |
||||||
|
* @param g 绘制上下文 |
||||||
|
* @param x x |
||||||
|
* @param y y |
||||||
|
* @param width 区域宽度 |
||||||
|
* @param height 区域高度 |
||||||
|
*/ |
||||||
|
default void paintWithAnimation(Component c, Graphics g, int x, int y, int width, int height) { |
||||||
|
AnimatedPainterSupport.paint(this, c, g, x, y, width, height); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 绘制给定的(动画)值。 |
||||||
|
* 从 paintWithAnimation(Component, Graphics, int, int, int, int)调用。 |
||||||
|
* |
||||||
|
* @param c 动画所属组件 |
||||||
|
* @param g 绘制上下文 |
||||||
|
* @param x x |
||||||
|
* @param y y |
||||||
|
* @param width 区域宽度 |
||||||
|
* @param height 区域高度 |
||||||
|
* @param animatedValues 动画值,介于先前的值 getValues(Component) 和返回的最新值 getValues(Component) 之间 |
||||||
|
*/ |
||||||
|
void paintAnimated(Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues); |
||||||
|
|
||||||
|
/** |
||||||
|
* 从 animator 调用以重新绘制区域。 |
||||||
|
* 用于限制重绘区域。例如,如果希望只有底部边框有动画。如果对多个边框进行动画处理(例如底部和右侧),那么单独的重绘是没有意义的, |
||||||
|
* 因为 Swing 重绘管理器会合并区域并重绘整个组件。默认实现会重新绘制整个给定区域。 |
||||||
|
*/ |
||||||
|
default void repaintDuringAnimation(Component c, int x, int y, int width, int height) { |
||||||
|
c.repaint(x, y, width, height); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取组件的值。 |
||||||
|
* 如果值发生更改,则此类将从旧值动画到新值。如果返回多个值,则每个值都有自己独立的动画。 |
||||||
|
* 对于切换按钮,0 表示关闭和 1 表示打开 |
||||||
|
*/ |
||||||
|
float[] getValues(Component c); |
||||||
|
|
||||||
|
/** |
||||||
|
* 动画是否启用 |
||||||
|
*/ |
||||||
|
default boolean isAnimationEnabled() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回动画的持续时间(以毫秒为单位)(默认值为 150)。 |
||||||
|
*/ |
||||||
|
default int getAnimationDuration() { |
||||||
|
return 150; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回动画的分辨率(以毫秒为单位)(默认值为 10)。分辨率是计时事件之间的时间量。 |
||||||
|
*/ |
||||||
|
default int getAnimationResolution() { |
||||||
|
return 10; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回动画的插值器。默认值为 {@link CubicBezierEasing#STANDARD_EASING}. |
||||||
|
*/ |
||||||
|
default Interpolator getAnimationInterpolator() { |
||||||
|
return CubicBezierEasing.STANDARD_EASING; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回给定值索引和值的动画持续时间(以毫秒为单位)(默认值为 150)。 |
||||||
|
*/ |
||||||
|
default int getAnimationDuration(int valueIndex, float value) { |
||||||
|
return getAnimationDuration(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回给定值索引和值的动画分辨率(以毫秒为单位)(默认值为 10)。分辨率是计时事件之间的时间量 |
||||||
|
*/ |
||||||
|
default int getAnimationResolution(int valueIndex, float value) { |
||||||
|
return getAnimationResolution(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回给定值索引和值的动画插值器。默认值为 {@link CubicBezierEasing#STANDARD_EASING}. |
||||||
|
*/ |
||||||
|
default Interpolator getAnimationInterpolator(int valueIndex, float value) { |
||||||
|
return getAnimationInterpolator(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回用于存储动画支持的客户端属性键。 |
||||||
|
*/ |
||||||
|
default Object getClientPropertyKey() { |
||||||
|
return getClass(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 保存位置,提供给 repaintDuringAnimation(Component, int, int, int, int)使用重新绘制动画区域。 |
||||||
|
* 仅当传递到的 paintWithAnimation(Component, Graphics, int, int, int, int) 图形上下文使用转换的位置时才需要。 |
||||||
|
*/ |
||||||
|
static void saveRepaintLocation(AnimatedPainter painter, Component c, int x, int y) { |
||||||
|
AnimatedPainterSupport.saveRepaintLocation(painter, c, x, y); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,167 @@ |
|||||||
|
package com.fine.theme.utils; |
||||||
|
|
||||||
|
import com.formdev.flatlaf.util.Animator; |
||||||
|
|
||||||
|
import javax.swing.JComponent; |
||||||
|
import java.awt.Component; |
||||||
|
import java.awt.Graphics; |
||||||
|
import java.awt.Graphics2D; |
||||||
|
|
||||||
|
/** |
||||||
|
* 动画支持类,用于存储动画状态并实现动画。 |
||||||
|
* |
||||||
|
* @author vito |
||||||
|
* @since 12.0 |
||||||
|
* Created on 2024/1/29 |
||||||
|
*/ |
||||||
|
class AnimatedPainterSupport { |
||||||
|
private int valueIndex; |
||||||
|
private float startValue; |
||||||
|
private float targetValue; |
||||||
|
private float animatedValue; |
||||||
|
private float fraction; |
||||||
|
|
||||||
|
private Animator animator; |
||||||
|
|
||||||
|
// last bounds of the paint area needed to repaint while animating
|
||||||
|
private int x; |
||||||
|
private int y; |
||||||
|
private int width; |
||||||
|
private int height; |
||||||
|
|
||||||
|
static void paint(AnimatedPainter painter, Component c, Graphics g, |
||||||
|
int x, int y, int width, int height) { |
||||||
|
if (!isAnimationEnabled(painter, c)) { |
||||||
|
// paint without animation if animation is disabled or
|
||||||
|
// component is not a JComponent and therefore does not support
|
||||||
|
// client properties, which are required to keep animation state
|
||||||
|
painter.paintAnimated(c, (Graphics2D) g, x, y, width, height, painter.getValues(c)); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// get component values
|
||||||
|
float[] values = painter.getValues(c); |
||||||
|
|
||||||
|
JComponent jc = (JComponent) c; |
||||||
|
Object key = painter.getClientPropertyKey(); |
||||||
|
AnimatedPainterSupport[] ass = (AnimatedPainterSupport[]) jc.getClientProperty(key); |
||||||
|
|
||||||
|
// check whether length of values array has changed
|
||||||
|
if (ass != null && ass.length != values.length) { |
||||||
|
// cancel all running animations
|
||||||
|
for (AnimatedPainterSupport as : ass) { |
||||||
|
if (as.animator != null) { |
||||||
|
as.animator.cancel(); |
||||||
|
} |
||||||
|
} |
||||||
|
ass = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (ass == null) { |
||||||
|
ass = new AnimatedPainterSupport[values.length]; |
||||||
|
jc.putClientProperty(key, ass); |
||||||
|
} |
||||||
|
|
||||||
|
for (int i = 0; i < ass.length; i++) { |
||||||
|
AnimatedPainterSupport as = ass[i]; |
||||||
|
float value = values[i]; |
||||||
|
|
||||||
|
if (as == null) { |
||||||
|
// painted first time --> do not animate, but remember current component value
|
||||||
|
as = new AnimatedPainterSupport(); |
||||||
|
as.valueIndex = i; |
||||||
|
as.startValue = as.targetValue = as.animatedValue = value; |
||||||
|
ass[i] = as; |
||||||
|
} else if (value != as.targetValue) { |
||||||
|
// value changed --> (re)start animation
|
||||||
|
|
||||||
|
int animationDuration = painter.getAnimationDuration(as.valueIndex, value); |
||||||
|
|
||||||
|
// do not animate if animation duration (for current value) is zero
|
||||||
|
if (animationDuration <= 0) { |
||||||
|
if (as.animator != null) { |
||||||
|
as.animator.cancel(); |
||||||
|
as.animator = null; |
||||||
|
} |
||||||
|
as.startValue = as.targetValue = as.animatedValue = value; |
||||||
|
as.fraction = 0; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (as.animator == null) { |
||||||
|
// create animator
|
||||||
|
AnimatedPainterSupport as2 = as; |
||||||
|
as.animator = new Animator(1, fraction -> { |
||||||
|
// check whether component was removed while animation is running
|
||||||
|
if (!c.isDisplayable()) { |
||||||
|
as2.animator.stop(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// compute animated value
|
||||||
|
as2.animatedValue = as2.startValue + ((as2.targetValue - as2.startValue) * fraction); |
||||||
|
as2.fraction = fraction; |
||||||
|
|
||||||
|
// repaint
|
||||||
|
painter.repaintDuringAnimation(c, as2.x, as2.y, as2.width, as2.height); |
||||||
|
}, () -> { |
||||||
|
as2.startValue = as2.animatedValue = as2.targetValue; |
||||||
|
as2.animator = null; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (as.animator.isRunning()) { |
||||||
|
// if animation is still running, restart it from the current
|
||||||
|
// animated value to the new target value with reduced duration
|
||||||
|
as.animator.cancel(); |
||||||
|
int duration2 = (int) (animationDuration * as.fraction); |
||||||
|
if (duration2 > 0) |
||||||
|
as.animator.setDuration(duration2); |
||||||
|
as.startValue = as.animatedValue; |
||||||
|
} else { |
||||||
|
// new animation
|
||||||
|
as.animator.setDuration(animationDuration); |
||||||
|
|
||||||
|
as.animatedValue = as.startValue; |
||||||
|
} |
||||||
|
|
||||||
|
// update animator for new value
|
||||||
|
as.animator.setResolution(painter.getAnimationResolution(as.valueIndex, value)); |
||||||
|
as.animator.setInterpolator(painter.getAnimationInterpolator(as.valueIndex, value)); |
||||||
|
|
||||||
|
// start animation
|
||||||
|
as.targetValue = value; |
||||||
|
as.animator.start(); |
||||||
|
} |
||||||
|
|
||||||
|
as.x = x; |
||||||
|
as.y = y; |
||||||
|
as.width = width; |
||||||
|
as.height = height; |
||||||
|
} |
||||||
|
|
||||||
|
float[] animatedValues = new float[ass.length]; |
||||||
|
for (int i = 0; i < ass.length; i++) { |
||||||
|
animatedValues[i] = ass[i].animatedValue; |
||||||
|
} |
||||||
|
|
||||||
|
painter.paintAnimated(c, (Graphics2D) g, x, y, width, height, animatedValues); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isAnimationEnabled(AnimatedPainter painter, Component c) { |
||||||
|
return Animator.useAnimation() && painter.isAnimationEnabled() && c instanceof JComponent; |
||||||
|
} |
||||||
|
|
||||||
|
static void saveRepaintLocation(AnimatedPainter painter, Component c, int x, int y) { |
||||||
|
if (!isAnimationEnabled(painter, c)) |
||||||
|
return; |
||||||
|
|
||||||
|
AnimatedPainterSupport[] ass = (AnimatedPainterSupport[]) ((JComponent) c).getClientProperty(painter.getClientPropertyKey()); |
||||||
|
if (ass != null) { |
||||||
|
for (AnimatedPainterSupport as : ass) { |
||||||
|
as.x = x; |
||||||
|
as.y = y; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue