Initial project
This commit is contained in:
51
src/main/java/com/imyeyu/fx/BindingUtils.java
Normal file
51
src/main/java/com/imyeyu/fx/BindingUtils.java
Normal file
@ -0,0 +1,51 @@
|
||||
package com.imyeyu.fx;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import com.imyeyu.utils.Calc;
|
||||
import javafx.scene.control.SplitPane;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @version 2024-04-13 13:46
|
||||
*/
|
||||
public final class BindingUtils {
|
||||
|
||||
/**
|
||||
* 多个布尔值绑定属性与运算
|
||||
*
|
||||
* @param values 布尔值绑定
|
||||
* @return 与运算绑定
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static BooleanBinding and(ObservableValue<Boolean>...values) {
|
||||
return Bindings.createBooleanBinding(()-> Arrays.stream(values).allMatch(ObservableValue::getValue), values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个布尔值绑定属性或运算
|
||||
*
|
||||
* @param values 布尔值绑定
|
||||
* @return 或运算绑定
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static BooleanBinding or(ObservableValue<Boolean>...values) {
|
||||
return Bindings.createBooleanBinding(()-> Arrays.stream(values).anyMatch(ObservableValue::getValue), values);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 双精度转整型字符串绑定属性
|
||||
*
|
||||
* @param property 双精度浮点型属性
|
||||
* @return 字符串绑定属性
|
||||
*/
|
||||
public static StringBinding integerStringBinding(DoubleProperty property) {
|
||||
return Bindings.createStringBinding(() -> String.valueOf(Calc.round(property.get())), property);
|
||||
}
|
||||
}
|
||||
65
src/main/java/com/imyeyu/fx/ObservableUtils.java
Normal file
65
src/main/java/com/imyeyu/fx/ObservableUtils.java
Normal file
@ -0,0 +1,65 @@
|
||||
package com.imyeyu.fx;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @version 2024-04-13 13:46
|
||||
*/
|
||||
public final class ObservableUtils {
|
||||
|
||||
/**
|
||||
* 目标列表项有且仅有一项数据
|
||||
*
|
||||
* @param list 列表
|
||||
* @return 监听对象
|
||||
*/
|
||||
public static BooleanBinding onlyOnceInList(ObservableList<?> list) {
|
||||
return onlyOnceInList(list, list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标列表项有且仅有一项数据
|
||||
*
|
||||
* @param list 列表
|
||||
* @param dependencies 触发器
|
||||
* @return 监听对象
|
||||
*/
|
||||
public static BooleanBinding onlyOnceInList(ObservableList<?> list, Observable... dependencies) {
|
||||
return Bindings.createBooleanBinding(() -> list != null && list.size() == 1, dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标列表项至少 2 个数据
|
||||
*
|
||||
* @param list 列表
|
||||
* @return 监听对象
|
||||
*/
|
||||
public static BooleanBinding multiInList(ObservableList<?> list) {
|
||||
return multiInList(list, list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标列表项至少 2 个数据
|
||||
*
|
||||
* @param list 列表
|
||||
* @param dependencies 触发器
|
||||
* @return 监听对象
|
||||
*/
|
||||
public static BooleanBinding multiInList(ObservableList<?> list, Observable... dependencies) {
|
||||
return Bindings.createBooleanBinding(() -> list != null && 2 <= list.size(), dependencies);
|
||||
}
|
||||
|
||||
public static <T> void onChange(ObservableValue<T> observableValue, CallbackArg<T> callback) {
|
||||
observableValue.addListener((obs, o, n) -> callback.handler(n));
|
||||
}
|
||||
|
||||
public static <T> void debug(ObservableValue<T> observableValue) {
|
||||
observableValue.addListener((obs, o, n) -> System.out.println(n));
|
||||
}
|
||||
}
|
||||
318
src/main/java/com/imyeyu/fx/TimiFX.java
Normal file
318
src/main/java/com/imyeyu/fx/TimiFX.java
Normal file
@ -0,0 +1,318 @@
|
||||
package com.imyeyu.fx;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.PopupWindow;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import javafx.stage.WindowEvent;
|
||||
import com.imyeyu.fx.utils.ScreenFX;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.utils.Calc;
|
||||
import com.imyeyu.utils.OS;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* <b>TimiFX</b> - JavaFX 开发工具
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-02-14 10:51
|
||||
*/
|
||||
public final class TimiFX {
|
||||
|
||||
/**
|
||||
* 阻止取消选择
|
||||
* <pre>
|
||||
* addEventFilter(MouseEvent.MOUSE_PRESSED, EVENT_CONSUME_TGBTN);
|
||||
* </pre>
|
||||
*/
|
||||
public static final EventHandler<MouseEvent> EVENT_CONSUME_TG_BTN = e -> {
|
||||
if (e.getSource() instanceof ToggleButton btn && btn.isSelected()) {
|
||||
e.consume();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 十六进制颜色含透明度:0xFF00FF00
|
||||
*
|
||||
* @param color 颜色
|
||||
* @return 十六进制颜色字符串
|
||||
*/
|
||||
public static String toHexString(Color color) {
|
||||
int r = Calc.round(color.getRed() * 255) << 24;
|
||||
int g = Calc.round(color.getGreen() * 255) << 16;
|
||||
int b = Calc.round(color.getBlue() * 255) << 8;
|
||||
int a = Calc.round(color.getOpacity() * 255);
|
||||
return String.format("0x%08X", (r + g + b + a));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析十六进制颜色字符串
|
||||
*
|
||||
* @param hexColorString 十六进制颜色字符串
|
||||
* @return 颜色
|
||||
*/
|
||||
public static Color fromHexString(String hexColorString) {
|
||||
return Color.valueOf(hexColorString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新设置 SVG 大小
|
||||
*
|
||||
* @param svg SVG 路径
|
||||
* @param scale 缩放倍率
|
||||
* @return SVG 路径
|
||||
*/
|
||||
public static String resizeSVG(String svg, double scale) {
|
||||
Pattern compile = Pattern.compile("\\d+\\.?\\d+");
|
||||
Matcher matcher = compile.matcher(svg);
|
||||
return matcher.replaceAll(matchResult -> {
|
||||
double d = Double.parseDouble(matchResult.group()) * scale;
|
||||
if (d % 1 == 0) {
|
||||
return String.valueOf((int) d);
|
||||
} else {
|
||||
return "%.2f".formatted(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置相对居中窗体
|
||||
*
|
||||
* @param owner 依赖窗体
|
||||
* @param window 居中窗体
|
||||
*/
|
||||
public static void relativeCenter(Window owner, Window window) {
|
||||
double x = owner.getX() + owner.getWidth() * .5 - window.getWidth() * .5;
|
||||
double y = owner.getY() + owner.getHeight() * .382 - window.getHeight() * .5;
|
||||
|
||||
double idle = Math.abs(owner.getY() + 30 - y);
|
||||
if (y < owner.getY()) {
|
||||
// 防止跃出相对窗体上部分
|
||||
y += idle;
|
||||
}
|
||||
if (ScreenFX.outOfScreen(x, y)) {
|
||||
// 坐标无效或窗体完全越出屏幕,设置到主屏幕
|
||||
relativeCenter4PrimaryScreen(window);
|
||||
}
|
||||
window.setX(x);
|
||||
window.setY(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置窗体到主屏幕中间(偏上少许)
|
||||
*
|
||||
* @param stage 窗体
|
||||
*/
|
||||
public static void relativeCenter4PrimaryScreen(Window stage) {
|
||||
relativeCenter4Screen(ScreenFX.primary, stage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置窗体到屏幕中间(偏上少许)
|
||||
*
|
||||
* @param screen 屏幕
|
||||
* @param stage 窗体
|
||||
*/
|
||||
public static void relativeCenter4Screen(Screen screen, Window stage) {
|
||||
if (screen == null) {
|
||||
relativeCenter4PrimaryScreen(stage);
|
||||
} else {
|
||||
Rectangle2D r2d = screen.getBounds();
|
||||
double x = r2d.getMinX() + r2d.getWidth() * .5 - stage.getWidth() * .5;
|
||||
double y = r2d.getMinY() + r2d.getHeight() * .382 - stage.getHeight() * .5;
|
||||
|
||||
double idle = Math.abs(r2d.getMinY() + 30 - y);
|
||||
if (y < r2d.getMinY()) {
|
||||
// 防止跃出屏幕上部分
|
||||
y += idle;
|
||||
}
|
||||
if (Double.isNaN(x) || Double.isNaN(y)) {
|
||||
stage.centerOnScreen();
|
||||
} else {
|
||||
stage.setX(x);
|
||||
stage.setY(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对于某窗体居中显示新窗体(需预设宽高)
|
||||
*
|
||||
* @param owner 依赖窗体
|
||||
* @param window 需显示的窗体
|
||||
*/
|
||||
public static void showCenter(Window owner, Window window) {
|
||||
if (!window.isShowing()) {
|
||||
relativeCenter(owner, window);
|
||||
}
|
||||
if (Double.isNaN(window.getX()) || Double.isNaN(window.getY())) {
|
||||
final EventHandler<WindowEvent> onShown = window.getOnShown();
|
||||
window.setOnShown(e -> {
|
||||
if (onShown != null) {
|
||||
onShown.handle(e);
|
||||
}
|
||||
window.sizeToScene();
|
||||
relativeCenter(owner, window);
|
||||
});
|
||||
}
|
||||
if (window instanceof Stage stage) {
|
||||
requestTop(stage);
|
||||
} else if (window instanceof PopupWindow popupWindow) {
|
||||
popupWindow.show(owner);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求焦点,执行显示、置顶并聚焦
|
||||
*
|
||||
* @param stage 窗体
|
||||
*/
|
||||
public static void requestTop(Stage stage) {
|
||||
stage.show();
|
||||
if (stage.isIconified()) {
|
||||
stage.setIconified(false);
|
||||
}
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.requestFocus();
|
||||
stage.setAlwaysOnTop(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据布尔值监听动态切换节点 CSS 类
|
||||
*
|
||||
* @param node 节点
|
||||
* @param booleanProperty 布尔值监听
|
||||
* @param onTrue 为 true 时的 css 类
|
||||
* @param onFalse 为 false 时的 css 类
|
||||
*/
|
||||
public static void toggleStyleClass(Node node, ReadOnlyBooleanProperty booleanProperty, String onTrue, String onFalse) {
|
||||
if (booleanProperty.get()) {
|
||||
node.getStyleClass().remove(onFalse);
|
||||
node.getStyleClass().add(onTrue);
|
||||
}
|
||||
booleanProperty.addListener((obs, o, isTrue) -> {
|
||||
if (isTrue) {
|
||||
node.getStyleClass().remove(onFalse);
|
||||
node.getStyleClass().add(onTrue);
|
||||
} else {
|
||||
node.getStyleClass().remove(onTrue);
|
||||
node.getStyleClass().add(onFalse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布尔值监听动态切换节点 CSS 类
|
||||
*
|
||||
* @param node 节点
|
||||
* @param booleanBinding 布尔值监听
|
||||
* @param onTrue 为 true 时的 css 类
|
||||
* @param onFalse 为 false 时的 css 类
|
||||
*/
|
||||
public static void toggleStyleClass4Binding(Node node, BooleanBinding booleanBinding, String onTrue, String onFalse) {
|
||||
if (booleanBinding.get()) {
|
||||
node.getStyleClass().remove(onFalse);
|
||||
node.getStyleClass().add(onTrue);
|
||||
}
|
||||
booleanBinding.addListener((obs, o, isTrue) -> {
|
||||
if (isTrue) {
|
||||
node.getStyleClass().remove(onFalse);
|
||||
node.getStyleClass().add(onTrue);
|
||||
} else {
|
||||
node.getStyleClass().remove(onTrue);
|
||||
node.getStyleClass().add(onFalse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 指向半透
|
||||
*
|
||||
* @param node 节点
|
||||
*/
|
||||
public static void hoverOpacity(Node node) {
|
||||
hoverOpacity(node, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指向半透
|
||||
*
|
||||
* @param handler 触发节点
|
||||
* @param node 节点
|
||||
*/
|
||||
public static void hoverOpacity(Node handler, Node node) {
|
||||
node.opacityProperty().bind(Bindings.when(handler.hoverProperty()).then(.7).otherwise(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 指向即聚焦该节点
|
||||
*
|
||||
* @param node 节点
|
||||
*/
|
||||
public static void hoverFocus(Node node) {
|
||||
node.hoverProperty().addListener((obs, o, isHover) -> {
|
||||
if (isHover) {
|
||||
node.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动指定项至控件可视范围的中央
|
||||
*
|
||||
* @param control 控件({@link javafx.scene.control.ListView}, {@link javafx.scene.control.TableView} 等)
|
||||
* @param index 指定项下标
|
||||
*/
|
||||
public static void scrollToCenter(Control control, int index) {
|
||||
try {
|
||||
if (control.getSkin() == null) {
|
||||
return;
|
||||
}
|
||||
// 偏移
|
||||
VirtualFlow<?> flow = Ref.getFieldValue(control.getSkin(), "flow", VirtualFlow.class);
|
||||
if (flow == null) {
|
||||
throw new UnsupportedOperationException("unsupported this control");
|
||||
}
|
||||
int offset = Calc.floor(flow.getHeight() * .5 / flow.getCell(0).getHeight());
|
||||
// 滚动
|
||||
Method method = Ref.getMethod(control.getClass(), "scrollTo", int.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(control, index - offset);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 重启程序,命令需自定
|
||||
*
|
||||
* @param application FX 程序
|
||||
* @param command 重启命令
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
public static void doRestart(Application application, String command) throws Exception {
|
||||
Platform.setImplicitExit(true);
|
||||
OS.runAfterShutdown(command);
|
||||
application.stop();
|
||||
}
|
||||
}
|
||||
179
src/main/java/com/imyeyu/fx/bean/Interpolates.java
Normal file
179
src/main/java/com/imyeyu/fx/bean/Interpolates.java
Normal file
@ -0,0 +1,179 @@
|
||||
package com.imyeyu.fx.bean;
|
||||
|
||||
import com.sun.scenario.animation.SplineInterpolator;
|
||||
|
||||
/**
|
||||
* 贝塞尔曲线,动画运动方式,常用 {@link #EASE_OUT_EXPO}
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-02-16 22:43
|
||||
*/
|
||||
public enum Interpolates {
|
||||
|
||||
/** EASE ( .25, .1, .25, 1) */ EASE ( .25, .1, .25, 1),
|
||||
/** LINEAR ( 0, 0, 1, 1) */ LINEAR ( 0, 0, 1, 1),
|
||||
/** EASE_IN ( .42, 0, 1, 1) */ EASE_IN ( .42, 0, 1, 1),
|
||||
/** EASE_OUT ( 0, 0, .58, 1) */ EASE_OUT ( 0, 0, .58, 1),
|
||||
/** EASE_IN_OUT ( .42, 0, .58, 1) */ EASE_IN_OUT ( .42, 0, .58, 1),
|
||||
/** EASE_IN_SINE ( .47, 0, .745, .715) */ EASE_IN_SINE ( .47, 0, .745, .715),
|
||||
/** EASE_OUT_SINE ( .39, .575, .565, 1) */ EASE_OUT_SINE ( .39, .575, .565, 1),
|
||||
/** EASE_IN_OUT_SINE (.445, .05, .55, .95) */ EASE_IN_OUT_SINE (.445, .05, .55, .95),
|
||||
/** EASE_IN_QUAD ( .55, .085, .68, .53) */ EASE_IN_QUAD ( .55, .085, .68, .53),
|
||||
/** EASE_OUT_QUAD ( .25, .46, .45, .94) */ EASE_OUT_QUAD ( .25, .46, .45, .94),
|
||||
/** EASE_IN_OUT_QUAD (.455, .03, .515, .955) */ EASE_IN_OUT_QUAD (.455, .03, .515, .955),
|
||||
/** EASE_IN_CUBIC ( .55, .055, .675, .19) */ EASE_IN_CUBIC ( .55, .055, .675, .19),
|
||||
/** EASE_OUT_CUBIC (.215, .61, .355, 1) */ EASE_OUT_CUBIC (.215, .61, .355, 1),
|
||||
/** EASE_IN_OUT_CUBIC (.645, .045, .355, 1) */ EASE_IN_OUT_CUBIC (.645, .045, .355, 1),
|
||||
/** EASE_IN_QUART (.895, .03, .685, .22) */ EASE_IN_QUART (.895, .03, .685, .22),
|
||||
/** EASE_OUT_QUART (.165, .84, .44, 1) */ EASE_OUT_QUART (.165, .84, .44, 1),
|
||||
/** EASE_IN_OUT_QUART ( .77, 0, .175, 1) */ EASE_IN_OUT_QUART ( .77, 0, .175, 1),
|
||||
/** EASE_IN_QUINT (.755, .05, .855, .06) */ EASE_IN_QUINT (.755, .05, .855, .06),
|
||||
/** EASE_OUT_QUINT ( .23, 1, .32, 1) */ EASE_OUT_QUINT ( .23, 1, .32, 1),
|
||||
/** EASE_IN_OUT_QUINT ( .86, 0, .07, 1) */ EASE_IN_OUT_QUINT ( .86, 0, .07, 1),
|
||||
/** EASE_IN_EXPO ( .95, .05, .795, .035) */ EASE_IN_EXPO ( .95, .05, .795, .035),
|
||||
/** EASE_OUT_EXPO ( .19, 1, .22, 1) */ EASE_OUT_EXPO ( .19, 1, .22, 1),
|
||||
/** EASE_IN_OUT_EXPO ( 1, 0, 0, 1) */ EASE_IN_OUT_EXPO ( 1, 0, 0, 1),
|
||||
/** EASE_IN_CIRC ( .6, .04, .98, .335) */ EASE_IN_CIRC ( .6, .04, .98, .335),
|
||||
/** EASE_OUT_CIRC (.075, .82, .165, 1) */ EASE_OUT_CIRC (.075, .82, .165, 1),
|
||||
/** EASE_IN_BACK ( .6, .28, .735, .045) */ EASE_IN_BACK ( .6, .28, .735, .045);
|
||||
|
||||
final SplineInterpolator value;
|
||||
|
||||
final double x1, y1, x2, y2;
|
||||
|
||||
Interpolates(double px1, double py1, double px2, double py2) {
|
||||
x1 = px1;
|
||||
y1 = py1;
|
||||
x2 = px2;
|
||||
y2 = py2;
|
||||
value = new SplineInterpolator(px1, py1, px2, py2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造二次贝塞尔点,值域为 [0, 1]
|
||||
*
|
||||
* @param precision 精度,构造数据量,不可小于 4
|
||||
* @return 贝塞尔点列表,第二维 [0]: x 轴坐标,[1]: y 轴坐标
|
||||
*/
|
||||
public double[][] buildBezierPoint(double precision) {
|
||||
return buildBezierPoint(1, precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造二次贝塞尔点
|
||||
*
|
||||
* @param boost 增幅,默认 1,值域为 [0, 1]
|
||||
* @param precision 精度,构造数据量,不可小于 4
|
||||
* @return 贝塞尔点列表,第二维 [0]: x 轴坐标,[1]: y 轴坐标
|
||||
*/
|
||||
public double[][] buildBezierPoint(double boost, double precision) {
|
||||
if (precision < 4) {
|
||||
throw new IllegalArgumentException("precision can not lessthan 4: " + precision);
|
||||
}
|
||||
precision = 1D / precision;
|
||||
|
||||
Double[][] cps = new Double[4][2];
|
||||
cps[0][0] = cps[0][1] = 0D;
|
||||
cps[1][0] = x1;
|
||||
cps[1][1] = y1;
|
||||
cps[2][0] = x2;
|
||||
cps[2][1] = y2;
|
||||
cps[3][0] = cps[3][1] = 1D;
|
||||
|
||||
double[][] result = new double[(int) Math.ceil(1D / precision) + 1][2];
|
||||
|
||||
double[][] p;
|
||||
int n = cps.length - 1;
|
||||
int j, k, l = 0;
|
||||
for (float i = 0; i <= 1; i += precision, l++) {
|
||||
p = new double[n + 1][2];
|
||||
for (j = 0; j <= n; j++) {
|
||||
p[j][0] = cps[j][0];
|
||||
p[j][1] = cps[j][1];
|
||||
}
|
||||
for (k = 1; k <= n; k++) {
|
||||
for (j = 0; j <= n - k; j++) {
|
||||
p[j][0] = (1 - i) * p[j][0] + i * p[j + 1][0];
|
||||
p[j][1] = (1 - i) * p[j][1] + i * p[j + 1][1];
|
||||
}
|
||||
}
|
||||
p[0][0] *= boost;
|
||||
p[0][1] *= boost;
|
||||
|
||||
result[l][0] = p[0][0];
|
||||
result[l][1] = p[0][1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造二次贝塞尔点值,值域为 [0, 1]
|
||||
*
|
||||
* @param precision 精度,构造数据量,不可小于 4
|
||||
* @return 贝塞尔点列表,值为 y 轴坐标
|
||||
*/
|
||||
public double[] buildBezierPointValue(double precision) {
|
||||
return buildBezierPointValue(1, precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造二次贝塞尔点值
|
||||
*
|
||||
* @param boost 增幅,默认 1,值域为 [0, 1]
|
||||
* @param precision 精度,构造数据量,不可小于 4
|
||||
* @return 贝塞尔点列表,值为 y 轴坐标
|
||||
*/
|
||||
public double[] buildBezierPointValue(double boost, double precision) {
|
||||
double[][] points = buildBezierPoint(boost, precision);
|
||||
double[] result = new double[points.length];
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
result[i] = points[i][1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控制点 1 的横坐标
|
||||
*
|
||||
* @return 控制的 1 的横坐标
|
||||
*/
|
||||
public double getX1() {
|
||||
return x1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控制点 1 的纵坐标
|
||||
*
|
||||
* @return 控制的 1 的纵坐标
|
||||
*/
|
||||
public double getY1() {
|
||||
return y1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控制点 2 的横坐标
|
||||
*
|
||||
* @return 控制的 2 的横坐标
|
||||
*/
|
||||
public double getX2() {
|
||||
return x2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控制点 2 的纵坐标
|
||||
*
|
||||
* @return 控制的 2 的纵坐标
|
||||
*/
|
||||
public double getY2() {
|
||||
return y2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插值器
|
||||
*
|
||||
* @return 插值器
|
||||
*/
|
||||
public SplineInterpolator getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
317
src/main/java/com/imyeyu/fx/config/BindingsConfig.java
Normal file
317
src/main/java/com/imyeyu/fx/config/BindingsConfig.java
Normal file
@ -0,0 +1,317 @@
|
||||
package com.imyeyu.fx.config;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.FloatProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleFloatProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import com.imyeyu.config.BaseConverter;
|
||||
import com.imyeyu.config.ConfigLoader;
|
||||
import com.imyeyu.fx.TimiFX;
|
||||
import com.imyeyu.fx.utils.BgFill;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 配置绑定
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-10-26 10:27
|
||||
*/
|
||||
public final class BindingsConfig {
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2024-04-14 09:30
|
||||
*/
|
||||
public abstract static class PropertyConverter<T extends Property<?>, K> extends BaseConverter<T, Object> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected K serialize(Field field, T t) {
|
||||
if (t.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
return (K) t.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2024-04-28 15:23
|
||||
*/
|
||||
public interface ObjectPropertyConverter<T> {
|
||||
|
||||
T serialize(Object t);
|
||||
|
||||
Object deserialize(String data);
|
||||
}
|
||||
|
||||
public static final PropertyConverter<StringProperty, String> STRING = new PropertyConverter<>() {
|
||||
|
||||
@Override
|
||||
protected StringProperty deserialize(Field field, Object data) {
|
||||
SimpleStringProperty property = new SimpleStringProperty();
|
||||
if (data != null) {
|
||||
property.set(data.toString());
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
public static final PropertyConverter<IntegerProperty, Integer> INTEGER = new PropertyConverter<>() {
|
||||
|
||||
@Override
|
||||
protected IntegerProperty deserialize(Field field, Object data) {
|
||||
SimpleIntegerProperty property = new SimpleIntegerProperty();
|
||||
if (data != null) {
|
||||
property.set(Integer.parseInt(data.toString()));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
public static final PropertyConverter<LongProperty, Long> LONG = new PropertyConverter<>() {
|
||||
|
||||
@Override
|
||||
protected LongProperty deserialize(Field field, Object data) {
|
||||
SimpleLongProperty property = new SimpleLongProperty();
|
||||
if (data != null) {
|
||||
property.set(Long.parseLong(data.toString()));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
public static final PropertyConverter<DoubleProperty, Double> DOUBLE = new PropertyConverter<>() {
|
||||
|
||||
@Override
|
||||
protected DoubleProperty deserialize(Field field, Object data) {
|
||||
SimpleDoubleProperty property = new SimpleDoubleProperty();
|
||||
if (data != null) {
|
||||
property.set(Double.parseDouble(data.toString()));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
public static final PropertyConverter<FloatProperty, Float> FLOAT = new PropertyConverter<>() {
|
||||
|
||||
@Override
|
||||
protected FloatProperty deserialize(Field field, Object data) {
|
||||
SimpleFloatProperty property = new SimpleFloatProperty();
|
||||
if (data != null) {
|
||||
property.set(Float.parseFloat(data.toString()));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
public static final PropertyConverter<BooleanProperty, Boolean> BOOLEAN = new PropertyConverter<>() {
|
||||
|
||||
@Override
|
||||
protected BooleanProperty deserialize(Field field, Object data) {
|
||||
SimpleBooleanProperty property = new SimpleBooleanProperty();
|
||||
if (data != null) {
|
||||
property.set(Boolean.parseBoolean(data.toString()));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** 颜色转换,配置值格式为 0x11223344 */
|
||||
public static final BaseConverter<ObjectProperty<Color>, String> COLOR = new BaseConverter<>() {
|
||||
|
||||
@Override
|
||||
protected String serialize(Field field, ObjectProperty<Color> property) {
|
||||
return TimiFX.toHexString(property.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObjectProperty<Color> deserialize(Field field, String data) {
|
||||
SimpleObjectProperty<Color> property = new SimpleObjectProperty<>();
|
||||
if (data != null) {
|
||||
property.set(TimiFX.fromHexString(data));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
/** 颜色配置转换 */
|
||||
public static final BaseConverter<ObjectProperty<Paint>, String> PAINT = new BaseConverter<>() {
|
||||
|
||||
@Override
|
||||
protected String serialize(Field field, ObjectProperty<Paint> property) {
|
||||
return TimiFX.toHexString(Color.valueOf(property.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObjectProperty<Paint> deserialize(Field field, String data) {
|
||||
SimpleObjectProperty<Paint> property = new SimpleObjectProperty<>();
|
||||
if (data != null) {
|
||||
property.set(Paint.valueOf(data));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
/** 基本填充背景颜色配置转换 */
|
||||
public static final BaseConverter<ObjectProperty<Background>, String> BG_FILL = new BaseConverter<>() {
|
||||
|
||||
@Override
|
||||
protected String serialize(Field field, ObjectProperty<Background> property) {
|
||||
return TimiFX.toHexString(Color.valueOf(property.getValue().getFills().getFirst().getFill().toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObjectProperty<Background> deserialize(Field field, String data) {
|
||||
SimpleObjectProperty<Background> property = new SimpleObjectProperty<>();
|
||||
if (data != null) {
|
||||
property.set(new BgFill(data).build());
|
||||
}
|
||||
return property;
|
||||
}
|
||||
};
|
||||
|
||||
public static final BaseConverter<ObjectProperty<?>, String> OBJECT = new BaseConverter<>() {
|
||||
|
||||
@Override
|
||||
protected String serialize(Field field, ObjectProperty<?> property) {
|
||||
Type genericType = field.getGenericType();
|
||||
if (genericType instanceof ParameterizedType pt) {
|
||||
try {
|
||||
Class<?> propertyGenericClass = Class.forName(pt.getActualTypeArguments()[0].getTypeName());
|
||||
if (Enum.class.isAssignableFrom(propertyGenericClass)) {
|
||||
if (property.get() == null) {
|
||||
return null;
|
||||
}
|
||||
return property.get().toString();
|
||||
}
|
||||
if (OBJECT_CONVERTER_MAP.containsKey(propertyGenericClass)) {
|
||||
return OBJECT_CONVERTER_MAP.get(propertyGenericClass).serialize(property.get()).toString();
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("not found " + field.getType() + " property converter");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected ObjectProperty<?> deserialize(Field field, String data) {
|
||||
Type genericType = field.getGenericType();
|
||||
if (genericType instanceof ParameterizedType pt) {
|
||||
try {
|
||||
Class<?> propertyGenericClass = Class.forName(pt.getActualTypeArguments()[0].getTypeName());
|
||||
if (Enum.class.isAssignableFrom(propertyGenericClass)) {
|
||||
SimpleObjectProperty<Object> property = new SimpleObjectProperty<>();
|
||||
property.set(Ref.toType((Class<Enum<?>>) propertyGenericClass, data));
|
||||
return property;
|
||||
}
|
||||
if (OBJECT_CONVERTER_MAP.containsKey(propertyGenericClass)) {
|
||||
SimpleObjectProperty<Object> property = new SimpleObjectProperty<>();
|
||||
property.set(OBJECT_CONVERTER_MAP.get(propertyGenericClass).deserialize(data));
|
||||
return property;
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("not found " + genericType + " property converter");
|
||||
}
|
||||
};
|
||||
|
||||
public static final BaseConverter<ObservableList<?>, List<?>> OBS_LIST = new BaseConverter<>() {
|
||||
|
||||
@Override
|
||||
protected List<?> serialize(Field field, ObservableList<?> obsList) {
|
||||
return new ArrayList<>(obsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObservableList<?> deserialize(Field field, List<?> list) {
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
return FXCollections.observableList(list);
|
||||
}
|
||||
};
|
||||
|
||||
public static final BaseConverter<ObservableSet<?>, Set<?>> OBS_SET = new BaseConverter<>() {
|
||||
|
||||
@Override
|
||||
protected Set<?> serialize(Field field, ObservableSet<?> obsSet) {
|
||||
return new HashSet<>(obsSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObservableSet<?> deserialize(Field field, Set<?> set) {
|
||||
if (set == null) {
|
||||
set = new HashSet<>();
|
||||
}
|
||||
return FXCollections.observableSet(set);
|
||||
}
|
||||
};
|
||||
|
||||
public static final Map<Class<?>, PropertyConverter<?, ?>> DEFAULT_CONVERTER_MAP = Collections.unmodifiableMap(new HashMap<>() {{
|
||||
put(StringProperty.class, STRING);
|
||||
put(IntegerProperty.class, INTEGER);
|
||||
put(LongProperty.class, LONG);
|
||||
put(DoubleProperty.class, DOUBLE);
|
||||
put(FloatProperty.class, FLOAT);
|
||||
put(BooleanProperty.class, BOOLEAN);
|
||||
}});
|
||||
|
||||
public static final Map<Class<?>, ObjectPropertyConverter<?>> OBJECT_CONVERTER_MAP = new HashMap<>();
|
||||
|
||||
public static <T> ConfigLoader<T> build(String path, Class<T> clazz) {
|
||||
return build(path, path, clazz);
|
||||
}
|
||||
|
||||
public static <T> ConfigLoader<T> build(String srcPath, String distPath, Class<T> clazz) {
|
||||
ConfigLoader<T> loader = new ConfigLoader<>(srcPath, distPath, clazz);
|
||||
addAllFXConverter(loader);
|
||||
return loader;
|
||||
}
|
||||
|
||||
public static void addAllFXConverter(ConfigLoader<?> loader) {
|
||||
for (Map.Entry<Class<?>, BindingsConfig.PropertyConverter<?, ?>> item : BindingsConfig.DEFAULT_CONVERTER_MAP.entrySet()) {
|
||||
loader.addConverter(item.getKey(), item.getValue());
|
||||
}
|
||||
loader.addConverter(ObjectProperty.class, BindingsConfig.OBJECT);
|
||||
loader.addConverter(ObservableSet.class, BindingsConfig.OBS_SET);
|
||||
loader.addConverter(ObservableList.class, BindingsConfig.OBS_LIST);
|
||||
}
|
||||
}
|
||||
301
src/main/java/com/imyeyu/fx/draggable/Draggable.java
Normal file
301
src/main/java/com/imyeyu/fx/draggable/Draggable.java
Normal file
@ -0,0 +1,301 @@
|
||||
package com.imyeyu.fx.draggable;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
/**
|
||||
* 抽象拖动属性
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-03-13 09:43
|
||||
*/
|
||||
abstract class Draggable {
|
||||
|
||||
/** 最小 X 坐标 */
|
||||
protected DoubleProperty minX;
|
||||
|
||||
/** 最大 X 坐标 */
|
||||
protected DoubleProperty maxX;
|
||||
|
||||
/** 最小 Y 坐标 */
|
||||
protected DoubleProperty minY;
|
||||
|
||||
/** 最大 Y 坐标 */
|
||||
protected DoubleProperty maxY;
|
||||
|
||||
/** 偏移补偿 X 值 */
|
||||
protected DoubleProperty offsetX;
|
||||
|
||||
/** 偏移补偿 Y 值 */
|
||||
protected DoubleProperty offsetY;
|
||||
|
||||
/** 是否启用 */
|
||||
protected BooleanProperty enable;
|
||||
|
||||
/** 正否正在拖动 */
|
||||
protected BooleanProperty dragging;
|
||||
|
||||
/** 触发按钮 */
|
||||
protected ObjectProperty<MouseButton> eventButton;
|
||||
|
||||
{
|
||||
minX = new SimpleDoubleProperty(Double.NaN);
|
||||
maxX = new SimpleDoubleProperty(Double.NaN);
|
||||
minY = new SimpleDoubleProperty(Double.NaN);
|
||||
maxY = new SimpleDoubleProperty(Double.NaN);
|
||||
offsetX = new SimpleDoubleProperty(0);
|
||||
offsetY = new SimpleDoubleProperty(0);
|
||||
enable = new SimpleBooleanProperty(true);
|
||||
dragging = new SimpleBooleanProperty(false);
|
||||
eventButton = new SimpleObjectProperty<>(MouseButton.PRIMARY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最小 X 值
|
||||
*
|
||||
* @return 最小 X 值
|
||||
*/
|
||||
public double getMinX() {
|
||||
return minX.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最小 X 值属性
|
||||
*
|
||||
* @return 最小 X 值属性
|
||||
*/
|
||||
public DoubleProperty minXProperty() {
|
||||
return minX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最小 X 值
|
||||
*
|
||||
* @param minX 最小 X 值
|
||||
*/
|
||||
public void setMinX(double minX) {
|
||||
this.minX.set(minX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大 X 值
|
||||
*
|
||||
* @return 最大 X 值
|
||||
*/
|
||||
public double getMaxX() {
|
||||
return maxX.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大 Y 值属性
|
||||
*
|
||||
* @return 最大 Y 值属性
|
||||
*/
|
||||
public DoubleProperty maxXProperty() {
|
||||
return maxX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大 X 值
|
||||
*
|
||||
* @param maxX 最大 X 值
|
||||
*/
|
||||
public void setMaxX(double maxX) {
|
||||
this.maxX.set(maxX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最小 Y 值
|
||||
*
|
||||
* @return 最小 Y 值
|
||||
*/
|
||||
public double getMinY() {
|
||||
return minY.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最小 Y 值属性
|
||||
*
|
||||
* @return 最小 Y 值属性
|
||||
*/
|
||||
public DoubleProperty minYProperty() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最小 Y 值
|
||||
*
|
||||
* @param minY 最小 Y 值
|
||||
*/
|
||||
public void setMinY(double minY) {
|
||||
this.minY.set(minY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大 Y 值
|
||||
*
|
||||
* @return 最小 Y 值
|
||||
*/
|
||||
public double getMaxY() {
|
||||
return maxY.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最小 Y 值属性
|
||||
*
|
||||
* @return 最小 Y 值属性
|
||||
*/
|
||||
public DoubleProperty maxYProperty() {
|
||||
return maxY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大 Y 值
|
||||
*
|
||||
* @param maxY 最大 Y 值
|
||||
*/
|
||||
public void setMaxY(double maxY) {
|
||||
this.maxY.set(maxY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取偏移 X 值
|
||||
*
|
||||
* @return 偏移 X 值
|
||||
*/
|
||||
public double getOffsetX() {
|
||||
return offsetX.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取偏移 X 值属性
|
||||
*
|
||||
* @return 偏移 X 值属性
|
||||
*/
|
||||
public DoubleProperty offsetXProperty() {
|
||||
return offsetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置偏移 X 值
|
||||
*
|
||||
* @param offsetX 偏移 X 值
|
||||
*/
|
||||
public void setOffsetX(double offsetX) {
|
||||
this.offsetX.set(offsetX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取偏移 Y 值
|
||||
*
|
||||
* @return 偏移 Y 值
|
||||
*/
|
||||
public double getOffsetY() {
|
||||
return offsetY.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取偏移 Y 值属性
|
||||
*
|
||||
* @return 偏移 Y 值属性
|
||||
*/
|
||||
public DoubleProperty offsetYProperty() {
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置偏移 Y 值
|
||||
*
|
||||
* @param offsetY 偏移 Y 值
|
||||
*/
|
||||
public void setOffsetY(double offsetY) {
|
||||
this.offsetY.set(offsetY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否启用功能
|
||||
*
|
||||
* @return true 为启用
|
||||
*/
|
||||
public boolean isEnable() {
|
||||
return enable.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否启用功能属性
|
||||
*
|
||||
* @return 是否启用功能属性
|
||||
*/
|
||||
public BooleanProperty enableProperty() {
|
||||
return enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否启用功能
|
||||
*
|
||||
* @param enable true 为启用
|
||||
*/
|
||||
public void setEnable(boolean enable) {
|
||||
this.enable.set(enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否正在拖动
|
||||
*
|
||||
* @return true 为正在拖动
|
||||
*/
|
||||
public boolean isDragging() {
|
||||
return dragging.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否正在拖动属性
|
||||
*
|
||||
* @return 拖动属性
|
||||
*/
|
||||
public ReadOnlyBooleanProperty draggingProperty() {
|
||||
return dragging;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否正在拖动
|
||||
*
|
||||
* @param dragging true 为正在拖动
|
||||
*/
|
||||
protected void setDragging(boolean dragging) {
|
||||
this.dragging.set(dragging);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取触发拖动事件的按钮
|
||||
*
|
||||
* @return 触发拖动事件的按钮
|
||||
*/
|
||||
public MouseButton getEventButton() {
|
||||
return eventButton.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取触发拖动事件的按钮属性
|
||||
*
|
||||
* @return 触发拖动事件的按钮属性
|
||||
*/
|
||||
public ObjectProperty<MouseButton> eventButtonProperty() {
|
||||
return eventButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置触发拖动事件的按钮
|
||||
*
|
||||
* @param eventButton 触发拖动事件的按钮
|
||||
*/
|
||||
public void setEventButton(MouseButton eventButton) {
|
||||
this.eventButton.set(eventButton);
|
||||
}
|
||||
}
|
||||
68
src/main/java/com/imyeyu/fx/draggable/DraggableNode.java
Normal file
68
src/main/java/com/imyeyu/fx/draggable/DraggableNode.java
Normal file
@ -0,0 +1,68 @@
|
||||
package com.imyeyu.fx.draggable;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
/**
|
||||
* 让组件可以自由拖动,此操作不影响布局,所有偏移限制基于变换坐标,也就是组件默认位置为原点
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-02-04 15:59
|
||||
*/
|
||||
public class DraggableNode extends Draggable {
|
||||
|
||||
private double oldTranslateX = -1, oldTranslateY = -1, oldScreenX = -1, oldScreenY = -1;
|
||||
|
||||
/**
|
||||
* 默认构造器
|
||||
*
|
||||
* @param node 组件
|
||||
*/
|
||||
public DraggableNode(Node node) {
|
||||
this(node, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认构造器
|
||||
*
|
||||
* @param handler 触发组件
|
||||
* @param target 移动组件
|
||||
*/
|
||||
public DraggableNode(Node handler, Node target) {
|
||||
handler.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
if (isEnable()) {
|
||||
if (getEventButton() != null && e.getButton() != getEventButton()) {
|
||||
return;
|
||||
}
|
||||
oldTranslateX = target.getTranslateX();
|
||||
oldTranslateY = target.getTranslateY();
|
||||
oldScreenX = e.getScreenX();
|
||||
oldScreenY = e.getScreenY();
|
||||
setDragging(true);
|
||||
}
|
||||
});
|
||||
handler.addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
|
||||
if (isDragging()) {
|
||||
double newX = oldTranslateX + e.getScreenX() - oldScreenX;
|
||||
double newY = oldTranslateY + e.getScreenY() - oldScreenY;
|
||||
|
||||
if (!Double.isNaN(minX.get())) {
|
||||
newX = Math.max(newX, minX.get());
|
||||
}
|
||||
if (!Double.isNaN(maxX.get())) {
|
||||
newX = Math.min(newX, maxX.get());
|
||||
}
|
||||
if (!Double.isNaN(minY.get())) {
|
||||
newY = Math.max(newY, minY.get());
|
||||
}
|
||||
if (!Double.isNaN(maxY.get())) {
|
||||
newY = Math.min(newY, maxY.get());
|
||||
}
|
||||
|
||||
target.setTranslateX(newX + offsetX.get());
|
||||
target.setTranslateY(newY + offsetY.get());
|
||||
}
|
||||
});
|
||||
handler.addEventFilter(MouseEvent.MOUSE_RELEASED, e -> setDragging(false));
|
||||
}
|
||||
}
|
||||
42
src/main/java/com/imyeyu/fx/draggable/DraggableWindow.java
Normal file
42
src/main/java/com/imyeyu/fx/draggable/DraggableWindow.java
Normal file
@ -0,0 +1,42 @@
|
||||
package com.imyeyu.fx.draggable;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* 让组件触发拖动窗体,通常在没有系统边框时使用
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-03-13 14:22
|
||||
*/
|
||||
public class DraggableWindow extends Draggable {
|
||||
|
||||
private double oldX, oldY;
|
||||
|
||||
/**
|
||||
* 标准构造
|
||||
*
|
||||
* @param window 窗体
|
||||
* @param target 触发组件
|
||||
*/
|
||||
public DraggableWindow(Window window, Node target) {
|
||||
target.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
if (isEnable()) {
|
||||
if (getEventButton() != null && e.getButton() != getEventButton()) {
|
||||
return;
|
||||
}
|
||||
oldX = e.getX();
|
||||
oldY = e.getY();
|
||||
setDragging(true);
|
||||
}
|
||||
});
|
||||
target.addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
|
||||
if (isDragging()) {
|
||||
window.setX(e.getScreenX() - oldX + offsetX.get());
|
||||
window.setY(e.getScreenY() - oldY + offsetY.get());
|
||||
}
|
||||
});
|
||||
target.addEventFilter(MouseEvent.MOUSE_RELEASED, e -> setDragging(false));
|
||||
}
|
||||
}
|
||||
64
src/main/java/com/imyeyu/fx/task/PublicTask.java
Normal file
64
src/main/java/com/imyeyu/fx/task/PublicTask.java
Normal file
@ -0,0 +1,64 @@
|
||||
package com.imyeyu.fx.task;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
|
||||
/**
|
||||
* Task 提升权限,所有操作视 UI 线程状态选择性调度(UI 繁忙时不更新),数据计算不应在更新回调中
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-01-08 16:24
|
||||
*/
|
||||
abstract class PublicTask<T> extends Task<T> {
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
*
|
||||
* @param value 数据对象
|
||||
*/
|
||||
@Override
|
||||
public void updateValue(T value) {
|
||||
super.updateValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息
|
||||
*
|
||||
* @param message 消息
|
||||
*/
|
||||
@Override
|
||||
public void updateMessage(String message) {
|
||||
super.updateMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度(自动计算百分比)
|
||||
*
|
||||
* @param workDone 已完成
|
||||
* @param max 最大
|
||||
*/
|
||||
@Override
|
||||
public void updateProgress(long workDone, long max) {
|
||||
super.updateProgress(workDone, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度(自动计算百分比)
|
||||
*
|
||||
* @param workDone 已完成
|
||||
* @param max 最大
|
||||
*/
|
||||
@Override
|
||||
public void updateProgress(double workDone, double max) {
|
||||
super.updateProgress(workDone, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新标题
|
||||
*
|
||||
* @param title 标题
|
||||
*/
|
||||
@Override
|
||||
public void updateTitle(String title) {
|
||||
super.updateTitle(title);
|
||||
}
|
||||
}
|
||||
301
src/main/java/com/imyeyu/fx/task/RunAsync.java
Normal file
301
src/main/java/com/imyeyu/fx/task/RunAsync.java
Normal file
@ -0,0 +1,301 @@
|
||||
package com.imyeyu.fx.task;
|
||||
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
import com.imyeyu.java.bean.CallbackReturn;
|
||||
|
||||
/**
|
||||
* 异步线程执行,常用的构建异步线程任务
|
||||
*
|
||||
* <pre>
|
||||
* // 快速构造
|
||||
* RunAsync.run(() -> {}); // 快速异步执行 {@link #run(Callback)}
|
||||
* RunAsync.run(() -> {}, () -> {}); // 快速异步回调 {@link #run(Callback, Callback)}
|
||||
* RunAsync.run(() -> 100, v -> {}); // 快速异步返回 {@link #callbackReturn(CallbackReturn, CallbackArg)}
|
||||
* RunAsync.later(() -> {}, 1000); // 延时一秒执行 {@link #later(Callback, long)}
|
||||
*
|
||||
* // 标准构造
|
||||
* new RunAsync<T>() {
|
||||
*
|
||||
* @Override
|
||||
* protected T call() throws Exception {
|
||||
*
|
||||
* // 这里不是 FX 线程,可以执行一些长时间的事情
|
||||
*
|
||||
* update(new T()); // 主动更新数据
|
||||
* progress(.6); // 更新进度
|
||||
* return new T(); // 返回更新数据
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void onUpdate(T t) {
|
||||
* // 数据更新事件,属于 FX 线程,视 UI 线程状态回调
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void onUpdateProgress(double progress) {
|
||||
* // 更新进度事件,属于 FX 线程,视 UI 线程状态回调
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void onFinish() {
|
||||
* // 线程执行完成,属于 FX 线程
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void onException(Throwable e) {
|
||||
* // 线程异常,属于 FX 线程
|
||||
* }
|
||||
* }.start(); <-- 千万别忘了启动任务
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since
|
||||
*
|
||||
* @param <T> 数据处理返回类型
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2021-02-13 12:56
|
||||
*/
|
||||
public abstract class RunAsync<T> extends Service<T> {
|
||||
|
||||
/** 任务对象 */
|
||||
protected PublicTask<T> task;
|
||||
|
||||
/** 最近一次更新数据 */
|
||||
protected T lastT;
|
||||
|
||||
protected boolean isInterrupted = false;
|
||||
|
||||
@Override
|
||||
protected Task<T> createTask() {
|
||||
task = new PublicTask<>() {
|
||||
|
||||
protected T call() throws Exception {
|
||||
return RunAsync.this.call();
|
||||
}
|
||||
};
|
||||
task.setOnScheduled(e -> isInterrupted = false);
|
||||
task.setOnSucceeded(e -> {
|
||||
onFinish();
|
||||
onFinish(lastT);
|
||||
onFinally();
|
||||
isInterrupted = false;
|
||||
});
|
||||
task.setOnFailed(e -> {
|
||||
onFinally();
|
||||
isInterrupted = false;
|
||||
});
|
||||
task.valueProperty().addListener((obs, o, t) -> {
|
||||
lastT = t;
|
||||
onUpdate(t);
|
||||
});
|
||||
task.messageProperty().addListener((obs, o, msg) -> onMessage(msg));
|
||||
task.progressProperty().addListener((obs, o, p) -> onProgress(p.doubleValue()));
|
||||
task.exceptionProperty().addListener((obs, o, e) -> onException(e));
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行事件(非 FX 线程)
|
||||
*
|
||||
* @return 处理结果
|
||||
* @throws Exception 处理异常
|
||||
*/
|
||||
protected abstract T call() throws Exception;
|
||||
|
||||
/**
|
||||
* 主动更新数据,通常是非 FX 线程的 call 里触发,{@link #onUpdate(T)} 里视 UI 线程状态回调
|
||||
*
|
||||
* @param t 更新值
|
||||
*/
|
||||
protected void update(T t) {
|
||||
lastT = t;
|
||||
task.updateValue(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动更新进度,非 FX 线程的 call 里触发,{@link #onProgress(double)} 里视 UI 线程状态回调
|
||||
*
|
||||
* @param progress 进度值
|
||||
*/
|
||||
protected void progress(double progress) {
|
||||
task.updateProgress(progress, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息,非 FX 线程的 call 里触发,{@link #onMessage(String)} 里视 UI 线程状态回调
|
||||
*
|
||||
* @param msg 消息
|
||||
*/
|
||||
protected void message(String msg) {
|
||||
task.updateMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据更新事件,{@link #update(T)} 或 {@link #call()} 返回参数触发,属于 FX 线程
|
||||
*
|
||||
* @param t 更新数据
|
||||
*/
|
||||
protected void onUpdate(T t) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度事件,{@link #progress(double)} 触发,属于 FX 线程
|
||||
*
|
||||
* @param progress 进度值
|
||||
*/
|
||||
protected void onProgress(double progress) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息
|
||||
*
|
||||
* @param msg 消息
|
||||
*/
|
||||
protected void onMessage(String msg) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/** 完成事件(FX 线程,只要线程结束就会调用) */
|
||||
protected void onFinish() {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成事件(FX 线程,只要线程结束就会调用,参数为 {@link #update(T)} 或 {@link #call()} 最后一次更新值)
|
||||
*
|
||||
* @param t 执行事件返回值
|
||||
*/
|
||||
protected void onFinish(T t) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 发生异常(FX 线程)
|
||||
*
|
||||
* @param e 异常
|
||||
*/
|
||||
protected void onException(Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
/** 线程完成监听,无论正常完成还是出现异常,都会触发此方法 */
|
||||
protected void onFinally() {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/** 中断任务 */
|
||||
public void interrupt() {
|
||||
isInterrupted = true;
|
||||
}
|
||||
|
||||
// ---------- 静态构造 ----------
|
||||
|
||||
/**
|
||||
* 快速构造稍后执行,触发事件属于 FX 线程
|
||||
*
|
||||
* @param callback 异步事件
|
||||
* @param delay 延时时间(毫秒)
|
||||
*/
|
||||
public static void later(Callback callback, long delay) {
|
||||
run(() -> {
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造异步任务
|
||||
*
|
||||
* @param callback 异步回调
|
||||
*/
|
||||
public static void run(Callback callback) {
|
||||
run(callback, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造异步任务
|
||||
*
|
||||
* @param asyncCallback 异步回调
|
||||
* @param finishCallback 异步完成回调
|
||||
*/
|
||||
public static void run(Callback asyncCallback, Callback finishCallback) {
|
||||
run(asyncCallback, finishCallback, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造异步任务
|
||||
*
|
||||
* @param asyncCallback 异步回调
|
||||
* @param exceptionCallback 异常回调
|
||||
*/
|
||||
public static void run(Callback asyncCallback, CallbackArg<Throwable> exceptionCallback) {
|
||||
run(asyncCallback, null, exceptionCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造异步任务(私有为了可读性,既然主要事件都用上了那应该使用 new {@link RunAsync}() 来保持可读性)
|
||||
*
|
||||
* @param asyncCallback 异步回调
|
||||
* @param finishCallback 异步完成回调
|
||||
* @param exceptionCallback 异常回调
|
||||
*/
|
||||
private static void run(Callback asyncCallback, Callback finishCallback, CallbackArg<Throwable> exceptionCallback) {
|
||||
new RunAsync<Void>() {
|
||||
|
||||
@Override
|
||||
protected Void call() {
|
||||
asyncCallback.handler();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish() {
|
||||
try {
|
||||
if (finishCallback != null) {
|
||||
finishCallback.handler();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
if (exceptionCallback != null) {
|
||||
exceptionCallback.handler(e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造异步任务
|
||||
*
|
||||
* @param asyncCallback 异步回调
|
||||
* @param finishCallback 异步完成回调
|
||||
* @param <T> 回调数据类型
|
||||
*/
|
||||
public static <T> void callbackReturn(CallbackReturn<T> asyncCallback, CallbackArg<T> finishCallback) {
|
||||
new RunAsync<T>() {
|
||||
|
||||
@Override
|
||||
protected T call() {
|
||||
return asyncCallback.handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(T t) {
|
||||
finishCallback.handler(t);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
||||
65
src/main/java/com/imyeyu/fx/task/RunAsyncDaemon.java
Normal file
65
src/main/java/com/imyeyu/fx/task/RunAsyncDaemon.java
Normal file
@ -0,0 +1,65 @@
|
||||
package com.imyeyu.fx.task;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
|
||||
/**
|
||||
* 异步守护执行,如果 {@link RunAsync} 超过指定时间没有回调,将触发 onTimeout
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-01-07 17:33
|
||||
*/
|
||||
public abstract class RunAsyncDaemon<T> extends RunAsync<T> {
|
||||
|
||||
private final int timeout;
|
||||
|
||||
/** true 为回调成功 */
|
||||
protected boolean isCallBack;
|
||||
|
||||
/** 默认构造器(执行超时 2 秒) */
|
||||
public RunAsyncDaemon() {
|
||||
this(2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造器
|
||||
*
|
||||
* @param timeout 执行超时限时(毫秒)
|
||||
*/
|
||||
public RunAsyncDaemon(int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<T> createTask() {
|
||||
super.createTask();
|
||||
|
||||
isCallBack = false;
|
||||
task.setOnSucceeded(e -> {
|
||||
onFinish();
|
||||
onFinish(lastT);
|
||||
onFinally();
|
||||
isCallBack = true;
|
||||
});
|
||||
task.setOnFailed(e -> {
|
||||
onFinally();
|
||||
isCallBack = true;
|
||||
});
|
||||
task.setOnRunning(e -> new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(timeout);
|
||||
if (!isCallBack) {
|
||||
onTimeout();
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
throw new TimiException(TimiCode.ERROR, "interrupted error", ex);
|
||||
}
|
||||
}).start());
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/** 超时事件 */
|
||||
protected abstract void onTimeout();
|
||||
}
|
||||
352
src/main/java/com/imyeyu/fx/task/RunAsyncScheduled.java
Normal file
352
src/main/java/com/imyeyu/fx/task/RunAsyncScheduled.java
Normal file
@ -0,0 +1,352 @@
|
||||
package com.imyeyu.fx.task;
|
||||
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
import com.imyeyu.java.bean.CallbackReturn;
|
||||
|
||||
/**
|
||||
* 异步任务,支持多次调度
|
||||
*
|
||||
* <pre>
|
||||
* // ---------- 快速构造 ----------
|
||||
*
|
||||
* // 每秒执行一次,参数 2 不是 FX 线程
|
||||
* // {@link RunAsyncScheduled#call(Duration, Callback)}
|
||||
* RunAsyncScheduled.call(Duration.seconds(1), () -> {});
|
||||
*
|
||||
* // 每秒执行一次,参数 2 是 FX 线程
|
||||
* // {@link RunAsyncScheduled#finish(Duration, Callback)}
|
||||
* RunAsyncScheduled.finish(Duration.seconds(1), () -> {});
|
||||
*
|
||||
* // 动态周期执行,参数 2 不是 FX 线程
|
||||
* // {@link RunAsyncScheduled#call(CallbackArg, Callback)}
|
||||
* RunAsyncScheduled.call(scheduled -> {}, () -> {});
|
||||
*
|
||||
* // 动态周期执行,参数 2 是 FX 线程
|
||||
* // {@link RunAsyncScheduled#finish(CallbackArg, Callback)}
|
||||
* RunAsyncScheduled.finish(scheduled -> {}, () -> {});
|
||||
*
|
||||
* // 每秒执行一次并返回,参数 3 (FX 线程)的入参是参数 2 (非 FX 线程)的返回
|
||||
* // {@link RunAsyncScheduled#call(Duration, CallbackReturn, CallbackArg)}
|
||||
* RunAsyncScheduled.call(Duration.seconds(1), () -> "result", result -> {});
|
||||
*
|
||||
* // ---------- 标准构造 ----------
|
||||
* new RunAsyncScheduled<T>() {
|
||||
*
|
||||
* @Override
|
||||
* protected void onScheduled() {
|
||||
* // 任务开始后执行,此时可调整任务的周期和延时
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected T call() {
|
||||
* // 这里不是 FX 线程,可以执行一些长时间的事情
|
||||
*
|
||||
* update(new T()); // 主动更新数据
|
||||
* progress(.6); // 更新进度
|
||||
* return new T(); // 返回更新数据
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void onUpdate(T t) {
|
||||
* // 数据更新事件,属于 FX 线程,视 UI 线程状态回调
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void onUpdateProgress(double progress) {
|
||||
* // 更新进度事件,属于 FX 线程,视 UI 线程状态回调
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void onFinish() {
|
||||
* // 线程执行完成,属于 FX 线程
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void onException(Throwable e) {
|
||||
* // 线程异常,属于 FX 线程
|
||||
* }
|
||||
* }.start(); <-- 千万别忘了启动任务
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-01-14 17:05
|
||||
*/
|
||||
public abstract class RunAsyncScheduled<T> extends ScheduledService<T> {
|
||||
|
||||
/** 执行任务 */
|
||||
protected PublicTask<T> task;
|
||||
|
||||
/** 最近一次更新数据 */
|
||||
protected T lastT;
|
||||
|
||||
protected boolean isInterrupted = false;
|
||||
|
||||
/** 默认构造器 */
|
||||
public RunAsyncScheduled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准构造
|
||||
*
|
||||
* @param period 周期
|
||||
*/
|
||||
public RunAsyncScheduled(Duration period) {
|
||||
setPeriod(period);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<T> createTask() {
|
||||
task = new PublicTask<>() {
|
||||
|
||||
protected T call() throws Exception {
|
||||
return RunAsyncScheduled.this.call();
|
||||
}
|
||||
};
|
||||
task.setOnScheduled(e -> {
|
||||
onScheduled();
|
||||
isInterrupted = false;
|
||||
});
|
||||
task.setOnRunning(e -> onRunning());
|
||||
task.setOnSucceeded(e -> {
|
||||
onFinish();
|
||||
onFinish(lastT);
|
||||
onFinally();
|
||||
isInterrupted = false;
|
||||
});
|
||||
task.setOnFailed(e -> {
|
||||
onFinally();
|
||||
isInterrupted = false;
|
||||
});
|
||||
task.valueProperty().addListener((obs, o, t) -> {
|
||||
lastT = t;
|
||||
onUpdate(t);
|
||||
});
|
||||
task.progressProperty().addListener((obs, o, p) -> onUpdateProgress(p.doubleValue()));
|
||||
task.exceptionProperty().addListener((obs, o, e) -> onException(e));
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行事件(非 FX 线程)
|
||||
*
|
||||
* @return 处理结果
|
||||
* @throws Exception 处理异常
|
||||
*/
|
||||
protected T call() throws Exception {
|
||||
// 子类实现
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 每次开始运行时触发,在延时计算前触发 */
|
||||
protected void onScheduled() {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/** {@link #call()} 运行时触发 */
|
||||
protected void onRunning() {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动更新数据,通常是非 FX 线程的 call 里触发,{@link #onUpdate(T)} 里视 UI 线程状态回调
|
||||
*
|
||||
* @param t 更新值
|
||||
*/
|
||||
protected void update(T t) {
|
||||
lastT = t;
|
||||
task.updateValue(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动更新进度,非 FX 线程的 call 里触发,{@link #onUpdateProgress(double)} 里视 UI 线程状态回调
|
||||
*
|
||||
* @param progress 进度值
|
||||
*/
|
||||
protected void progress(double progress) {
|
||||
task.updateProgress(progress, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据更新事件,{@link #update(T)} 或 {@link #call()} 返回参数触发,属于 FX 线程
|
||||
*
|
||||
* @param t 更新数据
|
||||
*/
|
||||
protected void onUpdate(T t) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度事件,{@link #progress(double)} 触发,属于 FX 线程
|
||||
*
|
||||
* @param progress 进度值
|
||||
*/
|
||||
protected void onUpdateProgress(double progress) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/** 完成事件(FX 线程,只要线程结束就会调用) */
|
||||
protected void onFinish() {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成事件(FX 线程,只要线程结束就会调用,参数为 {@link #update(T)} 或 {@link #call()} 最后一次更新值)
|
||||
*
|
||||
* @param t 执行事件返回值
|
||||
*/
|
||||
protected void onFinish(T t) {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/** 线程完成监听,无论正常完成还是出现异常,都会触发此方法 */
|
||||
protected void onFinally() {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/**
|
||||
* 发生异常(FX 线程)
|
||||
*
|
||||
* @param e 异常
|
||||
*/
|
||||
protected void onException(Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
/** 中断任务 */
|
||||
public void interrupt() {
|
||||
isInterrupted = true;
|
||||
}
|
||||
|
||||
// ---------- 静态构造 ----------
|
||||
|
||||
/**
|
||||
* 快速构造多次调度任务
|
||||
*
|
||||
* @param period 周期
|
||||
* @param asyncCallback 异步线程执行
|
||||
* @return 任务
|
||||
*/
|
||||
public static RunAsyncScheduled<Void> call(Duration period, Callback asyncCallback) {
|
||||
RunAsyncScheduled<Void> scheduled = new RunAsyncScheduled<>() {
|
||||
|
||||
@Override
|
||||
protected Void call() {
|
||||
asyncCallback.handler();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
scheduled.setPeriod(period);
|
||||
scheduled.start();
|
||||
return scheduled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造多次调度任务
|
||||
*
|
||||
* @param period 周期
|
||||
* @param asyncCallback 异步线程回调
|
||||
* @return 任务
|
||||
*/
|
||||
public static RunAsyncScheduled<Void> finish(Duration period, Callback asyncCallback) {
|
||||
RunAsyncScheduled<Void> scheduled = new RunAsyncScheduled<>() {
|
||||
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
return super.call();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish() {
|
||||
asyncCallback.handler();
|
||||
}
|
||||
};
|
||||
scheduled.setPeriod(period);
|
||||
scheduled.start();
|
||||
return scheduled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造多次调度任务
|
||||
*
|
||||
* @param period 周期
|
||||
* @param asyncCallback 异步线程执行
|
||||
* @param finishCallback 异步线程执行完成回调
|
||||
* @return 任务
|
||||
* @param <T> 执行数据回调
|
||||
*/
|
||||
public static <T> RunAsyncScheduled<T> call(Duration period, CallbackReturn<T> asyncCallback, CallbackArg<T> finishCallback) {
|
||||
RunAsyncScheduled<T> scheduled = new RunAsyncScheduled<>() {
|
||||
|
||||
@Override
|
||||
protected T call() {
|
||||
return asyncCallback.handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(T t) {
|
||||
finishCallback.handler(t);
|
||||
}
|
||||
};
|
||||
scheduled.setPeriod(period);
|
||||
scheduled.start();
|
||||
return scheduled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造动态周期调度任务
|
||||
*
|
||||
* @param scheduleEvent 执行前调度,可重置执行周期
|
||||
* @param asyncCallback 异步线程执行
|
||||
* @return 任务
|
||||
*/
|
||||
public static RunAsyncScheduled<Void> call(CallbackArg<RunAsyncScheduled<Void>> scheduleEvent, Callback asyncCallback) {
|
||||
RunAsyncScheduled<Void> scheduled = new RunAsyncScheduled<>() {
|
||||
|
||||
@Override
|
||||
protected void onScheduled() {
|
||||
scheduleEvent.handler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() {
|
||||
asyncCallback.handler();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
scheduled.start();
|
||||
return scheduled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造多次调度任务
|
||||
*
|
||||
* @param scheduleEvent 执行前调度,可重置执行周期
|
||||
* @param finishCallback 异步线程执行完成回调
|
||||
* @return 任务
|
||||
*/
|
||||
public static RunAsyncScheduled<Void> finish(CallbackArg<RunAsyncScheduled<Void>> scheduleEvent, Callback finishCallback) {
|
||||
RunAsyncScheduled<Void> scheduled = new RunAsyncScheduled<>() {
|
||||
|
||||
@Override
|
||||
protected void onScheduled() {
|
||||
scheduleEvent.handler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish() {
|
||||
finishCallback.handler();
|
||||
}
|
||||
};
|
||||
scheduled.start();
|
||||
return scheduled;
|
||||
}
|
||||
}
|
||||
164
src/main/java/com/imyeyu/fx/utils/Anchor.java
Normal file
164
src/main/java/com/imyeyu/fx/utils/Anchor.java
Normal file
@ -0,0 +1,164 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
|
||||
/**
|
||||
* 锚点布局简化操作
|
||||
*
|
||||
* <pre>
|
||||
* Anchor.def(node, top, right, bottom, left);
|
||||
* Anchor.def(node, topRightBottomLeft);
|
||||
* Anchor.def(node, topBottom, leftRight);
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-05-16 23:31
|
||||
*/
|
||||
public class Anchor extends AnchorPane {
|
||||
|
||||
/**
|
||||
* {@link #leftTop(Node, Number, Number)} 的语义化方法
|
||||
*
|
||||
* @param node 组件
|
||||
* @param x 距离左边
|
||||
* @param y 距离上边
|
||||
*/
|
||||
public static void xy(Node node, Number x, Number y) {
|
||||
def(node, y, null, null, x);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据左上角定位
|
||||
*
|
||||
* @param node 组件
|
||||
* @param left 距离左边
|
||||
* @param top 距离上边
|
||||
*/
|
||||
public static void leftTop(Node node, Number left, Number top) {
|
||||
def(node, top, null, null, left);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据右上角定位
|
||||
*
|
||||
* @param node 组件
|
||||
* @param right 距离右边
|
||||
* @param top 距离上边
|
||||
*/
|
||||
public static void rightTop(Node node, Number right, Number top) {
|
||||
def(node, top, right, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据左下角定位
|
||||
*
|
||||
* @param node 组件
|
||||
* @param left 距离左边
|
||||
* @param bottom 距离下边
|
||||
*/
|
||||
public static void leftBottom(Node node, Number left, Number bottom) {
|
||||
def(node, null, null, bottom, left);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据右下角定位
|
||||
*
|
||||
* @param node 组件
|
||||
* @param right 距离右边
|
||||
* @param bottom 距离下边
|
||||
*/
|
||||
public static void rightBottom(Node node, Number right, Number bottom) {
|
||||
def(node, null, right, bottom, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了上边,其他贴紧父级组件
|
||||
*
|
||||
* @param node 组件
|
||||
*/
|
||||
public static void exTop(Node node) {
|
||||
def(node, null, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了左边,其他贴紧父级组件
|
||||
*
|
||||
* @param node 组件
|
||||
*/
|
||||
public static void exLeft(Node node) {
|
||||
def(node, 0, 0, 0, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了右边,其他贴紧父级组件
|
||||
*
|
||||
* @param node 组件
|
||||
*/
|
||||
public static void exRight(Node node) {
|
||||
def(node, 0, null, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了下边,其他贴紧父级组件
|
||||
*
|
||||
* @param node 组件
|
||||
*/
|
||||
public static void exBottom(Node node) {
|
||||
def(node, 0, 0, null, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 四边完全贴紧父级组件
|
||||
*
|
||||
* @param node 组件
|
||||
*/
|
||||
public static void def(Node node) {
|
||||
def(node, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 AnchorPane 四边边距
|
||||
*
|
||||
* @param node 组件
|
||||
* @param size 大小
|
||||
*/
|
||||
public static void def(Node node, Number size) {
|
||||
def(node, size, size, size, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 AnchorPane 上下和左右边距
|
||||
*
|
||||
* @param node 组件
|
||||
* @param topBottom 上下
|
||||
* @param leftRight 左右
|
||||
*/
|
||||
public static void def(Node node, Number topBottom, Number leftRight) {
|
||||
def(node, topBottom, leftRight, topBottom, leftRight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 AnchorPane 四边间距,传值 null 为不设定
|
||||
*
|
||||
* @param node 组件
|
||||
* @param top 上
|
||||
* @param right 右
|
||||
* @param bottom 下
|
||||
* @param left 左
|
||||
*/
|
||||
public static void def(Node node, Number top, Number right, Number bottom, Number left) {
|
||||
if (top != null) {
|
||||
setTopAnchor(node, top.doubleValue());
|
||||
}
|
||||
if (left != null) {
|
||||
setLeftAnchor(node, left.doubleValue());
|
||||
}
|
||||
if (right != null) {
|
||||
setRightAnchor(node, right.doubleValue());
|
||||
}
|
||||
if (bottom != null) {
|
||||
setBottomAnchor(node, bottom.doubleValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
356
src/main/java/com/imyeyu/fx/utils/AnimationRenderer.java
Normal file
356
src/main/java/com/imyeyu/fx/utils/AnimationRenderer.java
Normal file
@ -0,0 +1,356 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.util.Duration;
|
||||
import com.imyeyu.fx.task.RunAsyncScheduled;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* 动画帧渲染器,可控帧率,在 JVM 启动参数含<b>-Djavafx.animation.fullspeed=true</b>时,预设 FPS
|
||||
* 才可以突破屏幕刷新率。
|
||||
*
|
||||
* <pre>
|
||||
* Box box = new Box(128, 128, 128);
|
||||
* box.setDrawMode(DrawMode.LINE);
|
||||
* box.setCullFace(CullFace.BACK);
|
||||
* box.setMaterial(new PhongMaterial(RED));
|
||||
* box.setRotationAxis(new Point3D(0, 64, 0));
|
||||
*
|
||||
* // 以 240 FPS 每秒 90 度旋转一个 3D 立方体
|
||||
* AnimationRenderer renderer = new AnimationRenderer(240);
|
||||
* renderer.addRenderCallback(deltaSecond -> {
|
||||
* box.setRotate(box.getRotate() + 90 * deltaSecond);
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-03-08 09:48
|
||||
*/
|
||||
public class AnimationRenderer {
|
||||
|
||||
/** 渲染回调 */
|
||||
protected final LinkedList<CallbackArg<Double>> renderCallbacks;
|
||||
|
||||
/** 动画渲染队列 */
|
||||
protected final LinkedList<Animation> animations;
|
||||
|
||||
/** 渲染器 */
|
||||
protected final AnimationTimer timer;
|
||||
|
||||
/** 平均帧生成时间 */
|
||||
protected final DoubleProperty mpf;
|
||||
|
||||
/** 平均帧率 */
|
||||
protected final IntegerProperty fps;
|
||||
|
||||
/** 状态计时器 */
|
||||
protected final RunAsyncScheduled<?> statusTimer;
|
||||
|
||||
/** 当前帧 */
|
||||
private double nowNanos;
|
||||
|
||||
/** 上一帧 */
|
||||
private double lastNanos;
|
||||
|
||||
/** 累计帧差(纳秒) */
|
||||
private double deltaNanos;
|
||||
|
||||
/** 累计帧差(秒) */
|
||||
private double deltaSecond;
|
||||
|
||||
/** 当前帧差 */
|
||||
private double betweenNanos;
|
||||
|
||||
/** 标准帧生成时间(纳秒) */
|
||||
private double NPF;
|
||||
|
||||
/** 预设渲染帧率 */
|
||||
private int prefFPS;
|
||||
|
||||
/** 默认构造,60 FPS */
|
||||
public AnimationRenderer() {
|
||||
this(60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准构造
|
||||
*
|
||||
* @param prefFPS 预设帧率
|
||||
*/
|
||||
public AnimationRenderer(int prefFPS) {
|
||||
setPrefFPS(prefFPS);
|
||||
|
||||
fps = new SimpleIntegerProperty(0);
|
||||
mpf = new SimpleDoubleProperty(0);
|
||||
renderCallbacks = new LinkedList<>();
|
||||
animations = new LinkedList<>();
|
||||
|
||||
timer = new AnimationTimer() {
|
||||
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
nowNanos = now;
|
||||
if (0 < lastNanos) {
|
||||
// 计算帧差
|
||||
deltaNanos += betweenNanos = nowNanos - lastNanos;
|
||||
|
||||
// 累计帧差大于最小帧生成时间(足够渲染下一帧)
|
||||
if (NPF <= deltaNanos) {
|
||||
deltaSecond = deltaNanos * 1E-9;
|
||||
|
||||
synchronized (renderCallbacks) {
|
||||
for (CallbackArg<Double> renderCallback : renderCallbacks) {
|
||||
renderCallback.handler(deltaSecond);
|
||||
}
|
||||
}
|
||||
synchronized (animations) {
|
||||
// 动画
|
||||
Iterator<Animation> iterator = animations.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Animation next = iterator.next();
|
||||
if (next.diedAt < millis()) {
|
||||
next.callback.handler(deltaSecond, 1);
|
||||
if (next.onFinishedEvent != null) {
|
||||
next.onFinishedEvent.handler();
|
||||
}
|
||||
iterator.remove();
|
||||
} else {
|
||||
next.callback.handler(deltaSecond, (millis() - next.startAt) / next.ttl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 消耗剩余帧差
|
||||
deltaNanos = deltaNanos % NPF;
|
||||
}
|
||||
}
|
||||
lastNanos = nowNanos;
|
||||
}
|
||||
};
|
||||
|
||||
// 状态计算
|
||||
statusTimer = RunAsyncScheduled.finish(Duration.seconds(1), new Callback() {
|
||||
|
||||
long total = 0, old;
|
||||
|
||||
{
|
||||
renderCallbacks.add(nowNanos -> total++);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handler() {
|
||||
// 帧生成时间
|
||||
mpf.set(betweenNanos * 1E-6);
|
||||
|
||||
// 帧率
|
||||
fps.set((int) (total - old));
|
||||
old = total;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 启动 */
|
||||
public void start() {
|
||||
nowNanos = lastNanos = 0;
|
||||
deltaNanos = NPF;
|
||||
timer.start();
|
||||
}
|
||||
|
||||
/** 停止 */
|
||||
public void stop() {
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加渲染动画
|
||||
*
|
||||
* @param duration 持续时间
|
||||
* @param callback 动画回调
|
||||
*/
|
||||
public void render(Duration duration, AnimationCallback callback) {
|
||||
render(duration, callback, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加渲染动画
|
||||
*
|
||||
* @param duration 持续时间
|
||||
* @param callback 动画回调
|
||||
* @param onFinishedEvent 动画完成回调
|
||||
*/
|
||||
public void render(Duration duration, AnimationCallback callback, Callback onFinishedEvent) {
|
||||
synchronized (animations) {
|
||||
Animation animation = new Animation();
|
||||
animation.startAt = nowNanos * 1E-6;
|
||||
animation.diedAt = animation.startAt + duration.toMillis();
|
||||
animation.ttl = duration.toMillis();
|
||||
animation.callback = callback;
|
||||
animation.onFinishedEvent = onFinishedEvent;
|
||||
animations.add(animation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加渲染回调
|
||||
*
|
||||
* @param callback 回调
|
||||
*/
|
||||
public void addRenderCallback(CallbackArg<Double> callback) {
|
||||
synchronized (renderCallbacks) {
|
||||
renderCallbacks.add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除渲染回调
|
||||
*
|
||||
* @param callback 回调
|
||||
*/
|
||||
public void removeRenderCallback(CallbackArg<Double> callback) {
|
||||
synchronized (renderCallbacks) {
|
||||
renderCallbacks.remove(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预设 FPS
|
||||
*
|
||||
* @return 预设 FPS
|
||||
*/
|
||||
public int getPrefFPS() {
|
||||
return prefFPS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预设 FPS,渲染器会尽量匹配此帧率渲染,可能会突破少许,系统资源紧张时实际渲染帧率会低于预设
|
||||
*
|
||||
* @param prefFPS FPS 取值范围 [1, 1000]
|
||||
*/
|
||||
public void setPrefFPS(int prefFPS) {
|
||||
if (prefFPS < 1) {
|
||||
throw new IllegalArgumentException("pref fps can not less then 1");
|
||||
}
|
||||
this.prefFPS = prefFPS;
|
||||
this.NPF = 1E9 / prefFPS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取帧生成时间(毫秒)
|
||||
*
|
||||
* @return 帧生成时间
|
||||
*/
|
||||
public double getMPF() {
|
||||
return mpf.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取帧生成时间属性(毫秒)
|
||||
*
|
||||
* @return 帧生成时间属性(毫秒)
|
||||
*/
|
||||
public ReadOnlyDoubleProperty mpfProperty() {
|
||||
return mpf;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前渲染 FPS
|
||||
*
|
||||
* @return FPS
|
||||
*/
|
||||
public int getFPS() {
|
||||
return fps.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前渲染 FPS 属性
|
||||
*
|
||||
* @return FPS 属性
|
||||
*/
|
||||
public ReadOnlyIntegerProperty fpsProperty() {
|
||||
return fps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取累计帧差(纳秒)
|
||||
*
|
||||
* @return 累计帧差(纳秒)
|
||||
*/
|
||||
public double deltaNanos() {
|
||||
return deltaNanos;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取累计帧差(毫秒)
|
||||
*
|
||||
* @return 累计帧差(毫秒)
|
||||
*/
|
||||
public double deltaMillis() {
|
||||
return deltaNanos * 1E-6;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取累计帧差(秒)
|
||||
*
|
||||
* @return 累计帧差(秒)
|
||||
*/
|
||||
public double deltaSecond() {
|
||||
return deltaNanos * 1E-9;
|
||||
}
|
||||
|
||||
/** @return 当前帧(纳秒) */
|
||||
public double nanos() {
|
||||
return nowNanos;
|
||||
}
|
||||
|
||||
/** @return 当前帧(毫秒) */
|
||||
public double millis() {
|
||||
return nowNanos * 1E-6;
|
||||
}
|
||||
|
||||
/** @return 当前帧(秒) */
|
||||
public double second() {
|
||||
return nowNanos * 1E-9;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性动画
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-05-16 16:02
|
||||
*/
|
||||
public static class Animation {
|
||||
|
||||
double diedAt;
|
||||
double startAt;
|
||||
double ttl;
|
||||
AnimationCallback callback;
|
||||
Callback onFinishedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性动画回调
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-05-14 10:10
|
||||
*/
|
||||
public interface AnimationCallback {
|
||||
|
||||
/**
|
||||
* 处理器
|
||||
*
|
||||
* @param deltaSecond 帧差
|
||||
* @param percent 动画进度百分比,取值范围 [0, 1]
|
||||
*/
|
||||
void handler(double deltaSecond, double percent);
|
||||
}
|
||||
}
|
||||
198
src/main/java/com/imyeyu/fx/utils/BgFill.java
Normal file
198
src/main/java/com/imyeyu/fx/utils/BgFill.java
Normal file
@ -0,0 +1,198 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.CycleMethod;
|
||||
import javafx.scene.paint.LinearGradient;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.paint.Stop;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 快速构建简单背景填充,支持渐变背景。{@link #build()} 作为导出作用,最后执行
|
||||
*
|
||||
* <pre>
|
||||
* new BgFill("red").build() // 创建简单的背景填充
|
||||
* new BgFill("red", "blue").toRight().build() // 创建由红到蓝,从左往右的渐变填充背景
|
||||
* new BgFill("red").radii(bgRadii).insets(bgInsets).build() // 设置圆角率和边距
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-02-13 12:52
|
||||
*/
|
||||
public class BgFill {
|
||||
|
||||
private Paint color;
|
||||
private Stop[] stops;
|
||||
private Paint linearGradient;
|
||||
|
||||
/** 自定义圆角和内边距 */
|
||||
private Insets insets;
|
||||
private CornerRadii radii;
|
||||
|
||||
/**
|
||||
* 纯色构造器
|
||||
*
|
||||
* @param color 颜色
|
||||
*/
|
||||
public BgFill(Paint color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯色构造器
|
||||
*
|
||||
* @param color 颜色
|
||||
*/
|
||||
public BgFill(String color) {
|
||||
this.color = Paint.valueOf(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渐变构造器
|
||||
*
|
||||
* @param start 起点颜色
|
||||
* @param end 结束颜色
|
||||
*/
|
||||
public BgFill(String start, String end) {
|
||||
stops = new Stop[]{new Stop(0, Color.valueOf(start)), new Stop(1, Color.valueOf(end))};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置四角圆角大小
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgFill raddi(double size) {
|
||||
this.radii = new CornerRadii(size);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置四角圆角大小
|
||||
*
|
||||
* @param topLeft 左上
|
||||
* @param topRight 右上
|
||||
* @param bottomRight 右下
|
||||
* @param bottomLeft 左下
|
||||
* @param asPercent true 为百分比数值
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgFill raddi(double topLeft, double topRight, double bottomRight, double bottomLeft, boolean asPercent) {
|
||||
this.radii = new CornerRadii(topLeft, topRight, bottomRight, bottomLeft, asPercent);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内边距
|
||||
*
|
||||
* @param insets 内边距
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgFill insets(Insets insets) {
|
||||
this.insets = insets;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内边距
|
||||
*
|
||||
* @param size 四边边距
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgFill insets(double size) {
|
||||
return insets(size, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内边距
|
||||
*
|
||||
* @param topBottom 上下边距
|
||||
* @param leftRight 左右边距
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgFill insets(double topBottom, double leftRight) {
|
||||
this.insets = new Insets(topBottom, leftRight, topBottom, leftRight);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内边距
|
||||
*
|
||||
* @param top 上边距
|
||||
* @param right 右边距
|
||||
* @param bottom 下边距
|
||||
* @param left 左边距
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgFill insets(double top, double right, double bottom, double left) {
|
||||
this.insets = new Insets(top, right, bottom, left);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从左往右渐变
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgFill toRight() {
|
||||
if (stops == null) {
|
||||
throw new NullPointerException("please use BgFill(start, end) constructor ");
|
||||
}
|
||||
linearGradient = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从上往下渐变
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgFill toBottom() {
|
||||
if (stops == null) {
|
||||
throw new NullPointerException("please use BgFill(start, end) constructor ");
|
||||
}
|
||||
linearGradient = new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, stops);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出背景
|
||||
*
|
||||
* @return 背景
|
||||
*/
|
||||
public Background build() {
|
||||
return new Background(buildFill(linearGradient == null ? color : linearGradient));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出背景填充
|
||||
*
|
||||
* @param color 颜色
|
||||
* @return 背景填充
|
||||
*/
|
||||
public BackgroundFill buildFill(Paint color) {
|
||||
radii = radii == null ? CornerRadii.EMPTY : radii;
|
||||
insets = insets == null ? Insets.EMPTY : insets;
|
||||
return new BackgroundFill(color, radii, insets);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造测试背景
|
||||
*
|
||||
* @return 背景
|
||||
*/
|
||||
public static Background test() {
|
||||
Random r = new Random();
|
||||
StringBuilder sb = new StringBuilder("#");
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(r.nextInt(8));
|
||||
}
|
||||
return new BgFill(sb.toString()).build();
|
||||
}
|
||||
}
|
||||
288
src/main/java/com/imyeyu/fx/utils/BgImage.java
Normal file
288
src/main/java/com/imyeyu/fx/utils/BgImage.java
Normal file
@ -0,0 +1,288 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundImage;
|
||||
import javafx.scene.layout.BackgroundPosition;
|
||||
import javafx.scene.layout.BackgroundRepeat;
|
||||
import javafx.scene.layout.BackgroundSize;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* 背景图片封装。{@link #build()} 作为导出作用,最后执行
|
||||
*
|
||||
* <p>默认原图大小,不平铺,居原点
|
||||
*
|
||||
* <pre>
|
||||
* // 适应缩放的 Cover 背景图片构建
|
||||
* new BgImage("/cover.png").cover().build()
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-02-13 12:53
|
||||
*/
|
||||
public class BgImage {
|
||||
|
||||
private BackgroundRepeat repeatX, repeatY;
|
||||
|
||||
private Side sideH, sideV;
|
||||
private double posH, posV, sizeW, sizeH;
|
||||
private boolean posHAsPercent, posVAsPercent, sizeWAsPercent, sizeHAsPercent, isContain, isCover;
|
||||
|
||||
private final Image img;
|
||||
|
||||
/**
|
||||
* 背景图构造器
|
||||
*
|
||||
* @param url 图片位置(程序内资源)
|
||||
*/
|
||||
public BgImage(String url) {
|
||||
this(new Image(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* 背景图构造器
|
||||
*
|
||||
* @param file 图片文件(程序外资源)
|
||||
* @throws FileNotFoundException 找不到文件
|
||||
*/
|
||||
public BgImage(File file) throws FileNotFoundException {
|
||||
this(new Image(new FileInputStream(file)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 背景图构造器
|
||||
*
|
||||
* @param img 图像
|
||||
*/
|
||||
public BgImage(Image img) {
|
||||
this.img = img;
|
||||
// 初始化数据
|
||||
repeatX = repeatY = BackgroundRepeat.REPEAT;
|
||||
sideH = Side.LEFT;
|
||||
sideV = Side.TOP;
|
||||
posH = posV = 0;
|
||||
sizeW = sizeH = -1;
|
||||
posHAsPercent = posVAsPercent = sizeWAsPercent = sizeHAsPercent = isContain = isCover = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 双轴平铺方式
|
||||
*
|
||||
* @param repeat 填充方式
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage repeat(BackgroundRepeat repeat) {
|
||||
repeatX = repeatY = repeat;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* X 轴平铺方式
|
||||
*
|
||||
* @param repeat 填充方式
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage repeatX(BackgroundRepeat repeat) {
|
||||
repeatX = repeat;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Y 轴平铺方式
|
||||
*
|
||||
* @param repeat 填充方式
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage repeatY(BackgroundRepeat repeat) {
|
||||
repeatY = repeat;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 背景位置
|
||||
*
|
||||
* @param sideH 相对水平位置,左或右
|
||||
* @param posH 相对水平距离
|
||||
* @param hAsPercent true 为水平距离是百分比
|
||||
* @param sideV 相对垂直位置,上或下
|
||||
* @param posV 相对垂直距离
|
||||
* @param vAsPercent true 为垂直距离是百分比
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage pos(Side sideH, double posH, boolean hAsPercent, Side sideV, double posV, boolean vAsPercent) {
|
||||
this.sideH = sideH;
|
||||
this.sideV = sideV;
|
||||
this.posH = posH;
|
||||
this.posV = posV;
|
||||
this.posHAsPercent = hAsPercent;
|
||||
this.posVAsPercent = vAsPercent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 水平对齐方式
|
||||
*
|
||||
* @param side 相对位置
|
||||
* @param size 相对距离
|
||||
* @param asPercent true 百分比数据
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage horizontal(Side side, double size, boolean asPercent) {
|
||||
sideH = side;
|
||||
posH = size;
|
||||
posHAsPercent = asPercent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 居左
|
||||
*
|
||||
* @param size 距离
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage left(double size) {
|
||||
return horizontal(Side.LEFT, size, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 居右
|
||||
*
|
||||
* @param size 距离
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage right(double size) {
|
||||
return horizontal(Side.RIGHT, size, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 垂直对齐方式
|
||||
*
|
||||
* @param side 相对位置
|
||||
* @param size 相对距离
|
||||
* @param asPercent true 百分比数据
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage vertical(Side side, double size, boolean asPercent) {
|
||||
sideV = side;
|
||||
posV = size;
|
||||
posVAsPercent = asPercent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 居上
|
||||
*
|
||||
* @param size 距离
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage top(double size) {
|
||||
return vertical(Side.TOP, size, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 居下
|
||||
*
|
||||
* @param size 距离
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage bottom(double size) {
|
||||
return vertical(Side.BOTTOM, size, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 坐标轴定位
|
||||
*
|
||||
* @param x 轴
|
||||
* @param y 轴
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage xy(double x, double y) {
|
||||
sideH = Side.LEFT;
|
||||
sideV = Side.TOP;
|
||||
posH = x;
|
||||
posV = y;
|
||||
posHAsPercent = posVAsPercent = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像大小
|
||||
*
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
* @param widthAsPercent true 为参数是百分比
|
||||
* @param heightAsPercent true 为参数是百分比
|
||||
* @param isContain true 为尽量最大化图像
|
||||
* @param isCover true 为保持比例
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage size(double width, double height, boolean widthAsPercent, boolean heightAsPercent, boolean isContain, boolean isCover) {
|
||||
sizeW = width;
|
||||
sizeH = height;
|
||||
sizeWAsPercent = widthAsPercent;
|
||||
sizeHAsPercent = heightAsPercent;
|
||||
this.isContain = isContain;
|
||||
this.isCover = isCover;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自适应保持比例时尽量最大化的背景
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage cover() {
|
||||
return size(100, 100, true, true, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 居中背景图片
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public BgImage center() {
|
||||
sideH = Side.LEFT;
|
||||
sideV = Side.TOP;
|
||||
posH = posV = 0;
|
||||
posHAsPercent = posVAsPercent = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @return 最终构造背景 */
|
||||
public Background build() {
|
||||
return new Background(new BackgroundImage(img, repeatX, repeatY, new BackgroundPosition(sideH, posH, posHAsPercent, sideV, posV, posVAsPercent), new BackgroundSize(sizeW, sizeH, sizeWAsPercent, sizeHAsPercent, isContain, isCover)));
|
||||
}
|
||||
|
||||
/**
|
||||
* JavaFX 设置组件背景,底色为默认
|
||||
*
|
||||
* @param node 节点
|
||||
* @param url 背景路径
|
||||
* @param width 宽度
|
||||
* @param x X 轴偏移
|
||||
* @param y Y 轴偏移
|
||||
*/
|
||||
public static void setBg(Node node, String url, int width, int x, int y) {
|
||||
node.setStyle("-fx-background-size: " + width + ";" + "-fx-background-image: url('" + url + "');" + "-fx-background-insets: 0;" + "-fx-background-repeat: no-repeat;" + "-fx-background-position: " + x + " " + y);
|
||||
}
|
||||
|
||||
/**
|
||||
* JavaFX 设置组件背景,底色为透明
|
||||
*
|
||||
* @param node 节点
|
||||
* @param url 背景路径
|
||||
* @param width 宽度
|
||||
* @param x X 轴偏移
|
||||
* @param y Y 轴偏移
|
||||
*/
|
||||
public static void setBgTp(Node node, String url, int width, int x, int y) {
|
||||
node.setStyle("-fx-background-size: " + width + ";" + "-fx-background-image: url('" + url + "');" + "-fx-background-color: transparent;" + "-fx-background-insets: 0;" + "-fx-background-repeat: no-repeat;" + "-fx-background-position: " + x + " " + y);
|
||||
}
|
||||
}
|
||||
467
src/main/java/com/imyeyu/fx/utils/BorderStroke.java
Normal file
467
src/main/java/com/imyeyu/fx/utils/BorderStroke.java
Normal file
@ -0,0 +1,467 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 快速构建边框。{@link #build()} 作为导出作用,最后执行
|
||||
*
|
||||
* <br>示例
|
||||
* <pre>
|
||||
* new BorderStroke("red").build(); // 红色边框,默认样式(1 像素宽度直线,无圆角无内边距)
|
||||
* new BorderStroke("red").width(0, 1, 0, 1).build() // 红色直线左右边框
|
||||
* new BorderStroke("red").top().build() // 红色直线上边框
|
||||
* new BorderStroke("red").radius(.5, true).build() // 红色完全圆角边框
|
||||
* </pre>
|
||||
* {@link #top()}, {@link #right()}, {@link #bottom()}, {@link #left()} 不可重复使用,因为这是针对性边框宽度设置,重复使用只有最后一个有效
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-02-14 10:42
|
||||
*/
|
||||
public class BorderStroke {
|
||||
|
||||
/** 空的边框 */
|
||||
public static final Border EMPTY = Border.EMPTY;
|
||||
|
||||
/** 无边框样式 */
|
||||
public static final BorderStrokeStyle NONE = BorderStrokeStyle.NONE;
|
||||
|
||||
/** 连续边框 */
|
||||
public static final BorderStrokeStyle SOLID = BorderStrokeStyle.SOLID;
|
||||
|
||||
/** 断续边框 */
|
||||
public static final BorderStrokeStyle DASHED = BorderStrokeStyle.DASHED;
|
||||
|
||||
/** 点状边框 */
|
||||
public static final BorderStrokeStyle DOTTED = BorderStrokeStyle.DOTTED;
|
||||
|
||||
// 简写说明: TRBL 代表上右下左
|
||||
|
||||
/** 颜色 */
|
||||
private final Paint cT, cR, cB, cL;
|
||||
|
||||
/** 宽度 */
|
||||
private double wT, wR, wB, wL;
|
||||
|
||||
/** 圆角 */
|
||||
private double crTL, crTR, crBR, crBL;
|
||||
|
||||
/** 内边距 */
|
||||
private double pT, pR, pB, pL;
|
||||
|
||||
/** 圆角是否百分比 */
|
||||
private boolean crPercent = false;
|
||||
|
||||
/** 样式 */
|
||||
private BorderStrokeStyle bssT, bssR, bssB, bssL;
|
||||
|
||||
/**
|
||||
* 边框构造
|
||||
*
|
||||
* @param top 上边框颜色
|
||||
* @param right 右边框颜色
|
||||
* @param bottom 下边框颜色
|
||||
* @param left 左边框颜色
|
||||
*/
|
||||
public BorderStroke(String top, String right, String bottom, String left) {
|
||||
this(Paint.valueOf(top), Paint.valueOf(right), Paint.valueOf(bottom), Paint.valueOf(left));
|
||||
}
|
||||
|
||||
/**
|
||||
* 边框构造
|
||||
*
|
||||
* @param color 边框颜色
|
||||
*/
|
||||
public BorderStroke(String color) {
|
||||
this(color, color, color, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 边框构造
|
||||
*
|
||||
* @param color 边框颜色
|
||||
*/
|
||||
public BorderStroke(Paint color) {
|
||||
this(color, color, color, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 边框构造
|
||||
*
|
||||
* @param top 上边框颜色
|
||||
* @param right 右边框颜色
|
||||
* @param bottom 下边框颜色
|
||||
* @param left 左边框颜色
|
||||
*/
|
||||
public BorderStroke(Paint top, Paint right, Paint bottom, Paint left) {
|
||||
cT = top;
|
||||
cR = right;
|
||||
cB = bottom;
|
||||
cL = left;
|
||||
// 默认参数
|
||||
wT = wR = wB = wL = 1;
|
||||
crTL = crTR = crBR = crBL = 0;
|
||||
pT = pR = pB = pL = 0;
|
||||
bssT = bssR = bssB = bssL = SOLID;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1 像素上边框
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke top() {
|
||||
return top(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1 像素右边框
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke right() {
|
||||
return right(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1 像素下边框
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke bottom() {
|
||||
return bottom(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1 像素左边框
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke left() {
|
||||
return left(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上边框
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke top(double size) {
|
||||
return width(size, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 右边框
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke right(double size) {
|
||||
return width(0, size, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下边框
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke bottom(double size) {
|
||||
return width(0, 0, size, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 左边框
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke left(double size) {
|
||||
return width(0, 0, 0, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了上边框,其他设置为 1 像素边框
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke exTop() {
|
||||
return exTop(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了右边框,其他设置为 1 像素边框
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke exRight() {
|
||||
return exRight(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了下边框,其他设置为 1 像素边框
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke exBottom() {
|
||||
return exBottom(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了左边框,其他设置为 1 像素边框
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke exLeft() {
|
||||
return exLeft(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了上边框,其他设置边框
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke exTop(double size) {
|
||||
return width(0, size, size, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了右边框,其他设置边框
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke exRight(double size) {
|
||||
return width(size, 0, size, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了下边框,其他设置边框
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke exBottom(double size) {
|
||||
return width(size, size, 0, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 除了左边框,其他设置边框
|
||||
*
|
||||
* @param size 大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke exLeft(double size) {
|
||||
return width(size, size, size, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置宽度
|
||||
*
|
||||
* @param all 所有边框
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke width(double all) {
|
||||
return width(all, all);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置宽度
|
||||
*
|
||||
* @param topBottom 上下
|
||||
* @param leftRight 左右
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke width(double topBottom, double leftRight) {
|
||||
return width(topBottom, leftRight, topBottom, leftRight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置宽度
|
||||
*
|
||||
* @param top 上
|
||||
* @param right 右
|
||||
* @param bottom 下
|
||||
* @param left 左
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke width(double top, double right, double bottom, double left) {
|
||||
wT = top;
|
||||
wR = right;
|
||||
wB = bottom;
|
||||
wL = left;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置圆角
|
||||
*
|
||||
* @param all 所有大小
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke radius(double all) {
|
||||
return radius(all, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置圆角
|
||||
*
|
||||
* @param all 所有大小
|
||||
* @param asPercent 是否为百分比值(默认否)
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke radius(double all, boolean asPercent) {
|
||||
return radius(all, all, all, all, asPercent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置圆角
|
||||
*
|
||||
* @param topLeft 左上角大小
|
||||
* @param topRight 右上角大小
|
||||
* @param bottomRight 右下角大小
|
||||
* @param bottomLeft 左下角大小
|
||||
* @param asPercent 是否为百分比值
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke radius(double topLeft, double topRight, double bottomRight, double bottomLeft, boolean asPercent) {
|
||||
crTL = topLeft;
|
||||
crTR = topRight;
|
||||
crBR = bottomRight;
|
||||
crBL = bottomLeft;
|
||||
crPercent = asPercent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚线样式
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke dashed() {
|
||||
return style(DASHED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点阵样式
|
||||
*
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke dotted() {
|
||||
return style(DOTTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置样式
|
||||
*
|
||||
* @param all 所有边框样式
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke style(BorderStrokeStyle all) {
|
||||
return style(all, all, all, all);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置样式
|
||||
*
|
||||
* @param top 上
|
||||
* @param right 右
|
||||
* @param bottom 下
|
||||
* @param left 左
|
||||
* @return 构造器
|
||||
*/
|
||||
public BorderStroke style(BorderStrokeStyle top, BorderStrokeStyle right, BorderStrokeStyle bottom, BorderStrokeStyle left) {
|
||||
bssT = top;
|
||||
bssR = right;
|
||||
bssB = bottom;
|
||||
bssL = left;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内边距
|
||||
*
|
||||
* @param all 所有边框内边距
|
||||
* @return 边框构造器
|
||||
*/
|
||||
public BorderStroke padding(double all) {
|
||||
return padding(all, all);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内边距
|
||||
*
|
||||
* @param topBottom 上下
|
||||
* @param leftRight 左右
|
||||
* @return 边框构造器
|
||||
*/
|
||||
public BorderStroke padding(double topBottom, double leftRight) {
|
||||
return padding(topBottom, leftRight, topBottom, leftRight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内边距
|
||||
*
|
||||
* @param top 上
|
||||
* @param right 右
|
||||
* @param bottom 下
|
||||
* @param left 左
|
||||
* @return 边框构造器
|
||||
*/
|
||||
public BorderStroke padding(double top, double right, double bottom, double left) {
|
||||
pT = top;
|
||||
pR = right;
|
||||
pB = bottom;
|
||||
pL = left;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造边框
|
||||
*
|
||||
* @return 边框
|
||||
*/
|
||||
public Border build() {
|
||||
return new Border(
|
||||
new javafx.scene.layout.BorderStroke(
|
||||
cT, cR, cB, cL,
|
||||
bssT, bssR, bssB, bssL,
|
||||
new CornerRadii(crTL, crTR, crBR, crBL, crPercent),
|
||||
new BorderWidths(wT, wR, wB, wL),
|
||||
new Insets(pT, pR, pB, pL)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造测试边框
|
||||
*
|
||||
* @param color 颜色
|
||||
* @return 边框
|
||||
*/
|
||||
public static Border test(String color) {
|
||||
return new BorderStroke(color).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构造测试边框
|
||||
*
|
||||
* @return 边框
|
||||
*/
|
||||
public static Border test() {
|
||||
Random r = new Random();
|
||||
StringBuilder sb = new StringBuilder("#");
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(r.nextInt(8));
|
||||
}
|
||||
return test(sb.toString());
|
||||
}
|
||||
}
|
||||
175
src/main/java/com/imyeyu/fx/utils/Column.java
Normal file
175
src/main/java/com/imyeyu/fx/utils/Column.java
Normal file
@ -0,0 +1,175 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
/**
|
||||
* GridPane 列属性构造
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-12-17 23:45
|
||||
*/
|
||||
public class Column extends ColumnConstraints {
|
||||
|
||||
/** 通用键列 */
|
||||
public static final Column KEY = Column.build();
|
||||
|
||||
/** 通用值列 */
|
||||
public static final Column VALUE = Column.build().notFill().alwaysPriority();
|
||||
|
||||
/** 通用值列(填充) */
|
||||
public static final Column VALUE_FILL = Column.build().fill().alwaysPriority();
|
||||
|
||||
/**
|
||||
* 默认构造器({@link #build(HPos)} 静态构造)
|
||||
*
|
||||
* @param align 水平对齐方式
|
||||
*/
|
||||
private Column(HPos align) {
|
||||
setHalignment(align);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最小宽度
|
||||
*
|
||||
* @param minWidth 最小宽度
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column min(double minWidth) {
|
||||
setMinWidth(minWidth);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大宽度
|
||||
*
|
||||
* @param maxWidth 最大宽度
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column max(double maxWidth) {
|
||||
setMaxWidth(maxWidth);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置宽度
|
||||
*
|
||||
* @param width 宽度
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column width(double width) {
|
||||
setPrefWidth(width);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 以百分比设置宽度
|
||||
*
|
||||
* @param percentWidth 宽度
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column percentWidth(double percentWidth) {
|
||||
setPercentWidth(percentWidth);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保持子组件宽度填充
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column fill() {
|
||||
setFillWidth(true);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子组件宽度不需填充
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column notFill() {
|
||||
setFillWidth(false);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 左对齐
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column left() {
|
||||
setHalignment(HPos.LEFT);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 居中对齐
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column center() {
|
||||
setHalignment(HPos.CENTER);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 右对齐
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column right() {
|
||||
setHalignment(HPos.RIGHT);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列宽总是保持跟随容器尺寸变化
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column alwaysPriority() {
|
||||
setHgrow(Priority.ALWAYS);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列宽总是不保持跟随容器尺寸变化
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column neverPriority() {
|
||||
setHgrow(Priority.NEVER);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列宽不受其他列影响时可跟随容器尺寸变化
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Column sometimesPriority() {
|
||||
setHgrow(Priority.SOMETIMES);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态构造列属性对象
|
||||
*
|
||||
* @return 列属性对象
|
||||
*/
|
||||
public static Column build() {
|
||||
return new Column(HPos.LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态构造列属性对象
|
||||
*
|
||||
* @param align 水平对齐方式
|
||||
* @return 本实例
|
||||
*/
|
||||
public static Column build(HPos align) {
|
||||
return new Column(align);
|
||||
}
|
||||
}
|
||||
72
src/main/java/com/imyeyu/fx/utils/DirectorySelector.java
Normal file
72
src/main/java/com/imyeyu/fx/utils/DirectorySelector.java
Normal file
@ -0,0 +1,72 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 文件夹选择器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2022-04-28 17:48
|
||||
*/
|
||||
public class DirectorySelector {
|
||||
|
||||
/** 选择器 */
|
||||
private final DirectoryChooser chooser;
|
||||
|
||||
/** 默认构造器,路径为当前程序运行路径 */
|
||||
public DirectorySelector() {
|
||||
this("./");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造器
|
||||
*
|
||||
* @param path 默认路径
|
||||
*/
|
||||
public DirectorySelector(String path) {
|
||||
chooser = new DirectoryChooser();
|
||||
|
||||
File dir = new File(Objects.requireNonNullElse(path, ""));
|
||||
if (dir.exists()) {
|
||||
if (dir.isDirectory()) {
|
||||
chooser.setInitialDirectory(dir);
|
||||
} else {
|
||||
chooser.setInitialDirectory(dir.getParentFile());
|
||||
}
|
||||
} else {
|
||||
chooser.setInitialDirectory(new File("./"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标题
|
||||
*
|
||||
* @param title 标题
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
chooser.setTitle(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示文件夹选择
|
||||
*
|
||||
* @param window 依赖窗体
|
||||
* @return 选择的文件夹
|
||||
*/
|
||||
public File show(Window window) {
|
||||
return chooser.showDialog(window);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选择器
|
||||
*
|
||||
* @return 选择器
|
||||
*/
|
||||
public DirectoryChooser getChooser() {
|
||||
return chooser;
|
||||
}
|
||||
}
|
||||
103
src/main/java/com/imyeyu/fx/utils/FileSelector.java
Normal file
103
src/main/java/com/imyeyu/fx/utils/FileSelector.java
Normal file
@ -0,0 +1,103 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 文件选择器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2022-04-14 19:38
|
||||
*/
|
||||
public class FileSelector {
|
||||
|
||||
/** 选择器 */
|
||||
private final FileChooser chooser;
|
||||
|
||||
/** 默认构造器 */
|
||||
public FileSelector() {
|
||||
this("./");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造文件选择器
|
||||
*
|
||||
* @param path 默认路径
|
||||
*/
|
||||
public FileSelector(String path) {
|
||||
chooser = new FileChooser();
|
||||
|
||||
File dir = new File(Objects.requireNonNullElse(path, ""));
|
||||
if (dir.exists()) {
|
||||
if (dir.isFile()) {
|
||||
chooser.setInitialDirectory(dir.getParentFile());
|
||||
} else {
|
||||
chooser.setInitialDirectory(dir);
|
||||
}
|
||||
} else {
|
||||
chooser.setInitialDirectory(new File("./"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标题
|
||||
*
|
||||
* @param title 标题
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
chooser.setTitle(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加格式限制
|
||||
*
|
||||
* @param description 说明
|
||||
* @param format 格式
|
||||
*/
|
||||
public void addFilter(String description, String... format) {
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(description, format));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单选文件
|
||||
*
|
||||
* @param window 依赖窗体
|
||||
* @return 选择的文件
|
||||
*/
|
||||
public File single(Window window) {
|
||||
return chooser.showOpenDialog(window);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多选文件
|
||||
*
|
||||
* @param window 依赖窗体
|
||||
* @return 选择的文件
|
||||
*/
|
||||
public List<File> multi(Window window) {
|
||||
return chooser.showOpenMultipleDialog(window);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
*
|
||||
* @param window 依赖窗体
|
||||
* @return 选择的文件夹
|
||||
*/
|
||||
public File save(Window window) {
|
||||
return chooser.showSaveDialog(window);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选择器
|
||||
*
|
||||
* @return 选择器
|
||||
*/
|
||||
public FileChooser getChooser() {
|
||||
return chooser;
|
||||
}
|
||||
}
|
||||
53
src/main/java/com/imyeyu/fx/utils/NoSelectionModel.java
Normal file
53
src/main/java/com/imyeyu/fx/utils/NoSelectionModel.java
Normal file
@ -0,0 +1,53 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.MultipleSelectionModel;
|
||||
|
||||
/**
|
||||
* 空的选择器,适用于 ListView
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
*
|
||||
* 夜雨 创建于 2021-05-16 23:29
|
||||
*/
|
||||
public class NoSelectionModel<T> extends MultipleSelectionModel<T> {
|
||||
|
||||
public ObservableList<Integer> getSelectedIndices() {
|
||||
return FXCollections.emptyObservableList();
|
||||
}
|
||||
|
||||
public ObservableList<T> getSelectedItems() {
|
||||
return FXCollections.emptyObservableList();
|
||||
}
|
||||
|
||||
public void selectIndices(int index, int... indices) {}
|
||||
|
||||
public void selectAll() {}
|
||||
|
||||
public void selectFirst() {}
|
||||
|
||||
public void selectLast() {}
|
||||
|
||||
public void clearAndSelect(int index) {}
|
||||
|
||||
public void select(int index) {}
|
||||
|
||||
public void select(T obj) {}
|
||||
|
||||
public void clearSelection(int index) {}
|
||||
|
||||
public void clearSelection() {}
|
||||
|
||||
public boolean isSelected(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void selectPrevious() {}
|
||||
|
||||
public void selectNext() {}
|
||||
}
|
||||
165
src/main/java/com/imyeyu/fx/utils/Row.java
Normal file
165
src/main/java/com/imyeyu/fx/utils/Row.java
Normal file
@ -0,0 +1,165 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.RowConstraints;
|
||||
|
||||
/**
|
||||
* GridPane 行属性构造
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-12-17 23:45
|
||||
*/
|
||||
public class Row extends RowConstraints {
|
||||
|
||||
/**
|
||||
* 默认构造器({@link #build(VPos)} 静态构造)
|
||||
*
|
||||
* @param align 垂直对齐方式
|
||||
*/
|
||||
private Row(VPos align) {
|
||||
setValignment(align);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最小高度
|
||||
*
|
||||
* @param minHeight 最小高度
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row min(double minHeight) {
|
||||
setMinHeight(minHeight);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大高度
|
||||
*
|
||||
* @param maxHeight 最大高度
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row max(double maxHeight) {
|
||||
setMaxHeight(maxHeight);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置高度
|
||||
*
|
||||
* @param height 高度
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row height(double height) {
|
||||
setPrefHeight(height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 以百分比设置高度
|
||||
*
|
||||
* @param percentHeight 高度
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row percentHeight(double percentHeight) {
|
||||
setPercentHeight(percentHeight);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保持子组件高度填充
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row fill() {
|
||||
setFillHeight(true);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子组件高度不需填充
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row notFill() {
|
||||
setFillHeight(false);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶部对齐
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row top() {
|
||||
setValignment(VPos.TOP);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 居中对齐
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row center() {
|
||||
setValignment(VPos.CENTER);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* 底部对齐
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row bottom() {
|
||||
setValignment(VPos.BOTTOM);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行高总是保持跟随容器尺寸变化
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row alwaysPriority() {
|
||||
setVgrow(Priority.ALWAYS);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行高总是不保持跟随容器尺寸变化
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row neverPriority() {
|
||||
setVgrow(Priority.NEVER);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行高不受其他行影响时可跟随容器尺寸变化
|
||||
*
|
||||
* @return 本实例
|
||||
*/
|
||||
public Row sometimesPriority() {
|
||||
setVgrow(Priority.SOMETIMES);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态构造行属性对象
|
||||
*
|
||||
* @return 行属性对象
|
||||
*/
|
||||
public static Row build() {
|
||||
return new Row(VPos.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态构造行属性对象
|
||||
*
|
||||
* @param align 垂直对齐方式
|
||||
* @return 本实例
|
||||
*/
|
||||
public static Row build(VPos align) {
|
||||
return new Row(align);
|
||||
}
|
||||
}
|
||||
122
src/main/java/com/imyeyu/fx/utils/ScreenFX.java
Normal file
122
src/main/java/com/imyeyu/fx/utils/ScreenFX.java
Normal file
@ -0,0 +1,122 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.BoundingBox;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import com.imyeyu.utils.Digest;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多屏操作,可在所有屏幕显示标识,并提供选择参考
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-11-11 21:02
|
||||
*/
|
||||
public class ScreenFX extends Stage {
|
||||
|
||||
/** 主屏幕 */
|
||||
public static final Screen primary = Screen.getPrimary();
|
||||
|
||||
/** 所有屏幕 */
|
||||
public static final List<Screen> SCREENS;
|
||||
|
||||
static {
|
||||
SCREENS = new ArrayList<>(Screen.getScreens());
|
||||
SCREENS.sort((s1, s2) -> (int) (s2.getBounds().getMinX() - s1.getBounds().getMinX()));
|
||||
Screen.getScreens().addListener((ListChangeListener<Screen>) c -> {
|
||||
SCREENS.clear();
|
||||
SCREENS.addAll(Screen.getScreens());
|
||||
SCREENS.sort((s1, s2) -> (int) (s2.getBounds().getMinX() - s1.getBounds().getMinX()));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 静态功能 ----------
|
||||
|
||||
/**
|
||||
* 此屏幕参数 MD5
|
||||
*
|
||||
* @param screen 屏幕
|
||||
* @return MD5
|
||||
*/
|
||||
public static String md5(Screen screen) throws NoSuchAlgorithmException {
|
||||
Rectangle2D r = screen.getBounds();
|
||||
return Digest.md5(String.valueOf(r.getMinX()) + r.getMinY() + r.getMaxX() + r.getMaxY() + r.getWidth() + r.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取坐标所在屏幕
|
||||
*
|
||||
* @param x 坐标
|
||||
* @param y 坐标
|
||||
* @return 屏幕(在所有屏幕之外时返回 null)
|
||||
*/
|
||||
public static Screen getScreenByXY(double x, double y) {
|
||||
for (int i = 0; i < SCREENS.size(); i++) {
|
||||
if (isInScreen(SCREENS.get(i), x, y)) {
|
||||
return SCREENS.get(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 该坐标是否溢出屏幕
|
||||
*
|
||||
* @param x 坐标
|
||||
* @param y 坐标
|
||||
* @return true 为溢出
|
||||
*/
|
||||
public static boolean outOfScreen(double x, double y) {
|
||||
return getScreenByXY(x, y) == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定该坐标是否在屏幕内
|
||||
*
|
||||
* @param screen 屏幕
|
||||
* @param x 坐标
|
||||
* @param y 坐标
|
||||
* @return true 为存在
|
||||
*/
|
||||
public static boolean isInScreen(Screen screen, double x, double y) {
|
||||
final Rectangle2D r = screen.getBounds();
|
||||
return !(x < r.getMinX() || r.getMaxX() < x || y < r.getMinY() || r.getMaxY() < y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务栏属性
|
||||
*
|
||||
* @param screen 屏幕
|
||||
* @return 任务栏属性
|
||||
*/
|
||||
public static Bounds getTaskbarBounds(Screen screen) {
|
||||
if (screen == null) {
|
||||
return null;
|
||||
}
|
||||
Rectangle2D sb = screen.getBounds();
|
||||
Rectangle2D vb = screen.getVisualBounds();
|
||||
if (sb.getMinY() < vb.getMinY()) {
|
||||
// 顶
|
||||
return new BoundingBox(sb.getMinX(), sb.getMinY(), sb.getWidth(), sb.getHeight() - vb.getHeight());
|
||||
}
|
||||
if (vb.getMaxY() < sb.getMaxY()) {
|
||||
// 底
|
||||
return new BoundingBox(sb.getMinX(), vb.getMaxY(), sb.getWidth(), sb.getHeight() - vb.getHeight());
|
||||
}
|
||||
if (sb.getMinX() < vb.getMinX()) {
|
||||
// 左
|
||||
return new BoundingBox(sb.getMinX(), sb.getMinY(), sb.getWidth() - vb.getWidth(), sb.getHeight());
|
||||
}
|
||||
if (vb.getMaxX() < sb.getMaxX()) {
|
||||
// 右
|
||||
return new BoundingBox(vb.getMaxX(), sb.getMinY(), sb.getWidth() - vb.getWidth(), sb.getHeight());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
372
src/main/java/com/imyeyu/fx/utils/SmoothScroll.java
Normal file
372
src/main/java/com/imyeyu/fx/utils/SmoothScroll.java
Normal file
@ -0,0 +1,372 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import com.sun.javafx.scene.control.VirtualScrollBar;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.control.skin.ComboBoxListViewSkin;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.util.Duration;
|
||||
import com.imyeyu.fx.bean.Interpolates;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 平滑滚动控制,使控件滚动平滑,此功能需要较多 GPU 性能
|
||||
*
|
||||
* <p><b>注意:虚拟滚动面板需要通过列表项计算理论可滚动高度,所以列表项必须统一高度</b></p>
|
||||
*
|
||||
* <pre>
|
||||
* SmoothScroll.textarea(new TextArea()); // 文本域平滑滚动
|
||||
* SmoothScroll.scrollPane(new ScrollPane()); // 平滑滚动面板
|
||||
* SmoothScroll.virtual(new ListView()); // 列表平滑滚动(虚拟面板平滑滚动,表格、树形结构等懒加载组件都是虚拟面板)
|
||||
* SmoothScroll.reflectSkin(new TextArea(), "scrollPane"); // 自定反射皮肤平滑滚动面板(反射组件 skin 里的滚动面板)
|
||||
* </pre>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-05-24 16:04
|
||||
*/
|
||||
public class SmoothScroll {
|
||||
|
||||
private static final double[] FRICTIONS = {.99, .1, .05, .04, .03, .02, .01, .04, .01, .008, .008, .008, .008, .0006, .0005, .00003, .00001};
|
||||
private static final double[] PUSHES = {.5};
|
||||
private static final double[] SCROLL_TO_FRICTIONS = Interpolates.EASE_OUT_QUINT.buildBezierPointValue(75);
|
||||
|
||||
// ---------- 虚拟面板滚动(列表项需统一高度) ----------
|
||||
|
||||
|
||||
/**
|
||||
* 平滑滚动 ComboBox 的选项列表
|
||||
*
|
||||
* @param comboBox 选择器
|
||||
* @deprecated 1.3.4 过时,1.6.0 移除,请使用 {@link #comboBox(ComboBox)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static void combobox(ComboBox<?> comboBox) {
|
||||
comboBox(comboBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑滚动 ComboBox 的选项列表
|
||||
*
|
||||
* @param comboBox 选择器
|
||||
*/
|
||||
public static void comboBox(ComboBox<?> comboBox) {
|
||||
if (comboBox.getSkin() == null) {
|
||||
comboBox.skinProperty().addListener((obs, o, obj) -> {
|
||||
try {
|
||||
virtual(Ref.getClassFieldValue(obj, ComboBoxListViewSkin.class, "listView", ListView.class));
|
||||
} catch (IllegalAccessException | NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
virtual(Ref.getClassFieldValue(comboBox.getSkin(), ComboBoxListViewSkin.class, "listView", ListView.class));
|
||||
} catch (IllegalAccessException | NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反射控件虚拟滚动面板
|
||||
*
|
||||
* @param control 控件
|
||||
*/
|
||||
public static void virtual(Control control) {
|
||||
if (control.getSkin() == null) {
|
||||
control.skinProperty().addListener((obs, o, obj) -> {
|
||||
try {
|
||||
virtual(control, Ref.getFieldValue(obj, "flow", VirtualFlow.class));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
virtual(control, Ref.getFieldValue(control.getSkin(), "flow", VirtualFlow.class));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟滚动面板平滑滚动
|
||||
*
|
||||
* @param control 组件
|
||||
* @param virtualFlow 虚拟滚动面板
|
||||
*/
|
||||
public static void virtual(Control control, VirtualFlow<?> virtualFlow) {
|
||||
try {
|
||||
VirtualScrollBar vbar = Ref.getFieldValue(virtualFlow, "vbar", VirtualScrollBar.class);
|
||||
DoubleProperty theoreticalHeight = new SimpleDoubleProperty(0); // 理论总高度
|
||||
DoubleProperty cellHeight = new SimpleDoubleProperty(0); // 行高
|
||||
|
||||
final double[] derivatives = new double[FRICTIONS.length];
|
||||
final double[] lastVPos = {0};
|
||||
|
||||
vbar.setUnitIncrement(5);
|
||||
|
||||
// 列表项高度和理论高度计算
|
||||
Callback calcHeight = () -> {
|
||||
if (0 < virtualFlow.getCellCount() && virtualFlow.getCell(0) != null) {
|
||||
cellHeight.set(virtualFlow.getCell(0).getHeight());
|
||||
theoreticalHeight.set(virtualFlow.getCellCount() * cellHeight.get());
|
||||
}
|
||||
};
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
|
||||
final EventHandler<ScrollEvent> scrollHandler = event -> {
|
||||
calcHeight.handler();
|
||||
if (virtualFlow.getHeight() < theoreticalHeight.get()) {
|
||||
vbar.valueProperty().set(lastVPos[0]);
|
||||
if (event.getEventType() == ScrollEvent.SCROLL) {
|
||||
double direction = 0 < event.getDeltaY() ? -1 : 1;
|
||||
for (int j = 0; j < PUSHES.length; j++) {
|
||||
derivatives[j] += direction * PUSHES[j];
|
||||
}
|
||||
if (timeline.getStatus() == Animation.Status.STOPPED) {
|
||||
timeline.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
};
|
||||
Callback calcScrollKeyFrame = () -> {
|
||||
timeline.stop();
|
||||
timeline.getKeyFrames().clear();
|
||||
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(.6), e -> {
|
||||
for (int j = 0; j < derivatives.length; j++) {
|
||||
derivatives[j] *= FRICTIONS[j];
|
||||
}
|
||||
for (int j = 1; j < derivatives.length; j++) {
|
||||
derivatives[j] += derivatives[j - 1];
|
||||
}
|
||||
double dy = derivatives[derivatives.length - 1];
|
||||
vbar.valueProperty().set(Math.min(Math.max(vbar.getValue() + dy / theoreticalHeight.get(), 0), 1));
|
||||
lastVPos[0] = vbar.getValue();
|
||||
if (Math.abs(dy) < 1) {
|
||||
if (Math.abs(dy) < .001) {
|
||||
timeline.stop();
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
virtualFlow.heightProperty().addListener((obs, o, h) -> calcHeight.handler());
|
||||
virtualFlow.cellCountProperty().addListener((obs, o, cell) -> {
|
||||
vbar.setValue(lastVPos[0]);
|
||||
calcHeight.handler();
|
||||
calcScrollKeyFrame.handler();
|
||||
});
|
||||
calcScrollKeyFrame.handler();
|
||||
if (vbar.getParent() != null) {
|
||||
vbar.getParent().addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
|
||||
vbar.getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
vbar.setOnMouseReleased(e -> lastVPos[0] = vbar.getValue());
|
||||
vbar.parentProperty().addListener((obs, o, n) -> {
|
||||
if (o != null) {
|
||||
o.removeEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
|
||||
o.removeEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
if (n != null) {
|
||||
n.addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
|
||||
n.addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
});
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
|
||||
// scrollTo 的动画实现
|
||||
Timeline scrollToTimeLine = new Timeline();
|
||||
CallbackArg<Integer> buildScrollToFrame = index -> {
|
||||
if (scrollToTimeLine.getStatus() == Animation.Status.RUNNING) {
|
||||
scrollToTimeLine.stop();
|
||||
}
|
||||
scrollToTimeLine.getKeyFrames().clear();
|
||||
|
||||
double fromValue = vbar.getValue();
|
||||
double offset = virtualFlow.getHeight() / cellHeight.get() * cellHeight.get();
|
||||
double toValue = cellHeight.get() * index / (theoreticalHeight.get() - offset);
|
||||
if (1 <= toValue) {
|
||||
toValue = lastVPos[0] = 1;
|
||||
}
|
||||
double between = toValue - fromValue;
|
||||
lastVPos[0] = toValue;
|
||||
if (vbar.getValue() == toValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
double keyMillis = 260D / SCROLL_TO_FRICTIONS.length;
|
||||
KeyFrame frame;
|
||||
for (int i = 0; i < SCROLL_TO_FRICTIONS.length; i++) {
|
||||
double endValue;
|
||||
if (between < 0) {
|
||||
endValue = fromValue - Math.abs(between) * SCROLL_TO_FRICTIONS[i];
|
||||
} else {
|
||||
endValue = fromValue + Math.abs(between) * SCROLL_TO_FRICTIONS[i];
|
||||
}
|
||||
frame = new KeyFrame(Duration.millis(i * keyMillis), new KeyValue(vbar.valueProperty(), endValue));
|
||||
scrollToTimeLine.getKeyFrames().add(frame);
|
||||
}
|
||||
};
|
||||
|
||||
if (control instanceof TableView<?> tableView) {
|
||||
tableView.setOnScrollTo(e -> {
|
||||
buildScrollToFrame.handler(e.getScrollTarget());
|
||||
scrollToTimeLine.play();
|
||||
});
|
||||
}
|
||||
if (control instanceof ListView<?> listView) {
|
||||
listView.setOnScrollTo(e -> {
|
||||
buildScrollToFrame.handler(e.getScrollTarget());
|
||||
scrollToTimeLine.play();
|
||||
});
|
||||
}
|
||||
if (control instanceof TreeView<?> treeView) {
|
||||
treeView.setOnScrollTo(e -> {
|
||||
buildScrollToFrame.handler(e.getScrollTarget());
|
||||
scrollToTimeLine.play();
|
||||
});
|
||||
}
|
||||
if (control instanceof TreeTableView<?> treeTableView) {
|
||||
treeTableView.setOnScrollTo(e -> {
|
||||
buildScrollToFrame.handler(e.getScrollTarget());
|
||||
scrollToTimeLine.play();
|
||||
});
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 滚动面板 ----------
|
||||
|
||||
/**
|
||||
* 文本域平滑滚动
|
||||
*
|
||||
* @param textArea 文本域
|
||||
*/
|
||||
public static void textarea(TextArea textArea) {
|
||||
reflectSkin(textArea, "scrollPane");
|
||||
}
|
||||
|
||||
/**
|
||||
* 反射控件平滑滚动面板
|
||||
*
|
||||
* @param control 控件
|
||||
* @param fieldName 反射字段
|
||||
*/
|
||||
public static void reflectSkin(Control control, String fieldName) {
|
||||
control.skinProperty().addListener((obs, o, skin) -> {
|
||||
try {
|
||||
scrollPaneV(Ref.getFieldValue(skin, fieldName, ScrollPane.class));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("not found reflect control ScrollPane", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑滚动面板
|
||||
*
|
||||
* @param scrollPane 滚动面板
|
||||
*/
|
||||
public static void scrollPane(ScrollPane scrollPane) {
|
||||
scrollPane(scrollPane, scrollPane.vvalueProperty(), Bounds::getHeight);
|
||||
scrollPane(scrollPane, scrollPane.hvalueProperty(), Bounds::getWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑垂直滚动面板
|
||||
*
|
||||
* @param scrollPane 滚动面板
|
||||
*/
|
||||
public static void scrollPaneV(ScrollPane scrollPane) {
|
||||
scrollPane(scrollPane, scrollPane.vvalueProperty(), Bounds::getHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑水平滚动面板
|
||||
*
|
||||
* @param scrollPane 滚动面板
|
||||
*/
|
||||
public static void scrollPaneH(ScrollPane scrollPane) {
|
||||
scrollPane(scrollPane, scrollPane.hvalueProperty(), Bounds::getWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑滚动面板
|
||||
*
|
||||
* @param scrollPane 滚动面板
|
||||
* @param scrollProperty 滚动轴属性
|
||||
* @param sizeFunc 高度回调
|
||||
*/
|
||||
public static void scrollPane(ScrollPane scrollPane, DoubleProperty scrollProperty, Function<Bounds, Double> sizeFunc) {
|
||||
final double[] derivatives = new double[FRICTIONS.length];
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
|
||||
final EventHandler<ScrollEvent> scrollHandler = event -> {
|
||||
if (event.getEventType() == ScrollEvent.SCROLL) {
|
||||
int direction = 0 < event.getDeltaY() ? -1 : 1;
|
||||
for (int i = 0; i < PUSHES.length; i++) {
|
||||
derivatives[i] += direction * PUSHES[i];
|
||||
}
|
||||
if (timeline.getStatus() == Animation.Status.STOPPED) {
|
||||
timeline.play();
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
};
|
||||
if (scrollPane.getContent().getParent() != null) {
|
||||
scrollPane.getContent().getParent().addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
|
||||
scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
scrollPane.getContent().parentProperty().addListener((obs, o, n) -> {
|
||||
if (o != null) {
|
||||
o.removeEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
|
||||
o.removeEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
if (n != null) {
|
||||
n.addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
|
||||
n.addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
});
|
||||
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(.6), (event) -> {
|
||||
for (int i = 0; i < derivatives.length; i++) {
|
||||
derivatives[i] *= FRICTIONS[i];
|
||||
}
|
||||
for (int i = 1; i < derivatives.length; i++) {
|
||||
derivatives[i] += derivatives[i - 1];
|
||||
}
|
||||
double dy = derivatives[derivatives.length - 1];
|
||||
double size = sizeFunc.apply(scrollPane.getContent().getLayoutBounds());
|
||||
scrollProperty.set(Math.min(Math.max(scrollProperty.get() + dy / size, 0), 1));
|
||||
if (Math.abs(dy) < .001) {
|
||||
timeline.stop();
|
||||
}
|
||||
}));
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
}
|
||||
}
|
||||
68
src/main/java/com/imyeyu/fx/utils/StringConverters.java
Normal file
68
src/main/java/com/imyeyu/fx/utils/StringConverters.java
Normal file
@ -0,0 +1,68 @@
|
||||
package com.imyeyu.fx.utils;
|
||||
|
||||
import javafx.util.StringConverter;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.utils.Time;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* FX 常用字符串转换
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-02-01 19:51
|
||||
*/
|
||||
public class StringConverters {
|
||||
|
||||
/** 日期转换 */
|
||||
public static StringConverter<LocalDate> DATE = new StringConverter<>() {
|
||||
|
||||
@Override
|
||||
public String toString(LocalDate date) {
|
||||
if (date == null) {
|
||||
return null;
|
||||
} else {
|
||||
return Time.toDate(Time.fromLocalDate(date));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate fromString(String string) {
|
||||
try {
|
||||
return Time.toLocalDateTime(Time.date.parse(string).getTime()).toLocalDate();
|
||||
} catch (ParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用枚举转换,此转换反射枚举项的 name 字段
|
||||
*
|
||||
* @param enumClass 枚举类
|
||||
* @return 转换器
|
||||
* @param <T> 枚举类型
|
||||
*/
|
||||
public static <T extends Enum<T>> StringConverter<T> type(Class<T> enumClass) {
|
||||
return new StringConverter<>() {
|
||||
|
||||
@Override
|
||||
public String toString(T object) {
|
||||
if (object == null) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return Ref.getFieldValue(object, "name", String.class);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromString(String string) {
|
||||
return Ref.toType(enumClass, string);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user