247 lines
6.8 KiB
Java
247 lines
6.8 KiB
Java
package com.imyeyu.fx.ui.components;
|
|
|
|
import com.imyeyu.fx.bean.Interpolates;
|
|
|
|
import javafx.animation.KeyFrame;
|
|
import javafx.animation.KeyValue;
|
|
import javafx.animation.Timeline;
|
|
import javafx.application.Platform;
|
|
import javafx.beans.binding.Bindings;
|
|
import javafx.beans.property.DoubleProperty;
|
|
import javafx.beans.property.SimpleDoubleProperty;
|
|
import javafx.collections.ListChangeListener;
|
|
import javafx.geometry.Bounds;
|
|
import javafx.scene.control.SkinBase;
|
|
import javafx.scene.control.ToggleButton;
|
|
import javafx.scene.layout.HBox;
|
|
import javafx.scene.layout.Priority;
|
|
import javafx.scene.layout.Region;
|
|
import javafx.scene.layout.StackPane;
|
|
import javafx.util.Duration;
|
|
|
|
/**
|
|
* 单选按钮组皮肤
|
|
*
|
|
* @author 夜雨
|
|
* @since 2026-06-26
|
|
*/
|
|
public class RadioButtonGroupSkin extends SkinBase<RadioButtonGroup> {
|
|
|
|
/** 高度补偿 */
|
|
private static final double HEIGHT_COMPENSATE = 2;
|
|
|
|
/** 动画时长 */
|
|
private static final Duration SLIDER_DURATION = Duration.millis(180);
|
|
|
|
/** 按钮容器 */
|
|
private final HBox box;
|
|
|
|
/** 滑块 */
|
|
private final Region slider;
|
|
|
|
/** 滑块 x */
|
|
private final DoubleProperty sliderX;
|
|
|
|
/** 滑块 y */
|
|
private final DoubleProperty sliderY;
|
|
|
|
/** 滑块宽度 */
|
|
private final DoubleProperty sliderWidth;
|
|
|
|
/** 滑块高度 */
|
|
private final DoubleProperty sliderHeight;
|
|
|
|
/** 滑块动画 */
|
|
private Timeline sliderTimeline;
|
|
|
|
/**
|
|
* 构造按钮组皮肤
|
|
*
|
|
* @param control 按钮组控件
|
|
*/
|
|
public RadioButtonGroupSkin(RadioButtonGroup control) {
|
|
super(control);
|
|
|
|
sliderX = new SimpleDoubleProperty();
|
|
sliderY = new SimpleDoubleProperty();
|
|
sliderWidth = new SimpleDoubleProperty();
|
|
sliderHeight = new SimpleDoubleProperty();
|
|
sliderTimeline = new Timeline();
|
|
|
|
box = new HBox();
|
|
box.getStyleClass().add(RadioButtonGroup.STYLE_CLASS_BOX);
|
|
|
|
slider = new Region();
|
|
slider.getStyleClass().add(RadioButtonGroup.STYLE_CLASS_SLIDER);
|
|
slider.setManaged(false);
|
|
slider.setMouseTransparent(true);
|
|
|
|
sliderX.addListener((obs, o, n) -> applySliderBounds());
|
|
sliderY.addListener((obs, o, n) -> applySliderBounds());
|
|
sliderWidth.addListener((obs, o, n) -> applySliderBounds());
|
|
sliderHeight.addListener((obs, o, n) -> applySliderBounds());
|
|
|
|
box.alignmentProperty().bind(control.alignmentProperty());
|
|
box.spacingProperty().bind(Bindings.createDoubleBinding(
|
|
() -> control.getMode() == RadioButtonGroup.Mode.BUTTON && control.getSpacing() < 1 ? -1 : control.getSpacing(),
|
|
control.modeProperty(),
|
|
control.spacingProperty()
|
|
));
|
|
getChildren().add(new StackPane() {{
|
|
getStyleClass().add(RadioButtonGroup.STYLE_CLASS_BOX);
|
|
getChildren().addAll(slider, box);
|
|
}});
|
|
|
|
rebuildButtons();
|
|
initListeners(control);
|
|
updateMode();
|
|
}
|
|
|
|
/**
|
|
* 初始化监听
|
|
*
|
|
* @param control 控件
|
|
*/
|
|
private void initListeners(RadioButtonGroup control) {
|
|
control.getButtons().addListener((ListChangeListener<ToggleButton>) change -> rebuildButtons());
|
|
control.selectedButtonProperty().addListener((obs, o, n) -> syncSlider());
|
|
control.modeProperty().addListener((obs, o, n) -> {
|
|
updateMode();
|
|
syncSlider();
|
|
});
|
|
box.layoutBoundsProperty().addListener((obs, o, n) -> {
|
|
syncSlider();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 重建按钮结构
|
|
*/
|
|
private void rebuildButtons() {
|
|
box.getChildren().setAll(getSkinnable().getButtons());
|
|
syncButtonStyleClass();
|
|
syncSlider();
|
|
}
|
|
|
|
/**
|
|
* 同步按钮样式
|
|
*/
|
|
private void syncButtonStyleClass() {
|
|
int size = getSkinnable().getButtons().size();
|
|
for (int i = 0; i < size; i++) {
|
|
ToggleButton button = getSkinnable().getButtons().get(i);
|
|
button.getStyleClass().removeAll(
|
|
RadioButtonGroup.STYLE_CLASS_BUTTON,
|
|
RadioButtonGroup.STYLE_CLASS_FIRST,
|
|
RadioButtonGroup.STYLE_CLASS_MIDDLE,
|
|
RadioButtonGroup.STYLE_CLASS_LAST,
|
|
RadioButtonGroup.STYLE_CLASS_SINGLE
|
|
);
|
|
button.getStyleClass().add(RadioButtonGroup.STYLE_CLASS_BUTTON);
|
|
button.selectedProperty().addListener((obs, o, n) -> button.setViewOrder(n ? -1 : 0));
|
|
button.setViewOrder(button.isSelected() ? -1 : 0);
|
|
button.layoutBoundsProperty().addListener((obs, o, n) -> syncSlider());
|
|
button.boundsInParentProperty().addListener((obs, o, n) -> syncSlider());
|
|
|
|
if (size == 1) {
|
|
button.getStyleClass().add(RadioButtonGroup.STYLE_CLASS_SINGLE);
|
|
continue;
|
|
}
|
|
if (i == 0) {
|
|
button.getStyleClass().add(RadioButtonGroup.STYLE_CLASS_FIRST);
|
|
continue;
|
|
}
|
|
if (i == size - 1) {
|
|
button.getStyleClass().add(RadioButtonGroup.STYLE_CLASS_LAST);
|
|
continue;
|
|
}
|
|
button.getStyleClass().add(RadioButtonGroup.STYLE_CLASS_MIDDLE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 刷新按钮尺寸策略
|
|
*/
|
|
private void refreshButtonSizing() {
|
|
boolean sliderMode = getSkinnable().getMode() == RadioButtonGroup.Mode.SLIDER;
|
|
for (ToggleButton button : getSkinnable().getButtons()) {
|
|
button.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
|
|
button.setPrefHeight(Region.USE_COMPUTED_SIZE);
|
|
if (sliderMode) {
|
|
HBox.setHgrow(button, Priority.NEVER);
|
|
button.setMaxWidth(Region.USE_PREF_SIZE);
|
|
}
|
|
else {
|
|
HBox.setHgrow(button, Priority.ALWAYS);
|
|
button.setMaxWidth(Double.MAX_VALUE);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 应用滑块位置和尺寸
|
|
*/
|
|
private void applySliderBounds() {
|
|
slider.resizeRelocate(sliderX.get(), sliderY.get(), sliderWidth.get(), sliderHeight.get());
|
|
}
|
|
|
|
/**
|
|
* 更新模式
|
|
*/
|
|
private void updateMode() {
|
|
boolean sliderMode = getSkinnable().getMode() == RadioButtonGroup.Mode.SLIDER;
|
|
slider.setVisible(sliderMode);
|
|
}
|
|
|
|
/**
|
|
* 同步滑块位置
|
|
*/
|
|
private void syncSlider() {
|
|
if (getSkinnable().getMode() != RadioButtonGroup.Mode.SLIDER) {
|
|
sliderTimeline.stop();
|
|
sliderX.set(0);
|
|
sliderY.set(0);
|
|
sliderWidth.set(0);
|
|
sliderHeight.set(0);
|
|
return;
|
|
}
|
|
ToggleButton button = getSkinnable().getSelectedButton();
|
|
if (button == null || !box.getChildren().contains(button)) {
|
|
sliderTimeline.stop();
|
|
sliderX.set(0);
|
|
sliderY.set(0);
|
|
sliderWidth.set(0);
|
|
sliderHeight.set(0);
|
|
return;
|
|
}
|
|
Bounds bounds = button.getBoundsInParent();
|
|
double targetX = bounds.getMinX() + 2;
|
|
double targetWidth = Math.max(0, bounds.getWidth() - 4);
|
|
double targetHeight = Math.max(0, box.getHeight() - 4);
|
|
double targetY = 2;
|
|
|
|
if (sliderWidth.get() <= 0 || sliderHeight.get() <= 0) {
|
|
sliderTimeline.stop();
|
|
sliderX.set(targetX);
|
|
sliderY.set(targetY);
|
|
sliderWidth.set(targetWidth);
|
|
sliderHeight.set(targetHeight);
|
|
return;
|
|
}
|
|
|
|
sliderTimeline.stop();
|
|
sliderTimeline = new Timeline(
|
|
new KeyFrame(
|
|
SLIDER_DURATION,
|
|
new KeyValue(sliderX, targetX, Interpolates.EASE_OUT_EXPO.getValue()),
|
|
new KeyValue(sliderY, targetY, Interpolates.EASE_OUT_EXPO.getValue()),
|
|
new KeyValue(sliderWidth, targetWidth, Interpolates.EASE_OUT_EXPO.getValue()),
|
|
new KeyValue(sliderHeight, targetHeight, Interpolates.EASE_OUT_EXPO.getValue())
|
|
)
|
|
);
|
|
sliderTimeline.play();
|
|
}
|
|
}
|
|
|
|
|