modules/controls/src/main/java/javafx/scene/control/skin/VirtualFlow.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization

@@ -21,23 +21,33 @@
  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 
-package com.sun.javafx.scene.control.skin;
+package javafx.scene.control.skin;
 
 import com.sun.javafx.scene.control.Logging;
+import com.sun.javafx.scene.control.Properties;
+import com.sun.javafx.scene.control.VirtualScrollBar;
+import com.sun.javafx.scene.control.skin.Utils;
 import com.sun.javafx.scene.traversal.Algorithm;
 import com.sun.javafx.scene.traversal.Direction;
 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
 import com.sun.javafx.scene.traversal.TraversalContext;
 import javafx.animation.KeyFrame;
 import javafx.animation.Timeline;
 import javafx.beans.InvalidationListener;
 import javafx.beans.Observable;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.BooleanPropertyBase;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.value.ChangeListener;
 import javafx.collections.ObservableList;
 import javafx.event.EventDispatcher;
 import javafx.event.EventHandler;
 import javafx.geometry.Orientation;

@@ -46,29 +56,42 @@
 import javafx.scene.Node;
 import javafx.scene.Parent;
 import javafx.scene.Scene;
 import javafx.scene.control.Cell;
 import javafx.scene.control.IndexedCell;
+import javafx.scene.control.ListCell;
 import javafx.scene.control.ScrollBar;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.input.ScrollEvent;
 import javafx.scene.layout.Region;
 import javafx.scene.layout.StackPane;
 import javafx.scene.shape.Rectangle;
 import javafx.util.Callback;
 import javafx.util.Duration;
 import sun.util.logging.PlatformLogger;
