Explorar o código

MFXProgressBar and MFXStepperSkin revision

MFXProgressBar:
:sparkles: Added a new styleable property to specify the indeterminate animation speed

MFXProgressBarSkin:
:boom: Ditched the old buggy skin from JFoenix. The new skin uses Rectangles and a Group to build the progress bar. Now each progress bar (the indeterminate animation moves two bars) has its own style class so you can style them individually

MFXStepperSkin:
:recycle: Reviewed and simplified the progress bar
palexdev %!s(int64=4) %!d(string=hai) anos
pai
achega
11680add47

+ 9 - 6
demo/src/main/resources/io/github/palexdev/materialfx/demo/css/ProgressBarDemo.css

@@ -16,12 +16,15 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#custom > .track {
-    -fx-background-color: derive(salmon, 60%);
+#custom .track {
+    -fx-fill: derive(salmon, 70%);
 }
 
-#custom > .bar,
-#custom:indeterminate > .bar {
-    -fx-background-color: salmon;
-    -fx-padding: 2.0;
+#custom .bar1,
+#custom:indeterminate .bar1 {
+    -fx-fill: linear-gradient(to bottom right, #6A6AF8 0%, #C850C0 30%, darkorange 100%);
+}
+
+#custom:indeterminate .bar2 {
+    -fx-fill: linear-gradient(to bottom right, #F4D03F 0%, #10d7b4 100%);
 }

+ 56 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXProgressBar.java

@@ -20,9 +20,12 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.skins.MFXProgressBarSkin;
+import javafx.css.*;
 import javafx.scene.control.ProgressBar;
 import javafx.scene.control.Skin;
 
