title: Creating a simple custom scroll view
body: <p>This is somehow a general question about scroll views, I want to learn the basics of a scroll view and how to implement one on my own because it is essential as part of most dynamic GUI. You may ask, <em>Why not simply use the one provided by the platform?</em> My answer would be, aside from it's fun to learn new stuff, it's nice to see things customized the way you want it to be. Simply put, I want to create just a simple custom scroll view and try to understand how it is working behind the scene.</p>
  <p>Moving on, what I currently have to present here is just the simplest example of the UI I came up with. Basically, it is a <code>Pane</code> that serves as the viewport for the entire content and contains a single vertical scrollbar on its right edge, just like normal scroll views, but I just added a little transition which animates the scrollbar's width on mouse hover.</p>
  <p><strong>ScrollContainer</strong> class</p>
  <pre><code>import javafx.animation.KeyFrame;
  import javafx.animation.KeyValue;
  import javafx.animation.Timeline;
  import javafx.scene.layout.Pane;
  import javafx.scene.layout.Region;
  import javafx.scene.shape.Rectangle;
  import javafx.util.Duration;
  
  /**
   * ScrollContainer
   *
   * A container for scrolling large content.
   */
  public class ScrollContainer extends Pane {
  
      private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
      private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content
  
      /**
       * Construct a new ScrollContainer
       */
      public ScrollContainer() {
          super();
  
          scrollBar = new VerticalScrollBar();
          getChildren().add(scrollBar);
  
          rectangle = new Rectangle();
          rectangle.widthProperty().bind(widthProperty());
          rectangle.heightProperty().bind(heightProperty());
          setClip(rectangle);
      }
  
      @Override
      protected void layoutChildren() {
          super.layoutChildren();
  
          // Layout scrollbar to the edge of container, and fit the viewport's height as well
          scrollBar.resize(scrollBar.getWidth(), getHeight());
          scrollBar.setLayoutX(getWidth() - scrollBar.getWidth());
      }
  
      /**
       * VerticalScrollBar
       */
      private class VerticalScrollBar extends Region {
          // Temporary scrubber's height.
          // TODO: Figure out the computation for scrubber's height.
          private static final double SCRUBBER_LENGTH = 100;
  
          private double initialY; // Initial mouse position when dragging the scrubber
          private Timeline widthTransition; // Transforms width of scrollbar on hover
          private Region scrubber; // Indicator about the content's visible area
  
          /**
           * Construct a new VerticalScrollBar
           */
          private VerticalScrollBar() {
              super();
  
              // Scrollbar's initial width
              setPrefWidth(7);
  
              widthTransition = new Timeline(
                      new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
                      new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
              );
  
              scrubber = new Region();
              scrubber.setStyle(&quot;-fx-background-color: rgba(0,0,0, 0.25)&quot;);
              scrubber.setOnMousePressed(event -&gt; initialY = event.getY());
              scrubber.setOnMouseDragged(event -&gt; {
                  // Moves the scrubber vertically within the scrollbar.
                  // TODO: Figure out the proper way of handling scrubber movement, an onScroll mouse wheel function, ect.
                  double initialScrollY = event.getSceneY() - initialY;
                  double maxScrollY = getHeight() - SCRUBBER_LENGTH;
                  double minScrollY = 0;
                  if (initialScrollY &gt;= minScrollY &amp;&amp; initialScrollY &lt;= maxScrollY) {
                      scrubber.setTranslateY(initialScrollY);
                  }
              });
              getChildren().add(scrubber);
  
              // Animate scrollbar's width on mouse enter and exit
              setOnMouseEntered(event -&gt; {
                  widthTransition.setRate(1);
                  widthTransition.play();
              });
              setOnMouseExited(event -&gt; {
                  widthTransition.setRate(-1);
                  widthTransition.play();
              });
          }
  
          @Override
          protected void layoutChildren() {
              super.layoutChildren();
  
              // Layout scrubber to fit the scrollbar's width
              scrubber.resize(getWidth(), SCRUBBER_LENGTH);
          }
      }
  }
  </code></pre>
  <p><strong>Main</strong> class</p>
  <pre><code>import javafx.application.Application;
  import javafx.scene.Scene;
  import javafx.scene.control.Label;
  import javafx.scene.layout.StackPane;
  import javafx.stage.Stage;
  
  public class Main extends Application {
  
      @Override
      public void start(Stage primaryStage) {
          Label lorem = new Label();
          lorem.setStyle(&quot;-fx-padding: 20px;&quot;);
          lorem.setText(&quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit. &quot; +
                  &quot;Integer ut ornare enim, a rutrum nisl. &quot; +
                  &quot;Proin eros felis, rutrum at pharetra viverra, elementum quis lacus. &quot; +
                  &quot;Nam sit amet sollicitudin nibh, ac mattis lectus. &quot; +
                  &quot;Sed mattis ullamcorper sapien, a pulvinar turpis hendrerit vel. &quot; +
                  &quot;Fusce nec diam metus. In vel dui lacus. &quot; +
                  &quot;Sed imperdiet ipsum euismod aliquam rhoncus. &quot; +
                  &quot;Morbi sagittis mauris ac massa pretium, vel placerat purus porta. &quot; +
                  &quot;Suspendisse orci leo, sagittis eu orci vitae, porttitor sagittis odio. &quot; +
                  &quot;Proin iaculis enim sed ipsum sodales, at congue ante blandit. &quot; +
                  &quot;Etiam mattis erat nec dolor vestibulum, quis interdum sem pellentesque. &quot; +
                  &quot;Nullam accumsan ex non lacus sollicitudin interdum.&quot;);
          lorem.setWrapText(true);
  
          StackPane content = new StackPane();
          content.setPrefSize(300, 300);
          content.setMinSize(300, 300);
          content.setMaxSize(300, 300);
          content.setStyle(&quot;-fx-background-color: white;&quot;);
          content.getChildren().add(lorem);
  
          ScrollContainer viewport = new ScrollContainer();
          viewport.setStyle(&quot;-fx-background-color: whitesmoke&quot;);
          viewport.getChildren().add(0, content);
  
          primaryStage.setScene(new Scene(viewport, 300, 150));
          primaryStage.show();
      }
  
      public static void main(String[] args) {
          launch(args);
      }
  }
  </code></pre>
  <p>I wanted to see a working example showing just the basic art of scrolling; like the proper way of handling the thumb's animation movement, computation of scrollbar's thumb length, and lastly, the required total unit or amount to move the content. I think these three parts are the keys to the core of a scroll view.</p>
  <p>P.S<br />
  I also want to see the use of the <code>onScroll</code> event in JavaFX, right now all I know is the common used mouse events. Thank you in advance.</p>
  <hr />
  <h1>UPDATE</h1>
  <p>I've added a <code>BlockIncrement</code> function to the answer of sir @fabian below. It will basically just move the thumb to the current position of the pointer while keeping the [0, 1] range value. All credits and thanks goes to him.</p>
  <blockquote>
  <p>This is for others who were looking for something like this idea of
  <em>custom scroll view</em>, hope you might find this reference useful in the future.</p>
  </blockquote>
  <pre><code>public class ScrollContainer extends Region {
  
      private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
      private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content
  
      /**
       * Construct a new ScrollContainer
       */
      public ScrollContainer() {
          setOnScroll(evt -&gt; {
              double viewportHeight = getHeight();
              double contentHeight = getContentHeight();
              if (contentHeight &gt; viewportHeight) {
                  double delta = evt.getDeltaY() / (viewportHeight - contentHeight);
                  if (Double.isFinite(delta)) {
                      scrollBar.setValue(scrollBar.getValue() + delta);
                  }
              }
          });
  
          scrollBar = new VerticalScrollBar();
          getChildren().add(scrollBar);
  
          rectangle = new Rectangle();
          setClip(rectangle);
      }
  
      private Node content;
  
      public void setContent(Node content) {
          if (this.content != null) {
              // remove old content
              getChildren().remove(this.content);
          }
          if (content != null) {
              // add new content
              getChildren().add(0, content);
          }
          this.content = content;
      }
  
      private double getContentHeight() {
          return content == null ? 0 : content.getLayoutBounds().getHeight();
      }
  
      @Override
      protected void layoutChildren() {
          super.layoutChildren();
  
          double w = getWidth();
          double h = getHeight();
  
          double sw = scrollBar.getWidth();
  
          double viewportWidth = w - sw;
          double viewportHeight = h;
  
          if (content != null) {
              double contentHeight = getContentHeight();
              double vValue = scrollBar.getValue();
  
              // position content according to scrollbar value
              content.setLayoutY(Math.min(0, viewportHeight - contentHeight) * vValue);
          }
  
          // Layout scrollbar to the edge of container, and fit the viewport's height as well
          scrollBar.resize(sw, h);
          scrollBar.setLayoutX(viewportWidth);
  
          // resize clip
          rectangle.setWidth(w);
          rectangle.setHeight(h);
      }
  
      /**
       * VerticalScrollBar
       */
      private class VerticalScrollBar extends Region {
  
          private boolean thumbPressed; // Indicates that the scrubber was pressed
  
          private double initialValue;
          private double initialY; // Initial mouse position when dragging the scrubber
          private Timeline widthTransition; // Transforms width of scrollbar on hover
          private Region scrubber; // Indicator about the content's visible area
  
          private double value;
  
          private void setValue(double v) {
              value = v;
          }
  
          private double getValue() {
              return value;
          }
  
          private double calculateScrubberHeight() {
              double h = getHeight();
              return h * h / getContentHeight();
          }
  
          /**
           * Construct a new VerticalScrollBar
           */
          private VerticalScrollBar() {
              // Scrollbar's initial width
              setPrefWidth(7);
  
              widthTransition = new Timeline(
                      new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
                      new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
              );
  
              scrubber = new Region();
              scrubber.setStyle(&quot;-fx-background-color: rgba(0,0,0, 0.25)&quot;);
              scrubber.setOnMousePressed(event -&gt; {
                  initialY = scrubber.localToParent(event.getX(), event.getY()).getY();
                  initialValue = value;
                  thumbPressed = true;
              });
              scrubber.setOnMouseDragged(event -&gt; {
                  if (thumbPressed) {
                      double currentY = scrubber.localToParent(event.getX(), event.getY()).getY();
                      double sH = calculateScrubberHeight();
                      double h = getHeight();
  
                      // calculate value change and prevent errors
                      double delta = (currentY - initialY) / (h - sH);
                      if (!Double.isFinite(delta)) {
                          delta = 0;
                      }
  
                      // keep value in range [0, 1]
                      double newValue = Math.max(0, Math.min(1, initialValue + delta));
                      value = newValue;
  
                      // layout thumb
                      requestLayout();
                  }
              });
              scrubber.setOnMouseReleased(event -&gt; thumbPressed = false);
              getChildren().add(scrubber);
  
              // Added BlockIncrement.
              // Pressing the `track` or the scrollbar itself will move and position the
              // scrubber to the pointer location, as well as the content prior to the
              // value changes.
              setOnMousePressed(event -&gt; {
                  if (!thumbPressed) {
                      double sH = calculateScrubberHeight();
                      double h = getHeight();
                      double pointerY = event.getY();
                      double delta = pointerY / (h - sH);
                      double newValue = Math.max(0, Math.min(1, delta));
  
                      // keep value in range [0, 1]
                      if (delta &gt; 1) {
                          newValue = 1;
                      }
                      value = newValue;
  
                      requestLayout();
                  }
              });
  
              // Animate scrollbar's width on mouse enter and exit
              setOnMouseEntered(event -&gt; {
                  widthTransition.setRate(1);
                  widthTransition.play();
              });
              setOnMouseExited(event -&gt; {
                  widthTransition.setRate(-1);
                  widthTransition.play();
              });
          }
  
          @Override
          protected void layoutChildren() {
              super.layoutChildren();
  
              double h = getHeight();
              double cH = getContentHeight();
  
              if (cH &lt;= h) {
                  // full size, if content does not excede viewport size
                  scrubber.resize(getWidth(), h);
              } else {
                  double sH = calculateScrubberHeight();
  
                  // move thumb to position
                  scrubber.setTranslateY(value * (h - sH));
  
                  // Layout scrubber to fit the scrollbar's width
                  scrubber.resize(getWidth(), sH);
              }
          }
      }
  }
  </code></pre>