Results 1 to 4 of 4
  1. #1
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Scroll bar knob is jumpy when component is growing

    I've written a JComponent that manipulates its own scrolling behavior as its size dynamically changes. The component itself seems to work exactly as I intended. The details are in this thread.

    The only remaining issue is a cosmetic one. When the component's size changes, it scrolls its preferred viewport back to the bottom if it was previously at the bottom. The problem is that when the component is growing rapidly, the scroll bar knob is jumpy. It ends up at the bottom like it's supposed to, but you see it jump up and then back down.

    I think I understand why this is happening. When JScrollPane handles a call to revalidate() on the EDT, it updates the size of its client and then updates the position of the scroll bar knob based on the current viewport position. Immediately after that, the EDT executes the task that scrolls the viewport back to the bottom. The result is that the scroll bar is briefly drawn with the knob away from the bottom.

    I can't see any way around this as far as the JComponent is concerned. There is no getPreferredScrollableViewport() method in the Scrollable interface. There should be, and JScrollPane should test it when it handles revalidate().

    Unless someone has a better suggestion, I'm going to try to extend Scrollable and JScrollPane and see if I can fix this.
    Get in the habit of using standard Java naming conventions!

  2. #2
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: Scroll bar knob is jumpy when component is growing

    Okay, this reduces but does not completely eliminate the jumpy scroll bar knob. I think a complete solution would require fully overriding the behavior of validate() instead of just calling super.validate().

    Do you think this is worthy of suggesting for future JDK versions? It would dramatically simplify things like keeping a component scrolled to the bottom. Instead of queuing a Runnable to handle the scrolling, you would just need to call revalidate().

    Java Code:
    import java.awt.Rectangle;
    
    import javax.swing.Scrollable;
    
    public interface EnhancedScrollable extends Scrollable {
    	public abstract Rectangle getPreferredScrollableViewport();
    }
    Java Code:
    import java.awt.Component;
    
    import javax.swing.JScrollPane;
    
    public class EnhancedJScrollPane extends JScrollPane {
    	private static final long serialVersionUID = 1L;
    	
    	// TODO: imitate other JScrollPane constructors
    	public EnhancedJScrollPane(Component view) {
    		super(view);
    	}
    
    	@Override
    	public void validate() {
    		super.validate();
    		Component view = viewport.getView();
    		if(view instanceof EnhancedScrollable) {
    			viewport.scrollRectToVisible(((EnhancedScrollable) view).getPreferredScrollableViewport());
    		}
    	}
    }
    Last edited by kjkrum; 03-16-2012 at 07:34 PM.
    Get in the habit of using standard Java naming conventions!

  3. #3
    DarrylBurke's Avatar
    DarrylBurke is offline Member
    Join Date
    Sep 2008
    Location
    Madgaon, Goa, India
    Posts
    11,188
    Rep Power
    19

    Default Re: Scroll bar knob is jumpy when component is growing

    Quote Originally Posted by kjkrum View Post
    Do you think this is worthy of suggesting for future JDK versions?
    I think the requirement is rare enough that this would be assigned low priority, and never be done. But that's theoretical. With the introduction of JavaFX 2, Swing has entered the maintenance phase, and only bug fixes may be attempted; not RFEs. This would be a RFE.

    db
    If you're forever cleaning cobwebs, it's time to get rid of the spiders.

  4. #4
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: Scroll bar knob is jumpy when component is growing

    Well, for future reference, I found a much better way to do this. The key change is to extend ViewportLayout and replace your JScrollPane viewport's layout manager with the modified one:

    Java Code:
    import java.awt.Component;
    import java.awt.Container;
    import java.awt.Dimension;
    import java.awt.Point;
    import java.awt.Rectangle;
    
    import javax.swing.JViewport;
    import javax.swing.ViewportLayout;
    
    public class StickyViewportLayout extends ViewportLayout {
    	private static final long serialVersionUID = 1L;
    
    	@Override
    	public void layoutContainer(Container parent) {
    		JViewport viewport = (JViewport) parent;
    		Component client = viewport.getView();
    		if(client instanceof StickyScrollable) {
    			StickyScrollable sticky = (StickyScrollable) client;
    			Rectangle visible = viewport.getViewRect();
    			Dimension size = viewport.getViewSize();
    			boolean atBottom = (visible.y == size.height - visible.height);
    			super.layoutContainer(parent);
    			if(atBottom) {
    				size = viewport.getViewSize();
    				visible.y = size.height - visible.height;
    				viewport.setViewPosition(visible.getLocation());
    				// without this, it appears to freeze at the bottom, then goes black
    				// because it always paints with this offset to fix the jitter problem
    				sticky.resetDesiredRelativeScroll();
    			}
    			else {
    				Point relativeScroll;
    				synchronized(sticky) {
    					relativeScroll = sticky.getDesiredRelativeScroll();
    					sticky.resetDesiredRelativeScroll();
    				}
    				visible.x += relativeScroll.x;
    				if(visible.x < 0) visible.x = 0;
    				visible.y += relativeScroll.y;
    				if(visible.y < 0) visible.y = 0;
    				viewport.setViewPosition(visible.getLocation());
    			}
    		}
    		else super.layoutContainer(parent);
    	}
    }
    For convenience, I've also extended JScrollPane to perform that replacement:

    Java Code:
    import java.awt.Component;
    
    import javax.swing.JScrollPane;
    
    public class StickyScrollPane extends JScrollPane {
    	private static final long serialVersionUID = 1L;
    	
    	public StickyScrollPane() {
    		super();
    		replaceViewportLayout();
    	}
    
    	public StickyScrollPane(Component view, int vsbPolicy, int hsbPolicy) {
    		super(view, vsbPolicy, hsbPolicy);
    		replaceViewportLayout();
    	}
    
    	public StickyScrollPane(Component view) {
    		super(view);
    		replaceViewportLayout();
    	}
    
    	public StickyScrollPane(int vsbPolicy, int hsbPolicy) {
    		super(vsbPolicy, hsbPolicy);
    		replaceViewportLayout();
    	}
    
    	protected void replaceViewportLayout() {
    		viewport.setLayout(new StickyViewportLayout());
    	}
    }
    And here's the StickyScrollable interface:

    Java Code:
    import java.awt.Point;
    
    import javax.swing.Scrollable;
    
    public interface StickyScrollable extends Scrollable {
    	// FIXME: these are dumb names, and Point is not the right type to use
    	public abstract Point getDesiredRelativeScroll();
    	public abstract void resetDesiredRelativeScroll();
    }
    The layout manager automatically keeps the viewport scrolled to the bottom if it's already at the bottom. The component doesn't have to worry about that at all. And if that's all I wanted to do, the methods in StickyScrollable wouldn't even be necessary. StickyScrollable could just be a marker interface.

    But if the component is displaying data from a scrolling circular buffer, a viewport position in the middle will show data scrolling by. StickyScrollable.getDesiredRelativeScroll() lets the layout manager know how much the component wants to be scrolled so that it continues to display the same data until it scrolls out of the buffer.

    This is all very fast -- at least ten times faster than any other approach I've tried. It's so fast that I ran into a whole different race condition: the buffer was being updated between revalidation and repainting. This was causing jitter when displaying the same data as it scrolls. The solution is kind of a hack, and I don't consider it finished.

    This is the method in my JComponent that gets called when something updates the buffer that backs it:

    Java Code:
    	@Override
    	synchronized public void extentsChanged() {
    		// compute deltas
    		Rectangle newExtents = buffer.getExtents();
    		int bufferYDelta = newExtents.y - oldExtents.y;
    		oldExtents.setBounds(newExtents);
    		int oldHeight = preferredSize.height;		
    		calculatePreferredSize(newExtents);
    		int componentHDelta = preferredSize.height - oldHeight;
    		
    		if(componentHDelta != 0) {
    			revalidate();
    			repaint();
    		}
    		else if(bufferYDelta != 0) {
    			desiredRelativeScroll.y -= bufferYDelta;
    			revalidate();
    			repaint();
    		}
    	}
    And this is its paintComponent method. It's worth noting that the value of desiredRelativeScroll you see here is in character cells, but the value returned by getDesiredRelativeScroll() is in pixels.

    Java Code:
    	@Override
    	protected void paintComponent(Graphics g) {
    		Rectangle clip = g.getClipBounds();
    		((Graphics2D) g).setBackground(VGAColors.background(VGAColors.BLACK));
    		// figure out the cell extents of the clip
    		int x1 = clip.x / glyphSize.width;
    		int y1 = clip.y / glyphSize.height;
    		int x2 = (int) Math.ceil((float)(clip.x + clip.width) / glyphSize.width);
    		int y2 = (int) Math.ceil((float)(clip.y + clip.height) / glyphSize.height);
    		// use block synchronization so the buffer can't change
    		// while the edt is in the middle of drawing it
    		synchronized(buffer) {
    			synchronized(this) {
    				Rectangle extents = buffer.getExtents();
    				for(int row = y1; row < y2; ++row) {
    					for(int col = x1; col < x2; ++col) {
    						// translate display coordinates into buffer coordinate system
    						// FIXME: adjusting by desiredRelativeScroll is kind of a hack
    						int bufCol = col + extents.x + desiredRelativeScroll.x;
    						int bufRow = row + extents.y + desiredRelativeScroll.y;
    						// draw each cell
    						if(extents.contains(bufCol, bufRow)) {
    							font.drawGlyph(buffer.getAttributes(bufCol, bufRow), g, col, row);
    						}
    						else {
    							g.clearRect(col * glyphSize.width, row * glyphSize.height, glyphSize.width, glyphSize.height);
    						}	
    					}
    				}
    			}
    		}
    	}
    Get in the habit of using standard Java naming conventions!

Similar Threads

  1. polygon growing
    By ericT in forum New To Java
    Replies: 1
    Last Post: 09-24-2011, 08:35 PM
  2. scroll popupmenu
    By jperson in forum New To Java
    Replies: 1
    Last Post: 11-18-2010, 05:17 AM
  3. smooth-scroll
    By designer in forum Java Applets
    Replies: 1
    Last Post: 07-21-2009, 07:10 PM
  4. Scroll message
    By getkiran in forum Java Applets
    Replies: 1
    Last Post: 03-05-2009, 04:29 AM
  5. Reading growing files
    By ionna22 in forum Advanced Java
    Replies: 0
    Last Post: 10-20-2008, 04:43 PM

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •