问题
My application contains a TextField
and a ListView
. The TextField
allows users to enter search terms that will filter the contents of the ListView
as they type.
The filtering process will match several fields within each DataItem
in the ListView
and return the results if any of them match.
What I want to do, however, is have those results prioritize items that match one particular field over the others.
For example, in the MCVE below, I have two items: Computer
and Paper
. The Computer
item has a keyword
for "paper," so searching for "paper" should return Computer
as a result.
However, since I also have an item called Paper
, the search should return Paper
at the top of the list. In the MCVE, though, the results are still alphabetized:
Question: How would I go about ensuring any matches to the DataItem.name
are listed above matches to a DataItem.keywords
?
EDIT: Entering "pap" in the search field should also return "Paper" at the top, followed by the remaining matches, as the partial search term partially matches the DataItem
name.
MCVE
DataItem.java:
import java.util.List;
public class DataItem {
// Instance properties
private final IntegerProperty id = new SimpleIntegerProperty();
private final StringProperty name = new SimpleStringProperty();
private final StringProperty description = new SimpleStringProperty();
// List of search keywords
private final ObjectProperty<List<String>> keywords = new SimpleObjectProperty<>();
public DataItem(int id, String name, String description, List<String> keywords) {
this.id.set(id);
this.name.set(name);
this.description.set(description);
this.keywords.set(keywords);
}
/**
* Creates a space-separated String of all the keywords; used for filtering later
*/
public String getKeywordsString() {
StringBuilder sb = new StringBuilder();
for (String keyword : keywords.get()) {
sb.append(keyword).append(" ");
}
return sb.toString();
}
public int getId() {
return id.get();
}
public IntegerProperty idProperty() {
return id;
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public String getDescription() {
return description.get();
}
public StringProperty descriptionProperty() {
return description;
}
public List<String> getKeywords() {
return keywords.get();
}
public ObjectProperty<List<String>> keywordsProperty() {
return keywords;
}
@Override
public String toString() {
return name.get();
}
}
Main.java:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class Main extends Application {
// TextField used for filtering the ListView
TextField txtSearch = new TextField();
// ListView to hold our DataItems
ListView<DataItem> dataItemListView = new ListView<>();
// The ObservableList of DataItems
ObservableList<DataItem> dataItems;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Add the search field and ListView to the layout
root.getChildren().addAll(txtSearch, dataItemListView);
// Build the dataItems List
dataItems = FXCollections.observableArrayList(buildDataItems());
// Add the filter logic
addSearchFilter();
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
/**
* Adds the functionality to filter the list dynamically as search terms are entered
*/
private void addSearchFilter() {
// Wrap the dataItems list in a filtered list, initially showing all items, alphabetically
FilteredList<DataItem> filteredList = new FilteredList<>(
dataItems.sorted(Comparator.comparing(DataItem::getName)));
// Add the predicate to filter the list whenever the search field changes
txtSearch.textProperty().addListener((observable, oldValue, newValue) ->
filteredList.setPredicate(dataItem -> {
// Clear any selection already present
dataItemListView.getSelectionModel().clearSelection();
// If the search field is empty, show all DataItems
if (newValue == null || newValue.isEmpty()) {
return true;
}
// Compare the DataItem's name and keywords with the search query (ignoring case)
String query = newValue.toLowerCase();
if (dataItem.getName().toLowerCase().contains(query)) {
// DataItem's name contains the search query
return true;
} else {
// Otherwise check if any of the search terms match those in the DataItem's keywords
// We split the query by space so we can match DataItems with multiple keywords
String[] searchTerms = query.split(" ");
boolean match = false;
for (String searchTerm : searchTerms) {
match = dataItem.getKeywordsString().toLowerCase().contains(searchTerm);
}
return match;
}
}));
// Wrap the filtered list in a SortedList
SortedList<DataItem> sortedList = new SortedList<>(filteredList);
// Update the ListView
dataItemListView.setItems(sortedList);
}
/**
* Generates a list of sample products
*/
private List<DataItem> buildDataItems() {
List<DataItem> dataItems = new ArrayList<>();
dataItems.add(new DataItem(
1, "School Supplies", "Learn things.",
Arrays.asList("pens", "pencils", "paper", "eraser")));
dataItems.add(new DataItem(
2, "Computer", "Do some things",
Arrays.asList("paper", "cpu", "keyboard", "monitor")));
dataItems.add(new DataItem(
3, "Keyboard", "Type things",
Arrays.asList("keys", "numpad", "input")));
dataItems.add(new DataItem(
4, "Printer", "Print things.",
Arrays.asList("paper", "ink", "computer")));
dataItems.add(new DataItem(
5, "Paper", "Report things.",
Arrays.asList("write", "printer", "notebook")));
return dataItems;
}
}
回答1:
If not mistaken you only need to find a way to sort your filtered results correctly. To keep it simple I will use this comparator instead of yours :
Comparator<DataItem> byName = new Comparator<DataItem>() {
@Override
public int compare(DataItem o1, DataItem o2) {
String searchKey = txtSearch.getText().toLowerCase();
int item1Score = findScore(o1.getName().toLowerCase(), searchKey);
int item2Score = findScore(o2.getName().toLowerCase(), searchKey);
if (item1Score > item2Score) {
return -1;
}
if (item2Score > item1Score) {
return 1;
}
return 0;
}
private int findScore(String itemName, String searchKey) {
int sum = 0;
if (itemName.startsWith(searchKey)) {
sum += 2;
}
if (itemName.contains(searchKey)) {
sum += 1;
}
return sum;
}
};
In the code above, I compare two DataItem. Each one will have a 'score' which depends on how similar their names are from our search keyword. For simplicity lets say we give 1 point if the searchKey
appeared in the name of our item and 2 points if the item name starts with the searchKey
, so now we can compare those two and sort them. If we return -1 the item1 will be placed first, if we return 1 then the item2 will be placed first and return 0 otherwise.
Here is addSearchFilter()
method I used in your example :
private void addSearchFilter() {
FilteredList<DataItem> filteredList = new FilteredList<>(dataItems);
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> filteredList.setPredicate(dataItem -> {
dataItemListView.getSelectionModel().clearSelection();
if (newValue == null || newValue.isEmpty()) {
return true;
}
String query = newValue.toLowerCase();
if (dataItem.getName().toLowerCase().contains(query)) {
return true;
} else {
String[] searchTerms = query.split(" ");
boolean match = false;
for (String searchTerm : searchTerms) {
match = dataItem.getKeywordsString().toLowerCase().contains(searchTerm);
}
return match;
}
}));
SortedList<DataItem> sortedList = new SortedList<>(filteredList);
Comparator<DataItem> byName = new Comparator<DataItem>() {
@Override
public int compare(DataItem o1, DataItem o2) {
String searchKey = txtSearch.getText().toLowerCase();
int item1Score = findScore(o1.getName().toLowerCase(), searchKey);
int item2Score = findScore(o2.getName().toLowerCase(), searchKey);
if (item1Score > item2Score) {
return -1;
}
if (item2Score > item1Score) {
return 1;
}
return 0;
}
private int findScore(String itemName, String searchKey) {
int sum = 0;
if (itemName.startsWith(searchKey)) {
sum += 2;
}
if (itemName.contains(searchKey)) {
sum += 1;
}
return sum;
}
};
sortedList.setComparator(byName);
dataItemListView.setItems(sortedList);
}
Of course the findScore()
could be more sophisticated if you want to create a more complex score system (for example checking upper and lower case letters, give more points depending the position of the keyword found in the item name etc).
回答2:
I may have found a different way to accomplish this. Instead of using a Predicate
, I've changed the ChangeListener
to just use a couple of loops and build a new List
manually:
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null || newValue.isEmpty()) {
// Reset the ListView to show all items
dataItemListView.setItems(dataItems);
return;
}
ObservableList<DataItem> filteredList = FXCollections.observableArrayList();
String query = newValue.toLowerCase().trim();
// First, look for exact matches within the DataItem's name
for (DataItem item : dataItems) {
if (item.getName().toLowerCase().contains(query)) {
filteredList.add(0, item);
} else {
// If the item's name doesn't match, we'll look through search terms instead
String[] searchTerms = query.split(" ");
for (String searchTerm : searchTerms) {
// If the item has this searchTerm and has not already been added to the filteredList, add it
// now
if (item.getKeywordsString().toLowerCase().contains(searchTerm)
&& !filteredList.contains(item)) {
filteredList.add(item);
}
}
}
}
dataItemListView.setItems(filteredList);
I'll leave the question unanswered for now to see if anyone has a solution for using the Predicate
as well.
来源:https://stackoverflow.com/questions/52434358/how-to-prioritize-rank-filteredlist-results-within-a-predicate