Swing: прокрутить до нижней части JScrollPane, в зависимости от текущего местоположения области просмотра
Я пытаюсь имитировать функциональность Adium и большинства других клиентов чата, которые я видел, причем полосы прокрутки продвигаются вниз, когда приходят новые сообщения, но только если вы уже там. Другими словами, если вы прокрутили несколько строк вверх и читаете, то, когда приходит новое сообщение, оно не переместится на нижнюю часть экрана; это было бы раздражающим. Но если вы прокрутите до конца, программа справедливо предполагает, что вы хотите видеть самые свежие сообщения в любое время, и поэтому автоматически прокручивается соответствующим образом.
У меня было время медведя, пытающегося подражать этому; платформа, кажется, борется с этим поведением любой ценой. Лучшее, что я могу сделать, это следующее:
В конструкторе:
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());
}
Теперь это работает, но это дрянное и не идеальное по двум причинам.
- При установке позиции каретки любой выбор пользователя в области чата стирается. Я могу представить, что это будет очень раздражающим, если он пытается скопировать / вставить.
- Поскольку продвижение панели прокрутки происходит после вставки текста, существует доля секунды, когда полоса прокрутки находится в неправильном положении, а затем она визуально переходит к концу. Это не идеально.
Прежде чем вы спросите, да, я прочитал этот пост в блоге о прокрутке текстовой области, но прокрутка по умолчанию не соответствует желаемому.
Другие связанные (но на мой взгляд, не совсем полезные в этом отношении) вопросы: Настройка полосы прокрутки на панели jscrollPane JScrollPane автоматически прокручивает все страницы вниз.
Любая помощь в этом отношении будет принята с благодарностью.
Редактировать:
Согласно совету 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(Rectangle 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 Thread позаботится об этом.
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;
}
}
Немного поздно, но вот что я нашел: 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;
}
}
Последние несколько дней я пробовал разные подходы к этой проблеме. Лучшее решение, которое я нашел, - это заменить менеджер макета в окне просмотра JScrollPane и реализовать там все необходимые функции прокрутки. Прыгающая ручка полосы прокрутки исчезла, и она сверкнула быстро - настолько быстро, что это создало еще одно условие гонки, с которым мне пришлось обходиться. Подробности здесь: http://www.java-forums.org/awt-swing/56808-scroll-bar-knob-jumpy-when-component-growing.html
Проверьте этот пример.
Хотя я бы изменил код, чтобы использовать следующее, что более эффективно:
caret.setDot(textArea.getDocument().getLength());
DefaultCaret caret = (DefaultCaret)textArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
public static void scrollToBottom(JComponent component)
{
Rectangle visibleRect = component.getVisibleRect();
visibleRect.y = component.getHeight() - visibleRect.height;
component.scrollRectToVisible(visibleRect);
}
Вы можете найти полную информацию здесь: Как сделать JTextPane автопрокруткой только тогда, когда полоса прокрутки внизу и блокировка прокрутки отключена?