add RadioButtonGroup
This commit is contained in:
@@ -0,0 +1,441 @@
|
|||||||
|
package com.imyeyu.fx.ui.components;
|
||||||
|
|
||||||
|
import com.imyeyu.fx.TimiFX;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Control;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单选按钮组组件
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2026-06-26
|
||||||
|
*/
|
||||||
|
public class RadioButtonGroup extends Control {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮组模式
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2026-06-26
|
||||||
|
*/
|
||||||
|
public enum Mode {
|
||||||
|
|
||||||
|
/** 按钮模式 */
|
||||||
|
BUTTON,
|
||||||
|
|
||||||
|
/** 滑块模式 */
|
||||||
|
SLIDER
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根样式 */
|
||||||
|
public static final String STYLE_CLASS = "radio-button-group";
|
||||||
|
|
||||||
|
/** 按钮模式样式 */
|
||||||
|
public static final String STYLE_CLASS_BUTTON_MODE = "button-mode";
|
||||||
|
|
||||||
|
/** 滑块模式样式 */
|
||||||
|
public static final String STYLE_CLASS_SLIDER_MODE = "slider-mode";
|
||||||
|
|
||||||
|
/** 容器样式 */
|
||||||
|
public static final String STYLE_CLASS_BOX = "radio-button-group-box";
|
||||||
|
|
||||||
|
/** 滑块样式 */
|
||||||
|
public static final String STYLE_CLASS_SLIDER = "radio-button-group-slider";
|
||||||
|
|
||||||
|
/** 按钮样式 */
|
||||||
|
public static final String STYLE_CLASS_BUTTON = "radio-button-group-button";
|
||||||
|
|
||||||
|
/** 首按钮样式 */
|
||||||
|
public static final String STYLE_CLASS_FIRST = "first";
|
||||||
|
|
||||||
|
/** 中间按钮样式 */
|
||||||
|
public static final String STYLE_CLASS_MIDDLE = "middle";
|
||||||
|
|
||||||
|
/** 末按钮样式 */
|
||||||
|
public static final String STYLE_CLASS_LAST = "last";
|
||||||
|
|
||||||
|
/** 单按钮样式 */
|
||||||
|
public static final String STYLE_CLASS_SINGLE = "single";
|
||||||
|
|
||||||
|
/** 按钮列表 */
|
||||||
|
private final ObservableList<ToggleButton> buttons;
|
||||||
|
|
||||||
|
/** 按钮组 */
|
||||||
|
private final ToggleGroup toggleGroup;
|
||||||
|
|
||||||
|
/** 当前选中按钮 */
|
||||||
|
private final ReadOnlyObjectWrapper<ToggleButton> selectedButton;
|
||||||
|
|
||||||
|
/** 当前选中下标 */
|
||||||
|
private final IntegerProperty selectedIndex;
|
||||||
|
|
||||||
|
/** 按钮间距 */
|
||||||
|
private final DoubleProperty spacing;
|
||||||
|
|
||||||
|
/** 布局对齐方式 */
|
||||||
|
private final ObjectProperty<Pos> alignment;
|
||||||
|
|
||||||
|
/** 当前模式 */
|
||||||
|
private final ObjectProperty<Mode> mode;
|
||||||
|
|
||||||
|
/** true 为允许清空选中 */
|
||||||
|
private final BooleanProperty allowClearSelection;
|
||||||
|
|
||||||
|
/** 禁止重复点击清空 */
|
||||||
|
private final javafx.event.EventHandler<MouseEvent> keepSelectionEvent = event -> {
|
||||||
|
if (!isAllowClearSelection() && event.getSource() instanceof ToggleButton button && button.isSelected()) {
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造器
|
||||||
|
*/
|
||||||
|
public RadioButtonGroup() {
|
||||||
|
buttons = FXCollections.observableArrayList();
|
||||||
|
toggleGroup = new ToggleGroup();
|
||||||
|
selectedButton = new ReadOnlyObjectWrapper<>();
|
||||||
|
selectedIndex = new SimpleIntegerProperty(-1);
|
||||||
|
spacing = new SimpleDoubleProperty();
|
||||||
|
alignment = new SimpleObjectProperty<>(Pos.CENTER_LEFT);
|
||||||
|
mode = new SimpleObjectProperty<>(Mode.BUTTON);
|
||||||
|
allowClearSelection = new SimpleBooleanProperty();
|
||||||
|
|
||||||
|
getStyleClass().addAll(STYLE_CLASS, STYLE_CLASS_BUTTON_MODE);
|
||||||
|
setFocusTraversable(false);
|
||||||
|
|
||||||
|
initSelectionSync();
|
||||||
|
initButtonsListener();
|
||||||
|
initModeStyleClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化选中同步
|
||||||
|
*/
|
||||||
|
private void initSelectionSync() {
|
||||||
|
toggleGroup.selectedToggleProperty().addListener((obs, o, toggle) -> {
|
||||||
|
if (toggle instanceof ToggleButton button) {
|
||||||
|
selectedButton.set(button);
|
||||||
|
selectedIndex.set(buttons.indexOf(button));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedButton.set(null);
|
||||||
|
selectedIndex.set(-1);
|
||||||
|
});
|
||||||
|
selectedIndex.addListener((obs, o, newIndex) -> {
|
||||||
|
int index = newIndex == null ? -1 : newIndex.intValue();
|
||||||
|
if (index < 0) {
|
||||||
|
if (isAllowClearSelection()) {
|
||||||
|
toggleGroup.selectToggle(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index < buttons.size()) {
|
||||||
|
toggleGroup.selectToggle(buttons.get(index));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化按钮监听
|
||||||
|
*/
|
||||||
|
private void initButtonsListener() {
|
||||||
|
buttons.addListener((ListChangeListener<ToggleButton>) change -> {
|
||||||
|
while (change.next()) {
|
||||||
|
if (change.wasRemoved()) {
|
||||||
|
for (ToggleButton button : change.getRemoved()) {
|
||||||
|
detachButton(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (change.wasAdded()) {
|
||||||
|
for (ToggleButton button : change.getAddedSubList()) {
|
||||||
|
attachButton(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncSelectionState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化模式样式
|
||||||
|
*/
|
||||||
|
private void initModeStyleClass() {
|
||||||
|
TimiFX.toggleStyleClass4Binding(this, mode.isEqualTo(Mode.SLIDER), STYLE_CLASS_SLIDER_MODE, STYLE_CLASS_BUTTON_MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂载按钮到当前组件
|
||||||
|
*
|
||||||
|
* @param button 按钮
|
||||||
|
*/
|
||||||
|
private void attachButton(ToggleButton button) {
|
||||||
|
if (button == null) {
|
||||||
|
throw new IllegalArgumentException("button must not be null");
|
||||||
|
}
|
||||||
|
button.setToggleGroup(toggleGroup);
|
||||||
|
button.addEventFilter(MouseEvent.MOUSE_PRESSED, keepSelectionEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载按钮
|
||||||
|
*
|
||||||
|
* @param button 按钮
|
||||||
|
*/
|
||||||
|
private void detachButton(ToggleButton button) {
|
||||||
|
button.removeEventFilter(MouseEvent.MOUSE_PRESSED, keepSelectionEvent);
|
||||||
|
if (button.getToggleGroup() == toggleGroup) {
|
||||||
|
button.setToggleGroup(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步当前选中状态
|
||||||
|
*/
|
||||||
|
private void syncSelectionState() {
|
||||||
|
ToggleButton currentSelectedButton = getSelectedButton();
|
||||||
|
if (currentSelectedButton != null && !buttons.contains(currentSelectedButton)) {
|
||||||
|
if (isAllowClearSelection()) {
|
||||||
|
toggleGroup.selectToggle(null);
|
||||||
|
}
|
||||||
|
else if (!buttons.isEmpty()) {
|
||||||
|
toggleGroup.selectToggle(buttons.getFirst());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int index = selectedIndex.get();
|
||||||
|
if (index < 0 || buttons.size() <= index) {
|
||||||
|
if (!buttons.isEmpty() && !isAllowClearSelection() && toggleGroup.getSelectedToggle() == null) {
|
||||||
|
toggleGroup.selectToggle(buttons.getFirst());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleGroup.selectToggle(buttons.get(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
return new RadioButtonGroupSkin(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮列表
|
||||||
|
*
|
||||||
|
* @return 按钮列表
|
||||||
|
*/
|
||||||
|
public ObservableList<ToggleButton> getButtons() {
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加按钮
|
||||||
|
*
|
||||||
|
* @param buttons 按钮列表
|
||||||
|
*/
|
||||||
|
public void addButton(ToggleButton... buttons) {
|
||||||
|
getButtons().addAll(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加文本按钮
|
||||||
|
*
|
||||||
|
* @param text 按钮文本
|
||||||
|
* @return 新增按钮
|
||||||
|
*/
|
||||||
|
public ToggleButton addButton(String text) {
|
||||||
|
ToggleButton button = new ToggleButton(text);
|
||||||
|
getButtons().add(button);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前选中按钮
|
||||||
|
*
|
||||||
|
* @param button 当前选中按钮
|
||||||
|
*/
|
||||||
|
public void setSelectedButton(ToggleButton button) {
|
||||||
|
if (button == null) {
|
||||||
|
if (isAllowClearSelection()) {
|
||||||
|
toggleGroup.selectToggle(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!buttons.contains(button)) {
|
||||||
|
throw new IllegalArgumentException("button must belong to current group");
|
||||||
|
}
|
||||||
|
toggleGroup.selectToggle(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前选中按钮
|
||||||
|
*
|
||||||
|
* @return 当前选中按钮
|
||||||
|
*/
|
||||||
|
public ToggleButton getSelectedButton() {
|
||||||
|
return selectedButton.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前选中按钮属性
|
||||||
|
*
|
||||||
|
* @return 当前选中按钮属性
|
||||||
|
*/
|
||||||
|
public ReadOnlyObjectProperty<ToggleButton> selectedButtonProperty() {
|
||||||
|
return selectedButton.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前选中下标
|
||||||
|
*
|
||||||
|
* @param selectedIndex 当前选中下标
|
||||||
|
*/
|
||||||
|
public void setSelectedIndex(int selectedIndex) {
|
||||||
|
this.selectedIndex.set(selectedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前选中下标
|
||||||
|
*
|
||||||
|
* @return 当前选中下标
|
||||||
|
*/
|
||||||
|
public int getSelectedIndex() {
|
||||||
|
return selectedIndex.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前选中下标属性
|
||||||
|
*
|
||||||
|
* @return 当前选中下标属性
|
||||||
|
*/
|
||||||
|
public IntegerProperty selectedIndexProperty() {
|
||||||
|
return selectedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置按钮间距
|
||||||
|
*
|
||||||
|
* @param spacing 按钮间距
|
||||||
|
*/
|
||||||
|
public void setSpacing(double spacing) {
|
||||||
|
this.spacing.set(Math.max(0, spacing));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮间距
|
||||||
|
*
|
||||||
|
* @return 按钮间距
|
||||||
|
*/
|
||||||
|
public double getSpacing() {
|
||||||
|
return spacing.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按钮间距属性
|
||||||
|
*
|
||||||
|
* @return 按钮间距属性
|
||||||
|
*/
|
||||||
|
public DoubleProperty spacingProperty() {
|
||||||
|
return spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置布局对齐方式
|
||||||
|
*
|
||||||
|
* @param alignment 布局对齐方式
|
||||||
|
*/
|
||||||
|
public void setAlignment(Pos alignment) {
|
||||||
|
this.alignment.set(alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取布局对齐方式
|
||||||
|
*
|
||||||
|
* @return 布局对齐方式
|
||||||
|
*/
|
||||||
|
public Pos getAlignment() {
|
||||||
|
return alignment.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取布局对齐方式属性
|
||||||
|
*
|
||||||
|
* @return 布局对齐方式属性
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Pos> alignmentProperty() {
|
||||||
|
return alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前模式
|
||||||
|
*
|
||||||
|
* @param mode 当前模式
|
||||||
|
*/
|
||||||
|
public void setMode(Mode mode) {
|
||||||
|
this.mode.set(mode == null ? Mode.BUTTON : mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前模式
|
||||||
|
*
|
||||||
|
* @return 当前模式
|
||||||
|
*/
|
||||||
|
public Mode getMode() {
|
||||||
|
return mode.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前模式属性
|
||||||
|
*
|
||||||
|
* @return 当前模式属性
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Mode> modeProperty() {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否允许清空选中
|
||||||
|
*
|
||||||
|
* @param allowClearSelection true 为允许清空选中
|
||||||
|
*/
|
||||||
|
public void setAllowClearSelection(boolean allowClearSelection) {
|
||||||
|
this.allowClearSelection.set(allowClearSelection);
|
||||||
|
if (!allowClearSelection && toggleGroup.getSelectedToggle() == null && !buttons.isEmpty()) {
|
||||||
|
toggleGroup.selectToggle(buttons.getFirst());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否允许清空选中
|
||||||
|
*
|
||||||
|
* @return true 为允许清空选中
|
||||||
|
*/
|
||||||
|
public boolean isAllowClearSelection() {
|
||||||
|
return allowClearSelection.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否允许清空选中属性
|
||||||
|
*
|
||||||
|
* @return 是否允许清空选中属性
|
||||||
|
*/
|
||||||
|
public BooleanProperty allowClearSelectionProperty() {
|
||||||
|
return allowClearSelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
.radio-button-group .radio-button-group-box {
|
||||||
|
-fx-spacing: 0;
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
}
|
||||||
|
.radio-button-group .radio-button-group-button {
|
||||||
|
-fx-padding: .25em .833333em;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-border-insets: 0;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
-fx-border-radius: 0;
|
||||||
|
-fx-border-style: solid;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
}
|
||||||
|
.radio-button-group.button-mode .radio-button-group-button {
|
||||||
|
-fx-background-color: -timi-fx-white-color;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-text-fill: -timi-fx-dark-gray-color;
|
||||||
|
}
|
||||||
|
.radio-button-group.button-mode .radio-button-group-button:hover {
|
||||||
|
-fx-background-color: -timi-fx-gray-white-color;
|
||||||
|
-fx-border-color: -timi-fx-border-color;
|
||||||
|
-fx-text-fill: -timi-fx-dark-gray-color;
|
||||||
|
}
|
||||||
|
.radio-button-group.button-mode .radio-button-group-button:selected,
|
||||||
|
.radio-button-group.button-mode .radio-button-group-button:selected:hover {
|
||||||
|
-fx-background-color: -timi-fx-color;
|
||||||
|
-fx-border-color: -timi-fx-color;
|
||||||
|
-fx-text-fill: -timi-fx-white-color;
|
||||||
|
}
|
||||||
|
.radio-button-group.slider-mode {
|
||||||
|
-fx-background-color: -timi-fx-light-gray-color;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
}
|
||||||
|
.radio-button-group.slider-mode .radio-button-group-button,
|
||||||
|
.radio-button-group.slider-mode .radio-button-group-button:hover,
|
||||||
|
.radio-button-group.slider-mode .radio-button-group-button:selected,
|
||||||
|
.radio-button-group.slider-mode .radio-button-group-button:selected:hover {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
-fx-border-style: solid;
|
||||||
|
-fx-border-width: 1;
|
||||||
|
-fx-text-fill: -timi-fx-dark-gray-color;
|
||||||
|
}
|
||||||
|
.radio-button-group .radio-button-group-slider {
|
||||||
|
-fx-background-color: -timi-fx-white-color;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
-fx-border-radius: 0;
|
||||||
|
-fx-effect: null;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.imyeyu.fx.ui.examples.view.pages;
|
package com.imyeyu.fx.ui.examples.view.pages;
|
||||||
|
|
||||||
import com.imyeyu.fx.ui.TimiFXUI;
|
import com.imyeyu.fx.ui.TimiFXUI;
|
||||||
import com.imyeyu.fx.ui.components.SegmentedButtonGroup;
|
import com.imyeyu.fx.ui.components.RadioButtonGroup;
|
||||||
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
|
import com.imyeyu.fx.ui.examples.component.AbstractDemoPane;
|
||||||
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
import com.imyeyu.fx.ui.examples.component.sidebar.SidebarItem;
|
||||||
import com.imyeyu.inject.annotation.Component;
|
import com.imyeyu.inject.annotation.Component;
|
||||||
@@ -10,6 +10,7 @@ import javafx.geometry.Pos;
|
|||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ToggleButton;
|
import javafx.scene.control.ToggleButton;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,24 +26,25 @@ public class ButtonDemo extends AbstractDemoPane {
|
|||||||
title.setText(SidebarItem.BUTTON.getText());
|
title.setText(SidebarItem.BUTTON.getText());
|
||||||
document.setText("暂无");
|
document.setText("暂无");
|
||||||
document.setUrl("");
|
document.setUrl("");
|
||||||
source.setText("src/main/java/com/imyeyu/fx/ui/components/SegmentedButtonGroup.java");
|
source.setText("src/main/java/com/imyeyu/fx/ui/components/RadioButtonGroup.java");
|
||||||
source.setUrl("");
|
source.setUrl("");
|
||||||
tips.setText("演示 SegmentedButtonGroup 的标准控件写法,按钮列表、选中状态和允许取消选择都是组件对外暴露的标准属性");
|
tips.setText("演示 RadioButtonGroup 的按钮模式和滑块模式,按钮列表、选中状态和允许取消选择都是组件对外暴露的标准属性");
|
||||||
|
|
||||||
SegmentedButtonGroup defaultGroup = new SegmentedButtonGroup();
|
RadioButtonGroup buttonModeGroup = new RadioButtonGroup();
|
||||||
defaultGroup.addButton("全部");
|
buttonModeGroup.addButton("全部");
|
||||||
defaultGroup.addButton("启用");
|
buttonModeGroup.addButton("启用");
|
||||||
defaultGroup.addButton("停用");
|
buttonModeGroup.addButton("停用");
|
||||||
defaultGroup.setSelectedIndex(0);
|
buttonModeGroup.setSelectedIndex(0);
|
||||||
|
|
||||||
SegmentedButtonGroup spacingGroup = new SegmentedButtonGroup();
|
RadioButtonGroup sliderModeGroup = new RadioButtonGroup();
|
||||||
spacingGroup.setSpacing(6);
|
sliderModeGroup.setMode(RadioButtonGroup.Mode.SLIDER);
|
||||||
spacingGroup.addButton("左对齐");
|
sliderModeGroup.addButton("左对齐");
|
||||||
spacingGroup.addButton("居中");
|
sliderModeGroup.addButton("居中");
|
||||||
spacingGroup.addButton("右对齐");
|
sliderModeGroup.addButton("右对齐");
|
||||||
spacingGroup.setSelectedIndex(1);
|
sliderModeGroup.setSelectedIndex(1);
|
||||||
|
|
||||||
SegmentedButtonGroup clearableGroup = new SegmentedButtonGroup();
|
RadioButtonGroup clearableGroup = new RadioButtonGroup();
|
||||||
|
clearableGroup.setMode(RadioButtonGroup.Mode.SLIDER);
|
||||||
clearableGroup.setAllowClearSelection(true);
|
clearableGroup.setAllowClearSelection(true);
|
||||||
clearableGroup.addButton("日");
|
clearableGroup.addButton("日");
|
||||||
clearableGroup.addButton("周");
|
clearableGroup.addButton("周");
|
||||||
@@ -63,7 +65,7 @@ public class ButtonDemo extends AbstractDemoPane {
|
|||||||
});
|
});
|
||||||
clearableGroup.selectedIndexProperty().addListener((obs, o, index) -> selectedIndexText.setText("当前下标: " + index.intValue()));
|
clearableGroup.selectedIndexProperty().addListener((obs, o, index) -> selectedIndexText.setText("当前下标: " + index.intValue()));
|
||||||
|
|
||||||
SegmentedButtonGroup customButtonGroup = new SegmentedButtonGroup();
|
RadioButtonGroup customButtonGroup = new RadioButtonGroup();
|
||||||
ToggleButton primary = new ToggleButton("概览");
|
ToggleButton primary = new ToggleButton("概览");
|
||||||
ToggleButton analysis = new ToggleButton("分析");
|
ToggleButton analysis = new ToggleButton("分析");
|
||||||
ToggleButton history = new ToggleButton("历史");
|
ToggleButton history = new ToggleButton("历史");
|
||||||
@@ -80,9 +82,9 @@ public class ButtonDemo extends AbstractDemoPane {
|
|||||||
setVgap(10);
|
setVgap(10);
|
||||||
|
|
||||||
int row = 0;
|
int row = 0;
|
||||||
addRow(row++, TimiFXUI.label("默认单选"), defaultGroup);
|
addRow(row++, TimiFXUI.label("按钮模式"), buttonModeGroup);
|
||||||
addRow(row++, TimiFXUI.label("带间距"), spacingGroup);
|
addRow(row++, TimiFXUI.label("滑块模式"), sliderModeGroup);
|
||||||
addRow(row++, TimiFXUI.label("允许取消"), clearableGroup);
|
addRow(row++, TimiFXUI.label("允许取消"), new HBox(clearableGroup));
|
||||||
addRow(row, TimiFXUI.label("外部按钮"), customButtonGroup);
|
addRow(row, TimiFXUI.label("外部按钮"), customButtonGroup);
|
||||||
}},
|
}},
|
||||||
selectedButtonText,
|
selectedButtonText,
|
||||||
|
|||||||
Reference in New Issue
Block a user