+
 import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.List;
 
 /**
- * Implementation of a virtualized container using a cell based mechanism.
+ * Implementation of a virtualized container using a cell based mechanism. This
+ * is used by the skin implementations for UI controls such as
+ * {@link javafx.scene.control.ListView}, {@link javafx.scene.control.TreeView},
+ * {@link javafx.scene.control.TableView}, and {@link javafx.scene.control.TreeTableView}.
+ *
+ * @since 9
  */
 public class VirtualFlow<T extends IndexedCell> extends Region {
 
+    /***************************************************************************
+     *                                                                         *
+     * Static fields                                                           *
+     *                                                                         *
+     **************************************************************************/
+
     /**
      * Scroll events may request to scroll about a number of "lines". We first
      * decide how big one "line" is - for fixed cell size it's clear,
      * for variable cell size we settle on a single number so that the scrolling
      * speed is consistent. Now if the line is so big that

@@ -76,273 +99,29 @@
      * them smaller to prevent the scrolling step to be too big (perhaps
      * even more than one page).
      */
     private static final int MIN_SCROLLING_LINES_PER_PAGE = 8;
                     
-    private boolean touchDetected = false;
-    private boolean mouseDown = false;
-
     /**
-     * There are two main complicating factors in the implementation of the
-     * VirtualFlow, which are made even more complicated due to the performance
-     * sensitive nature of this code. The first factor is the actual
-     * virtualization mechanism, wired together with the PositionMapper.
-     * The second complicating factor is the desire to do minimal layout
-     * and minimal updates to CSS.
-     *
-     * Since the layout mechanism runs at most once per pulse, we want to hook
-     * into this mechanism for minimal recomputation. Whenever a layout pass
-     * is run we record the width/height that the virtual flow was last laid
-     * out to. In subsequent passes, if the width/height has not changed then
-     * we know we only have to rebuild the cells. If the width or height has
-     * changed, then we can make appropriate decisions based on whether the
-     * width / height has been reduced or expanded.
-     *
-     * In various places, if requestLayout is called it is generally just
-     * used to indicate that some form of layout needs to happen (either the
-     * entire thing has to be reconstructed, or just the cells need to be
-     * reconstructed, generally).
-     *
-     * The accumCell is a special cell which is used in some computations
-     * when an actual cell for that item isn't currently available. However,
-     * the accumCell must be cleared whenever the cellFactory function is
-     * changed because we need to use the cells that come from the new factory.
-     *
-     * In addition to storing the lastWidth and lastHeight, we also store the
-     * number of cells that existed last time we performed a layout. In this
-     * way if the number of cells change, we can request a layout and when it
-     * occurs we can tell that the number of cells has changed and react
-     * accordingly.
-     *
-     * Because the VirtualFlow can be laid out horizontally or vertically a
-     * naming problem is present when trying to conceptualize and implement
-     * the flow. In particular, the words "width" and "height" are not
-     * precise when describing the unit of measure along the "virtualized"
-     * axis and the "orthogonal" axis. For example, the height of a cell when
-     * the flow is vertical is the magnitude along the "virtualized axis",
-     * and the width is along the axis orthogonal to it.
-     *
-     * Since "height" and "width" are not reliable terms, we use the words
-     * "length" and "breadth" to describe the magnitude of a cell along
-     * the virtualized axis and orthogonal axis. For example, in a vertical
-     * flow, the height=length and the width=breadth. In a horizontal axis,
-     * the height=breadth and the width=length.
+     * Indicates that this is a newly created cell and we need call impl_processCSS for it.
      *
-     * These terms are somewhat arbitrary, but chosen so that when reading
-     * most of the below code you can think in just one dimension, with
-     * helper functions converting width/height in to length/breadth, while
-     * also being different from width/height so as not to get confused with
-     * the actual width/height of a cell.
-     */
-    /**
-     * Indicates the primary direction of virtualization. If true, then the
-     * primary direction of virtualization is vertical, meaning that cells will
-     * stack vertically on top of each other. If false, then they will stack
-     * horizontally next to each other.
-     */
-    private BooleanProperty vertical;
-    public final void setVertical(boolean value) {
-        verticalProperty().set(value);
-    }
-
-    public final boolean isVertical() {
-        return vertical == null ? true : vertical.get();
-    }
-
-    public final BooleanProperty verticalProperty() {
-        if (vertical == null) {
-            vertical = new BooleanPropertyBase(true) {
-                @Override protected void invalidated() {                    
-                    pile.clear();
-                    sheetChildren.clear();
-                    cells.clear();
-                    lastWidth = lastHeight = -1;
-                    setMaxPrefBreadth(-1);
-                    setViewportBreadth(0);
-                    setViewportLength(0);
-                    lastPosition = 0;
-                    hbar.setValue(0);
-                    vbar.setValue(0);
-                    setPosition(0.0f);
-                    setNeedsLayout(true);
-                    requestLayout();
-                }
-
-                @Override
-                public Object getBean() {
-                    return VirtualFlow.this;
-                }
-
-                @Override
-                public String getName() {
-                    return "vertical";
-                }
-            };
-        }
-        return vertical;
-    }
-
-    /**
-     * Indicates whether the VirtualFlow viewport is capable of being panned
-     * by the user (either via the mouse or touch events).
-     */
-    private boolean pannable = true;
-    public boolean isPannable() { return pannable; }
-    public void setPannable(boolean value) { this.pannable = value; }
-
-    /**
-     * Indicates the number of cells that should be in the flow. The user of
-     * the VirtualFlow must set this appropriately. When the cell count changes
-     * the VirtualFlow responds by updating the visuals. If the items backing
-     * the cells change, but the count has not changed, you must call the
-     * reconfigureCells() function to update the visuals.
-     */
-    private int cellCount;
-    public int getCellCount() { return cellCount; }
-    public void setCellCount(int i) {
-        int oldCount = cellCount;
-        this.cellCount = i;
-        
-        boolean countChanged = oldCount != cellCount;
-
-        // ensure that the virtual scrollbar adjusts in size based on the current
-        // cell count.
-        if (countChanged) {
-            VirtualScrollBar lengthBar = isVertical() ? vbar : hbar;
-            lengthBar.setMax(i);
-        }
-
-        // I decided *not* to reset maxPrefBreadth here for the following
-        // situation. Suppose I have 30 cells and then I add 10 more. Just
-        // because I added 10 more doesn't mean the max pref should be
-        // reset. Suppose the first 3 cells were extra long, and I was
-        // scrolled down such that they weren't visible. If I were to reset
-        // maxPrefBreadth when subsequent cells were added or removed, then the
-        // scroll bars would erroneously reset as well. So I do not reset
-        // the maxPrefBreadth here.
-
-        // Fix for RT-12512, RT-14301 and RT-14864.
-        // Without this, the VirtualFlow length-wise scrollbar would not change
-        // as expected. This would leave items unable to be shown, as they
-        // would exist outside of the visible area, even when the scrollbar
-        // was at its maximum position.
-        // FIXME this should be only executed on the pulse, so this will likely
-        // lead to performance degradation until it is handled properly.
-        if (countChanged) {
-            layoutChildren();
-
-            // Fix for RT-13965: Without this line of code, the number of items in
-            // the sheet would constantly grow, leaking memory for the life of the
-            // application. This was especially apparent when the total number of
-            // cells changes - regardless of whether it became bigger or smaller.
-            sheetChildren.clear();
-
-            Parent parent = getParent();
-            if (parent != null) parent.requestLayout();
-        }
-        // TODO suppose I had 100 cells and I added 100 more. Further
-        // suppose I was scrolled to the bottom when that happened. I
-        // actually want to update the position of the mapper such that
-        // the view remains "stable".
-    }
-
-    /**
-     * The position of the VirtualFlow within its list of cells. This is a value
-     * between 0 and 1.
-     */
-    private double position;
-
-    public double getPosition() {
-        return position;
-    }
-
-    public void setPosition(double newPosition) {
-        boolean needsUpdate = this.position != newPosition;
-        this.position = com.sun.javafx.util.Utils.clamp(0, newPosition, 1);;
-        if (needsUpdate) {
-            requestLayout();
-        }
-    }
-    
-    /**
-     * For optimisation purposes, some use cases can trade dynamic cell length
-     * for speed - if fixedCellSize is greater than zero we'll use that rather
-     * than determine it by querying the cell itself.
+     * See RT-23616 for more details.
      */
-    private double fixedCellSize = 0;
-    private boolean fixedCellSizeEnabled = false;
-
-    public void setFixedCellSize(final double value) {
-        this.fixedCellSize = value;
-        this.fixedCellSizeEnabled = fixedCellSize > 0;
-        needsCellsLayout = true;
-        layoutChildren();
-    }
-    
-    /**
-     * Callback which is invoked whenever the VirtualFlow needs a new
-     * IndexedCell. The VirtualFlow attempts to reuse cells whenever possible
-     * and only creates the minimal number of cells necessary.
-     */
-    private Callback<VirtualFlow, T> createCell;
-    public Callback<VirtualFlow, T> getCreateCell() { return createCell; }
-    public void setCreateCell(Callback<VirtualFlow, T> cc) {
-        this.createCell = cc;
+    private static final String NEW_CELL = "newcell";
 
-        if (createCell != null) {
-            accumCell = null;
-            setNeedsLayout(true);
-            recreateCells();
-            if (getParent() != null) getParent().requestLayout();
-        }
-    }
+    private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987;
 
-    /**
-     * The maximum preferred size in the non-virtual direction. For example,
-     * if vertical, then this is the max pref width of all cells encountered.
-     * <p>
-     * In general, this is the largest preferred size in the non-virtual
-     * direction that we have ever encountered. We don't reduce this size
-     * unless instructed to do so, so as to reduce the amount of scroll bar
-     * jitter. The access on this variable is package ONLY FOR TESTING.
-     */
-    private double maxPrefBreadth;
-    protected final void setMaxPrefBreadth(double value) {
-        this.maxPrefBreadth = value;
-    }
-    protected final double getMaxPrefBreadth() {
-        return maxPrefBreadth;
-    }
 
-    /**
-     * The breadth of the viewport portion of the VirtualFlow as computed during
-     * the layout pass. In a vertical flow this would be the same as the clip
-     * view width. In a horizontal flow this is the clip view height.
-     * The access on this variable is package ONLY FOR TESTING.
-     */
-    private double viewportBreadth;
-    protected final void setViewportBreadth(double value) {
-        this.viewportBreadth = value;
-    }
-    protected final double getViewportBreadth() {
-        return viewportBreadth;
-    }
 
-    /**
-     * The length of the viewport portion of the VirtualFlow as computed
-     * during the layout pass. In a vertical flow this would be the same as the
-     * clip view height. In a horizontal flow this is the clip view width.
-     * The access on this variable is package ONLY FOR TESTING.
-     */
-    private double viewportLength;
-    void setViewportLength(double value) {
-        this.viewportLength = value;
-    }
-    protected double getViewportLength() {
-        return viewportLength;
-    }
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
 
+    private boolean touchDetected = false;
+    private boolean mouseDown = false;
 
     /**
      * The width of the VirtualFlow the last time it was laid out. We
      * use this information for several fast paths during the layout pass.
      */

@@ -395,13 +174,10 @@
      * off the beginning or the end, depending on the order of scrolling.
      * <p>
      * This is package private ONLY FOR TESTING
      */
     final ArrayLinkedList<T> cells = new ArrayLinkedList<T>();
-    protected List<T> getCells() {
-        return cells;
-    }
 
     /**
      * A structure containing cells that can be reused later. These are cells
      * that at one time were needed to populate the view, but now are no longer
      * needed. We keep them here until they are needed again.

@@ -436,23 +212,16 @@
      * The scroll bar used for scrolling horizontally. This has package access
      * ONLY for testing.
      */
     private VirtualScrollBar hbar = new VirtualScrollBar(this);
 
-    protected final VirtualScrollBar getHbar() {
-        return hbar;
-    }
     /**
      * The scroll bar used to scrolling vertically. This has package access
      * ONLY for testing.
      */
     private VirtualScrollBar vbar = new VirtualScrollBar(this);
 
-    protected final VirtualScrollBar getVbar() {
-        return vbar;
-    }
-
     /**
      * Control in which the cell's sheet is placed and forms the viewport. The
      * viewportBreadth and viewportLength are simply the dimensions of the
      * clipView. This has package access ONLY for testing.
      */

@@ -469,21 +238,49 @@
     // used for panning the virtual flow
     private double lastX;
     private double lastY;
     private boolean isPanning = false;
 
-    public VirtualFlow() {
-        getStyleClass().add("virtual-flow");
-        setId("virtual-flow");
-
-        // initContent
-        // --- sheet
-        sheet = new Group();
-        sheet.getStyleClass().add("sheet");
-        sheet.setAutoSizeChildren(false);
+    private boolean fixedCellSizeEnabled = false;
         
-        sheetChildren = sheet.getChildren();
+    private boolean needsReconfigureCells = false; // when cell contents are the same
+    private boolean needsRecreateCells = false; // when cell factory changed
+    private boolean needsRebuildCells = false; // when cell contents have changed
+    private boolean needsCellsLayout = false;
+    private boolean sizeChanged = false;
+    private final BitSet dirtyCells = new BitSet();
+
+    Timeline sbTouchTimeline;
+    KeyFrame sbTouchKF1;
+    KeyFrame sbTouchKF2;
+
+    private boolean needBreadthBar;
+    private boolean needLengthBar;
+    private boolean tempVisibility = false;
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates a new VirtualFlow instance.
+     */
+    public VirtualFlow() {
+        getStyleClass().add("virtual-flow");
+        setId("virtual-flow");
+
+        // initContent
+        // --- sheet
+        sheet = new Group();
+        sheet.getStyleClass().add("sheet");
+        sheet.setAutoSizeChildren(false);
+        
+        sheetChildren = sheet.getChildren();
 
         // --- clipView
         clipView = new ClippedContainer(this);
         clipView.setNode(sheet);
         getChildren().add(clipView);

@@ -526,13 +323,13 @@
         /*
         ** listen for ScrollEvents over the whole of the VirtualFlow
         ** area, the above dispatcher having removed the ScrollBars
         ** scroll event handling.
         */
-        setOnScroll(new EventHandler<javafx.scene.input.ScrollEvent>() {
+        setOnScroll(new EventHandler<ScrollEvent>() {
             @Override public void handle(ScrollEvent event) {
-                if (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
+                if (Properties.IS_TOUCH_SUPPORTED) {
                     if (touchDetected == false &&  mouseDown == false ) {
                         startSBReleasedAnimation();
                     }
                 }
                 /*

@@ -545,11 +342,11 @@
                             virtualDelta = event.getTextDeltaY() * lastHeight;
                             break;
                         case LINES:
                             double lineSize;
                             if (fixedCellSizeEnabled) {
-                                lineSize = fixedCellSize;
+                                lineSize = getFixedCellSize();
                             } else {
                                 // For the scrolling to be reasonably consistent
                                 // we set the lineSize to the average size
                                 // of all currently loaded lines.
                                 T lastCell = cells.getLast();

@@ -584,11 +381,11 @@
 
                 if (virtualDelta != 0.0) { 
                     /*
                     ** only consume it if we use it
                     */
-                    double result = adjustPixels(-virtualDelta);
+                    double result = scrollPixels(-virtualDelta);
                     if (result != 0.0) {
                         event.consume();
                     }
                 }
 

@@ -613,11 +410,11 @@
 
         addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
             @Override
             public void handle(MouseEvent e) {
                 mouseDown = true;
-                if (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
+                if (Properties.IS_TOUCH_SUPPORTED) {
                     scrollBarOn();
                 }
                 if (isFocusTraversable()) {
                     // We check here to see if the current focus owner is within
                     // this VirtualFlow, and if so we back-off from requesting

@@ -657,16 +454,16 @@
                         || hbar.getBoundsInParent().contains(e.getX(), e.getY()));
             }
         });
         addEventFilter(MouseEvent.MOUSE_RELEASED, e -> {
             mouseDown = false;
-            if (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
+            if (Properties.IS_TOUCH_SUPPORTED) {
                 startSBReleasedAnimation();
             }
         });
         addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
-            if (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
+            if (Properties.IS_TOUCH_SUPPORTED) {
                 scrollBarOn();
             }
             if (! isPanning || ! isPannable()) return;
 
             // With panning enabled, we support panning in both vertical

@@ -677,11 +474,11 @@
 
             // figure out the distance that the mouse moved in the virtual
             // direction, and then perform the movement along that axis
             // virtualDelta will contain the amount we actually did move
             double virtualDelta = isVertical() ? yDelta : xDelta;
-            double actual = adjustPixels(virtualDelta);
+            double actual = scrollPixels(virtualDelta);
             if (actual != 0) {
                 // update last* here, as we know we've just adjusted the
                 // scrollbar. This means we don't get the situation where a
                 // user presses-and-drags a long way past the min or max
                 // values, only to change directions and see the scrollbar

@@ -872,34 +669,281 @@
                 }
                 if (lastCell.isFocusTraversable()) return lastCell;
                 return selectPreviousBeforeIndex(lastCell.getIndex(), context);
             }
         }));
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * There are two main complicating factors in the implementation of the
+     * VirtualFlow, which are made even more complicated due to the performance
+     * sensitive nature of this code. The first factor is the actual
+     * virtualization mechanism, wired together with the PositionMapper.
+     * The second complicating factor is the desire to do minimal layout
+     * and minimal updates to CSS.
+     *
+     * Since the layout mechanism runs at most once per pulse, we want to hook
+     * into this mechanism for minimal recomputation. Whenever a layout pass
+     * is run we record the width/height that the virtual flow was last laid
+     * out to. In subsequent passes, if the width/height has not changed then
+     * we know we only have to rebuild the cells. If the width or height has
+     * changed, then we can make appropriate decisions based on whether the
+     * width / height has been reduced or expanded.
+     *
+     * In various places, if requestLayout is called it is generally just
+     * used to indicate that some form of layout needs to happen (either the
+     * entire thing has to be reconstructed, or just the cells need to be
+     * reconstructed, generally).
+     *
+     * The accumCell is a special cell which is used in some computations
+     * when an actual cell for that item isn't currently available. However,
+     * the accumCell must be cleared whenever the cellFactory function is
+     * changed because we need to use the cells that come from the new factory.
+     *
+     * In addition to storing the lastWidth and lastHeight, we also store the
+     * number of cells that existed last time we performed a layout. In this
+     * way if the number of cells change, we can request a layout and when it
+     * occurs we can tell that the number of cells has changed and react
+     * accordingly.
+     *
+     * Because the VirtualFlow can be laid out horizontally or vertically a
+     * naming problem is present when trying to conceptualize and implement
+     * the flow. In particular, the words "width" and "height" are not
+     * precise when describing the unit of measure along the "virtualized"
+     * axis and the "orthogonal" axis. For example, the height of a cell when
+     * the flow is vertical is the magnitude along the "virtualized axis",
+     * and the width is along the axis orthogonal to it.
+     *
+     * Since "height" and "width" are not reliable terms, we use the words
+     * "length" and "breadth" to describe the magnitude of a cell along
+     * the virtualized axis and orthogonal axis. For example, in a vertical
+     * flow, the height=length and the width=breadth. In a horizontal axis,
+     * the height=breadth and the width=length.
+     *
+     * These terms are somewhat arbitrary, but chosen so that when reading
+     * most of the below code you can think in just one dimension, with
+     * helper functions converting width/height in to length/breadth, while
+     * also being different from width/height so as not to get confused with
+     * the actual width/height of a cell.
+     */
 
+    // --- vertical
+    /**
+     * Indicates the primary direction of virtualization. If true, then the
+     * primary direction of virtualization is vertical, meaning that cells will
+     * stack vertically on top of each other. If false, then they will stack
+     * horizontally next to each other.
+     */
+    private BooleanProperty vertical;
+    public final void setVertical(boolean value) {
+        verticalProperty().set(value);
     }
 
-    void updateHbar() {
-        // Bring the clipView.clipX back to 0 if control is vertical or
-        // the hbar isn't visible (fix for RT-11666)
-        if (! isVisible() || getScene() == null) return;
+    public final boolean isVertical() {
+        return vertical == null ? true : vertical.get();
+    }
 
-        if (isVertical()) {
-            if (hbar.isVisible()) {
-                clipView.setClipX(hbar.getValue());
-            } else {
-                // all cells are now less than the width of the flow,
-                // so we should shift the hbar/clip such that
-                // everything is visible in the viewport.
-                clipView.setClipX(0);
+    public final BooleanProperty verticalProperty() {
+        if (vertical == null) {
+            vertical = new BooleanPropertyBase(true) {
+                @Override protected void invalidated() {
+                    pile.clear();
+                    sheetChildren.clear();
+                    cells.clear();
+                    lastWidth = lastHeight = -1;
+                    setMaxPrefBreadth(-1);
+                    setViewportBreadth(0);
+                    setViewportLength(0);
+                    lastPosition = 0;
                 hbar.setValue(0);
+                    vbar.setValue(0);
+                    setPosition(0.0f);
+                    setNeedsLayout(true);
+                    requestLayout();
+                }
+
+                @Override
+                public Object getBean() {
+                    return VirtualFlow.this;
+                }
+
+                @Override
+                public String getName() {
+                    return "vertical";
+                }
+            };
+        }
+        return vertical;
+    }
+
+    // --- pannable
+    /**
+     * Indicates whether the VirtualFlow viewport is capable of being panned
+     * by the user (either via the mouse or touch events).
+     */
+    private BooleanProperty pannable = new SimpleBooleanProperty(this, "pannable", true);
+    public final boolean isPannable() { return pannable.get(); }
+    public final void setPannable(boolean value) { pannable.set(value); }
+    public final BooleanProperty pannableProperty() { return pannable; }
+
+    // --- cell count
+    /**
+     * Indicates the number of cells that should be in the flow. The user of
+     * the VirtualFlow must set this appropriately. When the cell count changes
+     * the VirtualFlow responds by updating the visuals. If the items backing
+     * the cells change, but the count has not changed, you must call the
+     * reconfigureCells() function to update the visuals.
+     */
+    private IntegerProperty cellCount = new SimpleIntegerProperty(this, "cellCount", 0) {
+        private int oldCount = 0;
+
+        @Override protected void invalidated() {
+            int cellCount = get();
+
+            boolean countChanged = oldCount != cellCount;
+            oldCount = cellCount;
+
+            // ensure that the virtual scrollbar adjusts in size based on the current
+            // cell count.
+            if (countChanged) {
+                VirtualScrollBar lengthBar = isVertical() ? vbar : hbar;
+                lengthBar.setMax(cellCount);
+            }
+
+            // I decided *not* to reset maxPrefBreadth here for the following
+            // situation. Suppose I have 30 cells and then I add 10 more. Just
+            // because I added 10 more doesn't mean the max pref should be
+            // reset. Suppose the first 3 cells were extra long, and I was
+            // scrolled down such that they weren't visible. If I were to reset
+            // maxPrefBreadth when subsequent cells were added or removed, then the
+            // scroll bars would erroneously reset as well. So I do not reset
+            // the maxPrefBreadth here.
+
+            // Fix for RT-12512, RT-14301 and RT-14864.
+            // Without this, the VirtualFlow length-wise scrollbar would not change
+            // as expected. This would leave items unable to be shown, as they
+            // would exist outside of the visible area, even when the scrollbar
+            // was at its maximum position.
+            // FIXME this should be only executed on the pulse, so this will likely
+            // lead to performance degradation until it is handled properly.
+            if (countChanged) {
+                layoutChildren();
+
+                // Fix for RT-13965: Without this line of code, the number of items in
+                // the sheet would constantly grow, leaking memory for the life of the
+                // application. This was especially apparent when the total number of
+                // cells changes - regardless of whether it became bigger or smaller.
+                sheetChildren.clear();
+
+                Parent parent = getParent();
+                if (parent != null) parent.requestLayout();
+            }
+            // TODO suppose I had 100 cells and I added 100 more. Further
+            // suppose I was scrolled to the bottom when that happened. I
+            // actually want to update the position of the mapper such that
+            // the view remains "stable".
+        }
+    };
+    public final int getCellCount() { return cellCount.get(); }
+    public final void setCellCount(int value) { cellCount.set(value);  }
+    public final IntegerProperty cellCountProperty() { return cellCount; }
+
+
+    // --- position
+    /**
+     * The position of the VirtualFlow within its list of cells. This is a value
+     * between 0 and 1.
+     */
+    private DoubleProperty position = new SimpleDoubleProperty(this, "position") {
+        @Override public void setValue(Number v) {
+            super.setValue(com.sun.javafx.util.Utils.clamp(0, get(), 1));
+        }
+
+        @Override protected void invalidated() {
+            super.invalidated();
+            requestLayout();
+        }
+    };
+    public final double getPosition() { return position.get(); }
+    public final void setPosition(double value) { position.set(value); }
+    public final DoubleProperty positionProperty() { return position; }
+
+    // --- fixed cell size
+    /**
+     * For optimisation purposes, some use cases can trade dynamic cell length
+     * for speed - if fixedCellSize is greater than zero we'll use that rather
+     * than determine it by querying the cell itself.
+     */
+    private DoubleProperty fixedCellSize = new SimpleDoubleProperty(this, "fixedCellSize") {
+        @Override protected void invalidated() {
+            fixedCellSizeEnabled = get() > 0;
+            needsCellsLayout = true;
+            layoutChildren();
+        }
+    };
+    public final void setFixedCellSize(final double value) { fixedCellSize.set(value); }
+    public final double getFixedCellSize() { return fixedCellSize.get(); }
+    public final DoubleProperty fixedCellSizeProperty() { return fixedCellSize; }
+
+
+    // --- Cell Factory
+    private ObjectProperty<Callback<VirtualFlow<T>, T>> cellFactory;
+
+    /**
+     * Sets a new cell factory to use in the VirtualFlow. This forces all old
+     * cells to be thrown away, and new cells to be created with
+     * the new cell factory.
+     */
+    public final void setCellFactory(Callback<VirtualFlow<T>, T> value) {
+        cellFactoryProperty().set(value);
+    }
+
+    /**
+     * Returns the current cell factory.
+     */
+    public final Callback<VirtualFlow<T>, T> getCellFactory() {
+        return cellFactory == null ? null : cellFactory.get();
+    }
+
+    /**
+     * <p>Setting a custom cell factory has the effect of deferring all cell
+     * creation, allowing for total customization of the cell. Internally, the
+     * VirtualFlow is responsible for reusing cells - all that is necessary
+     * is for the custom cell factory to return from this function a cell
+     * which might be usable for representing any item in the VirtualFlow.
+     *
+     * <p>Refer to the {@link Cell} class documentation for more detail.
+     */
+    public final ObjectProperty<Callback<VirtualFlow<T>, T>> cellFactoryProperty() {
+        if (cellFactory == null) {
+            cellFactory = new SimpleObjectProperty<Callback<VirtualFlow<T>, T>>(this, "cellFactory") {
+                @Override protected void invalidated() {
+                    if (get() != null) {
+                        accumCell = null;
+                        setNeedsLayout(true);
+                        recreateCells();
+                        if (getParent() != null) getParent().requestLayout();
+                    }
             }
+            };
         }
+        return cellFactory;
     }
 
+
+
     /***************************************************************************
      *                                                                         *
-     *                          Layout Functionality                           *
+     * Public API                                                              *
      *                                                                         *
      **************************************************************************/
 
     /**
      * Overridden to implement somewhat more efficient support for layout. The

@@ -915,10 +959,11 @@
         // appear to impact performance (indeed, it may help), and resolves the
         // issue identified in RT-21417.
         setNeedsLayout(true);
     }
     
+    /** {@inheritDoc} */
     @Override protected void layoutChildren() {
         if (needsRecreateCells) {
             lastWidth = -1;
             lastHeight = -1;
             releaseCell(accumCell);

@@ -1010,11 +1055,11 @@
         // 'jump' (in height normally) when the user drags the virtual thumb as
         // that is the first time the layout would occur otherwise.
         boolean cellNeedsLayout = false;
         boolean thumbNeedsLayout = false;
 
-        if (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
+        if (Properties.IS_TOUCH_SUPPORTED) {
             if ((tempVisibility == true && (hbar.isVisible() == false || vbar.isVisible() == false)) ||
                 (tempVisibility == false && (hbar.isVisible() == true || vbar.isVisible() == true))) {
                 thumbNeedsLayout = true;
             }
         }

@@ -1025,12 +1070,12 @@
                 cellNeedsLayout = cell.isNeedsLayout();
                 if (cellNeedsLayout) break;
             }
         }
 
-
-        T firstCell = getFirstVisibleCell();
+        final int cellCount = getCellCount();
+        final T firstCell = getFirstVisibleCell();
 
         // If no cells need layout, we check other criteria to see if this
         // layout call is even necessary. If it is found that no layout is
         // needed, we just punt.
         if (! cellNeedsLayout && !thumbNeedsLayout) {

@@ -1209,537 +1254,382 @@
         lastPosition = getPosition();
 
         cleanPile();
     }
 
-    /**
-     * Adds all the cells prior to and including the given currentIndex, until
-     * no more can be added without falling off the flow. The startOffset
-     * indicates the distance from the leading edge (top) of the viewport to
-     * the leading edge (top) of the currentIndex.
-     */
-    protected void addLeadingCells(int currentIndex, double startOffset) {
-        // The offset will keep track of the distance from the top of the
-        // viewport to the top of the current index. We will increment it
-        // as we lay out leading cells.
-        double offset = startOffset;
-        // The index is the absolute index of the cell being laid out
-        int index = currentIndex;
-
-        // Offset should really be the bottom of the current index
-        boolean first = true; // first time in, we just fudge the offset and let
-                              // it be the top of the current index then redefine
-                              // it as the bottom of the current index thereafter
-        // while we have not yet laid out so many cells that they would fall
-        // off the flow, we will continue to create and add cells. The
-        // offset is our indication of whether we can lay out additional
-        // cells. If the offset is ever < 0, except in the case of the very
-        // first cell, then we must quit.
-        T cell = null;
-
-        // special case for the position == 1.0, skip adding last invisible cell
-        if (index == cellCount && offset == getViewportLength()) {
-            index--;
-            first = false;
-        }
-        while (index >= 0 && (offset > 0 || first)) {
-            cell = getAvailableCell(index);
-            setCellIndex(cell, index);
-            resizeCellSize(cell); // resize must be after config
-            cells.addFirst(cell);
-
-            // A little gross but better than alternatives because it reduces
-            // the number of times we have to update a cell or compute its
-            // size. The first time into this loop "offset" is actually the
-            // top of the current index. On all subsequent visits, it is the
-            // bottom of the current index.
-            if (first) {
-                first = false;
-            } else {
-                offset -= getCellLength(cell);
-            }
-
-            // Position the cell, and update the maxPrefBreadth variable as we go.
-            positionCell(cell, offset);
-            setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
-            cell.setVisible(true);
-            --index;
-        }
-
-        // There are times when after laying out the cells we discover that
-        // the top of the first cell which represents index 0 is below the top
-        // of the viewport. In these cases, we have to adjust the cells up
-        // and reset the mapper position. This might happen when items got
-        // removed at the top or when the viewport size increased.
-        if (cells.size() > 0) {
-            cell = cells.getFirst();
-            int firstIndex = getCellIndex(cell);
-            double firstCellPos = getCellPosition(cell);
-            if (firstIndex == 0 && firstCellPos > 0) {
-                setPosition(0.0f);
-                offset = 0;
-                for (int i = 0; i < cells.size(); i++) {
-                    cell = cells.get(i);
-                    positionCell(cell, offset);
-                    offset += getCellLength(cell);
-                }
-            }
-        } else {
-            // reset scrollbar to top, so if the flow sees cells again it starts at the top
-            vbar.setValue(0);
-            hbar.setValue(0);
-        }
-    }
-
-    /**
-     * Adds all the trailing cells that come <em>after</em> the last index in
-     * the cells ObservableList.
-     */
-    protected boolean addTrailingCells(boolean fillEmptyCells) {
-        // If cells is empty then addLeadingCells bailed for some reason and
-        // we're hosed, so just punt
-        if (cells.isEmpty()) return false;
-        
-        // While we have not yet laid out so many cells that they would fall
-        // off the flow, so we will continue to create and add cells. When the
-        // offset becomes greater than the width/height of the flow, then we
-        // know we cannot add any more cells.
-        T startCell = cells.getLast();
-        double offset = getCellPosition(startCell) + getCellLength(startCell);
-        int index = getCellIndex(startCell) + 1;
-        boolean filledWithNonEmpty = index <= cellCount;
-
-        final double viewportLength = getViewportLength();
-
-        // Fix for RT-37421, which was a regression caused by RT-36556
-        if (offset < 0 && !fillEmptyCells) {
-            return false;
-        }
-
-        //
-        // RT-36507: viewportLength - offset gives the maximum number of
-        // additional cells that should ever be able to fit in the viewport if
-        // every cell had a height of 1. If index ever exceeds this count,
-        // then offset is not incrementing fast enough, or at all, which means
-        // there is something wrong with the cell size calculation.
-        //
-        final double maxCellCount = viewportLength - offset;
-        while (offset < viewportLength) {
-            if (index >= cellCount) {
-                if (offset < viewportLength) filledWithNonEmpty = false;
-                if (! fillEmptyCells) return filledWithNonEmpty;
-                // RT-36507 - return if we've exceeded the maximum
-                if (index > maxCellCount) {
-                    final PlatformLogger logger = Logging.getControlsLogger();
-                    if (logger.isLoggable(PlatformLogger.Level.INFO)) {
-                        if (startCell != null) {
-                            logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
-                        } else {
-                            logger.info("index exceeds maxCellCount");
-                        }
-                    }
-                    return filledWithNonEmpty;
-                }
-            }
-            T cell = getAvailableCell(index);
-            setCellIndex(cell, index);
-            resizeCellSize(cell); // resize happens after config!
-            cells.addLast(cell);
-
-            // Position the cell and update the max pref
-            positionCell(cell, offset);
-            setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
-
-            offset += getCellLength(cell);
-            cell.setVisible(true);
-            ++index;
-        }
-
-        // Discover whether the first cell coincides with index #0. If after
-        // adding all the trailing cells we find that a) the first cell was
-        // not index #0 and b) there are trailing cells, then we have a
-        // problem. We need to shift all the cells down and add leading cells,
-        // one at a time, until either the very last non-empty cells is aligned
-        // with the bottom OR we have laid out cell index #0 at the first
-        // position.
-        T firstCell = cells.getFirst();
-        index = getCellIndex(firstCell);
-        T lastNonEmptyCell = getLastVisibleCell();
-        double start = getCellPosition(firstCell);
-        double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell);
-        if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells &&
-                lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) {
-
-            double prospectiveEnd = end;
-            double distance = viewportLength - end;
-            while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) {
-                index--;
-                T cell = getAvailableCell(index);
-                setCellIndex(cell, index);
-                resizeCellSize(cell); // resize must be after config
-                cells.addFirst(cell);
-                double cellLength = getCellLength(cell);
-                start -= cellLength;
-                prospectiveEnd += cellLength;
-                positionCell(cell, start);
-                setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
-                cell.setVisible(true);
-            }
-
-            // The amount by which to translate the cells down
-            firstCell = cells.getFirst();
-            start = getCellPosition(firstCell);
-            double delta = viewportLength - end;
-            if (getCellIndex(firstCell) == 0 && delta > (-start)) {
-                delta = (-start);
-            }
-            // Move things
-            for (int i = 0; i < cells.size(); i++) {
-                T cell = cells.get(i);
-                positionCell(cell, getCellPosition(cell) + delta);
-            }
-
-            // Check whether the first cell, subsequent to our adjustments, is
-            // now index #0 and aligned with the top. If so, change the position
-            // to be at 0 instead of 1.
-            start = getCellPosition(firstCell);
-            if (getCellIndex(firstCell) == 0 && start == 0) {
-                setPosition(0);
-            } else if (getPosition() != 1) {
-                setPosition(1);
-            }
-        }
-
-        return filledWithNonEmpty;
-    }
-
-    /**
-     * @return true if bar visibility changed
-     */
-    private boolean computeBarVisiblity() {
-        if (cells.isEmpty()) {
-            // In case no cells are set yet, we assume no bars are needed
-            needLengthBar = false;
-            needBreadthBar = false;
-            return true;
-        }
-
-        final boolean isVertical = isVertical();
-        boolean barVisibilityChanged = false;
-
-        VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
-        VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
-
-        final double viewportBreadth = getViewportBreadth();
-
-        final int cellsSize = cells.size();
-        for (int i = 0; i < 2; i++) {
-            final boolean lengthBarVisible = getPosition() > 0
-                    || cellCount > cellsSize
-                    || (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength())
-                    || (cellCount == cellsSize - 1 && barVisibilityChanged && needBreadthBar);
-
-            if (lengthBarVisible ^ needLengthBar) {
-                needLengthBar = lengthBarVisible;
-                barVisibilityChanged = true;
-            }
-
-            // second conditional removed for RT-36669.
-            final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth));
-            if (breadthBarVisible ^ needBreadthBar) {
-                needBreadthBar = breadthBarVisible;
-                barVisibilityChanged = true;
-            }
-        }
-
-        // Start by optimistically deciding whether the length bar and
-        // breadth bar are needed and adjust the viewport dimensions
-        // accordingly. If during layout we find that one or the other of the
-        // bars actually is needed, then we will perform a cleanup pass
-
-        if (!BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
-            updateViewportDimensions();
-            breadthBar.setVisible(needBreadthBar);
-            lengthBar.setVisible(needLengthBar);
-        } else {
-            breadthBar.setVisible(needBreadthBar && tempVisibility);
-            lengthBar.setVisible(needLengthBar && tempVisibility);
-        }
-
-        return barVisibilityChanged;
-    }
-
-    private void updateViewportDimensions() {
-        final boolean isVertical = isVertical();
-        final double breadthBarLength = snapSize(isVertical ? hbar.prefHeight(-1) : vbar.prefWidth(-1));
-        final double lengthBarBreadth = snapSize(isVertical ? vbar.prefWidth(-1) : hbar.prefHeight(-1));
-
-        setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
-        setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
-    }
-
-    private void initViewport() {
-        // Initialize the viewportLength and viewportBreadth to match the
-        // width/height of the flow
-        final boolean isVertical = isVertical();
-
-        updateViewportDimensions();
-
-        VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
-        VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
-
-        // If there has been a switch between the virtualized bar, then we
-        // will want to do some stuff TODO.
-        breadthBar.setVirtual(false);
-        lengthBar.setVirtual(true);
-    }
-
+    /** {@inheritDoc} */
     @Override protected void setWidth(double value) {
         if (value != lastWidth) {
             super.setWidth(value);
             sizeChanged = true;
             setNeedsLayout(true);
             requestLayout();
         }
     }
 
+    /** {@inheritDoc} */
     @Override protected void setHeight(double value) {
         if (value != lastHeight) {
             super.setHeight(value);
             sizeChanged = true;
             setNeedsLayout(true);
             requestLayout();
         }
     }
 
-    private void updateScrollBarsAndCells(boolean recreate) {
-        // Assign the hbar and vbar to the breadthBar and lengthBar so as
-        // to make some subsequent calculations easier.
-        final boolean isVertical = isVertical();
-        VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
-        VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
-
-        // We may have adjusted the viewport length and breadth after the
-        // layout due to scroll bars becoming visible. So we need to perform
-        // a follow up pass and resize and shift all the cells to fit the
-        // viewport. Note that the prospective viewport size is always >= the
-        // final viewport size, so we don't have to worry about adding
-        // cells during this cleanup phase.
-        fitCells();
-        
-        // Update cell positions.
-        // When rebuilding the cells, we add the cells and along the way compute
-        // the maxPrefBreadth. Based on the computed value, we may add
-        // the breadth scrollbar which changes viewport length, so we need
-        // to re-position the cells.
-        if (!cells.isEmpty()) {
-            final double currOffset = -computeViewportOffset(getPosition());
-            final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex();
-            final int size = cells.size();
-
-            // position leading cells
-            double offset = currOffset;
-
-            for (int i = currIndex - 1; i >= 0 && i < size; i--) {
-                final T cell = cells.get(i);
+    /**
+     * Get a cell which can be used in the layout. This function will reuse
+     * cells from the pile where possible, and will create new cells when
+     * necessary.
+     */
+    protected T getAvailableCell(int prefIndex) {
+        T cell = null;
 
-                offset -= getCellLength(cell);
+        // Fix for RT-12822. We try to retrieve the cell from the pile rather
+        // than just grab a random cell from the pile (or create another cell).
+        for (int i = 0, max = pile.size(); i < max; i++) {
+            T _cell = pile.get(i);
+            assert _cell != null;
 
-                positionCell(cell, offset);
+            if (getCellIndex(_cell) == prefIndex) {
+                cell = _cell;
+                pile.remove(i);
+                break;
+            }
+            cell = null;
             }
 
-            // position trailing cells
-            offset = currOffset;
-            for (int i = currIndex; i >= 0 && i < size; i++) {
-                final T cell = cells.get(i);
-                positionCell(cell, offset);
+        if (cell == null) {
+            if (pile.size() > 0) {
+                // we try to get a cell with an index that is the same even/odd
+                // as the prefIndex. This saves us from having to run so much
+                // css on the cell as it will not change from even to odd, or
+                // vice versa
+                final boolean prefIndexIsEven = (prefIndex & 1) == 0;
+                for (int i = 0, max = pile.size(); i < max; i++) {
+                    final T c = pile.get(i);
+                    final int cellIndex = getCellIndex(c);
 
-                offset += getCellLength(cell);
+                    if ((cellIndex & 1) == 0 && prefIndexIsEven) {
+                        cell = c;
+                        pile.remove(i);
+                        break;
+                    } else if ((cellIndex & 1) == 1 && ! prefIndexIsEven) {
+                        cell = c;
+                        pile.remove(i);
+                        break;
             }
         }
 
-        // Toggle visibility on the corner
-        corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible());
-
-        double sumCellLength = 0;
-        double flowLength = (isVertical ? getHeight() : getWidth()) -
-            (breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0);
-
-        final double viewportBreadth = getViewportBreadth();
-        final double viewportLength = getViewportLength();
-        
-        // Now position and update the scroll bars
-        if (breadthBar.isVisible()) {
-            /*
-            ** Positioning the ScrollBar
-            */
-            if (!BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
-                if (isVertical) {
-                    hbar.resizeRelocate(0, viewportLength,
-                        viewportBreadth, hbar.prefHeight(viewportBreadth));
-                } else {
-                    vbar.resizeRelocate(viewportLength, 0,
-                        vbar.prefWidth(viewportBreadth), viewportBreadth);
-                }
+                if (cell == null) {
+                    cell = pile.removeFirst();
             }
-            else {
-                if (isVertical) {
-                    hbar.resizeRelocate(0, (viewportLength-hbar.getHeight()),
-                        viewportBreadth, hbar.prefHeight(viewportBreadth));
                 } else {
-                    vbar.resizeRelocate((viewportLength-vbar.getWidth()), 0,
-                        vbar.prefWidth(viewportBreadth), viewportBreadth);
+                cell = getCellFactory().call(this);
+                cell.getProperties().put(NEW_CELL, null);
                 }
             }
 
-            if (getMaxPrefBreadth() != -1) {
-                double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth);
-                if (newMax != breadthBar.getMax()) {
-                    breadthBar.setMax(newMax);
+        if (cell.getParent() == null) {
+            sheetChildren.add(cell);
+        }
 
-                    double breadthBarValue = breadthBar.getValue();
-                    boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue;
-                    if (maxed || breadthBarValue > newMax) {
-                        breadthBar.setValue(newMax);
+        return cell;
                     }
 
-                    breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax);
+    /**
+     * This method will remove all cells from the VirtualFlow and remove them,
+     * adding them to the 'pile' (that is, a place from where cells can be used
+     * at a later date). This method is protected to allow subclasses to clean up
+     * appropriately.
+     */
+    protected void addAllToPile() {
+        for (int i = 0, max = cells.size(); i < max; i++) {
+            addToPile(cells.removeFirst());
                 }
             }
+
+    /**
+     * Gets a cell for the given index if the cell has been created and laid out.
+     * "Visible" is a bit of a misnomer, the cell might not be visible in the
+     * viewport (it may be clipped), but does distinguish between cells that
+     * have been created and are in use vs. those that are in the pile or
+     * not created.
+     */
+    public T getVisibleCell(int index) {
+        if (cells.isEmpty()) return null;
+
+        // check the last index
+        T lastCell = cells.getLast();
+        int lastIndex = getCellIndex(lastCell);
+        if (index == lastIndex) return lastCell;
+
+        // check the first index
+        T firstCell = cells.getFirst();
+        int firstIndex = getCellIndex(firstCell);
+        if (index == firstIndex) return firstCell;
+
+        // if index is > firstIndex and < lastIndex then we can get the index
+        if (index > firstIndex && index < lastIndex) {
+            T cell = cells.get(index - firstIndex);
+            if (getCellIndex(cell) == index) return cell;
         }
         
-        // determine how many cells there are on screen so that the scrollbar
-        // thumb can be appropriately sized
-        if (recreate && (lengthBar.isVisible() || BehaviorSkinBase.IS_TOUCH_SUPPORTED)) {
-            int numCellsVisibleOnScreen = 0;
-            for (int i = 0, max = cells.size(); i < max; i++) {
-                T cell = cells.get(i);
-                if (cell != null && !cell.isEmpty()) {
-                    sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth());
-                    if (sumCellLength > flowLength) {
-                        break;
+        // there is no visible cell for the specified index
+        return null;
                     }
 
-                    numCellsVisibleOnScreen++;
+    /**
+     * Locates and returns the last non-empty IndexedCell that is currently
+     * partially or completely visible. This function may return null if there
+     * are no cells, or if the viewport length is 0.
+     */
+    public T getLastVisibleCell() {
+        if (cells.isEmpty() || getViewportLength() <= 0) return null;
+
+        T cell;
+        for (int i = cells.size() - 1; i >= 0; i--) {
+            cell = cells.get(i);
+            if (! cell.isEmpty()) {
+                return cell;
                 }
             }
 
-            lengthBar.setMax(1);
-            if (numCellsVisibleOnScreen == 0 && cellCount == 1) {
-                    // special case to help resolve RT-17701 and the case where we have
-                // only a single row and it is bigger than the viewport
-                lengthBar.setVisibleAmount(flowLength / sumCellLength);
-            } else {
-                lengthBar.setVisibleAmount(numCellsVisibleOnScreen / (float) cellCount);
+        return null;
             }
+
+    /**
+     * Locates and returns the first non-empty IndexedCell that is partially or
+     * completely visible. This really only ever returns null if there are no
+     * cells or the viewport length is 0.
+     */
+    public T getFirstVisibleCell() {
+        if (cells.isEmpty() || getViewportLength() <= 0) return null;
+        T cell = cells.getFirst();
+        return cell.isEmpty() ? null : cell;
         }
 
-        if (lengthBar.isVisible()) {
-            // Fix for RT-11873. If this isn't here, we can have a situation where
-            // the scrollbar scrolls endlessly. This is possible when the cell
-            // count grows as the user hits the maximal position on the scrollbar
-            // (i.e. the list size dynamically grows as the user needs more).
-            //
-            // This code was commented out to resolve RT-14477 after testing
-            // whether RT-11873 can be recreated. It could not, and therefore
-            // for now this code will remained uncommented until it is deleted
-            // following further testing.
-//            if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) {
-//                lengthBar.setValue(0.99);
-//            }
+    /**
+     * Adjust the position of cells so that the specified cell
+     * will be positioned at the start of the viewport. The given cell must
+     * already be "live".
+     */
+    public void scrollToTop(T firstCell) {
+        if (firstCell != null) {
+            scrollPixels(getCellPosition(firstCell));
+        }
+    }
 
-            /*
-            ** Positioning the ScrollBar
+    /**
+     * Adjust the position of cells so that the specified cell
+     * will be positioned at the end of the viewport. The given cell must
+     * already be "live".
             */
-            if (!BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
-                if (isVertical) {
-                    vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength);
-                } else {
-                    hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1));
+    public void scrollToBottom(T lastCell) {
+        if (lastCell != null) {
+            scrollPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength());
                 }
             }
-            else {
-                if (isVertical) {
-                    vbar.resizeRelocate((viewportBreadth-vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength);
-                } else {
-                    hbar.resizeRelocate(0, (viewportBreadth-hbar.getHeight()), viewportLength, hbar.prefHeight(-1));
+
+    /**
+     * Adjusts the cells such that the selected cell will be fully visible in
+     * the viewport (but only just).
+     */
+    public void scrollTo(T cell) {
+        if (cell != null) {
+            final double start = getCellPosition(cell);
+            final double length = getCellLength(cell);
+            final double end = start + length;
+            final double viewportLength = getViewportLength();
+
+            if (start < 0) {
+                scrollPixels(start);
+            } else if (end > viewportLength) {
+                scrollPixels(end - viewportLength);
                 }
             }
         }
 
-        if (corner.isVisible()) {
-            if (!BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
-                corner.resize(vbar.getWidth(), hbar.getHeight());
-                corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight());
-            }
-            else {
-                corner.resize(vbar.getWidth(), hbar.getHeight());
-                corner.relocate(hbar.getLayoutX() + (hbar.getWidth()-vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight()-hbar.getHeight()));
-                hbar.resize(hbar.getWidth()-vbar.getWidth(), hbar.getHeight());
-                vbar.resize(vbar.getWidth(), vbar.getHeight()-hbar.getHeight());
+    /**
+     * Adjusts the cells such that the cell in the given index will be fully visible in
+     * the viewport.
+     */
+    public void scrollTo(int index) {
+        T cell = getVisibleCell(index);
+        if (cell != null) {
+            scrollTo(cell);
+        } else {
+            adjustPositionToIndex(index);
+            addAllToPile();
+            requestLayout();
             }
         }
 
-        clipView.resize(snapSize(isVertical ? viewportBreadth : viewportLength),
-                        snapSize(isVertical ? viewportLength : viewportBreadth));
+    /**
+     * Adjusts the cells such that the cell in the given index will be fully visible in
+     * the viewport, and positioned at the very top of the viewport.
+     */
+    public void scrollToTop(int index) {
+        boolean posSet = false;
 
-        // If the viewportLength becomes large enough that all cells fit
-        // within the viewport, then we want to update the value to match.
-        if (getPosition() != lengthBar.getValue()) {
-            lengthBar.setValue(getPosition());
+        if (index >= getCellCount() - 1) {
+            setPosition(1);
+            posSet = true;
+        } else if (index < 0) {
+            setPosition(0);
+            posSet = true;
+        }
+
+        if (! posSet) {
+            adjustPositionToIndex(index);
+            double offset = - computeOffsetForCell(index);
+            adjustByPixelAmount(offset);
         }
+
+        requestLayout();
     }
 
+//    //TODO We assume all the cell have the same length.  We will need to support
+//    // cells of different lengths.
+//    public void scrollToOffset(int offset) {
+//        scrollPixels(offset * getCellLength(0));
+//    }
+
     /**
-     * Adjusts the cells location and size if necessary. The breadths of all
-     * cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and
-     * the layout position will be updated if necessary based on index and
-     * offset.
+     * Given a delta value representing a number of pixels, this method attempts
+     * to move the VirtualFlow in the given direction (positive is down/right,
+     * negative is up/left) the given number of pixels. It returns the number of
+     * pixels actually moved.
      */
-    private void fitCells() {
-        double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
-        boolean isVertical = isVertical();
+    public double scrollPixels(final double delta) {
+        // Short cut this method for cases where nothing should be done
+        if (delta == 0) return 0;
 
-        // Note: Do not optimise this loop by pre-calculating the cells size and
-        // storing that into a int value - this can lead to RT-32828
+        final boolean isVertical = isVertical();
+        if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) ||
+                (! isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0;
+
+        double pos = getPosition();
+        if (pos == 0.0f && delta < 0) return 0;
+        if (pos == 1.0f && delta > 0) return 0;
+
+        adjustByPixelAmount(delta);
+        if (pos == getPosition()) {
+            // The pos hasn't changed, there's nothing to do. This is likely
+            // to occur when we hit either extremity
+            return 0;
+        }
+
+        // Now move stuff around. Translating by pixels fundamentally means
+        // moving the cells by the delta. However, after having
+        // done that, we need to go through the cells and see which cells,
+        // after adding in the translation factor, now fall off the viewport.
+        // Also, we need to add cells as appropriate to the end (or beginning,
+        // depending on the direction of travel).
+        //
+        // One simplifying assumption (that had better be true!) is that we
+        // will only make it this far in the function if the virtual scroll
+        // bar is visible. Otherwise, we never will pixel scroll. So as we go,
+        // if we find that the maxPrefBreadth exceeds the viewportBreadth,
+        // then we will be sure to show the breadthBar and update it
+        // accordingly.
+        if (cells.size() > 0) {
         for (int i = 0; i < cells.size(); i++) {
-            Cell<?> cell = cells.get(i);
-            if (isVertical) {
-                cell.resize(size, cell.prefHeight(size));
-            } else {
-                cell.resize(cell.prefWidth(size), size);
+                T cell = cells.get(i);
+                assert cell != null;
+                positionCell(cell, getCellPosition(cell) - delta);
+            }
+
+            // Fix for RT-32908
+            T firstCell = cells.getFirst();
+            double layoutY = firstCell == null ? 0 : getCellPosition(firstCell);
+            for (int i = 0; i < cells.size(); i++) {
+                T cell = cells.get(i);
+                assert cell != null;
+                double actualLayoutY = getCellPosition(cell);
+                if (actualLayoutY != layoutY) {
+                    // we need to shift the cell to layoutY
+                    positionCell(cell, layoutY);
             }
+
+                layoutY += getCellLength(cell);
         }
+            // end of fix for RT-32908
+            cull();
+            firstCell = cells.getFirst();
+
+            // Add any necessary leading cells
+            if (firstCell != null) {
+                int firstIndex = getCellIndex(firstCell);
+                double prevIndexSize = getCellLength(firstIndex - 1);
+                addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
+            } else {
+                int currentIndex = computeCurrentIndex();
+
+                // The distance from the top of the viewport to the top of the
+                // cell for the current index.
+                double offset = -computeViewportOffset(getPosition());
+
+                // Add all the leading and trailing cells (the call to add leading
+                // cells will add the current cell as well -- that is, the one that
+                // represents the current position on the mapper).
+                addLeadingCells(currentIndex, offset);
     }
 
-    private void cull() {
+            // Starting at the tail of the list, loop adding cells until
+            // all the space on the table is filled up. We want to make
+            // sure that we DO NOT add empty trailing cells (since we are
+            // in the full virtual case and so there are no trailing empty
+            // cells).
+            if (! addTrailingCells(false)) {
+                // Reached the end, but not enough cells to fill up to
+                // the end. So, remove the trailing empty space, and translate
+                // the cells down
+                final T lastCell = getLastVisibleCell();
+                final double lastCellSize = getCellLength(lastCell);
+                final double cellEnd = getCellPosition(lastCell) + lastCellSize;
         final double viewportLength = getViewportLength();
-        for (int i = cells.size() - 1; i >= 0; i--) {
+
+                if (cellEnd < viewportLength) {
+                    // Reposition the nodes
+                    double emptySize = viewportLength - cellEnd;
+                    for (int i = 0; i < cells.size(); i++) {
             T cell = cells.get(i);
-            double cellSize = getCellLength(cell);
-            double cellStart = getCellPosition(cell);
-            double cellEnd = cellStart + cellSize;
-            if (cellStart >= viewportLength || cellEnd < 0) {
-                addToPile(cells.remove(i));
+                        positionCell(cell, getCellPosition(cell) + emptySize);
+                    }
+                    setPosition(1.0f);
+                    // fill the leading empty space
+                    firstCell = cells.getFirst();
+                    int firstIndex = getCellIndex(firstCell);
+                    double prevIndexSize = getCellLength(firstIndex - 1);
+                    addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
             }
         }
     }
 
-    /***************************************************************************
-     *                                                                         *
-     *                Helper functions for working with cells                  *
-     *                                                                         *
-     **************************************************************************/
+        // Now throw away any cells that don't fit
+        cull();
 
-    /**
-     * Return the index for a given cell. This allows subclasses to customise
-     * how cell indices are retrieved.
-     */
-    protected int getCellIndex(T cell){
-        return cell.getIndex();
+        // Finally, update the scroll bars
+        updateScrollBarsAndCells(false);
+        lastPosition = getPosition();
+
+        // notify
+        return delta; // TODO fake
+    }
+
+    /** {@inheritDoc} */
+    @Override protected double computePrefWidth(double height) {
+        double w = isVertical() ? getPrefBreadth(height) : getPrefLength();
+        return w + vbar.prefWidth(-1);
     }
 
+    /** {@inheritDoc} */
+    @Override protected double computePrefHeight(double width) {
+        double h = isVertical() ? getPrefLength() : getPrefBreadth(width);
+        return h + hbar.prefHeight(-1);
+    }
 
     /**
      * Return a cell for the given index. This may be called for any cell,
      * including beyond the range defined by cellCount, in which case an
      * empty cell will be returned. The returned value should not be stored for

@@ -1770,13 +1660,13 @@
             return pile.get(0);
         }
 
         // We need to use the accumCell and return that
         if (accumCell == null) {
-            Callback<VirtualFlow,T> createCell = getCreateCell();
-            if (createCell != null) {
-                accumCell = createCell.call(this);
+            Callback<VirtualFlow<T>,T> cellFactory = getCellFactory();
+            if (cellFactory != null) {
+                accumCell = cellFactory.call(this);
                 accumCell.getProperties().put(NEW_CELL, null);
                 accumCellParent.getChildren().setAll(accumCell);
 
                 // Note the screen reader will attempt to find all
                 // the items inside the view to calculate the item count.

@@ -1798,674 +1688,906 @@
         resizeCellSize(accumCell);
         return accumCell;
     }
 
     /**
-     * After using the accum cell, it needs to be released!
+     * The VirtualFlow uses this method to set a cells index (rather than calling
+     * {@link IndexedCell#updateIndex(int)} directly), so it is a perfect place
+     * for subclasses to override if this if of interest.
+     *
+     * @param cell The cell whose index will be updated.
+     * @param index The new index for the cell.
      */
-    private void releaseCell(T cell) {
-        if (accumCell != null && cell == accumCell) {
-            accumCell.updateIndex(-1);
+    protected void setCellIndex(T cell, int index) {
+        assert cell != null;
+
+        cell.updateIndex(index);
+
+        // make sure the cell is sized correctly. This is important for both
+        // general layout of cells in a VirtualFlow, but also in cases such as
+        // RT-34333, where the sizes were being reported incorrectly to the
+        // ComboBox popup.
+        if ((cell.isNeedsLayout() && cell.getScene() != null) || cell.getProperties().containsKey(NEW_CELL)) {
+            cell.applyCss();
+            cell.getProperties().remove(NEW_CELL);
         }
     }
 
     /**
-     * This method is an experts-only method - if the requested index is not
-     * already an existing visible cell, it will create a cell for the
-     * given index and insert it into the sheet. From that point on it will be
-     * unmanaged, and is up to the caller of this method to manage it.
+     * Return the index for a given cell. This allows subclasses to customise
+     * how cell indices are retrieved.
      */
-    T getPrivateCell(int index)  {
-        T cell = null;
-
-        // If there are cells, then we will attempt to get an existing cell
-        if (! cells.isEmpty()) {
-            // First check the cells that have already been created and are
-            // in use. If this call returns a value, then we can use it
-            cell = getVisibleCell(index);
-            if (cell != null) {
-                // Force the underlying text inside the cell to be updated
-                // so that when the screen reader runs, it will match the
-                // text in the cell (force updateDisplayedText())
-                cell.layout();
-                return cell;
-            }
+    protected int getCellIndex(T cell){
+        return cell.getIndex();
         }
 
-        // check the existing sheet children
-        if (cell == null) {
-            for (int i = 0; i < sheetChildren.size(); i++) {
-                T _cell = (T) sheetChildren.get(i);
-                if (getCellIndex(_cell) == index) {
-                    return _cell;
-                }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Private implementation                                                  *
+     *                                                                         *
+     **************************************************************************/
+
+    final VirtualScrollBar getHbar() {
+        return hbar;
             }
+    final VirtualScrollBar getVbar() {
+        return vbar;
         }
 
-        if (cell == null) {
-            Callback<VirtualFlow, T> createCell = getCreateCell();
-            if (createCell != null) {
-                cell = createCell.call(this);
+    /**
+     * The maximum preferred size in the non-virtual direction. For example,
+     * if vertical, then this is the max pref width of all cells encountered.
+     * <p>
+     * In general, this is the largest preferred size in the non-virtual
+     * direction that we have ever encountered. We don't reduce this size
+     * unless instructed to do so, so as to reduce the amount of scroll bar
+     * jitter. The access on this variable is package ONLY FOR TESTING.
+     */
+    private double maxPrefBreadth;
+    private final void setMaxPrefBreadth(double value) {
+        this.maxPrefBreadth = value;
             }
+    final double getMaxPrefBreadth() {
+        return maxPrefBreadth;
         }
 
-        if (cell != null) {
-            setCellIndex(cell, index);
-            resizeCellSize(cell);
-            cell.setVisible(false);
-            sheetChildren.add(cell);
-            privateCells.add(cell);
+    /**
+     * The breadth of the viewport portion of the VirtualFlow as computed during
+     * the layout pass. In a vertical flow this would be the same as the clip
+     * view width. In a horizontal flow this is the clip view height.
+     * The access on this variable is package ONLY FOR TESTING.
+     */
+    private double viewportBreadth;
+    private final void setViewportBreadth(double value) {
+        this.viewportBreadth = value;
         }
-
-        return cell;
+    private final double getViewportBreadth() {
+        return viewportBreadth;
     }
 
-    private final List<T> privateCells = new ArrayList<>();
-
-    private void releaseAllPrivateCells() {
-        sheetChildren.removeAll(privateCells);
+    /**
+     * The length of the viewport portion of the VirtualFlow as computed
+     * during the layout pass. In a vertical flow this would be the same as the
+     * clip view height. In a horizontal flow this is the clip view width.
+     * The access on this variable is package ONLY FOR TESTING.
+     */
+    private double viewportLength;
+    void setViewportLength(double value) {
+        this.viewportLength = value;
+    }
+    double getViewportLength() {
+        return viewportLength;
     }
 
     /**
      * Compute and return the length of the cell for the given index. This is
      * called both internally when adjusting by pixels, and also at times
      * by PositionMapper (see the getItemSize callback). When called by
      * PositionMapper, it is possible that it will be called for some index
      * which is not associated with any cell, so we have to do a bit of work
      * to use a cell as a helper for computing cell size in some cases.
      */
-    protected double getCellLength(int index) {
-        if (fixedCellSizeEnabled) return fixedCellSize;
+    double getCellLength(int index) {
+        if (fixedCellSizeEnabled) return getFixedCellSize();
         
         T cell = getCell(index);
         double length = getCellLength(cell);
         releaseCell(cell);
         return length;
     }
 
     /**
      */
-    protected double getCellBreadth(int index) {
+    double getCellBreadth(int index) {
         T cell = getCell(index);
         double b = getCellBreadth(cell);
         releaseCell(cell);
         return b;
     }
 
     /**
      * Gets the length of a specific cell
      */
-    protected double getCellLength(T cell) {
+    double getCellLength(T cell) {
         if (cell == null) return 0;
-        if (fixedCellSizeEnabled) return fixedCellSize;
+        if (fixedCellSizeEnabled) return getFixedCellSize();
 
         return isVertical() ?
             cell.getLayoutBounds().getHeight()
             : cell.getLayoutBounds().getWidth();
     }
 
-//    private double getCellPrefLength(T cell) {
-//        return isVertical() ?
-//            cell.prefHeight(-1)
-//            : cell.prefWidth(-1);
-//    }
-
     /**
      * Gets the breadth of a specific cell
      */
-    protected double getCellBreadth(Cell cell) {
+    double getCellBreadth(Cell cell) {
         return isVertical() ?
             cell.prefWidth(-1)
             : cell.prefHeight(-1);
     }
 
     /**
      * Gets the layout position of the cell along the length axis
      */
-    protected double getCellPosition(T cell) {
+    double getCellPosition(T cell) {
         if (cell == null) return 0;
 
         return isVertical() ?
             cell.getLayoutY()
             : cell.getLayoutX();
     }
 
-    protected void positionCell(T cell, double position) {
+    private void positionCell(T cell, double position) {
         if (isVertical()) {
             cell.setLayoutX(0);
             cell.setLayoutY(snapSize(position));
         } else {
             cell.setLayoutX(snapSize(position));
             cell.setLayoutY(0);
         }
     }
 
-    protected void resizeCellSize(T cell) {
+    private void resizeCellSize(T cell) {
         if (cell == null) return;
         
-        if (isVertical()) {
-            double width = Math.max(getMaxPrefBreadth(), getViewportBreadth());
-            cell.resize(width, fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width)));
-        } else {
-            double height = Math.max(getMaxPrefBreadth(), getViewportBreadth());
-            cell.resize(fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height);
+        if (isVertical()) {
+            double width = Math.max(getMaxPrefBreadth(), getViewportBreadth());
+            cell.resize(width, fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width)));
+        } else {
+            double height = Math.max(getMaxPrefBreadth(), getViewportBreadth());
+            cell.resize(fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height);
+        }
+    }
+
+    private List<T> getCells() {
+        return cells;
+    }
+
+    // Returns last visible cell whose bounds are entirely within the viewport
+    T getLastVisibleCellWithinViewPort() {
+        if (cells.isEmpty() || getViewportLength() <= 0) return null;
+
+        T cell;
+        final double max = getViewportLength();
+        for (int i = cells.size() - 1; i >= 0; i--) {
+            cell = cells.get(i);
+            if (cell.isEmpty()) continue;
+
+            final double cellStart = getCellPosition(cell);
+            final double cellEnd = cellStart + getCellLength(cell);
+
+            // we use the magic +2 to allow for a little bit of fuzziness,
+            // this is to help in situations such as RT-34407
+            if (cellEnd <= (max + 2)) {
+                return cell;
+            }
+        }
+
+        return null;
+    }
+
+    // Returns first visible cell whose bounds are entirely within the viewport
+    T getFirstVisibleCellWithinViewPort() {
+        if (cells.isEmpty() || getViewportLength() <= 0) return null;
+
+        T cell;
+        for (int i = 0; i < cells.size(); i++) {
+            cell = cells.get(i);
+            if (cell.isEmpty()) continue;
+
+            final double cellStart = getCellPosition(cell);
+            if (cellStart >= 0) {
+                return cell;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Adds all the cells prior to and including the given currentIndex, until
+     * no more can be added without falling off the flow. The startOffset
+     * indicates the distance from the leading edge (top) of the viewport to
+     * the leading edge (top) of the currentIndex.
+     */
+    void addLeadingCells(int currentIndex, double startOffset) {
+        // The offset will keep track of the distance from the top of the
+        // viewport to the top of the current index. We will increment it
+        // as we lay out leading cells.
+        double offset = startOffset;
+        // The index is the absolute index of the cell being laid out
+        int index = currentIndex;
+
+        // Offset should really be the bottom of the current index
+        boolean first = true; // first time in, we just fudge the offset and let
+        // it be the top of the current index then redefine
+        // it as the bottom of the current index thereafter
+        // while we have not yet laid out so many cells that they would fall
+        // off the flow, we will continue to create and add cells. The
+        // offset is our indication of whether we can lay out additional
+        // cells. If the offset is ever < 0, except in the case of the very
+        // first cell, then we must quit.
+        T cell = null;
+
+        // special case for the position == 1.0, skip adding last invisible cell
+        if (index == getCellCount() && offset == getViewportLength()) {
+            index--;
+            first = false;
+        }
+        while (index >= 0 && (offset > 0 || first)) {
+            cell = getAvailableCell(index);
+            setCellIndex(cell, index);
+            resizeCellSize(cell); // resize must be after config
+            cells.addFirst(cell);
+
+            // A little gross but better than alternatives because it reduces
+            // the number of times we have to update a cell or compute its
+            // size. The first time into this loop "offset" is actually the
+            // top of the current index. On all subsequent visits, it is the
+            // bottom of the current index.
+            if (first) {
+                first = false;
+            } else {
+                offset -= getCellLength(cell);
+            }
+
+            // Position the cell, and update the maxPrefBreadth variable as we go.
+            positionCell(cell, offset);
+            setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
+            cell.setVisible(true);
+            --index;
+        }
+
+        // There are times when after laying out the cells we discover that
+        // the top of the first cell which represents index 0 is below the top
+        // of the viewport. In these cases, we have to adjust the cells up
+        // and reset the mapper position. This might happen when items got
+        // removed at the top or when the viewport size increased.
+        if (cells.size() > 0) {
+            cell = cells.getFirst();
+            int firstIndex = getCellIndex(cell);
+            double firstCellPos = getCellPosition(cell);
+            if (firstIndex == 0 && firstCellPos > 0) {
+                setPosition(0.0f);
+                offset = 0;
+                for (int i = 0; i < cells.size(); i++) {
+                    cell = cells.get(i);
+                    positionCell(cell, offset);
+                    offset += getCellLength(cell);
+                }
+            }
+        } else {
+            // reset scrollbar to top, so if the flow sees cells again it starts at the top
+            vbar.setValue(0);
+            hbar.setValue(0);
+        }
+    }
+
+    /**
+     * Adds all the trailing cells that come <em>after</em> the last index in
+     * the cells ObservableList.
+     */
+    boolean addTrailingCells(boolean fillEmptyCells) {
+        // If cells is empty then addLeadingCells bailed for some reason and
+        // we're hosed, so just punt
+        if (cells.isEmpty()) return false;
+
+        // While we have not yet laid out so many cells that they would fall
+        // off the flow, so we will continue to create and add cells. When the
+        // offset becomes greater than the width/height of the flow, then we
+        // know we cannot add any more cells.
+        T startCell = cells.getLast();
+        double offset = getCellPosition(startCell) + getCellLength(startCell);
+        int index = getCellIndex(startCell) + 1;
+        final int cellCount = getCellCount();
+        boolean filledWithNonEmpty = index <= cellCount;
+
+        final double viewportLength = getViewportLength();
+
+        // Fix for RT-37421, which was a regression caused by RT-36556
+        if (offset < 0 && !fillEmptyCells) {
+            return false;
+        }
+
+        //
+        // RT-36507: viewportLength - offset gives the maximum number of
+        // additional cells that should ever be able to fit in the viewport if
+        // every cell had a height of 1. If index ever exceeds this count,
+        // then offset is not incrementing fast enough, or at all, which means
+        // there is something wrong with the cell size calculation.
+        //
+        final double maxCellCount = viewportLength - offset;
+        while (offset < viewportLength) {
+            if (index >= cellCount) {
+                if (offset < viewportLength) filledWithNonEmpty = false;
+                if (! fillEmptyCells) return filledWithNonEmpty;
+                // RT-36507 - return if we've exceeded the maximum
+                if (index > maxCellCount) {
+                    final PlatformLogger logger = Logging.getControlsLogger();
+                    if (logger.isLoggable(PlatformLogger.Level.INFO)) {
+                        if (startCell != null) {
+                            logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
+                        } else {
+                            logger.info("index exceeds maxCellCount");
+                        }
+                    }
+                    return filledWithNonEmpty;
+                }
+            }
+            T cell = getAvailableCell(index);
+            setCellIndex(cell, index);
+            resizeCellSize(cell); // resize happens after config!
+            cells.addLast(cell);
+
+            // Position the cell and update the max pref
+            positionCell(cell, offset);
+            setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
+
+            offset += getCellLength(cell);
+            cell.setVisible(true);
+            ++index;
+        }
+
+        // Discover whether the first cell coincides with index #0. If after
+        // adding all the trailing cells we find that a) the first cell was
+        // not index #0 and b) there are trailing cells, then we have a
+        // problem. We need to shift all the cells down and add leading cells,
+        // one at a time, until either the very last non-empty cells is aligned
+        // with the bottom OR we have laid out cell index #0 at the first
+        // position.
+        T firstCell = cells.getFirst();
+        index = getCellIndex(firstCell);
+        T lastNonEmptyCell = getLastVisibleCell();
+        double start = getCellPosition(firstCell);
+        double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell);
+        if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells &&
+                lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) {
+
+            double prospectiveEnd = end;
+            double distance = viewportLength - end;
+            while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) {
+                index--;
+                T cell = getAvailableCell(index);
+                setCellIndex(cell, index);
+                resizeCellSize(cell); // resize must be after config
+                cells.addFirst(cell);
+                double cellLength = getCellLength(cell);
+                start -= cellLength;
+                prospectiveEnd += cellLength;
+                positionCell(cell, start);
+                setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
+                cell.setVisible(true);
+            }
+
+            // The amount by which to translate the cells down
+            firstCell = cells.getFirst();
+            start = getCellPosition(firstCell);
+            double delta = viewportLength - end;
+            if (getCellIndex(firstCell) == 0 && delta > (-start)) {
+                delta = (-start);
+            }
+            // Move things
+            for (int i = 0; i < cells.size(); i++) {
+                T cell = cells.get(i);
+                positionCell(cell, getCellPosition(cell) + delta);
+            }
+
+            // Check whether the first cell, subsequent to our adjustments, is
+            // now index #0 and aligned with the top. If so, change the position
+            // to be at 0 instead of 1.
+            start = getCellPosition(firstCell);
+            if (getCellIndex(firstCell) == 0 && start == 0) {
+                setPosition(0);
+            } else if (getPosition() != 1) {
+                setPosition(1);
         }
     }
 
-    protected void setCellIndex(T cell, int index) {
-        assert cell != null;
-
-        cell.updateIndex(index);
+        return filledWithNonEmpty;
+    }
 
-        // make sure the cell is sized correctly. This is important for both
-        // general layout of cells in a VirtualFlow, but also in cases such as
-        // RT-34333, where the sizes were being reported incorrectly to the
-        // ComboBox popup.
-        if ((cell.isNeedsLayout() && cell.getScene() != null) || cell.getProperties().containsKey(NEW_CELL)) {
-            cell.applyCss();
-            cell.getProperties().remove(NEW_CELL);
+    void reconfigureCells() {
+        needsReconfigureCells = true;
+        requestLayout();
         }
+
+    void recreateCells() {
+        needsRecreateCells = true;
+        requestLayout();
     }
 
-    /***************************************************************************
-     *                                                                         *
-     *                 Helper functions for cell management                    *
-     *                                                                         *
-     **************************************************************************/
+    void rebuildCells() {
+        needsRebuildCells = true;
+        requestLayout();
+    }
 
+    void requestCellLayout() {
+        needsCellsLayout = true;
+        requestLayout();
+    }
 
-    /**
-     * Indicates that this is a newly created cell and we need call impl_processCSS for it.
-     *
-     * See RT-23616 for more details.
-     */
-    private static final String NEW_CELL = "newcell";
+    void setCellDirty(int index) {
+        dirtyCells.set(index);
+        requestLayout();
+    }
 
-    /**
-     * Get a cell which can be used in the layout. This function will reuse
-     * cells from the pile where possible, and will create new cells when
-     * necessary.
+    private void startSBReleasedAnimation() {
+        if (sbTouchTimeline == null) {
+            /*
+            ** timeline to leave the scrollbars visible for a short
+            ** while after a scroll/drag
      */
-    protected T getAvailableCell(int prefIndex) {
-        T cell = null;
-        
-        // Fix for RT-12822. We try to retrieve the cell from the pile rather
-        // than just grab a random cell from the pile (or create another cell).
-        for (int i = 0, max = pile.size(); i < max; i++) {
-            T _cell = pile.get(i);
-            assert _cell != null;
+            sbTouchTimeline = new Timeline();
+            sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
+                tempVisibility = true;
+                requestLayout();
+            });
             
-            if (getCellIndex(_cell) == prefIndex) {
-                cell = _cell;
-                pile.remove(i);
-                break;
-            }
-            cell = null;
+            sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
+                if (touchDetected == false && mouseDown == false) {
+                    tempVisibility = false;
+                    requestLayout();
         }
-
-        if (cell == null) {
-            if (pile.size() > 0) {
-                // we try to get a cell with an index that is the same even/odd
-                // as the prefIndex. This saves us from having to run so much
-                // css on the cell as it will not change from even to odd, or
-                // vice versa
-                final boolean prefIndexIsEven = (prefIndex & 1) == 0;
-                for (int i = 0, max = pile.size(); i < max; i++) {
-                    final T c = pile.get(i);
-                    final int cellIndex = getCellIndex(c);
-
-                    if ((cellIndex & 1) == 0 && prefIndexIsEven) {
-                        cell = c;
-                        pile.remove(i);
-                        break;
-                    } else if ((cellIndex & 1) == 1 && ! prefIndexIsEven) {
-                        cell = c;
-                        pile.remove(i);
-                        break;
+            });
+            sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
                     }
+        sbTouchTimeline.playFromStart();
                 }
 
-                if (cell == null) {
-                    cell = pile.removeFirst();
-                }
-            } else {
-                cell = getCreateCell().call(this);
-                cell.getProperties().put(NEW_CELL, null);
-            }
+    private void scrollBarOn() {
+        tempVisibility = true;
+        requestLayout();
         }
 
-        if (cell.getParent() == null) {
-            sheetChildren.add(cell);
-        }
+    void updateHbar() {
+        // Bring the clipView.clipX back to 0 if control is vertical or
+        // the hbar isn't visible (fix for RT-11666)
+        if (! isVisible() || getScene() == null) return;
         
-        return cell;
+        if (isVertical()) {
+            if (hbar.isVisible()) {
+                clipView.setClipX(hbar.getValue());
+            } else {
+                // all cells are now less than the width of the flow,
+                // so we should shift the hbar/clip such that
+                // everything is visible in the viewport.
+                clipView.setClipX(0);
+                hbar.setValue(0);
     }
-
-    // protected to allow subclasses to clean up
-    protected void addAllToPile() {
-        for (int i = 0, max = cells.size(); i < max; i++) {
-            addToPile(cells.removeFirst());
         }
     }
 
     /**
-     * Puts the given cell onto the pile. This is called whenever a cell has
-     * fallen off the flow's start.
+     * @return true if bar visibility changed
      */
-    private void addToPile(T cell) {
-        assert cell != null;
-        pile.addLast(cell);
+    private boolean computeBarVisiblity() {
+        if (cells.isEmpty()) {
+            // In case no cells are set yet, we assume no bars are needed
+            needLengthBar = false;
+            needBreadthBar = false;
+            return true;
     }
 
-    private void cleanPile() {
-        boolean wasFocusOwner = false;
+        final boolean isVertical = isVertical();
+        boolean barVisibilityChanged = false;
 
-        for (int i = 0, max = pile.size(); i < max; i++) {
-            T cell = pile.get(i);
-            wasFocusOwner = wasFocusOwner || doesCellContainFocus(cell);
-            cell.setVisible(false);
+        VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
+        VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
+
+        final double viewportBreadth = getViewportBreadth();
+
+        final int cellsSize = cells.size();
+        final int cellCount = getCellCount();
+        for (int i = 0; i < 2; i++) {
+            final boolean lengthBarVisible = getPosition() > 0
+                    || cellCount > cellsSize
+                    || (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength())
+                    || (cellCount == cellsSize - 1 && barVisibilityChanged && needBreadthBar);
+
+            if (lengthBarVisible ^ needLengthBar) {
+                needLengthBar = lengthBarVisible;
+                barVisibilityChanged = true;
         }
 
-        // Fix for RT-35876: Rather than have the cells do weird things with
-        // focus (in particular, have focus jump between cells), we return focus
-        // to the VirtualFlow itself.
-        if (wasFocusOwner) {
-            requestFocus();
+            // second conditional removed for RT-36669.
+            final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth));
+            if (breadthBarVisible ^ needBreadthBar) {
+                needBreadthBar = breadthBarVisible;
+                barVisibilityChanged = true;
         }
     }
 
-    private boolean doesCellContainFocus(Cell<?> c) {
-        Scene scene = c.getScene();
-        final Node focusOwner = scene == null ? null : scene.getFocusOwner();
+        // Start by optimistically deciding whether the length bar and
+        // breadth bar are needed and adjust the viewport dimensions
+        // accordingly. If during layout we find that one or the other of the
+        // bars actually is needed, then we will perform a cleanup pass
 
-        if (focusOwner != null) {
-            if (c.equals(focusOwner)) {
-                return true;
+        if (!Properties.IS_TOUCH_SUPPORTED) {
+            updateViewportDimensions();
+            breadthBar.setVisible(needBreadthBar);
+            lengthBar.setVisible(needLengthBar);
+        } else {
+            breadthBar.setVisible(needBreadthBar && tempVisibility);
+            lengthBar.setVisible(needLengthBar && tempVisibility);
             }
 
-            Parent p = focusOwner.getParent();
-            while (p != null && ! (p instanceof VirtualFlow)) {
-                if (c.equals(p)) {
-                    return true;
-                }
-                p = p.getParent();
+        return barVisibilityChanged;
             }
+
+    private void updateViewportDimensions() {
+        final boolean isVertical = isVertical();
+        final double breadthBarLength = snapSize(isVertical ? hbar.prefHeight(-1) : vbar.prefWidth(-1));
+        final double lengthBarBreadth = snapSize(isVertical ? vbar.prefWidth(-1) : hbar.prefHeight(-1));
+
+        setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
+        setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
         }
 
-        return false;
+    private void initViewport() {
+        // Initialize the viewportLength and viewportBreadth to match the
+        // width/height of the flow
+        final boolean isVertical = isVertical();
+
+        updateViewportDimensions();
+
+        VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
+        VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
+
+        // If there has been a switch between the virtualized bar, then we
+        // will want to do some stuff TODO.
+        breadthBar.setVirtual(false);
+        lengthBar.setVirtual(true);
     }
 
-    /**
-     * Gets a cell for the given index if the cell has been created and laid out.
-     * "Visible" is a bit of a misnomer, the cell might not be visible in the
-     * viewport (it may be clipped), but does distinguish between cells that
-     * have been created and are in use vs. those that are in the pile or
-     * not created.
-     */
-    public T getVisibleCell(int index) {
-        if (cells.isEmpty()) return null;
+    private void updateScrollBarsAndCells(boolean recreate) {
+        // Assign the hbar and vbar to the breadthBar and lengthBar so as
+        // to make some subsequent calculations easier.
+        final boolean isVertical = isVertical();
+        VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
+        VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
+
+        // We may have adjusted the viewport length and breadth after the
+        // layout due to scroll bars becoming visible. So we need to perform
+        // a follow up pass and resize and shift all the cells to fit the
+        // viewport. Note that the prospective viewport size is always >= the
+        // final viewport size, so we don't have to worry about adding
+        // cells during this cleanup phase.
+        fitCells();
+
+        // Update cell positions.
+        // When rebuilding the cells, we add the cells and along the way compute
+        // the maxPrefBreadth. Based on the computed value, we may add
+        // the breadth scrollbar which changes viewport length, so we need
+        // to re-position the cells.
+        if (!cells.isEmpty()) {
+            final double currOffset = -computeViewportOffset(getPosition());
+            final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex();
+            final int size = cells.size();
 
-        // check the last index
-        T lastCell = cells.getLast();
-        int lastIndex = getCellIndex(lastCell);
-        if (index == lastIndex) return lastCell;
+            // position leading cells
+            double offset = currOffset;
 
-        // check the first index
-        T firstCell = cells.getFirst();
-        int firstIndex = getCellIndex(firstCell);
-        if (index == firstIndex) return firstCell;
+            for (int i = currIndex - 1; i >= 0 && i < size; i--) {
+                final T cell = cells.get(i);
 
-        // if index is > firstIndex and < lastIndex then we can get the index
-        if (index > firstIndex && index < lastIndex) {
-            T cell = cells.get(index - firstIndex);
-            if (getCellIndex(cell) == index) return cell;
-        }
+                offset -= getCellLength(cell);
 
-        // there is no visible cell for the specified index
-        return null;
+                positionCell(cell, offset);
     }
 
-    /**
-     * Locates and returns the last non-empty IndexedCell that is currently
-     * partially or completely visible. This function may return null if there
-     * are no cells, or if the viewport length is 0.
-     */
-    public T getLastVisibleCell() {
-        if (cells.isEmpty() || getViewportLength() <= 0) return null;
-
-        T cell;
-        for (int i = cells.size() - 1; i >= 0; i--) {
-            cell = cells.get(i);
-            if (! cell.isEmpty()) {
-                return cell;
-            }
-        }
+            // position trailing cells
+            offset = currOffset;
+            for (int i = currIndex; i >= 0 && i < size; i++) {
+                final T cell = cells.get(i);
+                positionCell(cell, offset);
 
-        return null;
+                offset += getCellLength(cell);
     }
-
-    /**
-     * Locates and returns the first non-empty IndexedCell that is partially or
-     * completely visible. This really only ever returns null if there are no
-     * cells or the viewport length is 0.
-     */
-    public T getFirstVisibleCell() {
-        if (cells.isEmpty() || getViewportLength() <= 0) return null;
-        T cell = cells.getFirst();
-        return cell.isEmpty() ? null : cell;
     }
 
-    // Returns last visible cell whose bounds are entirely within the viewport
-    public T getLastVisibleCellWithinViewPort() {
-        if (cells.isEmpty() || getViewportLength() <= 0) return null;
+        // Toggle visibility on the corner
+        corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible());
 
-        T cell;
-        final double max = getViewportLength();
-        for (int i = cells.size() - 1; i >= 0; i--) {
-            cell = cells.get(i);
-            if (cell.isEmpty()) continue;
+        double sumCellLength = 0;
+        double flowLength = (isVertical ? getHeight() : getWidth()) -
+                (breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0);
 
-            final double cellStart = getCellPosition(cell);
-            final double cellEnd = cellStart + getCellLength(cell);
+        final double viewportBreadth = getViewportBreadth();
+        final double viewportLength = getViewportLength();
 
-            // we use the magic +2 to allow for a little bit of fuzziness,
-            // this is to help in situations such as RT-34407
-            if (cellEnd <= (max + 2)) {
-                return cell;
+        // Now position and update the scroll bars
+        if (breadthBar.isVisible()) {
+            /*
+            ** Positioning the ScrollBar
+            */
+            if (!Properties.IS_TOUCH_SUPPORTED) {
+                if (isVertical) {
+                    hbar.resizeRelocate(0, viewportLength,
+                            viewportBreadth, hbar.prefHeight(viewportBreadth));
+                } else {
+                    vbar.resizeRelocate(viewportLength, 0,
+                            vbar.prefWidth(viewportBreadth), viewportBreadth);
             }
         }
-
-        return null;
+            else {
+                if (isVertical) {
+                    hbar.resizeRelocate(0, (viewportLength-hbar.getHeight()),
+                            viewportBreadth, hbar.prefHeight(viewportBreadth));
+                } else {
+                    vbar.resizeRelocate((viewportLength-vbar.getWidth()), 0,
+                            vbar.prefWidth(viewportBreadth), viewportBreadth);
+                }
     }
 
-    // Returns first visible cell whose bounds are entirely within the viewport
-    public T getFirstVisibleCellWithinViewPort() {
-        if (cells.isEmpty() || getViewportLength() <= 0) return null;
+            if (getMaxPrefBreadth() != -1) {
+                double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth);
+                if (newMax != breadthBar.getMax()) {
+                    breadthBar.setMax(newMax);
 
-        T cell;
-        for (int i = 0; i < cells.size(); i++) {
-            cell = cells.get(i);
-            if (cell.isEmpty()) continue;
+                    double breadthBarValue = breadthBar.getValue();
+                    boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue;
+                    if (maxed || breadthBarValue > newMax) {
+                        breadthBar.setValue(newMax);
+                    }
 
-            final double cellStart = getCellPosition(cell);
-            if (cellStart >= 0) {
-                return cell;
+                    breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax);
+                }
             }
         }
 
-        return null;
+        // determine how many cells there are on screen so that the scrollbar
+        // thumb can be appropriately sized
+        if (recreate && (lengthBar.isVisible() || Properties.IS_TOUCH_SUPPORTED)) {
+            final int cellCount = getCellCount();
+            int numCellsVisibleOnScreen = 0;
+            for (int i = 0, max = cells.size(); i < max; i++) {
+                T cell = cells.get(i);
+                if (cell != null && !cell.isEmpty()) {
+                    sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth());
+                    if (sumCellLength > flowLength) {
+                        break;
     }
 
-    /**
-     * Adjust the position of cells so that the specified cell
-     * will be positioned at the start of the viewport. The given cell must
-     * already be "live". This is bad public API!
-     */
-    public void showAsFirst(T firstCell) {
-        if (firstCell != null) {
-            adjustPixels(getCellPosition(firstCell));
+                    numCellsVisibleOnScreen++;
         }
     }
 
-    /**
-     * Adjust the position of cells so that the specified cell
-     * will be positioned at the end of the viewport. The given cell must
-     * already be "live". This is bad public API!
-     */
-    public void showAsLast(T lastCell) {
-        if (lastCell != null) {
-            adjustPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength());
+            lengthBar.setMax(1);
+            if (numCellsVisibleOnScreen == 0 && cellCount == 1) {
+                // special case to help resolve RT-17701 and the case where we have
+                // only a single row and it is bigger than the viewport
+                lengthBar.setVisibleAmount(flowLength / sumCellLength);
+            } else {
+                lengthBar.setVisibleAmount(numCellsVisibleOnScreen / (float) cellCount);
         }
     }
 
-    /**
-     * Adjusts the cells such that the selected cell will be fully visible in
-     * the viewport (but only just).
-     */
-    public void show(T cell) {
-        if (cell != null) {
-            final double start = getCellPosition(cell);
-            final double length = getCellLength(cell);
-            final double end = start + length;
-            final double viewportLength = getViewportLength();
+        if (lengthBar.isVisible()) {
+            // Fix for RT-11873. If this isn't here, we can have a situation where
+            // the scrollbar scrolls endlessly. This is possible when the cell
+            // count grows as the user hits the maximal position on the scrollbar
+            // (i.e. the list size dynamically grows as the user needs more).
+            //
+            // This code was commented out to resolve RT-14477 after testing
+            // whether RT-11873 can be recreated. It could not, and therefore
+            // for now this code will remained uncommented until it is deleted
+            // following further testing.
+//            if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) {
+//                lengthBar.setValue(0.99);
+//            }
 
-            if (start < 0) {
-                adjustPixels(start);
-            } else if (end > viewportLength) {
-                adjustPixels(end - viewportLength);
+            /*
+            ** Positioning the ScrollBar
+            */
+            if (!Properties.IS_TOUCH_SUPPORTED) {
+                if (isVertical) {
+                    vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength);
+                } else {
+                    hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1));
+                }
+            }
+            else {
+                if (isVertical) {
+                    vbar.resizeRelocate((viewportBreadth-vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength);
+                } else {
+                    hbar.resizeRelocate(0, (viewportBreadth-hbar.getHeight()), viewportLength, hbar.prefHeight(-1));
             }
         }
     }
 
-    public void show(int index) {
-        T cell = getVisibleCell(index);
-        if (cell != null) {
-            show(cell);
-        } else {
-            // See if the previous index is a visible cell
-            T prev = getVisibleCell(index - 1);
-            if (prev != null) {
-                // Need to add a new cell and then we can show it
-//                layingOut = true;
-                cell = getAvailableCell(index);
-                setCellIndex(cell, index);
-                resizeCellSize(cell); // resize must be after config
-                cells.addLast(cell);
-                positionCell(cell, getCellPosition(prev) + getCellLength(prev));
-                setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
-                cell.setVisible(true);
-                show(cell);
-//                layingOut = false;
-                return;
+        if (corner.isVisible()) {
+            if (!Properties.IS_TOUCH_SUPPORTED) {
+                corner.resize(vbar.getWidth(), hbar.getHeight());
+                corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight());
+            }
+            else {
+                corner.resize(vbar.getWidth(), hbar.getHeight());
+                corner.relocate(hbar.getLayoutX() + (hbar.getWidth()-vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight()-hbar.getHeight()));
+                hbar.resize(hbar.getWidth()-vbar.getWidth(), hbar.getHeight());
+                vbar.resize(vbar.getWidth(), vbar.getHeight()-hbar.getHeight());
             }
-            // See if the next index is a visible cell
-            T next = getVisibleCell(index + 1);
-            if (next != null) {
-//                layingOut = true;
-                cell = getAvailableCell(index);
-                setCellIndex(cell, index);
-                resizeCellSize(cell); // resize must be after config
-                cells.addFirst(cell);
-                positionCell(cell, getCellPosition(next) - getCellLength(cell));
-                setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
-                cell.setVisible(true);
-                show(cell);
-//                layingOut = false;
-                return;
             }
 
-            // In this case, we're asked to show a random cell
-//            layingOut = true;
-            adjustPositionToIndex(index);
-            addAllToPile();
-            requestLayout();
-//            layingOut = false;            
+        clipView.resize(snapSize(isVertical ? viewportBreadth : viewportLength),
+                snapSize(isVertical ? viewportLength : viewportBreadth));
+
+        // If the viewportLength becomes large enough that all cells fit
+        // within the viewport, then we want to update the value to match.
+        if (getPosition() != lengthBar.getValue()) {
+            lengthBar.setValue(getPosition());
         }
     }
 
-    public void scrollTo(int index) {
-        boolean posSet = false;
+    /**
+     * Adjusts the cells location and size if necessary. The breadths of all
+     * cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and
+     * the layout position will be updated if necessary based on index and
+     * offset.
+     */
+    private void fitCells() {
+        double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
+        boolean isVertical = isVertical();
         
-        if (index >= cellCount - 1) {
-            setPosition(1);
-            posSet = true;
-        } else if (index < 0) {
-            setPosition(0);
-            posSet = true;
+        // Note: Do not optimise this loop by pre-calculating the cells size and
+        // storing that into a int value - this can lead to RT-32828
+        for (int i = 0; i < cells.size(); i++) {
+            Cell<?> cell = cells.get(i);
+            if (isVertical) {
+                cell.resize(size, cell.prefHeight(size));
+            } else {
+                cell.resize(cell.prefWidth(size), size);
+            }
+        }
         }
         
-        if (! posSet) {
-            adjustPositionToIndex(index);
-            double offset = - computeOffsetForCell(index);
-            adjustByPixelAmount(offset);
+    private void cull() {
+        final double viewportLength = getViewportLength();
+        for (int i = cells.size() - 1; i >= 0; i--) {
+            T cell = cells.get(i);
+            double cellSize = getCellLength(cell);
+            double cellStart = getCellPosition(cell);
+            double cellEnd = cellStart + cellSize;
+            if (cellStart >= viewportLength || cellEnd < 0) {
+                addToPile(cells.remove(i));
         }
-        
-        requestLayout();        
     }
-    
-    //TODO We assume all the cell have the same length.  We will need to support
-    // cells of different lengths.
-    public void scrollToOffset(int offset) {
-        adjustPixels(offset * getCellLength(0));
     }    
     
     /**
-     * Given a delta value representing a number of pixels, this method attempts
-     * to move the VirtualFlow in the given direction (positive is down/right,
-     * negative is up/left) the given number of pixels. It returns the number of
-     * pixels actually moved.
+     * After using the accum cell, it needs to be released!
      */
-    public double adjustPixels(final double delta) {
-        // Short cut this method for cases where nothing should be done
-        if (delta == 0) return 0;
-
-        final boolean isVertical = isVertical();
-        if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) ||
-                (! isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0;
+    private void releaseCell(T cell) {
+        if (accumCell != null && cell == accumCell) {
+            accumCell.updateIndex(-1);
+        }
+    }
         
-        double pos = getPosition();
-        if (pos == 0.0f && delta < 0) return 0;
-        if (pos == 1.0f && delta > 0) return 0;
+    /**
+     * This method is an experts-only method - if the requested index is not
+     * already an existing visible cell, it will create a cell for the
+     * given index and insert it into the sheet. From that point on it will be
+     * unmanaged, and is up to the caller of this method to manage it.
+     */
+    T getPrivateCell(int index)  {
+        T cell = null;
 
-        adjustByPixelAmount(delta);
-        if (pos == getPosition()) {
-            // The pos hasn't changed, there's nothing to do. This is likely
-            // to occur when we hit either extremity
-            return 0;
+        // If there are cells, then we will attempt to get an existing cell
+        if (! cells.isEmpty()) {
+            // First check the cells that have already been created and are
+            // in use. If this call returns a value, then we can use it
+            cell = getVisibleCell(index);
+            if (cell != null) {
+                // Force the underlying text inside the cell to be updated
+                // so that when the screen reader runs, it will match the
+                // text in the cell (force updateDisplayedText())
+                cell.layout();
+                return cell;
         }
-
-        // Now move stuff around. Translating by pixels fundamentally means
-        // moving the cells by the delta. However, after having
-        // done that, we need to go through the cells and see which cells,
-        // after adding in the translation factor, now fall off the viewport.
-        // Also, we need to add cells as appropriate to the end (or beginning,
-        // depending on the direction of travel).
-        //
-        // One simplifying assumption (that had better be true!) is that we
-        // will only make it this far in the function if the virtual scroll
-        // bar is visible. Otherwise, we never will pixel scroll. So as we go,
-        // if we find that the maxPrefBreadth exceeds the viewportBreadth,
-        // then we will be sure to show the breadthBar and update it
-        // accordingly.
-        if (cells.size() > 0) {
-            for (int i = 0; i < cells.size(); i++) {
-                T cell = cells.get(i);
-                assert cell != null;
-                positionCell(cell, getCellPosition(cell) - delta);
             }
 
-            // Fix for RT-32908
-            T firstCell = cells.getFirst();
-            double layoutY = firstCell == null ? 0 : getCellPosition(firstCell);
-            for (int i = 0; i < cells.size(); i++) {
-                T cell = cells.get(i);
-                assert cell != null;
-                double actualLayoutY = getCellPosition(cell);
-                if (actualLayoutY != layoutY) {
-                    // we need to shift the cell to layoutY
-                    positionCell(cell, layoutY);
+        // check the existing sheet children
+        if (cell == null) {
+            for (int i = 0; i < sheetChildren.size(); i++) {
+                T _cell = (T) sheetChildren.get(i);
+                if (getCellIndex(_cell) == index) {
+                    return _cell;
                 }
-
-                layoutY += getCellLength(cell);
             }
-            // end of fix for RT-32908
-            cull();
-            firstCell = cells.getFirst();
-
-            // Add any necessary leading cells
-            if (firstCell != null) {
-                int firstIndex = getCellIndex(firstCell);
-                double prevIndexSize = getCellLength(firstIndex - 1);
-                addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
-            } else {
-                int currentIndex = computeCurrentIndex();
-
-                // The distance from the top of the viewport to the top of the
-                // cell for the current index.
-                double offset = -computeViewportOffset(getPosition());
-
-                // Add all the leading and trailing cells (the call to add leading
-                // cells will add the current cell as well -- that is, the one that
-                // represents the current position on the mapper).
-                addLeadingCells(currentIndex, offset);
             }
 
-            // Starting at the tail of the list, loop adding cells until
-            // all the space on the table is filled up. We want to make
-            // sure that we DO NOT add empty trailing cells (since we are
-            // in the full virtual case and so there are no trailing empty
-            // cells).
-            if (! addTrailingCells(false)) {
-                // Reached the end, but not enough cells to fill up to
-                // the end. So, remove the trailing empty space, and translate
-                // the cells down
-                final T lastCell = getLastVisibleCell();
-                final double lastCellSize = getCellLength(lastCell);
-                final double cellEnd = getCellPosition(lastCell) + lastCellSize;
-                final double viewportLength = getViewportLength();
-
-                if (cellEnd < viewportLength) {
-                    // Reposition the nodes
-                    double emptySize = viewportLength - cellEnd;
-                    for (int i = 0; i < cells.size(); i++) {
-                        T cell = cells.get(i);
-                        positionCell(cell, getCellPosition(cell) + emptySize);
+        if (cell == null) {
+            Callback<VirtualFlow<T>, T> cellFactory = getCellFactory();
+            if (cellFactory != null) {
+                cell = cellFactory.call(this);
                     }
-                    setPosition(1.0f);
-                    // fill the leading empty space
-                    firstCell = cells.getFirst();
-                    int firstIndex = getCellIndex(firstCell);
-                    double prevIndexSize = getCellLength(firstIndex - 1);
-                    addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
                 }
+
+        if (cell != null) {
+            setCellIndex(cell, index);
+            resizeCellSize(cell);
+            cell.setVisible(false);
+            sheetChildren.add(cell);
+            privateCells.add(cell);
             }
+
+        return cell;
         }
 
-        // Now throw away any cells that don't fit
-        cull();
+    private final List<T> privateCells = new ArrayList<>();
 
-        // Finally, update the scroll bars
-        updateScrollBarsAndCells(false);
-        lastPosition = getPosition();
+    private void releaseAllPrivateCells() {
+        sheetChildren.removeAll(privateCells);
+    }
 
-        // notify
-        return delta; // TODO fake
+    /**
+     * Puts the given cell onto the pile. This is called whenever a cell has
+     * fallen off the flow's start.
+     */
+    private void addToPile(T cell) {
+        assert cell != null;
+        pile.addLast(cell);
     }
 
-    private boolean needsReconfigureCells = false; // when cell contents are the same
-    private boolean needsRecreateCells = false; // when cell factory changed
-    private boolean needsRebuildCells = false; // when cell contents have changed
-    private boolean needsCellsLayout = false;
-    private boolean sizeChanged = false;
-    private final BitSet dirtyCells = new BitSet();
+    private void cleanPile() {
+        boolean wasFocusOwner = false;
     
-    public void reconfigureCells() {
-        needsReconfigureCells = true;
-        requestLayout();
+        for (int i = 0, max = pile.size(); i < max; i++) {
+            T cell = pile.get(i);
+            wasFocusOwner = wasFocusOwner || doesCellContainFocus(cell);
+            cell.setVisible(false);
     }
 
-    public void recreateCells() {
-        needsRecreateCells = true;
-        requestLayout();
+        // Fix for RT-35876: Rather than have the cells do weird things with
+        // focus (in particular, have focus jump between cells), we return focus
+        // to the VirtualFlow itself.
+        if (wasFocusOwner) {
+            requestFocus();
     }
-    
-    public void rebuildCells() {
-        needsRebuildCells = true;
-        requestLayout();
     }
 
-    public void requestCellLayout() {
-        needsCellsLayout = true;
-        requestLayout();
+    private boolean doesCellContainFocus(Cell<?> c) {
+        Scene scene = c.getScene();
+        final Node focusOwner = scene == null ? null : scene.getFocusOwner();
+
+        if (focusOwner != null) {
+            if (c.equals(focusOwner)) {
+                return true;
     }
 
-    public void setCellDirty(int index) {
-        dirtyCells.set(index);
-        requestLayout();
+            Parent p = focusOwner.getParent();
+            while (p != null && ! (p instanceof VirtualFlow)) {
+                if (c.equals(p)) {
+                    return true;
+                }
+                p = p.getParent();
+            }
     }
 
-    private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987;
+        return false;
+    }
 
     private double getPrefBreadth(double oppDimension) {
         double max = getMaxCellWidth(10);
 
         // This primarily exists for the case where we do not want the breadth

@@ -2480,40 +2602,28 @@
         return max;
     }
 
     private double getPrefLength() {
         double sum = 0.0;
-        int rows = Math.min(10, cellCount);
+        int rows = Math.min(10, getCellCount());
         for (int i = 0; i < rows; i++) {
             sum += getCellLength(i);
         }
         return sum;
     }
 
-    @Override protected double computePrefWidth(double height) {
-        double w = isVertical() ? getPrefBreadth(height) : getPrefLength();
-        return w + vbar.prefWidth(-1);
-    }
-
-    @Override protected double computePrefHeight(double width) {
-        double h = isVertical() ? getPrefLength() : getPrefBreadth(width);
-        return h + hbar.prefHeight(-1);
-    }
-    
     double getMaxCellWidth(int rowsToCount) {
         double max = 0.0;
         
         // we always measure at least one row
-        int rows = Math.max(1, rowsToCount == -1 ? cellCount : rowsToCount); 
+        int rows = Math.max(1, rowsToCount == -1 ? getCellCount() : rowsToCount);
         for (int i = 0; i < rows; i++) {
             max = Math.max(max, getCellBreadth(i));
         }
         return max;
     }
     
-    
-    
     // Old PositionMapper
     /**
      * Given a position value between 0 and 1, compute and return the viewport
      * offset from the "current" cell associated with that position value.
      * That is, if the return value of this function where used as a translation

@@ -2644,10 +2754,18 @@
 //        adjustByPixelAmount(numPixels);
 //    }
     // end of old PositionMapper code
     
     
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Support classes                                                         *
+     *                                                                         *
+     **************************************************************************/
+
     /**
      * A simple extension to Region that ensures that anything wanting to flow
      * outside of the bounds of the Region is clipped.
      */
     static class ClippedContainer extends Region {

@@ -2713,11 +2831,11 @@
      * we are inserting at the head or tail). It maintains an index to the start
      * and end of the array, so that it can efficiently expose iteration.
      * <p>
      * This class is package private solely for the sake of testing.
      */
-    public static class ArrayLinkedList<T> extends AbstractList<T> {
+    static class ArrayLinkedList<T> extends AbstractList<T> {
         /**
          * The array list backing this class. We default the size of the array
          * list to be fairly large so as not to require resizing during normal
          * use, and since that many ArrayLinkedLists won't be created it isn't
          * very painful to do so.

@@ -2812,11 +2930,11 @@
             return remove(lastIndex - firstIndex);
         }
 
         public T remove(int index) {
             if (index > lastIndex - firstIndex || index < 0) {
-                throw new java.lang.ArrayIndexOutOfBoundsException();
+                throw new ArrayIndexOutOfBoundsException();
             }
 
             // if the index == 0, then we're removing the first
             // item and can simply set it to null in the array and increment
             // the firstIndex unless there is only one item, in which case

@@ -2848,42 +2966,6 @@
                 array.set(lastIndex--, null);
                 return cell;
             }
         }
     }
-
-    Timeline sbTouchTimeline;
-    KeyFrame sbTouchKF1;
-    KeyFrame sbTouchKF2;
-
-    private boolean needBreadthBar;
-    private boolean needLengthBar;
-    private boolean tempVisibility = false;
-
-    protected void startSBReleasedAnimation() {
-        if (sbTouchTimeline == null) {
-            /*
-            ** timeline to leave the scrollbars visible for a short
-            ** while after a scroll/drag
-            */
-            sbTouchTimeline = new Timeline();
-            sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
-                tempVisibility = true;
-                requestLayout();
-            });
-
-            sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
-                if (touchDetected == false && mouseDown == false) {
-                    tempVisibility = false;
-                    requestLayout();
-                }
-            });
-            sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
-        }
-        sbTouchTimeline.playFromStart();
-    }
-
-    protected void scrollBarOn() {
-        tempVisibility = true;
-        requestLayout();
-    }
 }