Reputation: 365
I wanna implement endless scroll for listview.
The app receives data from server portionally and currently it shows only last 50 rows.
I'm using tornadofx
, here's the code:
MainController.kt
:
private fun setAllServerHistoryList() {
if (curSelectedLogId != -1) {
serverHistoryList.clear()
runAsync {
serverAPI.getServerHistory(curSelectedLogId)
} ui {
serverHistoryList.setAll(it)
}
}
}
MainView.kt
:
private val historyListView = listview(controller.serverHistoryList) {
selectionModel = NoSelectionModel()
cellFormat{...}
}
getServerHistory
makes GET
request to server and returns parsed data, I can easily ask other rows using offset
and count
parameters.
But I haven't realized yet how can I add listener to detect amount of rows left
to both sides of list (in case if I'm opening view not from the bottom but in range of 10000:10200), and how to add data to existing list with avoiding memory errors.
I've already read lots of answers about same question but most of them (if not all) about android and java apps.
Thanks in advance for all the responds.
UPD with rewritten solution from answers (cheers to @TornadoFX):
import javafx.collections.FXCollections
import javafx.scene.control.ListCell
import javafx.util.Callback
import tornadofx.*
data class ScrollableItemWrapper(val data : String, val lastItem : Boolean = false) {
val id = nextId() // always increasing
companion object {
private var idgen = 1 // faux static class member
fun nextId() = idgen++
}
}
class EndlessScrollView : View("Endless Scroll") {
var currentBatch = 1
var last_batch = 0
val TOTAL_NUM_BATCHES = 10
private val records = FXCollections.observableArrayList<ScrollableItemWrapper>()
override val root = listview(records) {
selectionModel = NoSelectionModel()
cellFactory = Callback { listView ->
object : ListCell<ScrollableItemWrapper>() {
private val listener = ChangeListener<Number> { obs, ov, nv ->
if ( userData != null ) {
val scrollable = userData as ScrollableItemWrapper
val pos = listView.height - this.height - nv.toDouble()
if ( scrollable.lastItem && pos > 0 && (pos < this.height) && index < last_batch*50) {
val currentPos = index
runAsync {
getNextBatch()
} ui {
listView.scrollTo(currentPos)
records.addAll( it )
}
}
}
}
init {
layoutYProperty().addListener(listener)
}
override fun updateItem(item: ScrollableItemWrapper?, empty: Boolean) {
super.updateItem(item, empty)
if( item != null && !empty ) {
text = "[${item.id}]" + item.data
userData = item
} else {
text = null
userData = null
}
}
}
}
}
init {
records.addAll( getNextBatch() )
}
private fun getNextBatch() : List<ScrollableItemWrapper> {
if( currentBatch < TOTAL_NUM_BATCHES ) {
last_batch++
return 1.rangeTo(50).map {
ScrollableItemWrapper("Batch ${currentBatch} Record ${it}", it == 50)
}.apply {
currentBatch++
}.toList()
} else {
return emptyList()
}
}
}
Upvotes: 2
Views: 726
Reputation: 233
Try this code which I've used in a plain JavaFX app. It relies on a side effect that has worked through multiple versions of JavaFX. A wrapper class is used to pair the data with a flag indicating whether or not an item is the last in a batch. In the cell factory, a listener is registered and activated when the layoutY property is given space. If layoutY increases from 0 -- implying that the cell is shown -- and the last item flag is set, more data is fetched.
That data is added but the scroll position is saved so that the ListView doesn't jump through the whole fetched set.
data class ScrollableItemWrapper(val data : String, val lastItem : Boolean = false) {
val id = nextId() // always increasing
companion object {
private var idgen = 1 // faux static class member
fun nextId() = idgen++
}
}
class EndlessScrollView : View("Endless Scroll") {
var currentBatch = 1
val TOTAL_NUM_BATCHES = 10
val records = mutableListOf<ScrollableItemWrapper>().observable()
override val root = listview(records) {
cellFactory = Callback {
object : ListCell<ScrollableItemWrapper>() {
private val listener = ChangeListener<Number> { obs, ov, nv ->
if ( userData != null ) {
val scrollable = userData as ScrollableItemWrapper
if( scrollable.lastItem &&
((listView.height - this.height - nv.toDouble()) > 0.0)) {
val currentPos = index
runAsync {
getNextBatch()
} ui {
listView.scrollTo(currentPos)
records.addAll( it )
}
}
}
}
init {
layoutYProperty().addListener(listener);
}
override fun updateItem(item: ScrollableItemWrapper?, empty: Boolean) {
super.updateItem(item, empty)
if( item != null && !empty ) {
text = "[${item.id}]" + item.data!!
userData = item
} else {
text = null
userData = null
}
}
protected fun finalize() {
layoutYProperty().removeListener(listener)
}
}
}
}
init {
records.addAll( getNextBatch() )
}
private fun getNextBatch() : List<ScrollableItemWrapper> {
if( currentBatch <= TOTAL_NUM_BATCHES ) {
return 'A'.rangeTo('Z').map {
ScrollableItemWrapper("Batch ${currentBatch} Record ${it}", it == 'Z')
}.apply {
currentBatch++
}.toList()
} else {
return emptyList()
}
}
}
Upvotes: 1