add RadioButtonGroup
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user