Java and Swing: an iPhone-like JScrollpane Component

Articles —> Java and Swing: an iPhone-like JScrollpane Component

Many mobile devices, including the iPhone and iPad, have the capability of scrolling content too big for its container simply with the swipe of a finger. Of course, this is handy for situations such as mobile devices in which the content to be displayed is much larger than the window, however this scenario is much less frequently encountered in desktop applications. Be that as it may, while entertaining the idea of a browser to view a genome for my application GeneCoder, I thought this might be a useful general behavior to implement.

The implementation was done in Java using a mouse and mouse motion listener, both of which control a Component within a JScrollPane. From a user's perspective:

  • Dragging the component results in movement of the Component within the JScrollPane.
  • Swipes - mouse drags followed by release of the mouse button - at a fast enough speed cause the component to scroll automatically in the direction of the swipe.
  • Scrolling as the result of a swipe continues but decreases over time.
  • The scrolling as a result of a swipe can be terminated by one of several conditions: a user initiation mouse press, the scroll bar(s) reach the extreme maximum or minimum, or the speed of scrolling has decreased below a threshold.

The DragScrollListener class is shown below, containing the code for implementing the mouse drag and scroll behavior. The class implements the MouseListener and MouseMotionListener interfaces to listen for the appropriate mouse behavior. Swipe behavior is implemented via a Swing Timer - used to 'animate' the scrolling. The class is self contained: for demonstration purposes the class can be run as a standalone application via its main method. Doing so constructs a JFrame containing a draggable Component: a large JPanel which simply paints randomly color squares.


import java.awt.*;

import java.awt.event.*;

import java.awt.geom.Point2D;

import java.beans.PropertyChangeEvent;

import java.beans.PropertyChangeListener;

import java.util.ArrayList;



import javax.swing.*;







/**

 * Listener to allow for iPhone like drag scrolling of a Component within a JScrollPane. 

 * @author Greg Cope

 * 

 * This program is free software: you can redistribute it and/or modify

 * it under the terms of the GNU General Public License as published by

 * the Free Software Foundation, either version 3 of the License, or

 * (at your option) any later version.

 *

 * This program is distributed in the hope that it will be useful,

 * but WITHOUT ANY WARRANTY; without even the implied warranty of

 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

 * GNU General Public License for more details.

 *

 * You should have received a copy of the GNU General Public License

 * along with this program.  If not, see <http://www.gnu.org/licenses/>.

 *

 */

public class DragScrollListener implements MouseListener, MouseMotionListener{



	//flags used to turn on/off draggable scrolling directions

	public static final int DRAGGABLE_HORIZONTAL_SCROLL_BAR = 1;

	public static final int DRAGGABLE_VERTICAL_SCROLL_BAR = 2;

	

	//defines the intensite of automatic scrolling.

	private int scrollingIntensity = 10;

	

	//value used to descrease scrolling intensity during animation

	private double damping = 0.05;



	//indicates the number of milliseconds between animation updates. 

	private int animationSpeed = 20;

	

	//Animation timer

	private javax.swing.Timer animationTimer = null;

	

	//the time of the last mouse drag event

	private long lastDragTime = 0;

	

	private Point lastDragPoint = null;

	

	//animation rates

	private double pixelsPerMSX;

	private double pixelsPerMSY;



	//flag which defines the draggable scroll directions

	private int scrollBarMask = DRAGABLE_HORIZONTAL_SCROLL_BAR | DRAGABLE_VERTICAL_SCROLL_BAR;

		

	//the draggable component

	private final Component draggableComponent;

	

	//the JScrollPane containing the component - programmatically determined. 

	private JScrollPane scroller = null;

	

	//the default cursor

	private Cursor defaultCursor;

	

	//List of drag speeds used to calculate animation speed

	//Uses the Point2D class to represent speeds rather than locations

	private java.util.List<Point2D.Double> dragSpeeds = new ArrayList<Point2D.Double>();

	

	public DragScrollListener(Component c){

		draggableComponent = c;

		defaultCursor = draggableComponent.getCursor();

		draggableComponent.addPropertyChangeListener(new PropertyChangeListener(){



			@Override

			public void propertyChange(PropertyChangeEvent arg0) {

				setScroller();

				defaultCursor = draggableComponent.getCursor();

			}

		});

		setScroller();

	}

	

