0

Continuing the discussion in this question, How to make JavaFX Slider to move in discrete steps? I added a "Play" button and a Timeline so that when the user clicks the "Play" button, the slider's knob repeatedly advances to the next tick mark until it reaches the end of the slider or the user clicks the "Stop" button. Unfortunately, the longer the animation runs, the more the knob moves between tick marks (and yes, setSnapToTicks is true). Also, the value of the slider isn't what I expected. Ideally, it should be "-60, -59, -58, etc." but the Timeline moves the slider knob in small fractions at a time (e.g. -55.188833333333335). I've been banging my head on this problem for a long time. Here is my code:

public class SliderTest extends Application {
StringConverter<Double> stringConverter = new StringConverter<>() {

    @Override
    public String toString( Double object ) {
        long seconds = object.longValue();
        long minutes = TimeUnit.SECONDS.toMinutes( seconds );
        long remainingseconds = Math.abs( seconds - TimeUnit.MINUTES.toSeconds( minutes ) );
        return String.format( "%02d", minutes ) + ":" + String.format( "%02d", remainingseconds );
    }

    @Override
    public Double fromString( String string ) {
        return null;
    }
};

@Override
public void start( Stage primaryStage ) throws Exception {
    try {
        Pane pane = new Pane();
        pane.setStyle( "-fx-background-color: black;" );

        Slider slider = new Slider( -60, 0, -60 );
        slider.setBlockIncrement( 60 );
        slider.setMajorTickUnit( 60 );
        slider.setMinorTickCount( 60 );
        slider.setShowTickLabels( true );
        slider.setShowTickMarks( true );
        slider.setSnapToTicks( true );
        slider.setLabelFormatter( stringConverter );
        slider.setPrefWidth( 1303 );

        Button playStopButton = new Button( "Play" );
        HBox buttonHBox = new HBox( playStopButton );
        buttonHBox.setAlignment( Pos.CENTER );
        Text sliderTime = new Text( "Time" );
        sliderTime.setFont( new Font( 12 ) );
        sliderTime.setFill( Color.WHITE );
        sliderTime.setText( stringConverter.toString( slider.getValue() ) );

        HBox hbox = new HBox( slider, sliderTime, buttonHBox );
        hbox.setPrefWidth( 1428 );
        hbox.setSpacing( 10 );
        hbox.setAlignment( Pos.TOP_CENTER );

        pane.getChildren().add( hbox );

        Scene scene = new Scene( pane );
        scene.getStylesheets().add( getClass().getResource( "/css/slider.css" ).toExternalForm() );
        primaryStage.setScene( scene );
        primaryStage.show();

        Timeline timeline = new Timeline(
                new KeyFrame( Duration.ZERO, new KeyValue( slider.valueProperty(), slider.getValue() ) ),
                new KeyFrame( Duration.seconds( -slider.getValue() ), new KeyValue( slider.valueProperty(), 0 ) ) );

        slider.valueProperty().addListener( ( observable, oldValue, newValue ) -> {
            slider.setValue( newValue.intValue() );
            
            System.out.println( slider.getValue() + " | " + newValue.doubleValue() );

            // Set the text to the slider's new value.
            sliderTime.setText( stringConverter.toString( slider.getValue() ) );

            if ( newValue.doubleValue() == 0 ) {
                // Reset the timeline and the text for the sliderButton.
                timeline.stop();
                timeline.getKeyFrames().clear();
                playStopButton.setText( "Stop" );
            }
        } );

        slider.setOnMousePressed( event -> {
            // On mouse down, reset the timeline.
            timeline.stop();
            timeline.getKeyFrames().clear();
        } );

        slider.setOnMouseReleased( event -> {
            // Set the new start position of the timeline.
            timeline.getKeyFrames().add(
                    new KeyFrame( Duration.ZERO, new KeyValue( slider.valueProperty(), slider.getValue() ) ) );
            timeline.getKeyFrames().add( new KeyFrame( Duration.seconds( -slider.getValue() ),
                    new KeyValue( slider.valueProperty(), 0 ) ) );

            // Play the animation only if it was playing before.
            if ( playStopButton.getText().equals( "Pause" ) ) {
                timeline.play();
            }
            // Change the image for the playStopButton to stop when the thumb is moved to zero.
            else if ( playStopButton.getText().equals( "Stop" ) && ( slider.getValue() < 0 ) ) {
                playStopButton.setText( "Play" );
            }
        } );

        playStopButton.setOnMouseClicked( event -> {
            // Ignore mouse clicks on the playStopButton when the slider is at time zero.
            if ( slider.getValue() != 0 ) {
                if ( ( timeline.getStatus() == Status.STOPPED ) || ( timeline.getStatus() == Status.PAUSED ) ) {
                    playStopButton.setText( "Pause" );
                    timeline.play();
                }
                else if ( timeline.getStatus() == Status.RUNNING ) {
                    playStopButton.setText( "Play" );
                    timeline.pause();
                }
            }
        } );
    } catch ( Exception e ) {
        e.printStackTrace();
    }
}

public static void main( String[] args ) {
    launch( args );
}
  • don't quite understand what you want to achieve .. on click the value should jump to the next tic and then? run further or just sit there (until what happens)? – kleopatra Sep 04 '20 at 09:19
  • It keeps jumping to the next tick until either it reaches the slider's max value or the user clicks on the "Stop" button. Then the animation stops. The problem is, the Timeline moves the knob in tiny increments so it may take a dozen iterations to go from one minor tick mark to the next. If you run my code, you can see this happening in the console. Is there a way to reduce this movement to a single iteration? – MasterMirror Sep 04 '20 at 17:10
  • repeating: what do you want to achieve? – kleopatra Sep 04 '20 at 21:36
  • I want to load an image when the knob reaches the next major tick mark. But since it takes a dozen or so iterations for the Timeline to move the knob between say, tick 61 and tick 60, my image won't get loaded because the slider's double value is almost never exactly 60. Does this make sense? – MasterMirror Sep 04 '20 at 23:08
  • hmm .. still don't understand. But anyway, don't try to solve that in the view: instead implement a model that has the value property, has a notion of threshold (when to load the next image) and loads a new whenever the next threshhold is reached. Then let all parts of the ui interact exclusively with the model (vs. with the other views). – kleopatra Sep 05 '20 at 14:20

1 Answers1

0

Part of the problem is in my setup of the Slider. The other part of the problem is my definition of the Timeline. Basically, I left it up to Java to define all of the values for the slider button to move to regardless of where the tick marks are, which isn't what I want (although it does make for smooth animations). But after making the following changes, the slider's button advances to the next tick mark every second which is what I want. Compared to before when the slider's button was always between two tick marks.

        slider.setMajorTickUnit( 1 );

        Timeline timeline = new Timeline(
                new KeyFrame( Duration.seconds( 1 ), event -> {
                    slider.setValue( slider.getValue() + 1 );
                } ) );
        timeline.setCycleCount( Math.abs( Double.valueOf( slider.getValue() ).intValue() ) );