Is there anyway to add an image to a JavaFX ListView?
This is how I am currently setting the list view items.
private ListView<String> friends;
private ObservableList<String> items;
items = FXCollections.observableArrayList(getFriends);
friends.setItems(items);
I'm also using the list view value as an id to know which was selected.
Implement a ListCell that displays the image and set a cellFactory on the ListView. The standard oracle tutorial has an example of a custom list cell implementation.
You would do something along the following lines:
friends.setCellFactory(listView -> new ListCell<String>() {
private ImageView imageView = new ImageView();
#Override
public void updateItem(String friend, boolean empty) {
super.updateItem(friend, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
Image image = getImageForFriend(friend);
imageView.setImage(image);
setText(friend);
setGraphic(imageView);
}
}
});
The updateItem(...) method can be called quite often, so it is probably better to preload the images and make them available to the cell, rather than creating them every time updateItem(...) is called.
remember to refresh or load your Listview
with myListViewWithPath.setItems(myObserverFilledWithImages);
#FXML
private void browseButton(ActionEvent event) throws Exception {
System.out.println("browseButton");
DirectoryChooser chooser = new DirectoryChooser();
File file = chooser.showDialog(myStage);
file = new File("E:\\FOLDER\\Imagen_File");
if (file != null) {
directoryField.setText(file.toString());
oImage.setAll(load(file.toPath()));
}
imageFilesList.setItems(oImage); //this one load or refresh the ListView
}
Related
I am trying to implement a list of files that can be selected from the RecyclerView Adapter class. While I understand it is not a good idea, I feel if I am able to accomplish this from within said class, it would be really helpful in the future.
My list item (Each individual item view for the RecyclerView) has the following structure:
|--------|----------------|
| ICON | DATA |
|--------|----------------|
Problem:
When a file is selected (by touching the icon portion of a file item), I change the background of that item to another color to denote that it has been selected.
However, when I scroll down to about 25 items later, another file has the same background color even though it's not selected (it does not show up in Log.d as being selected, nor was it in the test ArrayList that was used to store selected files).
It seems as though the item is only retaining the background change of the previous occupant.
Solution attempts:
Previously, only the variables related to each list item were declared in the RecyclerView ViewHolder class and all changes were made in the onBindViewHolder method. Now, all changes to be made have been moved to the ViewHolder class inside a method called bind. There was no change in behavior.
If I set the default background image during the very first step in onBindViewHolder, the behavior changes such that the items do not retain changes of previous occupants. However, on scrolling back, the background change for the target item reverts to the default background image.
Code:
public class RVA extends RecyclerView.Adapter<RVA.RVH>
{
private LayoutInflater inf;
private ArrayList<File> items;
// The var below is used to track the no. of selected items
// globally within the RVA class.
private int numberOfSelectedItems = 0;
public RVA(ArrayList<File> _items)
{
items = _items;
}
#Override
public RVA.RVH onCreateViewHolder(ViewGroup parent, int viewType)
{
inf = LayoutInflater.from(parent.getContext());
return new RVH(inf, parent);
}
#Override
public void onBindViewHolder(RVA.RVH holder, int position)
{
File listItem = items.get(position);
// 'binding' each file element to a respective host container.
holder.bind(listItem);
}
#Override
public int getItemCount()
{
return items.size();
}
#Override
public long getItemId(int position)
{
return position;
}
// The ViewHolder class.
// Initially it was just declared as class.
// There was no change observed after the 'final' modifier was added.
final class RVH extends RecyclerView.ViewHolder
{
private Context context;
private LinearLayout itemSelector;
private ImageView itemIcon;
private TextView itemName;
private TextView itemSize;
public RVH(LayoutInflater inf, ViewGroup parent)
{
super(inf.inflate(R.layout.list_item, parent, false));
context = parent.getContext();
// This is the SECOND outermost LinearLayout of each file item View.
// It was previously the parent Layout, but there was no difference due to change.
itemSelector = itemView.findViewById(R.id.item_selector);
// This is the icon ImageView.
itemIcon = itemView.findViewById(R.id.item_icon);
// These are the data TextViews.
itemName = itemView.findViewById(R.id.item_id);
itemSize = itemView.findViewById(R.id.item_size);
}
// The 'bind' method that registers changes.
public void bind(File fileItem)
{
String listItemName = fileItem.getName();
itemName.setText(listItemName);
//---- These are just changes to the icons depending on type. Works fine.
if(fileItem.isDirectory())
{
itemIcon.setImageResource(R.drawable.directory_icon);
itemSize.setText("Directory");
}
else
{
itemSize.setText(fileItem.length() + " B");
if(listItemName.endsWith(".jpg") || listItemName.endsWith(".jpeg") || listItemName.endsWith(".png") || listItemName.endsWith(".gif"))
{
Glide.with(context).load(fileItem).centerCrop().into(itemIcon);
}
else
{
itemIcon.setImageResource(R.drawable.file_icon);
}
}
//---- END
//---- This is the code which handles opening files according to type. Works fine.
itemSelector.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
if(numberOfSelectedItems == 0)
{
if(!itemSize.getText().toString().endsWith(" B"))
{
Intent loadListItemIntent = new Intent(context, DirectoryViewActivity.class);
loadListItemIntent.putExtra("ITEMPATH", fileItem.getPath());
context.startActivity(loadListItemIntent);
}
else
{
if(itemName.getText().toString().endsWith(".jpg") || itemName.getText().toString().endsWith(".jpeg") || itemName.getText().toString().endsWith(".png") || itemName.getText().toString().endsWith(".gif"))
{
Glide.with(context).load(fileItem).centerCrop().into(itemIcon);
Intent loadListItemIntent = new Intent(context, ImageActivity.class);
loadListItemIntent.putExtra("ITEMPATH", fileItem.getPath());
context.startActivity(loadListItemIntent);
}
else
{
Toast.makeText(context, "File needs proper application.", Toast.LENGTH_SHORT).show();
}
}
}
}
});
//---- END
//---- !!! THIS SECTION is where the problem manifests.
itemIcon.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
if(itemIcon.getTag().toString().equals("not_selected"))
{
// Incrementing based on selection.
++numberOfSelectedItems;
// Using a tag to identify/ denote whether item is selected.
itemIcon.setTag("selected");
// Changing the background & disabling file opening while in selection mode.
itemSelector.setBackgroundResource(R.drawable.list_item_selected);
itemSelector.setClickable(false);
itemSelector.setLongClickable(false);
// I use this odd method to send a message to the host Activity
// that we have entered selection mode & that the Activity should
// display some option buttons on the Action Bar.
if(context instanceof DirectoryViewActivity)
{
((DirectoryViewActivity)context).addSelectedItem(fileItem);
if(numberOfSelectedItems == 1)
{
((DirectoryViewActivity)context).setSelectionMode();
}
}
}
else
{
// Decrementing based on deselection.
--numberOfSelectedItems;
// Overwiting the tag to identify/ denote item is now unselected.
itemIcon.setTag("not_selected");
// Background changed back to default & file opening re-enabled.
itemSelector.setClickable(true);
itemSelector.setLongClickable(true);
itemSelector.setBackgroundResource(R.drawable.list_item_background);
// I use this method to send a message to the host Activity
// that we have exited selection mode & that the Activity should
// remove related option buttons from the Action Bar.
if(context instanceof DirectoryViewActivity)
{
((DirectoryViewActivity)context).removeSelectedItem(fileItem);
if(numberOfSelectedItems == 0)
{
((DirectoryViewActivity)context).voidSelectionMode();
}
}
}
}
});
}
}
}
This is because RecyclerView does not create views for all of your items in the list it create enough ViewHolder to fill up the screen and few more and when you scroll the old ViewHolder are bind to some other data in the adapter that is when the onBindViewHolder() is called , so basically what is happening here is you are setting the background of current ViewHolder on the screen and when you scroll the same ViewHolder in bind to the new data.
I think you have to check in the onBindViewHolder whether or not this is the item to which you want to set the background and then take the decision remove it if the item is not selected in the dataset set background if it is selected.
I am trying to create custom cells in a ListView , but every time I add a new item, the updateItem(TextFlow item, Boolean empty) is executed twice: one time it receives null and true, and the second time it does not (!null and false)
If I do not implement the setCellFactory method, then I can add the items to the table without problems.
ListView without custom cellFactory
However, when I do implement it, it simply creates 10 empty cells (where is the content?).
ListView with custom cellFactory
public class Controller implements Initializable {
#FXML
private ListView <TextFlow> console;
private ObservableList<TextFlow> data = FXCollections.observableArrayList();
public void initialize(URL location, ResourceBundle resources) {
console.setCellFactory(new Callback<ListView<TextFlow>, ListCell<TextFlow>>() {
#Override
public ListCell<TextFlow> call(ListView<TextFlow> param) {
return new ListCell<TextFlow>() {
#Override
protected void updateItem(TextFlow item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setItem(item);
setStyle("-fx-control-inner-background: blue;");
} else {
System.out.println("Item is null.");
}
}
};
}
});
for (int i = 0 ; i < 10; i++) {
Text txt = getStyledText("This is item number " + i + ".");
TextFlow textFlow = new TextFlow();
textFlow.getChildren().add(txt);
data.add(textFlow);
}
console.setItems(data);
}
private Text getStyledText (String inputText) {
Text text = new Text(inputText);
text.setFont(new Font("Courier New",12));
text.setFill(Paint.valueOf("#000000"));
return text;
}
}
updateItem can be called an arbitrary amount of times, different items may be passed and the cell can go from empty to non-empty and the other way round. ListView creates about as many cells as you see on screen and fills them with items. E.g. scrolling or modifications of the items list or resizing of the ListView can result in updates.
For this reason any cell needs to be able to deal with an arbitrary sequence of items (or null+empty) being passed to the updateItem method.
Furthermore you should avoid invoking setItem yourself, since super.updateItem does that already. Use setGraphic instead, if you want to display the item in the cell:
#Override
public ListCell<TextFlow> call(ListView<TextFlow> param) {
return new ListCell<TextFlow>() {
#Override
protected void updateItem(TextFlow item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setStyle("-fx-control-inner-background: blue;");
setGraphic(item);
} else {
setStyle(null);
setGraphic(null);
System.out.println("Item is null.");
}
}
};
}
I have a ListView that I am working to add a ContextMenu to. I have the ContextMenu working find but have another issue.
My setCellFactory code, used to setup the context menus:
lvAppetites.setCellFactory(lv -> {
ListCell<Appetite> cell = new ListCell<>();
ContextMenu contextMenu = new ContextMenu();
MenuItem editAppetiteMenu = new MenuItem();
editAppetiteMenu.textProperty().bind(Bindings.format("Edit ..."));
editAppetiteMenu.setOnAction(event -> {
// Code to load the editor window
editAppetite(cell.getItem());
});
contextMenu.getItems().add(editAppetiteMenu);
MenuItem deleteAppetiteMenu = new MenuItem();
deleteAppetiteMenu.textProperty().bind(Bindings.format("Delete ..."));
deleteAppetiteMenu.setOnAction(event -> {
// Code to delete the appetite
});
contextMenu.getItems().add(deleteAppetiteMenu);
contextMenu.getItems().add(new SeparatorMenuItem());
MenuItem addAppetiteMenu = new MenuItem();
addAppetiteMenu.textProperty().bind(Bindings.format("Add New ..."));
addAppetiteMenu.setOnAction(event -> {
// Code to delete the appetite
});
contextMenu.getItems().add(addAppetiteMenu);
cell.textProperty().bind(cell.itemProperty().asString());
// If nothing selected, remove the context menu
cell.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> {
if (isNowEmpty) {
cell.setContextMenu(null);
} else {
cell.setContextMenu(contextMenu);
}
});
return cell;
});
My ListView is searchable through a TextField with a listener; the listener filters the items in the ListView as the user types.
The problem now, is that as the list is filtered, any empty cells now display null.
From reading another question, I'm fairly confident that the ListView is still displaying a graphic for the removed cells. I know how to handle that within the ListView by overriding the updateItem method, but how would I handle this from within my setCellFactory method instead?
Is that even possible or will I need to refactor my entire ListView?
Thank you, as always, for all your help!
The problem arises from the line
cell.textProperty().bind(cell.itemProperty().asString());
When the cell is empty, the item will be null, so the binding will (I believe) evaluate to the string "null".
Try something that tests for the cell being empty or the item being null, e.g.
cell.textProperty().bind(Bindings
.when(cell.emptyProperty())
.then("")
.otherwise(cell.itemProperty().asString()));
or (thanks to #fabian for refining this version)
cell.textProperty().bind(Bindings.createStringBinding(
() -> Objects.toString(cell.getItem(), ""),
cell.itemProperty()));
If I use playList.getSelectionModel().select(1) - the highlighted selection will be the second row of the playlist. If I use playList.getSelectionModel().select(-1) to have no rows selected (as suggested elsewhere on StackOverflow) the first row will be selected. Does anyone have any idea of why this is not working? I would like for the first two rows of the listview to never be eligible for selection.
I am using JavaFX-8.
public class AudioPlayerFXMLController implements Initializable {
#FXML
private ListView playList;
private static ObservableList<Object> playListItems;
private static final String NEW_PLAYLIST = "New PlayList";
private static final String FRIEND_PLAYLIST = "Friend's PlayList";
#Override
public void initialize(URL url, ResourceBundle rb) {
playListItems = FXCollections.observableArrayList();
playListItems.add(NEW_PLAYLIST);
playListItems.add(FRIEND_PLAYLIST);
playList.setItems(FXCollections.observableList(playListItems));
playList.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> list) {
return new ImageCell();
}
});
playList.getSelectionModel().select(-1);
}
static class ImageCell extends ListCell<String> {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if ((this.getIndex() == 0) || (this.getIndex() == 1)) {
ImageView addSymbol;
addSymbol = ImageViewBuilder.create().image(new Image("/images/ic_add_grey600_15dp.png")).build();
addSymbol.fitHeightProperty();
setText(item);
setGraphic(addSymbol);
} else {
setText(null);
setGraphic(null);
}
}
}
}
Depending on which exact (update) version of jdk8 you are using, this might be the fix for RT-25679 (partially reverted in RT-38517) AFAIK, there is no public api to disable the auto-focus/select for the collection views in general. Only ListView has (undocumented! beware, they can change without notice) entries in its properties that disable the default behaviour
// disable selecting the first item on focus gain - this is
// not what is expected in the ComboBox control (unlike the
// ListView control, which does this).
listView.getProperties().put("selectOnFocusGain", false);
// introduced between 8u20 and 8u40b7
// with this, testfailures back to normal
listView.getProperties().put("selectFirstRowByDefault", false);
I am having a problem that I cannot figure out. I am taking a TreeView called treeModel and setting cells using setCellFactory as can be seen by the code. Now within the updateItem, I am setting a CheckBox as a graphic and am associating it with the CheckBoxTreeItem custom class called CheckBoxTreeItemModel. Now every time updateItem runs a new CheckBox is created and a new ChangeListener is created for it.
Now at first everything looks normal. Then I expand the direct children of the root, and begin checking item, but the listener seems to be called multiple time. For every level of TreeItems that is expanded, that is how many times the listener is called on one of the descendants of root. If I click on a child a few leaves down a parent, those listeners are then called multiple times as well. Its weird behavior that might be hard to explain, but the point is I don't think the listener is suppose to be called that many times. Its as if its cached. The problem code is below. Any help understanding why this may be happening would be greatly appreciated.
treeModel.setCellFactory(new Callback<TreeView<String>, TreeCell<String>>() {
#Override
public TreeCell<String> call(TreeView<String> param) {
return new TreeCell<String>() {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
final TreeCell<String> currCell = this;
this.setOnMouseClicked(new EventHandler<MouseEvent>() {
/*mouse event stuff completely unrelated to problem*/
});
if (empty) {
setText(null);
setGraphic(null);
}
else {
TreeItem<String> treeItem = getTreeItem();
if (treeItem instanceof CheckBoxTreeItemModel) {
System.out.println("Being called.");
final CheckBoxTreeItemModel chkTreeItem = (CheckBoxTreeItemModel) treeItem;
setText(item.toString());
CheckBox chk = new CheckBox();
chk.setSelected(chkTreeItem.getDeleteTick());
if(chkTreeItem.getListener() == null) {
ChangeListener<Boolean> listener = new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if(newValue) {
//was checked
System.out.println(chkTreeItem.toString()+" was checked!");
chkTreeItem.setDeleteTick(newValue);
}
else {
System.out.println(chkTreeItem.toString()+" was un-checked!");
chkTreeItem.setDeleteTick(newValue);
}
}//end of changed method
};
chkTreeItem.setListener(listener);
}
chk.selectedProperty().removeListener(chkTreeItem.getListener());
chk.selectedProperty().addListener(chkTreeItem.getListener());
chk.indeterminateProperty().bindBidirectional(chkTreeItem.indeterminateProperty());
chk.selectedProperty().bindBidirectional(chkTreeItem.selectedProperty());
setGraphic(chk);
}
else {
setText(item.toString());
setGraphic(null);
}
}
}//end of updateItem
};
}//end of the call method
});
Suggested Approach
I advise scrapping most of your code and using inbuilt CheckBoxTreeCell and CheckBoxTreeItem classes instead. I'm not sure if the inbuilt cells match your requirements, but even if they don't, you could examine the source code for them and compare it with yours and it (should) start to give you a good idea on where you are going wrong.
Potential Issues in Your Code
Reproducing your issue would require more code than you currently supply. But some things to look for are:
Removing and adding the same listener is pointless:
// first line is redundant.
// all listener code is probably unnecessary as you already bindBidirectional.
chk.selectedProperty().removeListener(chkTreeItem.getListener());
chk.selectedProperty().addListener(chkTreeItem.getListener());
updateItem might be called many times for a given cell. don't create new nodes for the cell everytime, instead re-use existing nodes created for the cell.
// replace with a lazily instantiated CheckBox member reference in TreeCell instance.
CheckBox chk = new CheckBox();
You bindBiDirectional but never unbind those bound values.
// should also unbind this values.
chk.indeterminateProperty().bindBidirectional(chkTreeItem.indeterminateProperty());
chk.selectedProperty().bindBidirectional(chkTreeItem.selectedProperty());
Sample Code
Sample updateItem code from the inbuilt CheckBoxTreeCell code:
public class CheckBoxTreeCell<T> extends DefaultTreeCell<T> {
. . .
private final CheckBox checkBox;
private ObservableValue<Boolean> booleanProperty;
private BooleanProperty indeterminateProperty;
. . .
#Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
StringConverter c = getConverter();
TreeItem<T> treeItem = getTreeItem();
// update the node content
setText(c != null ? c.toString(treeItem) : (treeItem == null ? "" : treeItem.toString()));
checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
setGraphic(checkBox);
// uninstall bindings
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
if (indeterminateProperty != null) {
checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
}
// install new bindings.
// We special case things when the TreeItem is a CheckBoxTreeItem
if (treeItem instanceof CheckBoxTreeItem) {
CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
booleanProperty = cbti.selectedProperty();
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
indeterminateProperty = cbti.indeterminateProperty();
checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
} else {
Callback<TreeItem<T>, ObservableValue<Boolean>> callback = getSelectedStateCallback();
if (callback == null) {
throw new NullPointerException(
"The CheckBoxTreeCell selectedStateCallbackProperty can not be null");
}
booleanProperty = callback.call(treeItem);
if (booleanProperty != null) {
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
}
}
}
. . .
}