	private void setScroller(){

		Component c = getParentScroller(draggableComponent);

		if ( c != null ){

			scroller = (JScrollPane)c;

			

		}else{

			scroller = null;

		}

	}



	/**



	 * Sets the Draggable elements - the Horizontal or Vertical Direction. One

	 * can use a bitmasked 'or' (HORIZONTAL_SCROLL_BAR | VERTICAL_SCROLL_BAR ).

	 * @param mask One of HORIZONTAL_SCROLL_BAR, VERTICAL_SCROLL_BAR, or HORIZONTAL_SCROLL_BAR | VERTICAL_SCROLL_BAR 

	 */

	public void setDraggableElements(int mask){

		scrollBarMask = mask;

	}



	/**

	 * Sets the scrolling intensity - the default value being 5. Note, that this has an

	 * inverse relationship to intensity (1 has the biggest difference, higher numbers having

	 * less impact). 

	 * @param intensity The new intensity value (Note the inverse relationship).

	 */

	public void setScrollingIntensity(int intensity){

		scrollingIntensity = intensity;

	}



	/**

	 * Sets how frequently the animation will occur in milliseconds. Default 

	 * value is 30 milliseconds. 60+ will get a bit flickery.

	 * @param timing The timing, in milliseconds.

	 */

	public void setAnimationTiming(int timing){

		animationSpeed = timing;

	}

	

	/**

	 * Sets the animation damping. 

	 * @param damping The new value

	 */

	public void setDamping(double damping){

		this.damping = damping;

	}

	

	/**

	 * Empty implementation

	 */

	public void mouseEntered(MouseEvent e){}



	/**

	 * Empty implementation

	 */

	public void mouseExited(MouseEvent e){}



	/**

	 * Mouse pressed implementation

	 */

	public void mousePressed(MouseEvent e){	

		if ( animationTimer != null && animationTimer.isRunning() ){

			animationTimer.stop();

		}

		draggableComponent.setCursor(new Cursor(Cursor.MOVE_CURSOR));

		dragSpeeds.clear();

		lastDragPoint = e.getPoint();

	}



	/**

	 * Mouse released implementation. This determines if further animation

	 * is necessary and launches the appropriate times. 

	 */

	public void mouseReleased(MouseEvent e){

		draggableComponent.setCursor(defaultCursor);

		if ( scroller == null ){

			return;

		}



		//make sure the mouse ended in a dragging event

		long durationSinceLastDrag = System.currentTimeMillis() - lastDragTime;

		if ( durationSinceLastDrag > 20 ){

			return;

		}



		//get average speed for last few drags

		pixelsPerMSX = 0;

		pixelsPerMSY = 0;

		int j = 0;

		for ( int i = dragSpeeds.size() - 1; i >= 0 && i > dragSpeeds.size() - 6; i--, j++ ){

			pixelsPerMSX += dragSpeeds.get(i).x;

			pixelsPerMSY += dragSpeeds.get(i).y;

		}

		pixelsPerMSX /= -(double)j;

		pixelsPerMSY /= -(double)j;



		//start the timer

		if ( Math.abs(pixelsPerMSX) > 0 || Math.abs(pixelsPerMSY) > 0 ){

			animationTimer = new javax.swing.Timer(animationSpeed, new ScrollAnimator());

			animationTimer.start();

		}

	

	}



	/**

	 * Empty implementation

	 */

	public void mouseClicked(MouseEvent e){}

	

	/**

	 * MouseDragged implementation. Sets up timing and frame animation.

	 */

