Files
timi-fx-ui/src/main/java/com/imyeyu/fx/ui/components/RadioButtonGroupSkin.java
T
2026-07-25 15:22:48 +08:00

222 lines
6.1 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.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.Region;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
/**
* 单选按钮组皮肤
*
* @author 夜雨
* @since 2026-06-26
*/
public class RadioButtonGroupSkin extends SkinBase<RadioButtonGroup> {
/** 动画时长 */
private static final Duration SLIDER_DURATION = Duration.millis(320);
/** 按钮容器 */
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 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();
}
}