In JavaFX 1.3 a lot of work has gone into ListView, and extracting out the base virtualization engine (the secret sauce to high performance lists) such that it is able to be used in controls such as TreeView and TableView. At the same time we wanted to make it really easy for developers to customize what is shown in each ListView row. What we’ve ended up doing is creating the concept of a cell, which at any point in time represents at most one item in your ListView, and it is through the cell that you have total freedom about how to display your data. Finally, as noted, we’ve expanded this concept to also be used in exactly the same way in TreeView (which is a preview in JavaFX 1.3), and it is likely to also underpin any future TableView control.
The ListView and TreeView controls support a huge number of items, but the ‘cost’ of having the control actually visually represented is only what you see on screen. Of course, there is also the cost of simply having the data lying around (which is not really in the domain of the control to handle – needless to say – be smart 🙂 ). In other words, we are very smart about not creating nodes for list items that aren’t actually visible, and about updating the cell content based on user interactions such as scrolling and keyboard navigation. Also, each cell is reused, rather than being created and thrown away as necessary for each individual item in the ListView.
Of course, the other part of our job is to make easy to use API’s, so we’ve worked hard on this aspect as well. By default when you put an item into a ListView, we create a simple String-based cell that shows the text in the cell when necessary. In other words, if all you’re wanting is the same output as what you saw in JavaFX 1.2, you don’t need to change anything. On the other hand, if you want a more fancy ListView, that is where the concept of a cell factory comes into play. You can see a cell factory on a ListView control, and then whenever the ListView needs to update (because the visible items change), the cellFactory is consulted and you’re responsible for populating the cell as appropriate.
Prior to showing off the code, it would be useful to quickly summarize our key design goals, so that you’ll hopefully better understand why we did things the way we did. Our goals were the following:
- Both time and memory efficient for large data sets
- Easy to build and use libraries for custom cells
- Easy to customize cell visuals
- Easy to customize display formatting (12.34 as $12.34 or 1234% etc)
- Easy to extend for custom visuals
- Easy to have “panels” of data for the visuals
- Easy to animate the cell size or other properties
Right, now definitely seems like a good time to actually show off some code. As a first example, if I were to create a custom cell which formatted numbers such that they would appear as currency types, I might do so like this:
[jfx]
import javafx.scene.control.*;
ListView {
items: [1.23, 3.33, 4.83, 5.32, 6.32]
cellFactory: function() {
def cell:ListCell = ListCell {
node: Label {
text: bind if (cell.empty) then "" else "${%(,.2f cell.item as Number}"
}
}
}
}
[/jfx]
In this example the cellFactory is a simple function which returns a ListCell who’s node content is a Label. The text of that Label is bound to the cell’s item. Whenever the item associated with a Cell is changed (which might happen if the user scrolls and the cell is reused to represent some different item in the ListView), then the Label’s text is automatically updated. In addition, if the cell is “empty” (meaning it is used to fill out space in the ListView but doesn’t have any data associated with it), then we just use the empty String.
Cell factories are really very powerful. The custom cells that are created can be anything from simple strings to full blown panels with movies and any other arbitrary content. They can change their size dynamically, and they can be styled easily from CSS.
We make it extraordinarily simple in JavaFX to change the color of cells. Each cell can be styled directly from CSS. So for example, if you wanted to change the default background of cells in a ListView to be a bit more interesting you could do the following CSS:
.list-cell { -fx-padding: 3 3 3 3; -fx-background-color: honeydew; }
If you run an application with this style, you will notice that not all cells use this new color, only half of them. The other half (as it turns out, the odd numbered rows) are styled by a different rule in CSS which also needs to be specified:
.list-cell:odd { -fx-background-color: mistyrose; }
Yikes! Looks like a crazy pair of socks. Clearly Jasper was not involved in picking the colors for this posting 🙂
If you wanted to set the color of selected ListView cells to be yellow, you could add this to your CSS file (yikes, the result is truly fearsome!):
.list-cell:filled:selected:focused, .list-cell:filled:selected { -fx-background-color: yellow; -fx-text-fill: black; }
Each of these examples require no code changes. Simply update your CSS file to alter the colors. You can also use the “hover” and other pseudoclasses in CSS the same as with other controls.
Suppose you had a sequence of Numbers to display in a ListView and wanted to color all of the negative values red and all positive or 0 values black. One way to achieve this is with a custom cellFactory which changes the styleClass of the Cell based on whether the value is negative or positive:
[jfx]
import javafx.scene.control.*;
ListView {
items: [1.23, -3.33, -4.83, 5.32, -6.32]
cellFactory: function() {
def cell:ListCell = ListCell {
styleClass: bind if ((cell.item as Number) < 0) then "negative" else "list-cell"
node: Label { text: bind if (cell.empty) then "" else "{cell.item}" }
}
}
}
[/jfx]
You would then update the CSS file so that the text-fill of anything with a styleClass equal to “negative” would use red:
.negative { -fx-text-fill: red }
There is one problem with this solution in JavaFX 1.3 however, which is that we currently only support a single style class, whereas in HTML CSS multiple style classes are supported. We’re working hard on fixing this limitation. In the meantime, you will need to copy a chunk of the default caspian stylesheet for your “negative” class for this to work properly:
.negative { -fx-text-fill: red; -fx-cell-size: 24; -fx-padding: 3; -fx-background-color: -fx-control-inner-background; } .negative:focused { -fx-background-insets: 0, 1.4; -fx-background-color: -fx-focus-color, -fx-control-inner-background; } .negative:odd { -fx-background-color: derive(-fx-control-inner-background,-5%); } .negative:focused:odd { -fx-background-insets: 0, 1.4; -fx-background-color: -fx-focus-color, derive(-fx-control-inner-background,-5%); } .negative:filled:selected:focused, .negative:filled:selected { -fx-background: -fx-accent; -fx-background-color: -fx-selection-bar; -fx-text-fill: -fx-selection-bar-text; }
Cell Factories
Although the code necessary for creating cell factories is relatively terse, it is also repetitive boilerplate that could be (and should be) handled by libraries. For example, you could provide static functions which create various standard ListCell factories:
[jfx]
import javafx.scene.control.*;
function currencyCellFactory():ListCell {
def cell:ListCell = ListCell {
node: Label { text: bind if (cell.empty) then "" else "${%(,.2f cell.item as Number}" }
}
}
ListView {
items: [1.23, 5.23, 4.44, 5.93, 1.34, 33.23]
cellFactory: currencyCellFactory
}
[/jfx]
Or the following example might be used to generate a cell factory which can format dates according to some specified format String:
[jfx]
import javafx.scene.control.*;
function dateCellFactory(format:String):ListCell {
// … exercise left to the reader
ListCell { }
}
ListView {
items: [date1, date2, date3, date4]
cellFactory: dateCellFactory("%tF")
}
[/jfx]
There are times when you may wish to create much more elaborate factories based on many different parameters. One approach is to create a class which represents the state of the factory, and which has one or more factory functions on it. For example, I might write the following class:
[jfx]
public class MyCellFactory {
public var format:String;
public function listCellFactory():ListCell {
…
}
public function treeCellFactory():TreeCell {
…
}
}
[/jfx]
And then when creating my ListView or TreeView, I could instantiate an instance of MyCellFactory and then link to the appropriate cellFactory function defined on that class:
[jfx]
ListView {
items: [1.23, 3.24, 5.32]
cellFactory: MyCellFactory { format: "%(,.2f" }.listCellFactory
}
[/jfx]
In this way the parameters, functions, state, and so forth associated with the custom cell factories can be grouped together in a sensible way.
Building a panel of data
Since the content node of a Cell may be any node type, it is possible to build up more complicated Cell content than a single text string. For example, suppose you wanted to produce a ListView that was composed of a Label and a Button. Suppose each of these cells represents a Job object in your application’s domain model. Perhaps clicking the button is supposed to start a Job. You might build the cell like this:
[jfx]
ListView {
items: bind jobs
cellFactory: function() {
def cell:ListCell = ListCell {
node: HBox {
spacing: 7
content: [
Label { text: bind if (cell.empty) then "" else "{(cell.item as Job).title}" }
Button {
text: "Start"
visible: bind cell.selected and not cell.empty
action: function() {
def job = cell.item as Job;
job.start();
}
}
]
}
}
}
}
[/jfx]
When the Cell is selected, the visible
attribute of the Button changes to true and you can see the button. Click on the button and it will start the Job. You can create arbitrarily complicated Cells in this manner.
These are just a few of the many interesting things that can be done with the ListView control in JavaFX 1.3. We’ll have additional blog posts in the near future showing in more detail some specific tricks (such as how to make a cell change size dynamically, and some more Jasper-approved visual design tweaks to make ListView really stand out).
In the meantime, here’s a call to action for all you experts out there doing library work (hint hint, Stephen and Dean). I’m excited to see what kind of cool libraries of cell factories can be built to make the ListView control really sweet to use!
Excellent work! i am amaze to see how easy is to customize the cells of the List great work
I like this, especially the idea of only using one concept of cell for many components (to come)!
+1 for liking the idea of having a single concept for many components. I haven’t experimented with JavaFX too much yet, but I’ve read a ton of articles like this and it’s starting to impress me.
For me, this implementation does a very good job of giving me the control I need to make a component useful while shielding me from a lot of the complexity that would normally have me over engineering things in plain old Java.
I think you guys are striking a nice balance of flexibility vs complexity. I can see JavaFX being a very productive platform.
Good job!
Ryan
i want to know what is the motivation behind using cellfactory clousure i think u can use that syntax as well for example
Listview{
items:[
HBox{
content:[
Label{}
Button{}
]
}
]
}
The problem with this is that if you have, say, 2000 items in your list, then you would have 2000 HBoxes & Labels & Buttons when only 10 or so would be visible. This is a huge waste of memory and clearly doesn’t scale up (suppose you had 30,000 items in your list. Or 3 million).
Being able to have a cell factory is crucial to virtualization, which is itself crucial for virtualization.
Awesome hidden power in these ListView controls. Very deceptive.
Good article to showcase the power.
Articles such as this re-affirm my faith in the JavaFX platform.
Go JavaFX …
Great article (as usual)!
Are there limits to the flexibility that the cell factories support? If I have a really heterogeneous sequence of data to be rendered in a single list how would I go about creating wildly different nodes based on the type of data?
Thanks,
Keith
That is the one weakness with our approach, in that the cell factory produces a cell that needs to work with every row rather than providing, say, a row index and having the developer return a different cell based on the index or item at that index. However, the reason we didn’t do that is because then you’d have to maintain a cache of cells and be notified whenever they are in use or released and manage that pool yourself, which is really complicated.
So instead, what you have to do is return a single Cell which is capable of rendering any of the items. If you have, say, 2 different types of cells you want to display, then each Cell has to have 2 different sub-scenegraphs and then choose which one to use as the “node” based on the current index. SO for example:
cellFactory: function() {
def cell:ListCell = ListCell {
node: bind if (cell.item instanceof Customer) { /* some node to represent it*/ }
else if (cell.item instanceof Order) { /* some node to represent the Order */ }
}
}
Or you can just have your custom cell have a variable for each type of sub-scene and lazily create them on demand, and then in the onUpdate callback do the switch there instead of using “bind” as the above code would create & throw away the subscenes pretty frequently.
My calculation here was that it was better to provide a very simple cell factory interface rather than force developers to manage a pool of cells manually, with the assumption that there would typically not be a very large number of different renderings in a single list. If you find you have a use case where you really do have a lot of different cell types in a single list, then you could go all the way if you wanted to by doing something like a ListCellComposite subclass of ListCell that would take a sequence of ListCells, or have a ListCellCreator subclass that would have cell-pool functions built into it and a shared HashMap or something of cells.
For example, suppose you had 100 different cell types to render. Under the basic API we provide, if the ListView needed 20 cells, it would end up creating 20 cells which would potentially have 100 different sub-scenes — and that 2000 different subscenes could be quite taxing on memory and such. Instead, suppose your implementation of the ListCell could refer to a pool of subscenes. When it needs one, it checks it out and assigns it to its node. When it is done with it, it puts it back. In this way you have 20 cells and only another 20 sub-scenes at first. Over time if every variation was used you could get up to all 2000 different subscenes, but it is unlikely, so you would probably save memory. Of course they could be housed in soft references so they are free’d if memory gets tight.
Anyway, something to stew on 🙂
You are awesome.
The css was so fun..
It was so easy i didn’t even know.
Thanks
for the grt post!