Cristian
Cristian

Reputation: 7185

Java JScrollPane Remove Components Without Changing Viewport

When there are more than 50 components in my JScrollPane, every time the user decides to go to the next page, the first 10 components in the JScrollPane are cleared.

This functionality works fine and is desirable, but when the components are removed, the viewport/scroll is changed. The view of the scrollpane jumps back each time the components are cleared.

What I'd like is to keep the viewport at exactly the same place, while removing the first components from the JScrollPane.

At the moment I have a workaround, but it's not elegant, and although it finds where the user last was, it's choppy, and jumps the scrollpane which doesn't look right:

if (middlePanel.getComponents().length > 50)
{
    Component currentScroll = scrollPane.getViewport().getView();
    for (int counter = 0; counter < memeAnchors.size(); counter++)
    {
        middlePanel.remove(counter);
    }
    scrollPane.setViewportView(currentScroll);
    scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getValue() - 800);
}

Is what I want to do even possible?

Thanks in advance everyone,

Upvotes: 1

Views: 3723

Answers (1)

Hovercraft Full Of Eels
Hovercraft Full Of Eels

Reputation: 285430

OK, I lied about not responding.

One way to do it is as noted below in my SSCCE. Note use of Scrollable. Explanation is in comments:

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

@SuppressWarnings("serial")
public class Sscce extends JPanel {
   private static final int INITIAL_ROW_COUNT = 100;
   public static final int INCREMENTAL_ADD_ROW_COUNT = 10;
   private static final int MIN_VERT_PERCENT = 4;
   private static final int TIMER_DELAY = 250;
   private ViewportViewPanel viewportViewPanel = new ViewportViewPanel(
         new GridLayout(0, 1));
   private JScrollPane scrollPane = new JScrollPane(viewportViewPanel);
   private BoundedRangeModel vertModel;
   private Timer vertChangeTimer;
   private int viewportViewPanelIndex = 0;
   public Component firstViewedComp;

   public Sscce() {
      setLayout(new BorderLayout());
      add(scrollPane);

      for (viewportViewPanelIndex = 0; viewportViewPanelIndex < INITIAL_ROW_COUNT; viewportViewPanelIndex++) {
         viewportViewPanel.add(new ViewablePanel(viewportViewPanelIndex));
      }

      vertModel = scrollPane.getVerticalScrollBar().getModel();
      vertModel.addChangeListener(new VertModelChangeListener());
   }

   private class VertModelChangeListener implements ChangeListener {

      @Override
      public void stateChanged(ChangeEvent cEvt) {
         // if timer is running, get out of here
         if (vertChangeTimer != null && vertChangeTimer.isRunning()) {
            return;
         }

         // if haven't set firstViewedComp back to null (done in Timer) get out of here
         if (firstViewedComp != null) {
            return;
         }

         // check to see if near bottom
         int diff = vertModel.getMaximum() - vertModel.getValue()
               - vertModel.getExtent();
         int normalizedDiff = (100 * diff) / vertModel.getMaximum();

         // if not near bottom, get out of here
         if (normalizedDiff >= MIN_VERT_PERCENT) {
            return;
         }

         // create and start timer
         vertChangeTimer = new Timer(TIMER_DELAY, new VertChangeTimerListener());
         vertChangeTimer.setRepeats(false);
         vertChangeTimer.start();

         // get viewport and its rectangle
         JViewport viewport = scrollPane.getViewport();
         Rectangle viewRect = viewport.getViewRect();

         // find first component that is inside of viewport's rectangle
         Component[] components = viewportViewPanel.getComponents();
         for (Component component : components) {
            if (viewRect.contains(component.getBounds())) {
               if (firstViewedComp == null) {
                  firstViewedComp = component; // first component found
                  break;
               }
            }
         }

         // delete 10 components at start 
         // add 10 components add end
         for (int i = 0; i < INCREMENTAL_ADD_ROW_COUNT; i++) {
            viewportViewPanel.remove(components[i]);
            viewportViewPanel.add(new ViewablePanel(viewportViewPanelIndex));
            viewportViewPanelIndex++;
         }

         // redo laying out components and repainting the container
         viewportViewPanel.revalidate();
         viewportViewPanel.repaint();

         // scroll back to first viewed component, but give a little delay to allow
         // layout out above to complete. So queue it on the event queue via invokeLater
         SwingUtilities.invokeLater(new Runnable() {
            public void run() {
               viewportViewPanel.scrollRectToVisible(firstViewedComp
                     .getBounds());
            }
         });

      }
   }

   // the timer listener. it just nulls out the first viewed component
   private class VertChangeTimerListener implements ActionListener {
      @Override
      public void actionPerformed(ActionEvent e) {
         firstViewedComp = null;
      }
   }

   private static void createAndShowGui() {
      Sscce mainPanel = new Sscce();

      JFrame frame = new JFrame("Sscce");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(mainPanel);
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

// JPanel that is held by the JScrollPane's JViewport and that holds the smaller 
// JPanels. Note that it implements Scrollable
@SuppressWarnings("serial")
class ViewportViewPanel extends JPanel implements Scrollable {
   private static final int BLOCK = 8;

   public ViewportViewPanel(LayoutManager layout) {
      super(layout);
   }

   @Override
   public Dimension getPreferredScrollableViewportSize() {
      int scrollWidth = ViewablePanel.PREF_W;
      int scrollHeight = ViewablePanel.PREF_H * BLOCK;
      return new Dimension(scrollWidth, scrollHeight);
   }

   @Override
   public int getScrollableBlockIncrement(Rectangle visibleRectangle,
         int orientation, int direction) {
      if (orientation == SwingConstants.VERTICAL) {
         return ViewablePanel.PREF_H * (3 * BLOCK) / 4;
      }
      return 0;
   }

   @Override
   public boolean getScrollableTracksViewportHeight() {
      return false;
   }

   @Override
   public boolean getScrollableTracksViewportWidth() {
      return true;
   }

   @Override
   public int getScrollableUnitIncrement(Rectangle visibleRect,
         int orientation, int direction) {
      if (orientation == SwingConstants.VERTICAL) {
         return ViewablePanel.PREF_H;
      }
      return 1;
   }

}

// small JPanel. Many of these are held in a single GridLayout column 
// by the JPanel above.
@SuppressWarnings("serial")
class ViewablePanel extends JPanel {
   public static final int PREF_W = 400;
   public static final int PREF_H = 50;
   private int index;

   public ViewablePanel(int index) {
      this.setIndex(index);
      String title = "index " + index;
      setBorder(BorderFactory.createTitledBorder(title));
   }

   @Override
   public Dimension getPreferredSize() {
      return new Dimension(PREF_W, PREF_H);
   }

   public int getIndex() {
      return index;
   }

   public void setIndex(int index) {
      this.index = index;
   }

}

Upvotes: 1

Related Questions