Swing: прокрутка до нижней части JScrollPane, обусловленная текущим местоположением окна просмотра
Я пытаюсь имитировать функциональность Adium и большинства других клиентов чата, которые я видел, в котором полосы прокрутки продвигаются вниз, когда приходят новые сообщения, но только если вы уже там. Другими словами, если вы прокрутили несколько строк вверх и читаете, когда приходит новое сообщение, оно не будет прыгать в нижнюю часть экрана; это было бы раздражающе. Но если ты прокручиваем в самый низ, программа справедливо предполагает, что вы хотите увидеть самые последние сообщения во все времена, и поэтому auto-scrolls соответственно.
у меня был медведь времени, пытаясь имитировать это; платформа, кажется, борется с этим поведением Любой ценой. Лучшее, что я могу сделать, это:
В конструкторе:
JTextArea chatArea = new JTextArea();
JScrollPane chatAreaScrollPane = new JScrollPane(chatArea);
// We will manually handle advancing chat window
DefaultCaret caret = (DefaultCaret) chatArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
в методе, который обрабатывает новый текст, поступающий в:
boolean atBottom = isViewAtBottom();
// Append the text using styles etc to the chatArea
if (atBottom) {
scrollViewportToBottom();
}
public boolean isAtBottom() {
// Is the last line of text the last line of text visible?
Adjustable sb = chatAreaScrollPane.getVerticalScrollBar();
int val = sb.getValue();
int lowest = val + sb.getVisibleAmount();
int maxVal = sb.getMaximum();
boolean atBottom = maxVal == lowest;
return atBottom;
}
private void scrollToBottom() {
chatArea.setCaretPosition(chatArea.getDocument().getLength());
}
Теперь это работает, но это janky и не идеально по двум причинам.
- путем устанавливать положение каретки, любой выбор потребитель может иметь внутри область чата удалена. Я могу себе представить, что это будет очень раздражать, если он пытается скопировать/вставить.
- поскольку продвижение области прокрутки происходит после вставки текста, есть доля секунды, когда полоса прокрутки находится в неправильном положении, а затем она визуально переходит к концу. Это не идеально.
прежде чем вы спросите, да я читал это сообщение в блоге на Прокрутка Текстовой Области, но по умолчанию прокрутка вниз поведение не чего я хочу.
другие связанные (но, на мой взгляд, не совсем полезные в этом отношении) вопросы: установка полосы прокрутки на jscrollpane создание JScrollPane автоматически прокручивать весь путь вниз.
любая помощь в этом отношении была бы очень признательна.
Edit:
согласно совету Devon_C_Miller, у меня есть улучшенный способ прокрутки вниз, решая проблему № 1.
private void scrollToBottom() {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
int endPosition = chatArea.getDocument().getLength();
Rectangle bottom = chatArea.modelToView(endPosition);
chatArea.scrollRectToVisible(bottom);
}
catch (BadLocationException e) {
System.err.println("Could not scroll to " + e);
}
}
});
}
Я до сих пор есть проблема #2.
7 ответов
посмотри scrollRectToVisible (прямоугольник r) и modelToView (int pos)
Это должно дать вам то, что вы ищете, не нарушая выбор пользователя.
Что касается полосы прокрутки, попробуйте заставить прокрутку и добавление происходить на разных событиях. Например:
if (atBottom) {
// append new line & do scroll
SwingUtilities.invokerLater(new Runnable(){
public void run() {
// append text
}});
} else {
// append newline
// append text
}
на основе предыдущих ответов и дальнейшего Google я сделал тест, в котором поток непрерывно добавляет строки в JTextArea в JScrollPane. Я думаю, что здесь важно использовать invokeLater, так как JScrollBar должен быть обновлен с его новым максимальным значением, прежде чем мы перейдем к этому значению. Используя invokeLater, поток AWT позаботится об этом.
private class AppendThread extends Thread {
public void run() {
while (true) {
String s = "random number = " + Math.random() + "\n";
boolean scrollDown = textAreaBottomIsVisible();
textArea.append(s);
if (scrollDown) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
JScrollBar bar = scrollPane.getVerticalScrollBar();
bar.setValue(bar.getMaximum());
}
}
);
}
try {
Thread.sleep(1000);
} catch (Exception e) {
System.err.println("exception = " + e);
}
}
}
private boolean textAreaBottomIsVisible() {
Adjustable sb = scrollPane.getVerticalScrollBar();
int val = sb.getValue();
int lowest = val + sb.getVisibleAmount();
int maxVal = sb.getMaximum();
boolean atBottom = maxVal == lowest;
return atBottom;
}
}
зацените пример.
хотя, я бы изменил код, чтобы использовать следующее, которое более эффективно:
caret.setDot(textArea.getDocument().getLength());
Я провел последние несколько дней, пробуя разные подходы к этой точной задачи. Лучшее решение, которое я нашел, - заменить менеджер макетов окна просмотра JScrollPane и реализовать там все желаемое поведение прокрутки. Нервная ручка прокрутки исчезла, и она пылает так быстро , что это создало еще одно условие гонки, с которым мне пришлось работать. Подробности здесь: http://www.java-forums.org/awt-swing/56808-scroll-bar-knob-jumpy-when-component-growing.html
немного поздно, но вот что я нашел: http://www.camick.com/java/source/SmartScroller.java
по сути, вы можете использовать следующий код:
JScrollPane scrollPane = new JScrollPane(myOversizedComponent);
// Injects smartscroll behaviour to your scrollpane
new SmartScroller(scrollPane);
и вот класс SmartScroller:
import java.awt.Component;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
/**
* The SmartScroller will attempt to keep the viewport positioned based on
* the users interaction with the scrollbar. The normal behaviour is to keep
* the viewport positioned to see new data as it is dynamically added.
*
* Assuming vertical scrolling and data is added to the bottom:
*
* - when the viewport is at the bottom and new data is added,
* then automatically scroll the viewport to the bottom
* - when the viewport is not at the bottom and new data is added,
* then do nothing with the viewport
*
* Assuming vertical scrolling and data is added to the top:
*
* - when the viewport is at the top and new data is added,
* then do nothing with the viewport
* - when the viewport is not at the top and new data is added, then adjust
* the viewport to the relative position it was at before the data was added
*
* Similiar logic would apply for horizontal scrolling.
*/
public class SmartScroller implements AdjustmentListener
{
public final static int HORIZONTAL = 0;
public final static int VERTICAL = 1;
public final static int START = 0;
public final static int END = 1;
private int viewportPosition;
private JScrollBar scrollBar;
private boolean adjustScrollBar = true;
private int previousValue = -1;
private int previousMaximum = -1;
/**
* Convenience constructor.
* Scroll direction is VERTICAL and viewport position is at the END.
*
* @param scrollPane the scroll pane to monitor
*/
public SmartScroller(JScrollPane scrollPane)
{
this(scrollPane, VERTICAL, END);
}
/**
* Convenience constructor.
* Scroll direction is VERTICAL.
*
* @param scrollPane the scroll pane to monitor
* @param viewportPosition valid values are START and END
*/
public SmartScroller(JScrollPane scrollPane, int viewportPosition)
{
this(scrollPane, VERTICAL, viewportPosition);
}
/**
* Specify how the SmartScroller will function.
*
* @param scrollPane the scroll pane to monitor
* @param scrollDirection indicates which JScrollBar to monitor.
* Valid values are HORIZONTAL and VERTICAL.
* @param viewportPosition indicates where the viewport will normally be
* positioned as data is added.
* Valid values are START and END
*/
public SmartScroller(JScrollPane scrollPane, int scrollDirection, int viewportPosition)
{
if (scrollDirection != HORIZONTAL
&& scrollDirection != VERTICAL)
throw new IllegalArgumentException("invalid scroll direction specified");
if (viewportPosition != START
&& viewportPosition != END)
throw new IllegalArgumentException("invalid viewport position specified");
this.viewportPosition = viewportPosition;
if (scrollDirection == HORIZONTAL)
scrollBar = scrollPane.getHorizontalScrollBar();
else
scrollBar = scrollPane.getVerticalScrollBar();
scrollBar.addAdjustmentListener( this );
// Turn off automatic scrolling for text components
Component view = scrollPane.getViewport().getView();
if (view instanceof JTextComponent)
{
JTextComponent textComponent = (JTextComponent)view;
DefaultCaret caret = (DefaultCaret)textComponent.getCaret();
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
}
}
@Override
public void adjustmentValueChanged(final AdjustmentEvent e)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
checkScrollBar(e);
}
});
}
/*
* Analyze every adjustment event to determine when the viewport
* needs to be repositioned.
*/
private void checkScrollBar(AdjustmentEvent e)
{
// The scroll bar listModel contains information needed to determine
// whether the viewport should be repositioned or not.
JScrollBar scrollBar = (JScrollBar)e.getSource();
BoundedRangeModel listModel = scrollBar.getModel();
int value = listModel.getValue();
int extent = listModel.getExtent();
int maximum = listModel.getMaximum();
boolean valueChanged = previousValue != value;
boolean maximumChanged = previousMaximum != maximum;
// Check if the user has manually repositioned the scrollbar
if (valueChanged && !maximumChanged)
{
if (viewportPosition == START)
adjustScrollBar = value != 0;
else
adjustScrollBar = value + extent >= maximum;
}
// Reset the "value" so we can reposition the viewport and
// distinguish between a user scroll and a program scroll.
// (ie. valueChanged will be false on a program scroll)
if (adjustScrollBar && viewportPosition == END)
{
// Scroll the viewport to the end.
scrollBar.removeAdjustmentListener( this );
value = maximum - extent;
scrollBar.setValue( value );
scrollBar.addAdjustmentListener( this );
}
if (adjustScrollBar && viewportPosition == START)
{
// Keep the viewport at the same relative viewportPosition
scrollBar.removeAdjustmentListener( this );
value = value + maximum - previousMaximum;
scrollBar.setValue( value );
scrollBar.addAdjustmentListener( this );
}
previousValue = value;
previousMaximum = maximum;
}
}
public static void scrollToBottom(JComponent component)
{
Rectangle visibleRect = component.getVisibleRect();
visibleRect.y = component.getHeight() - visibleRect.height;
component.scrollRectToVisible(visibleRect);
}
вы можете найти полную информацию здесь:Как сделать jtextpane autoscroll только тогда, когда полоса прокрутки внизу и блокировка прокрутки выключена?
DefaultCaret caret = (DefaultCaret)textArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);