首页 > 解决方案 > JavaFX ListView 滚动导致数据访问负载

问题描述

在与我进行测试时,我注意到对提供项目JavaFX ListView的底层的大量访问。List

在大多数用例中,这可能没有问题。就我而言,这是一个问题,因为我List使用延迟加载。它仅包含有关元素的信息,并且当元素被访问(通过 get(index))时,元素被解析(懒惰),因此我寻找到达delay滚动位置的方法。

起作用的是创建一个ListViewwith hidden VBar,添加一个 custom Vertical ScrollBar,这正是我需要的 - delay(使用 aPauseTransistion直到用户停止一些毫秒。但这个解决方案是一个讨厌的解决方法......

所以我尝试了现有的方法来推迟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);
    }

});

但这些事件都没有被触发......

===更新===

除了@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);
    }

}

标签: listviewjavafx

解决方案


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);
    }
}

在此演示中,“Waiting”表示尚未要求加载项的单元格,“尚未加载”表示该单元格已要求加载项,但加载未完成。


推荐阅读