Прокрутка JavaFX ListView вызывает множество обращений к данным - PullRequest
0 голосов
/ 01 мая 2018

во время тестирования с JavaFX ListView я заметил большое количество обращений к базовому List, который предоставляет элементы.

В большинстве случаев это может быть не проблема. в моем случае это проблема, так как мой List использует Lazy Loading. Он содержит только информацию об элементе, и когда к элементу получают доступ (через get (index)), элемент получает разрешение (лениво), поэтому я ищу способ delay позиции прокрутки.

что работает, так это создание ListView со скрытым VBar, добавление пользовательского Vertical ScrollBar, который делает именно то, что мне нужно - delay (используя PauseTransistion, пока пользователь не остановится на несколько мс. но это решение - неприятный обходной путь ...

поэтому я попытался с помощью существующих методов отложить событие загрузки / прокрутки самого ListView.

ListView<Integer> list = new ListView<Integer>();

list.setOnScroll(new EventHandler<ScrollEvent>() {

   @Override
    public void handle(ScrollEvent e) {
        System.out.println("##### OnScroll: " + e);
    }

});

list.setOnScrollTo(new EventHandler<ScrollToEvent<Integer>>() {

        @Override
        public void handle(ScrollToEvent<Integer> e) {
        System.out.println("##### OnScrollTo: " + e);
    }
});

list.setOnScrollStarted(new EventHandler<ScrollEvent>() {

        @Override
        public void handle(ScrollEvent e) {
        System.out.println("##### OnScrollStarted: " + e);
    }

});

list.setOnScrollFinished(new EventHandler<ScrollEvent>() {

    @Override
        public void handle(ScrollEvent e) {
        System.out.println("##### OnScrollFinished: " + e);
    }

});

но НИКОГДА из этих событий не срабатывает ...

=== UPDATE ===

в дополнение к ответу @James_D я реализовал быстрый и грязный LazyLoadObservableList, который показывает количество запросов, выполненных через ListView

для этого код @ James_D просто нуждается в изменении двух строк:

 LazyLoadObservableList<LazyLoadItem<String>> lazyItems = new LazyLoadObservableList<>(items);
 list.setItems(lazyItems);

код этой обёртки:

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import javafx.beans.InvalidationListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

public class LazyLoadObservableList<E> implements ObservableList<E> {

    ObservableList<E> observableList;

    public LazyLoadObservableList(ObservableList<E> observableList) {
        this.observableList = observableList;
    }

    @Override
    public boolean add(E item) {
        return observableList.add(item);
    }

    @Override
    public void add(int index, E item) {
        observableList.add(index, item);
    }

    @Override
    public boolean addAll(Collection<? extends E> items) {
        return observableList.addAll(items);
    }

    @Override
    public boolean addAll(int index, Collection<? extends E> items) {
        return observableList.addAll(index, items);
    }

    @Override
    public void clear() {
        observableList.clear();
    }

    @Override
    public boolean contains(Object item) {
        return observableList.contains(item);
    }

    @Override
    public boolean containsAll(Collection<?> items) {
        return observableList.containsAll(items);
    }

    @Override
    public E get(int index) {
        System.out.println("ListView requested #"+index);
        return observableList.get(index);
    }

    @Override
    public int indexOf(Object item) {
        return observableList.indexOf(item);
    }

    @Override
    public boolean isEmpty() {
        return observableList.isEmpty();
    }

    @Override
    public Iterator<E> iterator() {
        return observableList.iterator();
    }

    @Override
    public int lastIndexOf(Object item) {
        return observableList.lastIndexOf(item);
    }

    @Override
    public ListIterator<E> listIterator() {
        return observableList.listIterator();
    }

    @Override
    public ListIterator<E> listIterator(int index) {
        return observableList.listIterator(index);
    }

    @Override
    public boolean remove(Object item) {
        return observableList.remove(item);
    }

    @Override
    public E remove(int index) {
        return observableList.remove(index);
    }

    @Override
    public boolean removeAll(Collection<?> items) {
        return observableList.removeAll(items);
    }

    @Override
    public boolean retainAll(Collection<?> items) {
        return observableList.retainAll(items);
    }

    @Override
    public E set(int index, E item) {
        return observableList.set(index, item);
    }

    @Override
    public int size() {
        return observableList.size();
    }

    @Override
    public List<E> subList(int fromIndex, int toIndex) {
        return observableList.subList(fromIndex, toIndex);
    }

    @Override
    public Object[] toArray() {
        return observableList.toArray();
    }

    @Override
    public <T> T[] toArray(T[] array) {
        return observableList.toArray(array);
    }

    @Override
    public void addListener(InvalidationListener invalidationListener) {
        observableList.addListener(invalidationListener);
    }

    @Override
    public void removeListener(InvalidationListener invalidationListener) {
        observableList.removeListener(invalidationListener);
    }

    @Override
    public boolean addAll(E... items) {
        return observableList.addAll(items);
    }

    @Override
    public void addListener(ListChangeListener<? super E> listChangeListener) {
        observableList.addListener(listChangeListener);
    }

    @Override
    public void remove(int fromIndex, int toIndex) {
        observableList.remove(fromIndex, toIndex);
    }

    @Override
    public boolean removeAll(E... items) {
        return observableList.removeAll(items);
    }

    @Override
    public void removeListener(ListChangeListener<? super E> listChangeListener) {
        observableList.removeListener(listChangeListener);
    }

    @Override
    public boolean retainAll(E... items) {
        return observableList.retainAll(items);
    }

    @Override
    public boolean setAll(E... items) {
        return observableList.setAll(items);
    }

    @Override
    public boolean setAll(Collection<? extends E> items) {
        return observableList.retainAll(items);
    }

}

1 Ответ

0 голосов
/ 01 мая 2018

Обработчики onScroll являются довольно низкоуровневыми обработчиками, которые реагируют на события мыши с колесом прокрутки (и аналогичные, например, для трековых площадок). Я почти уверен, что они не реагируют на перетаскивание полосы прокрутки (с точки зрения низкоуровневого события, которое является каким-либо событием перетаскивания мышью).

Перехват прокрутки здесь просто кажется неправильным подходом. Я думаю, что лучший подход здесь - через ячейку списка, где вы можете реализовать «Если элемент не загружен, и у меня некоторое время не запрашивался новый элемент, загрузите элемент».

Это, конечно, требует, чтобы сам элемент "знал", загружен он или нет. Так что вам понадобится какая-нибудь LazyLoadItem оболочка:

import java.util.concurrent.Executor;

import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.concurrent.Task;

public abstract class LazyLoadItem<T> {

    private T value ;
    private final T placeholder ;

    private final ReadOnlyBooleanWrapper loaded = new ReadOnlyBooleanWrapper();

    private boolean loadRequested ;

    private final Executor executor ;

    public LazyLoadItem(Executor executor, T placeholder) {
        this.executor = executor ;
        this.placeholder = placeholder ;
    }

    /**
     * Executed on background thread on first request to getValue().
     * @return The value.
     * @throws Exception
     */
    protected abstract T load() throws Exception ;

    /**
     * Requests to load the value in the background, and returns the value if loaded, or the placeholder otherwise.
     * Must be executed on the FX Application Thread.
     * @return The value, if loaded, otherwise the placeholder. If not loaded, spawns a task to load the value on a background thread.
     */
    public T getValue() {
        if (! loadRequested) {
            loadRequested = true ;
            Task<T> task = new Task<T>() {
                @Override
                protected T call() throws Exception {
                    return load();
                }
            };
            task.setOnSucceeded(e -> {
                value = task.getValue();
                loaded.set(true);
            });
            // do something more sensible here..
            task.setOnFailed(e -> task.getException().printStackTrace());
            executor.execute(task);
        }
        if (isLoaded()) {
            return value ;
        } else {
            return placeholder ;
        }
    }

    /**
     * Observable property indicating whether the value has been loaded. 
     * Must be called from the FX Application Thread.
     * @return
     */
    public final ReadOnlyBooleanProperty loadedProperty() {
        return this.loaded.getReadOnlyProperty();
    }


    /**
     * Whether or not the value has been loaded. Must be called from the FX Application Thread.
     * @return
     */
    public final boolean isLoaded() {
        return this.loadedProperty().get();
    }



}

Вот тривиальный пример реализации:

import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class MockLazyLoadItem extends LazyLoadItem<String> {

    // should not really be here: just for demo... Also limiting threads for demo
    private static final Executor EXEC = Executors.newFixedThreadPool(10, runnable -> {
        Thread t = new Thread(runnable);
        t.setDaemon(true);
        return t ;
    });

    private final Random rng = new Random();

    private final int id ;

    public MockLazyLoadItem(int id) {
        super(EXEC, "Not yet loaded...");
        this.id = id ;
    }

    @Override
    protected String load() throws Exception {
        System.out.println("loading item "+id);
        Thread.sleep(rng.nextInt(2000)+1000);
        return "Item "+id ;
    }

}

Теперь реализация вашей ячейки может делать что-то вроде

import javafx.animation.PauseTransition;
import javafx.scene.control.ListCell;
import javafx.util.Duration;

public class LazyLoadingListCell extends ListCell<LazyLoadItem<String>> {

    private PauseTransition pause = new PauseTransition(Duration.millis(1000));

    @Override
    protected void updateItem(LazyLoadItem<String> lazyItem, boolean empty) {
        pause.stop();
        super.updateItem(lazyItem, empty);
        if (empty) {
            setText("");
        } else if (lazyItem.isLoaded()) {
            setText(lazyItem.getValue());
        } else {
            pause.setOnFinished(e -> setText(lazyItem.getValue()));
            setText("Waiting...");
            pause.play();
        }
    }
}

Вот быстрый тест:

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;

public class LazyLoadListViewTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<LazyLoadItem<String>> list = new ListView<>();

        // Create list with extractor, so the list cell gets updated
        // when the loadedProperty() changes (i.e. when loading completes)
        ObservableList<LazyLoadItem<String>> items = FXCollections.observableArrayList(item -> new Observable[] {item.loadedProperty()});

        list.setCellFactory(lv -> new LazyLoadingListCell());

        for (int i = 1 ; i <= 1000 ; i++) {
            items.add(new MockLazyLoadItem(i));
        }

        list.setItems(items);

        Scene scene = new Scene(list);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

В этой демонстрации «Ожидание» указывает на ячейку, которая еще не просила элемент загрузить, а «Еще не загружен» указывает, что ячейка запросила элемент для загрузки, но загрузка не завершена.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...