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.
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.
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?
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.
Edit:
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 Scrollable
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.
@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<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<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
The basic approach is to let your custom component implement Scrollable and code the getTracksViewportWidth/-Height as needed.
Edit
exactly, that was the idea - but didn't work out as I expected: at the "turning point" when reaching the min the image is scaled to its preferred, even with an self-adjusting getPrefScrollable (which doesn't seem to have any effect, it's called once at the start)
Edit 2
With the help of the OP:
finally got it: (my initial attempt had it upside down ;-) keep the prefScrollable to the "real" pref and let pref return either minimum or pref, depending on whether or not the scrollbar should be visible.