title: How can I support scaling a view displayed in a JScrollPane to avoid displaying JScrollBars up to a minimum size?
body: <p>The behavior I want to implement is to display a view at 1:1 scale by default. If its container is made larger (by the user dynamically resizing the parent JFrame) the view should be scaled to fit the larger area; if made smaller, the view should be scaled to fit the smaller area - up to a limit. When smaller than a minimum size, scroll bars should appear with a viewport to support navigating around the view which is displayed at its minimum scale.</p>
  
  <p>I have a poorly-functioning implementation now, using a JScrollPane and a ComponentListener to determine when a resize has occurred. Based on the new size a new scale factor is set for painting the view to fit (up to the minimum scale), the preferredSize of the view is set and revalidate() is called. The problem is that this results in "jittery" display; when sizing past the points where a new scale is applied to avoid displaying scrollbars, the scrollbars appear, then disappear. I believe this is because my approach is reactive, not predictive. That is, the LayoutManager is only looking at the view's preferredSize and performing layout based on that. I'm listening for the resize of the JScrollPane and when it is too small only then changing the preferredSize of the view and then causing the container to be laid out again with the call to revalidate(). At least that's how I understand it.</p>
  
  <p>I've been reviewing the source for JScrollPane, JViewport and their respecive LayoutManagers. At this point I"m considering subclassing one or more to have a better (predictive, resulting in smoother sizing) implementation. This seems like behavior that others must have implemented before. Is there another way to use existing Containers/LayoutManagers/methods to do this without subclassing and risking unintended behavior across different LnF or platforms?</p>
  
  <p>Edit: I've hacked a prototype subclass of ScrollPaneLayout. It checks for the view's minimum size before adding scrollbars. This is working (in that the scrollbars do not appear until the viewport is smaller than the view's minimum size, instead of the original behavior which displayed scrollbars when the viewport was smaller than the view's preferred size) but when the scrollbars are displayed the view thinks it is still the preferred size, not the minimum size. I may have to hack at the ViewportLayout class as well, but this is quickly becoming something that I doubt will be robust.</p>
  
  <p><strong>Edit:</strong></p>
  
  <p>I went back to basics and tried the suggestions again, and have been succcessful. My attempts to overload the default behavior of ScrollPaneLayout and ViewportLayout, and the interactions between them was not the right direction. After my first attempt I was convinced that there was no way to avoid the flickering and instability of my "reactive" approach to fixing incorrect sizing after the LayoutManagers had done their thing. Fortunately, there is a way to make this work without subclassing either LayoutManager - and as stated, it's by implementing the <code>Scrollable</code> interface (which I had done correctly before, but had not made other changes to make it work). The trick is indeed to implement getScrollableTracksViewport() such that it returns true/false depending on the viewport size. What I hadn't done was update the view's preferredSize in addition to the other calculuations I was doing. This was a critical step. (Note that I'm also relying on listening to ComponentListener.componentResized() notifications to trigger the calculations needed to properly set the return values) The functioning code follows, thatnks for the help.</p>
  
  <pre><code>@SuppressWarnings("serial")
  class ScalingScreenPanel extends JScrollPane {
  
  private static final Dimension              PREFERRED_SIZE = new Dimension(800,200);
  
  private static ScalingScreen                sScreen;
  
  public ScalingScreenPanel() {
      setPreferredSize(PREFERRED_SIZE);
      getViewport().addComponentListener(new ComponentAdapter() {
          public void componentResized(ComponentEvent event) {
              sScreen.calculateSizes(event.getComponent().getSize());
          }
      });     
      setViewportView(sScreen=new ScalingScreen());       
  }
  public void setShow(Show pShow) {
      sScreen.setShow(pShow);
  }
  } // class PreviewScreenPanel
  
  @SuppressWarnings("serial")
  public abstract class ScalingScreen extends JPanel implements Scrollable {
  
  private static final Dimension              PREFERRED_SIZE = new Dimension(1000,100);
  private static final JLabel                 EMPTY_PANEL = new JLabel("Empty",SwingConstants.CENTER);
  
  private static final int                    DEFAULT_SIZE = 7;
  private static final int                    MINIMUM_SIZE = 5;
  private static final int                    SPACING = 3;
  private static final int                    MINIMUM_PITCH = MINIMUM_SIZE+SPACING; // Do not modify directly
  private static final int                    DEFAULT_PITCH = DEFAULT_SIZE+SPACING; // Do not modify directly
  
  protected int                               cSize;
  protected int                               cPitch;
  protected Dimension                         cOrigin;
  private Dimension                           cMinimumScreenSize;
  private Dimension                           cDefaultScreenSize;
  
  protected Dimension                         cCurrentScreenSize;
  private boolean                             cEnableVerticalScrollbar = false, cEnableHorizontalScrollbar = false;
  
  protected Dimension                         cGridSize = null;
  
  ScalingScreen() {
      cOrigin = new Dimension(0,0);
      add(EMPTY_PANEL);
  }
  public void setShow(Show pShow) {
      remove(EMPTY_PANEL);
      cGridSize = new Dimension(pShow.dimension());
      cMinimumScreenSize = new Dimension(cGridSize.width*MINIMUM_PITCH+SPACING,cGridSize.height*MINIMUM_PITCH+SPACING);
      cDefaultScreenSize = new Dimension(cGridSize.width*DEFAULT_PITCH+SPACING,cGridSize.height*DEFAULT_PITCH+SPACING);
      setMinimumSize(cMinimumScreenSize);
      setPreferredSize(cDefaultScreenSize);
      calculateSizes(getSize());
                  repaint();
  }
  public void calculateSizes(Dimension pViewportSize) {
      if (cGridSize==null) return;
      cPitch = Math.max(MINIMUM_PITCH,Math.min((pViewportSize.width-SPACING)/cGridSize.width,(pViewportSize.height-SPACING)/cGridSize.height));
      cSize = cPitch - SPACING;
      cOrigin = new Dimension((pViewportSize.width-(cPitch*cGridSize.width))/2,(pViewportSize.height-(cPitch*cGridSize.height))/2);
      cCurrentScreenSize = new Dimension(Math.max(pViewportSize.width,cMinimumScreenSize.width),Math.max(pViewportSize.height,cMinimumScreenSize.height));
      Dimension preferredSize = new Dimension();
      if (pViewportSize.width&lt;cMinimumScreenSize.width) {
          cOrigin.width = 0;
          cEnableHorizontalScrollbar = true;
          preferredSize.width = cMinimumScreenSize.width;
      } else {
          cOrigin.width = (pViewportSize.width-(cPitch*cGridSize.width))/2;
          cEnableHorizontalScrollbar = false;
          preferredSize.width = cDefaultScreenSize.width;         
      }
      if (pViewportSize.height&lt;cMinimumScreenSize.height) {
          cOrigin.height = 0;
          cEnableVerticalScrollbar = true;
          preferredSize.height = cMinimumScreenSize.height;
      } else {
          cOrigin.height = (pViewportSize.height-(cPitch*cGridSize.height))/2;
          cEnableVerticalScrollbar = false;
          preferredSize.height = cDefaultScreenSize.height;
      }
      setPreferredSize(preferredSize);
                  repaint();
  }
  
  // Methods to implement abstract Scrollable interface
  @Override
  public Dimension getPreferredScrollableViewportSize() {
      return getPreferredSize();
  }
  @Override
  public boolean getScrollableTracksViewportHeight() {
      return !cEnableVerticalScrollbar;
  }
  @Override
  public boolean getScrollableTracksViewportWidth() {
      return !cEnableHorizontalScrollbar;
  }
  @Override
  public int getScrollableBlockIncrement(Rectangle pVisibleRect, int pOrientation, int pDirection) {
      switch (pOrientation) {
      case SwingConstants.VERTICAL:
          return pVisibleRect.height/2;
      case SwingConstants.HORIZONTAL:
          return pVisibleRect.width/2;
      default:
          return 0;
      }       
  }
  @Override
  public int getScrollableUnitIncrement(Rectangle pVisibleRect,
          int pOrientation, int pDirection) {
      switch (pOrientation) {
      case SwingConstants.VERTICAL:
          return 1;
      case SwingConstants.HORIZONTAL:
          return 1;
      default:
          return 0;
      }
  }
        @Override
        public void paintComponent(Graphcs g) {
            // custom drawing stuff
        }
  } // class ScalingScreen
  </code></pre>