	public void mouseDragged(MouseEvent e){

		if ( scroller == null ){

			return;

		}

		Point p = e.getPoint();

		int diffx = p.x - lastDragPoint.x;

		int diffy = p.y - lastDragPoint.y;

		lastDragPoint = e.getPoint();

		

		//scroll the x axis

		if ( (scrollBarMask & DRAGABLE_HORIZONTAL_SCROLL_BAR ) != 0 ){

			getHorizontalScrollBar().setValue( getHorizontalScrollBar().getValue() - diffx);

		}

		//the Scrolling affects mouse locations - offset the last drag point to compensate

		lastDragPoint.x = lastDragPoint.x - diffx;

		

		//scroll the y axis

		if ( (scrollBarMask & DRAGABLE_VERTICAL_SCROLL_BAR ) != 0 ){

			getVerticalScrollBar().setValue( getVerticalScrollBar().getValue() - diffy);

		}

		//the Scrolling affects mouse locations - offset the last drag point to compensate

		lastDragPoint.y = lastDragPoint.y - diffy;

		

		//add a drag speed

		dragSpeeds.add( new Point2D.Double(

				(e.getPoint().x - lastDragPoint.x), 

				(e.getPoint().y - lastDragPoint.y) ) );



		lastDragTime = System.currentTimeMillis();

		

	}



	/**

	 * Empty

	 */

	public void mouseMoved(MouseEvent e){}





	/**

	 * Private inner class which accomplishes the animation. 

	 * @author Greg Cope

	 *

	 */

	private class ScrollAnimator implements ActionListener{



		/**

		 * Performs the animation through the setting of the JScrollBar values.

		 */

		public void actionPerformed(ActionEvent e){

			

			//damp the scrolling intensity

			pixelsPerMSX -= pixelsPerMSX * damping;

			pixelsPerMSY -= pixelsPerMSY * damping;

			

			//check to see if timer should stop.

			if ( Math.abs(pixelsPerMSX) < 0.01 && Math.abs(pixelsPerMSY) < 0.01  ){

				animationTimer.stop();

				return;

			}

			

			//calculate new X value

			int nValX = getHorizontalScrollBar().getValue() + (int)(pixelsPerMSX * scrollingIntensity);

			int nValY = getVerticalScrollBar().getValue() + (int)(pixelsPerMSY * scrollingIntensity);



			//Deal with out of scroll bounds

			if ( nValX <= 0 ){

				nValX = 0;

			}else if ( nValX >= getHorizontalScrollBar().getMaximum()){

				nValX = getHorizontalScrollBar().getMaximum();

			}

			if ( nValY <= 0 ){

				nValY = 0;

			}else if ( nValY >= getVerticalScrollBar().getMaximum() ){

				nValY = getVerticalScrollBar().getMaximum();

			}

			

			//Check again to see if timer should stop

			if ( (nValX == 0 || nValX == getHorizontalScrollBar().getMaximum()) && Math.abs(pixelsPerMSY) < 1  ){

				animationTimer.stop();

				return;

			}

			if ( (nValY == 0 || nValY == getVerticalScrollBar().getMaximum()) && Math.abs(pixelsPerMSX) < 1  ){

				animationTimer.stop();

				return;

			}

			

			//Set new values

			if ( (scrollBarMask & DRAGABLE_HORIZONTAL_SCROLL_BAR ) != 0 ){

				getHorizontalScrollBar().setValue(nValX);

			}

			if ( (scrollBarMask & DRAGABLE_VERTICAL_SCROLL_BAR ) != 0 ){

				getVerticalScrollBar().setValue(nValY);

			}



		}



	}



	/**

	 * Utility to retrieve the Horizontal Scroll Bar.

	 * @return

	 */

	private JScrollBar getHorizontalScrollBar(){

		return scroller.getHorizontalScrollBar();

	}



	/**

	 * Utility to retrieve the Vertical Scroll Bar

	 * @return

	 */	

	private JScrollBar getVerticalScrollBar(){

		return scroller.getVerticalScrollBar();

	}

	

	/**

	 * 

	 * @param c

	 * @return

	 */

	private Component getParentScroller(Component c){

		Container parent = c.getParent();

		if ( parent != null && parent instanceof Component ){

			Component parentC = (Component)parent;

			if ( parentC instanceof JScrollPane ){

				return parentC;

			}else{

				return getParentScroller(parentC);

			}

		}

		return null;

	}



	/**

	 * Testing main method as an SSCCE

	 * @param args

	 */

	public static void main(String[] args){



		JFrame frame = new JFrame();

		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		Drawer dr = new Drawer();

		JScrollPane pane = new JScrollPane(dr);

		DragScrollListener dl = new DragScrollListener(dr);

		dr.addMouseListener(dl);

		dr.addMouseMotionListener(dl);

		pane.setPreferredSize(new Dimension(300,300));

		frame.getContentPane().add(pane);

		frame.pack();

		frame.setVisible(true);

		pane.getHorizontalScrollBar().setValue(10);

	}



	/**

	 * Testing inner class that draws several squares of randomly selected colors.

	 * @author Greg Cope

	 *

	 */

	public static class Drawer extends JPanel{



		static final long serialVersionUID = 43214321L;

		

		int width = 10000;

		int height = 5000;

		Color[][] colors;



		/**

		 * Constructs the JPanel and random colors

		 */

		public Drawer(){

			super();

			setPreferredSize(new Dimension(width,height));

			colors = new Color[width/100][height/100];

			for ( int i = 0; i < colors.length; i++ ){

				for ( int j = 0; j < colors[i].length; j++ ){

					int r = (int)((255)*Math.random());

					int g = (int)((255)*Math.random());

					int b = (int)((255)*Math.random());

					colors[i][j] = new Color(r,g,b,150);

				}

			}

		}



		@Override 

		public void paintComponent(Graphics g){

			super.paintComponent(g);

			for ( int i = 0; i < width; i+= 100){

				for ( int j = 0; j < height; j+=100 ){

					g.setColor( colors[i/100][j/100] );

					g.fillRect(i+5, j + 5, 95, 95);

				}

			}

		}

	}

	

	

}

To use the class above, construct an instance by passing the Component one wishes to drag-scroll. Afterwards, add the listener to the Component (MouseListener and MouseMotionListener) - not done automatically to give the caller more transparency and control.

Of course scrolling such as this is useful for small devices such as the iPhone. For desktop applications its use is a little more esoteric. Scenarios where this could be handy include scenarios in which the content is huge relative to the screen - for instance viewing maps - be they genetic maps, geographic maps, or maps of outer space.




Comments

  • tamin X   -   March, 7, 2014

    looks interesting, unfortunately it does not compile

  • Greg Cope   -   March, 7, 2014

    My apologies. There was an issue with the code formatter on my website which ended up rendering the code incorrectly. Should be fixed now.

  • Alex Vogel   -   March, 5, 2015

    You made my day! Very good work. Its a shame that swing does not support touch devices although tablet devices are becoming more common.

    One little Typo:

    DRAGABLE_VERTICAL_SCROLL_BAR is used with one G

  • tekalouest khaltes   -   March, 30, 2015

    Hum, how can i add Jbutton ( with image ) inside this scrollpane ?

    thx

  • Sean Michnowski   -   March, 31, 2015

    Thank you so much for posting this scroll mechanism! It works beautifully and really does have that iPhone feel!

  • tekalouest khaltes   -   April, 7, 2015

    Hey ! when I add a button , I can't scroll when mouse is on this button. It only works when I swipe on the jpanel background. Any idea ?

  • Greg Cope   -   April, 7, 2015

    @tekalouest, I've never done this yet myself, but I'd suggest adding the appropriate MouseListener/MouseMotionListener to the JButton, and delegate the events to the DragScrollListener. I do not know what behavior you seek, but care must be taken to maintain any Listeners attached to the JButton itself so the behavior of it's default event handlers are not overridden.

  • Tekalouest Khaltes   -   April, 8, 2015

    Thank you so much for your greatfull code, and support :)

    I'll try this tonight and give you some feedback ! Cheers !

  • Noman S   -   April, 22, 2016

    Hi,

    Is it possible to use awt.Component instead of JPanel? I am using JCEF browser and its for the GUI part of browser return Component only and i want to apply this scrolling there but it is not doing anything on that part. Have any idea how to solve this

  • Greg Cope   -   November, 30, -0001

    @Noman, not sure what you are asking - JPanel extends Component, and thus is a Component.

  • harry vual   -   July, 10, 2019

    Very awesome many thanks !

    juste a little error name var DRAGGABLE_HORIZONTAL_SCROLL_BAR which is write with only one "g" sometime.

    But all is perfect thanks !

  • Ben Nemec   -   December, 20, 2019

    Thanks, helped a ton, worked like a charm!

Back to Articles


© 2008-2022 Greg Cope