Browse Source

Added new control: MFXProgressSpinner

Signed-off-by: PAlex404 <alessandro.parisi406@gmail.com>
PAlex404 4 years ago
parent
commit
dc21ac2642

+ 108 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXProgressSpinner.java

@@ -0,0 +1,108 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.skins.MFXProgressSpinnerSkin;
+import javafx.css.*;
+import javafx.scene.control.ProgressIndicator;
+import javafx.scene.control.Skin;
+import javafx.scene.layout.Region;
+
+import java.util.List;
+
+public class MFXProgressSpinner extends ProgressIndicator {
+    private static final StyleablePropertyFactory<MFXProgressSpinner> FACTORY = new StyleablePropertyFactory<>(ProgressIndicator.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-spinner";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-spinner.css").toString();
+
+    public MFXProgressSpinner() {
+        this(-1);
+    }
+
+    public MFXProgressSpinner(double progress) {
+        super(progress);
+        initialize();
+    }
+
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+    }
+
+    private final StyleableDoubleProperty radius = new SimpleStyleableDoubleProperty(
+            StyleableProperties.RADIUS,
+            this,
+            "radius",
+            Region.USE_COMPUTED_SIZE
+    );
+
+    private final StyleableDoubleProperty startingAngle = new SimpleStyleableDoubleProperty(
+            StyleableProperties.STARTING_ANGLE,
+            this,
+            "startingAngle",
+            360 - Math.random() * 720
+    );
+
+    public double getRadius() {
+        return radius.get();
+    }
+
+    public StyleableDoubleProperty radiusProperty() {
+        return radius;
+    }
+
+    public void setRadius(double radius) {
+        this.radius.set(radius);
+    }
+
+    public double getStartingAngle() {
+        return startingAngle.get();
+    }
+
+    public StyleableDoubleProperty startingAngleProperty() {
+        return startingAngle;
+    }
+
+    public void setStartingAngle(double startingAngle) {
+        this.startingAngle.set(startingAngle);
+    }
+
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXProgressSpinner, Number> RADIUS =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-radius",
+                        MFXProgressSpinner::radiusProperty,
+                        Region.USE_COMPUTED_SIZE
+                );
+
+        private static final CssMetaData<MFXProgressSpinner, Number> STARTING_ANGLE =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-starting-angle",
+                        MFXProgressSpinner::startingAngleProperty,
+                        360 - Math.random() * 720
+                );
+
+        static {
+            cssMetaDataList = List.of(RADIUS, STARTING_ANGLE);
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXProgressSpinnerSkin(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    protected List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return MFXProgressSpinner.getControlCssMetaDataList();
+    }
+}

+ 286 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXProgressSpinnerSkin.java

