Desktop Java

JavaFX Tip 15: ListView Autoscrolling

I recently had to implement autoscrolling functionality for FlexGanttFX and thought that my solution might be useful for others. You find the basic concepts of it in the listing below. The main idea is that a background thread is used to adjust the pixel location of the virtual flow node used by the list view. The thread starts when a drag over is detected “close” to the top or bottom edges. “Close” is defined by a proximity variable.

This code can obviously be improved by using a property for the proximity value and the types “Task” and “Service” for the threading work.
 
 

package com.dlsc;

import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.ListView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;

/*
 * Yes, unfortunately we need to use private API for this.
 */
import com.sun.javafx.scene.control.skin.VirtualFlow;

public class AutoscrollListView<T> extends ListView<T> {

	final double proximity = 20;

	public AutoscrollListView() {
		addEventFilter(MouseEvent.DRAG_DETECTED,
                      evt -> startDrag());
		addEventFilter(DragEvent.DRAG_OVER,
                      evt -> autoscrollIfNeeded(evt));
		addEventFilter(DragEvent.DRAG_EXITED,
                      evt -> stopAutoScrollIfNeeded(evt));
		addEventFilter(DragEvent.DRAG_DROPPED,
                      evt -> stopAutoScrollIfNeeded(evt));
		addEventFilter(DragEvent.DRAG_DONE,
                      evt -> stopAutoScrollIfNeeded(evt));
	}

	private void startDrag() {
		Dragboard db = startDragAndDrop(TransferMode.MOVE);
		ClipboardContent content = new ClipboardContent();

		/*
		 * We have to add some content, otherwise drag over
                 * will not be called.
		 */
		content.putString("dummy");
		db.setContent(content);
	}

	private void autoscrollIfNeeded(DragEvent evt) {
		evt.acceptTransferModes(TransferMode.ANY);

		/*
		 * Determine the "hot" region that will trigger automatic scrolling.
		 * Ideally we use the clipped container of the list view skin but when
		 * the rows are empty the dimensions of the clipped container will be
		 * 0x0. In this case we try to use the virtual flow.
		 */
		Region hotRegion = getClippedContainer();
		if (hotRegion.getBoundsInLocal().getWidth() < 1) {
			hotRegion = this;
			if (hotRegion.getBoundsInLocal().getWidth() < 1) {
				stopAutoScrollIfNeeded(evt);
				return;
			}
		}

		double yOffset = 0;

		// y offset

		double delta = evt.getSceneY() -
                                  hotRegion.localToScene(0, 0).getY();
		if (delta < proximity) {
			yOffset = -(proximity - delta);
		}

		delta = hotRegion.localToScene(0, 0).getY() +
                                  hotRegion.getHeight() -
				  evt.getSceneY();
		if (delta < proximity) {
			yOffset = proximity - delta;
		}

		if (yOffset != 0) {
			autoscroll(yOffset);
		} else {
			stopAutoScrollIfNeeded(evt);
		}
	}

	private VirtualFlow<?> getVirtualFlow() {
		return (VirtualFlow<?>) lookup("VirtualFlow");
	}

	private Region getClippedContainer() {

		/*
		 * Safest way to find the clipped container. lookup() does not work at
		 * all.
		 */
		for (Node child :
                             getVirtualFlow().getChildrenUnmodifiable()) {
			if (child.getStyleClass().
                                       contains("clipped-container")) {
				return (Region) child;
			}
		}

		return null;
	}

	class ScrollThread extends Thread {
		private boolean running = true;
		private double yOffset;

		public ScrollThread() {
			super("Autoscrolling List View");
			setDaemon(true);
		}

		@Override
		public void run() {

			/*
			 * Some initial delay, especially useful when
                         * dragging something in from the outside.
			 */

			try {
				Thread.sleep(300);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}

			while (running) {

				Platform.runLater(() -> {
					scrollY();
				});

				try {
					sleep(15);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

		private void scrollY() {
			VirtualFlow<?> flow = getVirtualFlow();
			flow.adjustPixels(yOffset);
		}

		public void stopRunning() {
			this.running = false;
		}

		public void setDelta(double yOffset) {
			this.yOffset = yOffset;
		}
	}

	private ScrollThread scrollThread;

	private void autoscroll(double yOffset) {
		if (scrollThread == null) {
			scrollThread = new ScrollThread();
			scrollThread.start();
		}

		scrollThread.setDelta(yOffset);
	}

	private void stopAutoScrollIfNeeded(DragEvent evt) {
		if (scrollThread != null) {
			scrollThread.stopRunning();
			scrollThread = null;
		}
	}
}
Reference: JavaFX Tip 15: ListView Autoscrolling from our JCG partner Dirk Lemmermann at the Pixel Perfect blog.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button