package com.imyeyu.fx.ui.components; import com.imyeyu.fx.icon.TimiFXIcon; import com.imyeyu.fx.ui.TimiFXUI; import javafx.beans.binding.Bindings; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import javafx.beans.value.ChangeListener; import javafx.event.EventHandler; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import java.util.ArrayList; import java.util.List; /** * 分页组件,支持省略页,滚动页,如 [<][1]..[5][6][7][8][9]..[20][>] *
 *     XPagination pagination = new XPagination();
 *     pagination.setSize(10);    // 单页数据量
 *     pagination.setLength(200); // 总数据量
 *     pagination.indexProperty((obs, o, newIndex) -> {
 *        // 监听激活下标
 *     });
 *     pagination.setIndex(6); // 激活页下标(0 开始,这是第 7 页)
 * 
* * @author 夜雨 * @since 2021-12-22 15:25 */ public class XPagination extends HBox implements TimiFXUI { private static final String STYLE_CLASS = "x-pagination"; /** 阻止取消选择 */ public static final EventHandler EVENT_TOGGLE_BUTTON = e -> { if (e.getSource() instanceof ToggleButton btn && btn.isSelected()) { e.consume(); } }; private final IconButton prev, next; // 步进翻页动态下标 private int prevI, nextI; private final LongProperty lp; // lengthProperty 总数据量 [0, N] private final IntegerProperty ip; // indexProperty 激活下标 [0, chunkProperty.value] private final IntegerProperty sp; // sizeProperty 单页数量 [1, N] private final IntegerProperty cp; // chunkProperty 页面数量 [1, N] /** 默认构造 */ public XPagination() { // 基本参数 lp = new SimpleLongProperty(); ip = new SimpleIntegerProperty(); sp = new SimpleIntegerProperty(); cp = new SimpleIntegerProperty(); // 更新页面大小和总数据量时重新计算页面数量 cp.bind(Bindings.createIntegerBinding(() -> { long total = lp.get(); int page = sp.get(); return (int) Math.ceil(1D * total / page); }, sp, lp)); // 页面组 List pageButtons = new ArrayList<>(); PageButton tb; // 上一页 prev = new IconButton(TimiFXIcon.fromName("ARROW_1_W")).withBackground(); prev.getStyleClass().add(CSS.BORDER_N); prev.setMaxHeight(Double.MAX_VALUE); prev.disableProperty().bind(ip.isEqualTo(0)); getChildren().add(prev); // 前置页 [1, 6] for (int i = 0; i < 6; i++) { tb = new PageButton(); tb.indexProperty.set(i); if (0 < i) { // 第一页保持显示,非第一页显示条件: // 1. 总页数小于 8,页码大于 i // 2. 总页数大于等于 8,激活下标小于 4(1 - 4 页) tb.visibleProperty().bind(cp.greaterThan(i).and(cp.lessThan(8)).or(cp.greaterThan(7).and(ip.lessThan(4)))); } else { tb.getStyleClass().add(CSS.BORDER_LR); } pageButtons.add(tb); getChildren().add(tb); } { // 左省略,总页数大于 7(有中间页),激活下标大于 3 时显示(第五页) Label leftEllipsis = new Label(".."); leftEllipsis.setAlignment(Pos.CENTER); leftEllipsis.setPrefWidth(32); leftEllipsis.setBorder(Stroke.RIGHT); leftEllipsis.setMaxHeight(Double.MAX_VALUE); leftEllipsis.visibleProperty().bind(cp.greaterThan(7).and(ip.greaterThan(3))); leftEllipsis.managedProperty().bind(leftEllipsis.visibleProperty()); getChildren().add(leftEllipsis); // 中间页,显示条件:总页数大于 8 ,激活下标大于 3 且小于总页数 - 4(小于 3 时前置页处理,大于总页数 - 4 时后置页处理) for (int i = 0; i < 5; i++) { tb = new PageButton(); tb.indexProperty.bind(ip.add(i - 2)); // 动态数值 tb.visibleProperty().bind(cp.greaterThan(8).and(ip.greaterThan(3).and(ip.lessThan(cp.subtract(4))))); pageButtons.add(tb); getChildren().add(tb); } // 右省略,显示条件:总页数大于 7,激活下标小于总页数 - 4(大于总页数 - 4 时后置页处理,不需要省略) Label rightEllipsis = new Label(".."); rightEllipsis.setAlignment(Pos.CENTER); rightEllipsis.setPrefWidth(32); rightEllipsis.setMaxHeight(Double.MAX_VALUE); rightEllipsis.setBorder(Stroke.RIGHT); rightEllipsis.visibleProperty().bind(cp.greaterThan(7).and(ip.lessThan(cp.subtract(4)))); rightEllipsis.managedProperty().bind(rightEllipsis.visibleProperty()); getChildren().add(rightEllipsis); } // 后置页 for (int i = 0; i < 6; i++) { tb = new PageButton(); tb.indexProperty.bind(cp.add(i - 6)); // 动态数值 if (i == 5) { // 页数达到 7 时最后一页始终显示 tb.visibleProperty().bind(cp.greaterThan(6)); } else { // 其他页显示条件:总页数大于 7,激活下标大于总页数 - 5(倒数第四页) tb.visibleProperty().bind(cp.greaterThan(7).and(ip.greaterThan(cp.subtract(5)))); } pageButtons.add(tb); getChildren().add(tb); } // 下一页 next = new IconButton(TimiFXIcon.fromName("ARROW_1_E")).withBackground(); next.setMaxHeight(Double.MAX_VALUE); next.getStyleClass().add(CSS.BORDER_N); next.disableProperty().bind(ip.isEqualTo(cp.subtract(1)).or(cp.isEqualTo(0))); getStyleClass().add(STYLE_CLASS); setBorder(Stroke.DEFAULT); setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); setAlignment(Pos.BOTTOM_CENTER); getChildren().add(next); // ---------- 事件 ---------- new ToggleGroup().getToggles().addAll(pageButtons); for (PageButton button : pageButtons) { // 阻止取消选择 button.addEventFilter(MouseEvent.MOUSE_PRESSED, EVENT_TOGGLE_BUTTON); } // 数据变动更新 ChangeListener paramsListener = (obs, o, n) -> { // 步进翻页 prevI = ip.get() - 1; nextI = ip.get() + 1; // 重置激活页 if (cp.get() - 1 < ip.get()) { ip.set(0); } // 主动选中 for (PageButton pageButton : pageButtons) { // 分页存在预设页码,只作触发事件用(如前置页的第五第六页),需要主动计算激活的按钮 if (ip.get() == pageButton.indexProperty.get() && pageButton.isVisible()) { pageButton.setSelected(true); return; } } }; sp.addListener(paramsListener); ip.addListener(paramsListener); lp.addListener(paramsListener); // 步进翻页 prev.setOnAction(e -> ip.set(prevI)); next.setOnAction(e -> ip.set(nextI)); } /** 选择上一页 */ public void prev() { prev.fire(); } /** 选择下一页 */ public void next() { next.fire(); } /** * 设置激活页下标,取值范围 [0, {@link #getChunk()}] * * @param index 激活页下标 */ public void setIndex(int index) { ip.set(index); } /** * 获取当前激活页下标 * * @return 当前激活页下标 */ public int getIndex() { return ip.get(); } /** * 获取激活页监听 * * @return 激活页监听 */ public IntegerProperty indexProperty() { return ip; } /** * 获取当前页面数量 * * @return 当前页面数量 */ public int getChunk() { return cp.get(); } /** * 获取页面数量监听 * * @return 页面数量监听 */ public ReadOnlyIntegerProperty chunkProperty() { return cp; } /** * 设置单页数量,取值范围 [1, Integer.MAX_VALUE] * * @param size 单页数量 */ public void setSize(int size) { if (size < 1) { throw new IllegalArgumentException("page size range of [1, Integer.MAX_VALUE]"); } sp.set(size); } /** * 当前单页数量 * * @return 单页数量 */ public int getSize() { return sp.get(); } /** * 获取单页数量监听 * * @return 单页数量监听 */ public IntegerProperty sizeProperty() { return sp; } /** * 设置总数据量,取值范围 [0, Long.MAX_VALUE] * * @param length 总数据量 */ public void setLength(long length) { if (length < 0) { throw new IllegalArgumentException("length range of [0, Long.MAX_VALUE]"); } lp.set(length); } /** * 获取当前总数据量 * * @return 当前总数据量 */ public long getLength() { return lp.get(); } /** * 获取总数据大小监听 * * @return 总数据大小监听 */ public LongProperty lengthProperty() { return lp; } /** * 页面按钮,分页组件内部调度 * * @author 夜雨 * @since 2022-01-27 01:42 */ private class PageButton extends ToggleButton implements TimiFXUI { /** 当前页码 */ final IntegerProperty indexProperty; public PageButton() { indexProperty = new SimpleIntegerProperty(); getStyleClass().addAll(CSS.BORDER_R, CSS.BG_BUTTON); // 页码即显示内容 textProperty().bind(indexProperty.add(1).asString()); managedProperty().bind(visibleProperty()); // 选中更新激活下标 selectedProperty().addListener((obs, o, isSelected) -> { if (isSelected) { XPagination.this.ip.set(indexProperty.get()); } }); } } }