+import java.util.List;
+
 /**
  * This is the implementation of a progress bar following Google's material design guidelines.
  * <p>
@@ -32,6 +35,7 @@ public class MFXProgressBar extends ProgressBar {
     //================================================================================
     // Properties
     //================================================================================
+    private static final StyleablePropertyFactory<MFXProgressBar> FACTORY = new StyleablePropertyFactory<>(ProgressBar.getClassCssMetaData());
     private final String STYLE_CLASS = "mfx-progress-bar";
     private final String STYLESHEETS = MFXResourcesLoader.load("css/MFXProgressBar.css");
 
@@ -55,6 +59,53 @@ public class MFXProgressBar extends ProgressBar {
         setPrefWidth(200);
     }
 
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+    private final StyleableDoubleProperty animationSpeed = new SimpleStyleableDoubleProperty(
+            StyleableProperties.ANIMATION_SPEED,
+            this,
+            "animationSpeed",
+            1.0
+    );
+
+    public double getAnimationSpeed() {
+        return animationSpeed.get();
+    }
+
+    /**
+     * Specifies the indeterminate animation speed.
+     */
+    public StyleableDoubleProperty animationSpeedProperty() {
+        return animationSpeed;
+    }
+
+    public void setAnimationSpeed(double animationSpeed) {
+        this.animationSpeed.set(animationSpeed);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXProgressBar, Number> ANIMATION_SPEED =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-animation-speed",
+                        MFXProgressBar::animationSpeedProperty,
+                        1.0
+                );
+
+        static {
+            cssMetaDataList = List.of(ANIMATION_SPEED);
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
     //================================================================================
     // Override Methods
     //================================================================================
@@ -63,6 +114,11 @@ public class MFXProgressBar extends ProgressBar {
         return new MFXProgressBarSkin(this);
     }
 
+    @Override
+    protected List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return getClassCssMetaData();
+    }
+
     @Override
     public String getUserAgentStylesheet() {
         return STYLESHEETS;

+ 135 - 137
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXProgressBarSkin.java

@@ -1,28 +1,15 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 package io.github.palexdev.materialfx.skins;
 
 import io.github.palexdev.materialfx.controls.MFXProgressBar;
 import javafx.animation.*;
+import javafx.scene.Group;
 import javafx.scene.control.SkinBase;
 import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
 import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.StrokeLineCap;
+import javafx.scene.shape.StrokeLineJoin;
+import javafx.scene.shape.StrokeType;
 import javafx.util.Duration;
 
 /**
@@ -32,13 +19,12 @@ public class MFXProgressBarSkin extends SkinBase<MFXProgressBar> {
     //================================================================================
     // Properties
     //================================================================================
-    private final StackPane track;
-    private final StackPane bar1;
-    private final StackPane bar2;
+    private final StackPane container;
+    private final Rectangle track;
+    private final Rectangle bar1;
+    private final Rectangle bar2;
 
-    private boolean wasIndeterminate = false;
-    private double barWidth = 0;
-    private ParallelTransition indeterminateTransition;
+    private ParallelTransition indeterminateAnimation;
 
     //================================================================================
     // Constructors
@@ -46,146 +32,179 @@ public class MFXProgressBarSkin extends SkinBase<MFXProgressBar> {
     public MFXProgressBarSkin(MFXProgressBar progressBar) {
         super(progressBar);
 
-        track = new StackPane();
-        track.getStyleClass().add("track");
+        track = buildRectangle("track");
+        track.heightProperty().bind(progressBar.heightProperty());
+        track.widthProperty().bind(progressBar.widthProperty());
+
+        bar1 = buildRectangle("bar1");
+        bar1.heightProperty().bind(progressBar.heightProperty());
+
+        bar2 = buildRectangle("bar2");
+        bar2.heightProperty().bind(progressBar.heightProperty());
+        bar2.visibleProperty().bind(progressBar.indeterminateProperty());
+
+        Rectangle clip = new Rectangle();
+        clip.heightProperty().bind(progressBar.heightProperty());
+        clip.widthProperty().bind(progressBar.widthProperty());
+        clip.arcHeightProperty().bind(track.arcHeightProperty());
+        clip.arcWidthProperty().bind(track.arcWidthProperty());
+
+        Group group = new Group(track, bar1, bar2);
+        group.setClip(clip);
+        group.setManaged(false);
 
-        bar1 = new StackPane();
-        bar1.getStyleClass().add("bar");
-        bar2 = new StackPane();
-        bar2.getStyleClass().add("bar");
+        container = new StackPane(group);
+        getChildren().setAll(container);
 
         setListeners();
-        getChildren().setAll(track, bar1, bar2);
     }
 
     //================================================================================
     // Methods
     //================================================================================
-    private Rectangle buildClip() {
+
+    /**
+     * Adds listeners for: progress, width, visible, parent,scene and animation speed properties.
+     */
+    private void setListeners() {
         MFXProgressBar progressBar = getSkinnable();
 
-        Rectangle clip = new Rectangle();
-        clip.widthProperty().bind(progressBar.widthProperty());
-        clip.heightProperty().bind(progressBar.heightProperty());
-        return clip;
+        progressBar.progressProperty().addListener((observable, oldValue, newValue) -> updateBars());
+        progressBar.widthProperty().addListener((observable, oldValue, newValue) -> {
+            resetBars();
+            updateBars();
+        });
+        progressBar.visibleProperty().addListener((observable, oldValue, newValue) -> {
+            resetBars();
+            updateBars();
+        });
+        progressBar.parentProperty().addListener((observable, oldValue, newValue) -> {
+            resetBars();
+            updateBars();
+        });
+        progressBar.sceneProperty().addListener((observable, oldValue, newValue) -> {
+            resetBars();
+            updateBars();
+        });
+        progressBar.animationSpeedProperty().addListener((observable, oldValue, newValue) -> {
+            resetBars();
+            updateBars();
+        });
     }
 
     /**
-     * Adds listeners for: width, visible, parent and scene properties.
+     * Responsible for updating the progress bar state.
+     * <p></p>
+     * If it is indeterminate calls {@link #playIndeterminateAnimation()}, otherwise calls
+     * {@link #resetBars()} and {@link #updateProgress()}.
      */
-    private void setListeners() {
+    protected void updateBars() {
         MFXProgressBar progressBar = getSkinnable();
 
-        progressBar.progressProperty().addListener(invalidated -> {
-            progressBar.requestLayout();
+        if (progressBar.isIndeterminate()) {
+            playIndeterminateAnimation();
+        } else {
+            resetBars();
             updateProgress();
-        });
-        progressBar.widthProperty().addListener((observable, oldValue, newValue) -> updateProgress());
-        progressBar.visibleProperty().addListener((observable, oldValue, newValue) -> updateAnimation());
-        progressBar.parentProperty().addListener((observable, oldValue, newValue) -> updateAnimation());
-        progressBar.sceneProperty().addListener((observable, oldValue, newValue) -> updateAnimation());
+        }
     }
 
     /**
-     * Resets the animation.
+     * Responsible for clearing the indeterminate animation (stop, clear children and set to null), and
+     * resetting the bars layout, scale and width properties.
      */
-    private void clearAnimation() {
-        if (indeterminateTransition != null) {
-            indeterminateTransition.stop();
-            indeterminateTransition.getChildren().clear();
-            indeterminateTransition = null;
+    protected void resetBars() {
+        if (indeterminateAnimation != null) {
+            indeterminateAnimation.stop();
+            indeterminateAnimation.getChildren().clear();
+            indeterminateAnimation = null;
         }
+
+        bar1.setLayoutX(0);
+        bar1.setScaleX(1.0);
+        bar1.setWidth(0);
+        bar2.setLayoutX(0);
+        bar2.setScaleX(1.0);
+        bar2.setWidth(0);
+    }
+
+    /**
+     * Responsible for calculating the bar width according to the current progress
+     * (so when the progress bar is not indeterminate).
+     */
+    protected void updateProgress() {
+        MFXProgressBar progressBar = getSkinnable();
+
+        double width = ((progressBar.getWidth()) * (progressBar.getProgress() * 100)) / 100;
+        bar1.setWidth(width);
     }
 
     /**
-     * Creates the animation for the indeterminate bar.
+     * If the indeterminate animation is already playing returns.
+     * <p></p>
+     * Responsible for building the indeterminate animation.
      */
-    private void createIndeterminateTimeline() {
+    protected void playIndeterminateAnimation() {
         MFXProgressBar progressBar = getSkinnable();
 
-        if (indeterminateTransition != null) {
-            clearAnimation();
+        if (indeterminateAnimation != null) {
+            return;
         }
 
         final double width = progressBar.getWidth() - (snappedLeftInset() + snappedRightInset());
-
-        KeyFrame kf0 = new KeyFrame(Duration.ZERO,
+        KeyFrame kf0 = new KeyFrame(Duration.ONE,
                 new KeyValue(bar1.scaleXProperty(), 0.7),
-                new KeyValue(bar1.translateXProperty(), -width),
-                new KeyValue(bar2.translateXProperty(), -width)
+                new KeyValue(bar1.layoutXProperty(), -width),
+                new KeyValue(bar1.widthProperty(), width / 2),
+                new KeyValue(bar2.layoutXProperty(), -width),
+                new KeyValue(bar2.widthProperty(), width / 2)
         );
         KeyFrame kf1 = new KeyFrame(Duration.millis(700),
                 new KeyValue(bar1.scaleXProperty(), 1.25, Interpolator.EASE_BOTH)
         );
         KeyFrame kf2 = new KeyFrame(Duration.millis(1300),
-                new KeyValue(bar1.translateXProperty(), width, Interpolator.LINEAR)
+                new KeyValue(bar1.layoutXProperty(), width, Interpolator.LINEAR)
         );
-        KeyFrame kf3 = new KeyFrame(Duration.millis(900),
+        KeyFrame kf3 = new KeyFrame(Duration.millis(1100),
                 new KeyValue(bar1.scaleXProperty(), 1.0, Interpolator.EASE_OUT)
         );
         KeyFrame kf4 = new KeyFrame(Duration.millis(1100),
-                new KeyValue(bar2.translateXProperty(), width * 2, Interpolator.LINEAR),
-                new KeyValue(bar2.scaleXProperty(), 2.25, Interpolator.EASE_BOTH)
+                new KeyValue(bar2.layoutXProperty(), width * 2, Interpolator.LINEAR),
+                new KeyValue(bar2.scaleXProperty(), 2, Interpolator.EASE_BOTH)
         );
 
         Timeline bar1Animation = new Timeline(kf0, kf1, kf2, kf3);
         Timeline bar2Animation = new Timeline(kf4);
         bar2Animation.setDelay(Duration.millis(1100));
 
-        indeterminateTransition = new ParallelTransition(bar1Animation, bar2Animation);
-        indeterminateTransition.setCycleCount(Timeline.INDEFINITE);
+        indeterminateAnimation = new ParallelTransition(bar1Animation, bar2Animation);
+        indeterminateAnimation.setCycleCount(Timeline.INDEFINITE);
+        indeterminateAnimation.setRate(progressBar.getAnimationSpeed());
+        indeterminateAnimation.play();
     }
 
     /**
-     * Pauses/Resumes the animation.
+     * Responsible for building the track and the bars for the progress bar.
      */
-    private void updateTimeline(boolean pause) {
+    protected Rectangle buildRectangle(String styleClass) {
         MFXProgressBar progressBar = getSkinnable();
 
-        if (progressBar.isIndeterminate()) {
-            if (indeterminateTransition == null) {
-                createIndeterminateTimeline();
-            }
-            if (pause) {
-                indeterminateTransition.pause();
-            } else {
-                indeterminateTransition.play();
-            }
-        }
-    }
-
-    private void updateAnimation() {
-        final boolean isTreeVisible = isTreeVisible();
-        if (indeterminateTransition != null) {
-            updateTimeline(!isTreeVisible);
-        } else if (isTreeVisible) {
-            createIndeterminateTimeline();
-        }
-    }
-
-    /**
-     * Updates the bar progress.
-     */
-    private void updateProgress() {
-        MFXProgressBar progressBar = getSkinnable();
-
-        final boolean isIndeterminate = progressBar.isIndeterminate();
-        if (!(isIndeterminate && wasIndeterminate)) {
-            barWidth = ((int) (progressBar.getWidth() - snappedLeftInset() - snappedRightInset()) * 2
-                    * Math.min(1, Math.max(0, progressBar.getProgress()))) / 2.0F;
-            progressBar.requestLayout();
-        }
-        wasIndeterminate = isIndeterminate;
-    }
-
-    private boolean isTreeVisible() {
-        MFXProgressBar progressBar = getSkinnable();
-        return progressBar.isVisible() && progressBar.getParent() != null && progressBar.getScene() != null;
+        Rectangle rectangle = new Rectangle();
+        rectangle.getStyleClass().setAll(styleClass);
+        rectangle.setStroke(Color.TRANSPARENT);
+        rectangle.setStrokeLineCap(StrokeLineCap.ROUND);
+        rectangle.setStrokeLineJoin(StrokeLineJoin.ROUND);
+        rectangle.setStrokeType(StrokeType.INSIDE);
+        rectangle.setStrokeWidth(0);
+/*        rectangle.arcHeightProperty().bind(progressBar.bordersRadiusProperty());
+        rectangle.arcWidthProperty().bind(progressBar.bordersRadiusProperty());*/
+        return rectangle;
     }
 
     //================================================================================
-    // Override Methods
+    // OverrideMethods
     //================================================================================
+
     @Override
     protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
         return Math.max(100, leftInset + bar1.prefWidth(getSkinnable().getWidth()) + rightInset);
@@ -193,12 +212,7 @@ public class MFXProgressBarSkin extends SkinBase<MFXProgressBar> {
 
     @Override
     protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
-        return topInset + bar1.prefHeight(width) + bottomInset;
-    }
-
-    @Override
-    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
-        return getSkinnable().prefWidth(height);
+        return Math.max(5, bar1.prefHeight(width)) + topInset + bottomInset;
     }
 
     @Override
@@ -207,33 +221,17 @@ public class MFXProgressBarSkin extends SkinBase<MFXProgressBar> {
     }
 
     @Override
-    public void dispose() {
-        super.dispose();
-
-        if (indeterminateTransition != null) {
-            clearAnimation();
-        }
+    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().prefWidth(height);
     }
 
     @Override
-    protected void layoutChildren(double x, double y, double w, double h) {
-        MFXProgressBar progressBar = getSkinnable();
-
-        track.resizeRelocate(x, y, w, h);
-        bar1.resizeRelocate(x, y, progressBar.isIndeterminate() ? w / 2 : barWidth, h);
-        bar2.resizeRelocate(x, y, progressBar.isIndeterminate() ? w / 2 : 0, h);
-
-        if (progressBar.isIndeterminate()) {
-            bar1.setTranslateX(-w);
-            bar2.setTranslateX(-w);
-            createIndeterminateTimeline();
-            indeterminateTransition.play();
-            progressBar.setClip(buildClip());
-        } else {
-            bar1.setTranslateX(0);
-            bar2.setTranslateX(0);
-            clearAnimation();
-            progressBar.setClip(null);
+    public void dispose() {
+        super.dispose();
+        if (indeterminateAnimation != null) {
+            indeterminateAnimation.stop();
+            indeterminateAnimation.getChildren().clear();
+            indeterminateAnimation = null;
         }
     }
 }

+ 89 - 75
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperSkin.java

@@ -28,7 +28,7 @@ import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
 import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
 import javafx.animation.*;
 import javafx.beans.InvalidationListener;
-import javafx.beans.value.ChangeListener;
+import javafx.beans.binding.Bindings;
 import javafx.geometry.Bounds;
 import javafx.geometry.Pos;
 import javafx.scene.Group;
@@ -40,8 +40,11 @@ import javafx.scene.layout.BorderPane;
 import javafx.scene.layout.HBox;
 import javafx.scene.layout.Region;
 import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
 import javafx.scene.shape.Rectangle;
-import javafx.stage.Window;
+import javafx.scene.shape.StrokeLineCap;
+import javafx.scene.shape.StrokeLineJoin;
+import javafx.scene.shape.StrokeType;
 import javafx.util.Duration;
 
 /**
@@ -50,72 +53,81 @@ import javafx.util.Duration;
  * It is basically a {@link BorderPane} with three sections: top, center, bottom.
  * <p>
  * At the top there is the {@link HBox} that contains the {@code MFXStepperToggles} and the progress bar
- * which is realized by using a group and two rectangles. One rectangle is for the background and the other is for the progress.
- * The first one is manually adjusted both for x property and width property.
+ * which is realized by using a group and two rectangles. One rectangle is for the background/track and the other is the progress/bar.
+ * The bar is manually adjusted according to the current selected toggle, its width is set using {@link MFXStepperToggle#getGraphicBounds()}
+ * (+10 to ensure that there's no white space between the bar and the toggle).
  * <p>
- * At the center there is a {@link StackPane} with a minimum size of {@code 400x400}, it is the content pane namely the node that
+ * At the center there is a {@link StackPane}, it is the content pane namely the node that
  * will contain the content specifies by each stepper toggle. The style class is set to "content-pane".
  * <p>
  * At the bottom there is the {@link HBox} that contains the previous and next buttons. The style class is set to "buttons-box".
  * <p></p>
  * The stepper skin is rather delicate because the progress bar is quite hard to manage since every layout change can
- * potentially break. The skin updates the layout by adding a listener to the {@link MFXStepper#needsLayoutProperty()}.
+ * potentially break it. The skin updates the layout by adding a listener to the {@link MFXStepper#needsLayoutProperty()}.
  * When it changes the progress must be computed again with {@link #computeProgress()}.
  * A workaround is also needed in case the progress bar is animated and the layout changes. Without the workaround the
  * progress bar layout is re-computed by using the animation so the reposition process is not instantaneous.
- * To fix this annoying UI issue a boolean flag (buttonWasPressed) is set to true only when buttons are pressed and then set to false when the animation finishes,
+ * To fix this annoying UI issue a boolean flag (buttonWasPressed) is set to true only when buttons are pressed and then set to false right after the layout update,
  * so every layout change is done without playing the animation.
  *
  * @see MFXStepperToggle
  */
 public class MFXStepperSkin extends SkinBase<MFXStepper> {
+    //================================================================================
+    // Properties
+    //================================================================================
     private final StackPane contentPane;
     private final HBox stepperBar;
     private final HBox buttonsBox;
     private final MFXButton nextButton;
     private final MFXButton previousButton;
-    private ChangeListener<Boolean> parentSizeListener;
-    private ChangeListener<Window> windowListener;
 
     // Progressbar
-    private final Group progressBar;
+    private final Group progressBarGroup;
     private final double height = 7;
-    private final Rectangle progressRect;
-    private final Rectangle backgroundRect;
-    private ParallelTransition progressAnimation;
+    private final Rectangle bar;
+    private final Rectangle track;
+    private Timeline progressAnimation;
     private boolean buttonWasPressed = false;
 
+    //================================================================================
+    // Constructors
+    //================================================================================
     public MFXStepperSkin(MFXStepper stepper) {
         super(stepper);
 
-        progressRect = new Rectangle(0, 0, 0, height);
-        progressRect.fillProperty().bind(stepper.progressColorProperty());
-        progressRect.strokeProperty().bind(stepper.progressBarBackgroundProperty());
-        progressRect.widthProperty().bind(stepper.widthProperty());
-        progressRect.arcWidthProperty().bind(stepper.progressBarBorderRadiusProperty());
-        progressRect.arcHeightProperty().bind(stepper.progressBarBorderRadiusProperty());
-        progressRect.getStyleClass().add("bar-progress");
-
-        backgroundRect = new Rectangle(0, height);
-        backgroundRect.fillProperty().bind(stepper.progressBarBackgroundProperty());
-        backgroundRect.arcWidthProperty().bind(stepper.progressBarBorderRadiusProperty());
-        backgroundRect.arcHeightProperty().bind(stepper.progressBarBorderRadiusProperty());
-        backgroundRect.getStyleClass().add("bar-background");
-
-        progressAnimation = new ParallelTransition();
-        progressAnimation.setInterpolator(MFXAnimationFactory.getInterpolatorV1());
-        progressAnimation.setOnFinished(event -> buttonWasPressed = false);
+        track = buildRectangle("track");
+        track.setHeight(height);
+        track.widthProperty().bind(stepper.widthProperty());
+
+        bar = buildRectangle("bar");
+        bar.setHeight(height);
+
+        Rectangle clip = new Rectangle();
+        clip.setHeight(height);
+        clip.widthProperty().bind(stepper.widthProperty());
+        clip.arcHeightProperty().bind(stepper.progressBarBorderRadiusProperty());
+        clip.arcWidthProperty().bind(stepper.progressBarBorderRadiusProperty());
 
-        progressBar = new Group(progressRect, backgroundRect);
-        progressBar.setManaged(false);
+        progressBarGroup = new Group(track, bar);
+        progressBarGroup.setManaged(false);
+        progressBarGroup.setClip(clip);
+
+        progressAnimation = new Timeline();
+        progressAnimation.setOnFinished(event -> buttonWasPressed = false);
 
-        stepperBar = new HBox(progressBar);
+        stepperBar = new HBox(progressBarGroup);
         stepperBar.spacingProperty().bind(stepper.spacingProperty());
         stepperBar.alignmentProperty().bind(stepper.alignmentProperty());
         stepperBar.getChildren().addAll(stepper.getStepperToggles());
         stepperBar.setMinHeight(100);
         stepperBar.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
 
+        progressBarGroup.layoutYProperty().bind(Bindings.createDoubleBinding(
+                () -> snapPositionY((stepperBar.getHeight() / 2.0) - (height / 2.0)),
+                stepperBar.heightProperty()
+        ));
+
         nextButton = new MFXButton("Next");
         nextButton.setManaged(false);
         nextButton.getRippleGenerator().setClipSupplier(() ->
@@ -143,16 +155,13 @@ public class MFXStepperSkin extends SkinBase<MFXStepper> {
         container.setBottom(buttonsBox);
         getChildren().add(container);
 
-        parentSizeListener = (observable, oldValue, newValue) -> {
-            if (!newValue) {
-                computeProgress();
-            }
-        };
-        windowListener = (observable, oldValue, newValue) -> computeProgress();
-
         setListeners();
     }
 
+    //================================================================================
+    // Methods
+    //================================================================================
+
     /**
      * Adds the following listeners and handlers/filters.
      * <p>
@@ -184,7 +193,7 @@ public class MFXStepperSkin extends SkinBase<MFXStepper> {
         stepper.getStepperToggles().addListener((InvalidationListener) invalidated -> {
             stepper.reset();
             stepperBar.getChildren().setAll(stepper.getStepperToggles());
-            stepperBar.getChildren().add(0, progressBar);
+            stepperBar.getChildren().add(0, progressBarGroup);
             stepper.next();
 
             PauseTransition pauseTransition = new PauseTransition(Duration.millis(250));
@@ -244,31 +253,23 @@ public class MFXStepperSkin extends SkinBase<MFXStepper> {
     }
 
     /**
-     * Responsible for computing the position and size of the rectangle used to show the progress.
-     * <p>
-     * Keep in mind that the rectangle which is moved is the background rectangle not the progress one.
-     * Think about it as the background rectangle covers the progress one and when some progress is made you want to
-     * uncover the progress one by moving the background one.
+     * Responsible for computing the width of the rectangle(bar) used to show the progress.
      * <p></p>
      * Three cases are evaluated:
-     * <p> - The stepper {@link MFXStepper#lastToggleProperty()} is true, so the background rectangle width will be 0.
-     * <p> - The current stepper toggle is not null, so the background rectangle width will be computed as follows.
+     * <p> - The stepper {@link MFXStepper#lastToggleProperty()} is true, so the bar's width is set to the stepper's width.
+     * <p> - The current stepper toggle is not null, so the bar's width is computed as follows:
      * The toggle's circle bounds are retrieved using {@link MFXStepperToggle#getGraphicBounds()}. The X is computed
-     * as the minX of those Bounds converted from local to parent using {@link Node#localToParent(Bounds)}. The width
-     * is computed as the stepper's width minus the previously calculated X.
-     * <p> - The current stepper toggle is null so the X is 0 and the width is equal to the stepper's width.
-     * <p></p>
-     * The computed values are used by {@link #updateProgressBar(double, double)}
+     * as the minX of those Bounds converted from local to parent using {@link Node#localToParent(Bounds)}.
+     * This value, +10 to ensure that there is not white space between the bar and the toggle, will be the bar's width.
+     * <p> - The current stepper toggle is null so the width is set to 0.
      * <p></p>
-     * It can be tricky to understand but with the given information it should be understandable, maybe draw it, it will
-     * be easier.
-     *
+     * The computed values are used by {@link #updateProgressBar(double)}
      */
     private void computeProgress() {
         MFXStepper stepper = getSkinnable();
 
         if (stepper.isLastToggle()) {
-            updateProgressBar(stepper.getWidth(), 0);
+            updateProgressBar(stepper.getWidth());
             return;
         }
 
@@ -277,37 +278,53 @@ public class MFXStepperSkin extends SkinBase<MFXStepper> {
             Bounds bounds = stepperToggle.getGraphicBounds();
             if (bounds != null) {
                 double minX = snapSizeX(stepperToggle.localToParent(bounds).getMinX());
-                double width = snapSizeX(stepper.getWidth() - minX);
-                updateProgressBar(minX, width);
+                updateProgressBar(minX + 10);
             }
         } else {
-            updateProgressBar(0, stepper.getWidth());
+            updateProgressBar(0);
         }
     }
 
     /**
-     * Sets the background rectangle x and width properties to the given values.
+     * Sets the bar's width property to the given value.
      * If the {@link MFXStepper#animatedProperty()} or the buttonWasPressed flag are false
-     * then the properties are updated immediately. Otherwise they are updated by two separate timelines
-     * played at the same time using a {@link ParallelTransition}.
+     * then the properties are updated immediately (without the animation). Otherwise they are updated by a timeline.
      */
-    private void updateProgressBar(double x, double width) {
+    private void updateProgressBar(double width) {
         MFXStepper stepper = getSkinnable();
         if (!stepper.isAnimated() || !buttonWasPressed) {
-            backgroundRect.setX(x);
-            backgroundRect.setWidth(width);
+            bar.setWidth(width);
             buttonWasPressed = false;
             return;
         }
 
-        KeyFrame keyFrame1 = new KeyFrame(Duration.millis(stepper.getAnimationDuration()), new KeyValue(backgroundRect.xProperty(), x));
-        KeyFrame keyFrame2 = new KeyFrame(Duration.millis(stepper.getAnimationDuration()), new KeyValue(backgroundRect.widthProperty(), width));
-        Timeline timeline1 = new Timeline(keyFrame1);
-        Timeline timeline2 = new Timeline(keyFrame2);
-        progressAnimation.getChildren().setAll(timeline1, timeline2);
+        KeyFrame kf = new KeyFrame(Duration.millis(stepper.getAnimationDuration()), new KeyValue(bar.widthProperty(), width, MFXAnimationFactory.getInterpolatorV2()));
+        progressAnimation.getKeyFrames().setAll(kf);
         progressAnimation.playFromStart();
     }
 
+    /**
+     * Responsible for building the track and the bar for the progress bar.
+     */
+    protected Rectangle buildRectangle(String styleClass) {
+        MFXStepper stepper = getSkinnable();
+
+        Rectangle rectangle = new Rectangle();
+        rectangle.getStyleClass().setAll(styleClass);
+        rectangle.setStroke(Color.TRANSPARENT);
+        rectangle.setStrokeLineCap(StrokeLineCap.ROUND);
+        rectangle.setStrokeLineJoin(StrokeLineJoin.ROUND);
+        rectangle.setStrokeType(StrokeType.INSIDE);
+        rectangle.setStrokeWidth(0);
+        rectangle.arcHeightProperty().bind(stepper.progressBarBorderRadiusProperty());
+        rectangle.arcWidthProperty().bind(stepper.progressBarBorderRadiusProperty());
+        return rectangle;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
     @Override
     protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
         return Math.max(super.computeMinWidth(height, topInset, leftInset, bottomInset, rightInset) + (getSkinnable().getExtraSpacing() * 2), 300);
@@ -335,16 +352,13 @@ public class MFXStepperSkin extends SkinBase<MFXStepper> {
             progressAnimation.stop();
         }
         progressAnimation = null;
-        parentSizeListener = null;
-        windowListener = null;
     }
 
     @Override
     protected void layoutChildren(double x, double y, double w, double h) {
         super.layoutChildren(x, y, w, h);
 
-        double barY = snapPositionY((stepperBar.getHeight() / 2.0) - (height / 2.0));
-        progressBar.resizeRelocate(0.0, barY, w, height);
+        progressBarGroup.resize(w, height);
 
         double bw = 125;
         double bh = 34;

+ 11 - 10
materialfx/src/main/resources/io/github/palexdev/materialfx/css/MFXProgressBar.css

@@ -16,18 +16,19 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-.mfx-progress-bar > .track {
-    -fx-background-color: #E0E0E0;
+.mfx-progress-bar .track {
+    -fx-fill: #E0E0E0;
 }
 
-.mfx-progress-bar > .bar,
-.mfx-progress-bar:indeterminate > .bar {
-    -fx-background-color: #0F9D58;
-    -fx-padding: 2.0;
+.mfx-progress-bar .bar1,
+.mfx-progress-bar:indeterminate .bar1,
+.mfx-progress-bar:indeterminate .bar2 {
+    -fx-fill: #0F9D58;
 }
 
-.mfx-progress-bar > .track,
-.mfx-progress-bar > .bar {
-    -fx-background-radius: 5;
-    -fx-background-insets: 0;
+.mfx-progress-bar .track,
+.mfx-progress-bar .bar1,
+.mfx-progress-bar .bar2 {
+    -fx-arc-height: 6;
+    -fx-arc-width: 6;
 }

+ 8 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/MFXStepper.css

@@ -26,6 +26,14 @@
     -mfx-progress-color: -mfx-base-color;
 }
 
+.mfx-stepper .track {
+    -fx-fill: -mfx-bar-background;
+}
+
+.mfx-stepper .bar {
+    -fx-fill: -mfx-progress-color;
+}
+
 .mfx-stepper .buttons-box .mfx-button {
     -mfx-depth-level: level1;
     -fx-background-color: white;