@@ -0,0 +1,286 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXProgressSpinner;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.scene.Group;
+import javafx.scene.control.SkinBase;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Arc;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Font;
+import javafx.scene.text.Text;
+import javafx.util.Duration;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class MFXProgressSpinnerSkin extends SkinBase<MFXProgressSpinner> {
+    private boolean isValid = false;
+    private boolean wasIndeterminate = false;
+    private double arcLength = -1;
+
+    private final Color greenColor;
+    private final Color redColor;
+    private final Color yellowColor;
+    private final Color blueColor;
+
+    private final Arc arc;
+    private final Arc track;
+    private final StackPane arcPane;
+    private final Rectangle fillRect;
+    private final Text text;
+
+    private Timeline timeline;
+
+    public MFXProgressSpinnerSkin(MFXProgressSpinner spinner) {
+        super(spinner);
+
+        blueColor = Color.valueOf("#4285f4");
+        redColor = Color.valueOf("#db4437");
+        yellowColor = Color.valueOf("#f4b400");
+        greenColor = Color.valueOf("#0F9D58");
+
+        arc = new Arc();
+        arc.setManaged(false);
+        arc.setStartAngle(0);
+        arc.setLength(180);
+        arc.getStyleClass().setAll("arc");
+        arc.setFill(Color.TRANSPARENT);
+        arc.setStrokeWidth(3);
+
+        track = new Arc();
+        track.setManaged(false);
+        track.setStartAngle(0);
+        track.setLength(360);
+        track.setStrokeWidth(3);
+        track.getStyleClass().setAll("track");
+        track.setFill(Color.TRANSPARENT);
+
+        fillRect = new Rectangle();
+        fillRect.setFill(Color.TRANSPARENT);
+
+        text = new Text();
+        text.getStyleClass().setAll("text", "percentage");
+
+        final Group group = new Group(fillRect, track, arc, text);
+        group.setManaged(false);
+
+        arcPane = new StackPane(group);
+        arcPane.setPrefSize(50, 50);
+        getChildren().setAll(arcPane);
+
+        setListeners();
+    }
+
+    private void setListeners() {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        spinner.indeterminateProperty().addListener((observable, oldValue, newValue) -> reset());
+        spinner.progressProperty().addListener((observable, oldValue, newValue) -> updateProgress());
+        spinner.visibleProperty().addListener((observable, oldValue, newValue) -> updateAnimation());
+        spinner.parentProperty().addListener((observable, oldValue, newValue) -> updateAnimation());
+        spinner.sceneProperty().addListener((observable, oldValue, newValue) -> updateAnimation());
+    }
+
+    protected void updateProgress() {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        final boolean isIndeterminate = spinner.isIndeterminate();
+        if (!(isIndeterminate && wasIndeterminate)) {
+            arcLength = -360 * spinner.getProgress();
+            spinner.requestLayout();
+        }
+        wasIndeterminate = isIndeterminate;
+    }
+
+    private void reset() {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        if (spinner.isIndeterminate()) {
+            if (timeline == null) {
+                createTransition();
+                timeline.play();
+            }
+        } else {
+            clearAnimation();
+            arc.setStartAngle(90);
+            updateProgress();
+        }
+    }
+
+    private void clearAnimation() {
+        if (timeline != null) {
+            timeline.stop();
+            timeline.getKeyFrames().clear();
+            timeline = null;
+        }
+    }
+
+    private void updateAnimation() {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        final boolean isTreeVisible = spinner.isVisible() &&
+                spinner.getParent() != null &&
+                spinner.getScene() != null;
+        if (timeline != null) {
+            pauseTimeline(!isTreeVisible);
+        } else if (isTreeVisible) {
+            createTransition();
+        }
+    }
+
+    private void createTransition() {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        if (!spinner.isIndeterminate()) return;
+        final Paint initialColor = arc.getStroke();
+        if (initialColor == null) {
+            arc.setStroke(blueColor);
+        }
+
+        KeyFrame endingFrame = new KeyFrame(Duration.seconds(5.6),
+                new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
+                new KeyValue(arc.startAngleProperty(), 1845 + spinner.getStartingAngle(), Interpolator.LINEAR));
+
+        List<KeyFrame> allFrames = Stream.of(
+                getKeyFrames(0, 0, initialColor == null ? blueColor : initialColor),
+                getKeyFrames(450, 1.4, initialColor == null ? redColor : initialColor),
+                getKeyFrames(900, 2.8, initialColor == null ? yellowColor : initialColor),
+                getKeyFrames(1350, 4.2, initialColor == null ? greenColor : initialColor)
+        ).flatMap(Collection::stream).collect(Collectors.toList());
+        allFrames.add(endingFrame);
+
+        if (timeline != null) {
+            timeline.stop();
+            timeline.getKeyFrames().clear();
+        }
+        timeline = new Timeline();
+        timeline.getKeyFrames().addAll(allFrames);
+        timeline.setCycleCount(Timeline.INDEFINITE);
+        timeline.setDelay(Duration.ZERO);
+        timeline.playFromStart();
+    }
+
+    private void pauseTimeline(boolean pause) {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        if (spinner.isIndeterminate()) {
+            if (timeline == null) {
+                createTransition();
+            }
+            if (pause) {
+                timeline.pause();
+            } else {
+                timeline.play();
+            }
+        }
+    }
+
+    private List<KeyFrame> getKeyFrames(double angle, double duration, Paint color) {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        KeyFrame kf1 = new KeyFrame(Duration.seconds(duration),
+                new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
+                new KeyValue(arc.startAngleProperty(), angle + 45 + spinner.getStartingAngle(), Interpolator.LINEAR));
+
+        KeyFrame kf2 = new KeyFrame(Duration.seconds(duration + 0.4),
+                new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
+                new KeyValue(arc.startAngleProperty(), angle + 90 + spinner.getStartingAngle(), Interpolator.LINEAR));
+
+        KeyFrame kf3 = new KeyFrame(Duration.seconds(duration + 0.7),
+                new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
+                new KeyValue(arc.startAngleProperty(), angle + 135 + spinner.getStartingAngle(), Interpolator.LINEAR));
+
+        KeyFrame kf4 = new KeyFrame(Duration.seconds(duration + 1.1),
+                new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
+                new KeyValue(arc.startAngleProperty(), angle + 435 + spinner.getStartingAngle(), Interpolator.LINEAR),
+                new KeyValue(arc.strokeProperty(), color, Interpolator.EASE_BOTH));
+
+        return List.of(kf1, kf2, kf3, kf4);
+    }
+
+    private void updateArcLayout(double radius, double arcSize) {
+        arc.setRadiusX(radius);
+        arc.setRadiusY(radius);
+        arc.setCenterX(arcSize / 2);
+        arc.setCenterY(arcSize / 2);
+
+        track.setRadiusX(radius);
+        track.setRadiusY(radius);
+        track.setCenterX(arcSize / 2);
+        track.setCenterY(arcSize / 2);
+        track.setStrokeWidth(arc.getStrokeWidth());
+    }
+
+    @Override
+    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        if (Region.USE_COMPUTED_SIZE == spinner.getRadius()) {
+            return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
+        } else {
+            return spinner.getRadius() * 2 + arc.getStrokeWidth() * 2;
+        }
+    }
+
+    @Override
+    protected double computeMaxWidth(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        if (Region.USE_COMPUTED_SIZE == spinner.getRadius()) {
+            return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
+        } else {
+            return spinner.getRadius() * 2 + arc.getStrokeWidth() * 2;
+        }
+    }
+
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return arcPane.prefWidth(-1);
+    }
+
+    @Override
+    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return arcPane.prefHeight(-1);
+    }
+
+    @Override
+    protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
+        MFXProgressSpinner spinner = getSkinnable();
+
+        final double strokeWidth = arc.getStrokeWidth();
+        final double radius = Math.min(contentWidth, contentHeight) / 2 - strokeWidth / 2;
+        final double arcSize = snapSizeX(radius * 2 + strokeWidth);
+
+        arcPane.resizeRelocate((contentWidth - arcSize) / 2 + 1, (contentHeight - arcSize) / 2 + 1, arcSize, arcSize);
+        updateArcLayout(radius, arcSize);
+
+        fillRect.setWidth(arcSize);
+        fillRect.setHeight(arcSize);
+
+        if (!isValid) {
+            reset();
+            isValid = true;
+        }
+
+        if (!spinner.isIndeterminate()) {
+            arc.setLength(arcLength);
+            if (text.isVisible()) {
+                final double progress = spinner.getProgress();
+                int intProgress = (int) Math.round(progress * 100.0);
+                Font font = text.getFont();
+                text.setFont(Font.font(font.getFamily(), radius / 1.7));
+                text.setText((progress > 1 ? 100 : intProgress) + "%");
+                text.relocate((arcSize - text.getLayoutBounds().getWidth()) / 2, (arcSize - text.getLayoutBounds().getHeight()) / 2);
+            }
+        }
+    }
+}

+ 12 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-spinner.css

@@ -0,0 +1,12 @@
+.mfx-spinner:determinate .arc {
+    -fx-stroke: #0F9D58;
+}
+
+.mfx-spinner .percentage {
+    -fx-font-family: "Comfortaa Medium";
+    -fx-font-smoothing-type: gray;
+}
+
+.mfx-spinner:determinate .percentage {
+    -fx-fill: #4d4d4d;
+}