java - JavaFX listview 使用线程在自定义单元格中加载错误的图像
问题描述
我在我的第一个 JavaFX 项目上工作,并且对 listview 自定义单元工厂有问题。这是我的代码
package ir.sadeghpro.instagram.cell;
import com.ibm.icu.util.PersianCalendar;
import ir.sadeghpro.insta.client.Comment;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;
import java.awt.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
public class DischargeComment extends ListCell<Comment> {
@FXML
private AnchorPane pane;
@FXML
private TextFlow lblComment;
@FXML
private Label lblDate;
@FXML
private Label lblTime;
@FXML
private Hyperlink lblUsername;
@FXML
private ImageView img;
public static String search = "";
private FXMLLoader mLLoader;
private static Map<String, Image> images = new HashMap<>();
@Override
protected void updateItem(Comment item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
if (mLLoader == null) {
mLLoader = new FXMLLoader(getClass().getClassLoader().getResource("cell/discharge_comment.fxml"));
mLLoader.setController(this);
try {
mLLoader.load();
} catch (IOException e) {
e.printStackTrace();
}
}
ObservableList<Node> children = lblComment.getChildren();
lblComment.setTextAlignment(TextAlignment.JUSTIFY);
children.clear();
if (!search.isEmpty() && item.getText().contains(search)) {
int lastIndex = 0;
for (int index = item.getText().indexOf(search); index >= 0; index = item.getText().indexOf(search, index + 1)) {
Text text = new Text(item.getText().substring(lastIndex, index));
text.setTextAlignment(TextAlignment.LEFT);
children.add(text);
text = new Text(item.getText().substring(index, index + search.length()));
text.setTextAlignment(TextAlignment.LEFT);
text.setFill(Color.RED);
children.add(text);
lastIndex = index + search.length();
}
if (lastIndex < item.getText().length()) {
Text text = new Text(item.getText().substring(lastIndex));
text.setTextAlignment(TextAlignment.LEFT);
children.add(text);
}
} else {
children.add(new Text(item.getText()));
}
PersianCalendar persianCalendar = new PersianCalendar();
persianCalendar.setTimeInMillis(item.getTimestamp() * 1000L);
lblDate.setText(persianCalendar.get(Calendar.YEAR) + "/" + (persianCalendar.get(Calendar.MONTH) + 1) + "/" + persianCalendar.get(Calendar.DAY_OF_MONTH));
lblTime.setText(persianCalendar.get(Calendar.HOUR) + ":" + persianCalendar.get(Calendar.MINUTE));
lblUsername.setText(item.getOwnerUsername());
Image image;
if ((image = images.get(item.getOwnerId())) == null) {
img.setImage(null);
new Thread(() -> {
Image image1 = new Image(item.getOwnerProfilePicUrl());
images.put(item.getOwnerId(), image1);
Platform.runLater(() -> img.setImage(image1));
}).start();
} else {
img.setImage(image);
}
Circle clip = new Circle(25, 25, 25);
img.setClip(clip);
lblUsername.setOnMouseClicked(e->{
try {
Desktop.getDesktop().browse(new URI("https://www.instagram.com/" + item.getOwnerUsername()));
} catch (IOException | URISyntaxException exception) {
exception.printStackTrace();
}
});
setText(null);
setGraphic(pane);
setHeight(Region.USE_COMPUTED_SIZE);
}
}
}
我的问题在第 107-114 行。在这一行中,如果用户的图像在我下载它并添加到 hashmap 图像之前没有下载,接下来将它添加到查看,它工作正常但是当滚动列表快速时可能会下载 100 个图像,因为在下载图像后的线程中添加到单元格 ImageView 甚至单元格消失并且不再显示,例如我在索引 10 中有单元格 X,在索引 25 中有单元格 Y,如果我快速滚动 X 显示在 Y 单元格中的一些时间图像
对不起,如果我没有解释清楚,因为这是我的第一个 JavaFX 项目
解决方案
private static Map<String, Image> images = new HashMap<>();
...
Image image;
if ((image = images.get(item.getOwnerId())) == null) {
img.setImage(null);
new Thread(() -> {
Image image1 = new Image(item.getOwnerProfilePicUrl());
images.put(item.getOwnerId(), image1);
Platform.runLater(() -> img.setImage(image1));
}).start();
} else {
img.setImage(image);
}
缓存图像是个好主意,但这样做是错误的。您在不同的线程上加载并插入图像到地图。由于您不同步访问,因此无法保证两个线程以相同的方式查看地图。还考虑到Image
提供异步加载 a 的方式这一事实,实际上Image
并不需要自己创建线程。
此外,对于大量数据,您可能希望删除 GUI 当前未使用的图像。使用SoftReference
s 将是一个好主意。
然而,主要问题是缺乏同步。如果你滚动得足够快,多个线程可能会为同一个单元格加载不同的图像,而你不知道最后一个启动的是否是最后一个执行的Platform.runLater
. 来自同一来源的多个图像可以并行加载。
也没有办法重用你的缓存。如果应用程序的其他部分需要图像,则无法以这种方式重用它们。
我的建议:
...
import java.awt.Desktop; // importing more classes from awt than neccessary could result in problems
...
public class DischargeComment extends ListCell<Comment> {
...
/**
* Constructor to pass external cache
* @param cache
*/
public DischargeComment(Map<String, SoftReference<Image>> cache) {
if (cache == null) {
throw new IllegalArgumentException();
}
this.cache = cache;
}
/**
* constructor using the default cache
*/
public DischargeComment() {
this(getDefaultCache());
}
private final Map<String, SoftReference<Image>> cache;
private static Map<String, SoftReference<Image>> defaultCache;
private static final URL FXML_URL = DischargeComment.class.getResource("cell/discharge_comment.fxml");
public static Map<String, SoftReference<Image>> getDefaultCache() {
if (defaultCache == null) {
defaultCache = new HashMap<>();
}
return defaultCache;
}
public static String search = "";
private boolean loaded = false; // no need for a reference to fxmlloader here
@Override
protected void updateItem(Comment item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
if (!loaded) {
FXMLLoader mLLoader = new FXMLLoader(FXML_URL);
mLLoader.setController(this);
try {
mLLoader.load();
img.setClip(new Circle(25, 25, 25));
loaded = true;
} catch (IOException e) {
e.printStackTrace();
}
}
...
// use single access here
// also use url as key
cache.compute(item.getOwnerProfilePicUrl(), (key, value) -> {
Image image = null;
if (value != null) {
image = value.get();
}
if (image == null) {
image = new Image(key, true); // load image in background
value = new SoftReference<>(image);
}
img.setImage(image);
return value;
});
lblUsername.setOnMouseClicked(e->{
try {
Desktop.getDesktop().browse(new URI("https://www.instagram.com/" + item.getOwnerUsername()));
} catch (IOException | URISyntaxException exception) {
exception.printStackTrace();
}
});
setText(null);
setGraphic(pane);
setPrefHeight(Region.USE_COMPUTED_SIZE); // don't set height to -1
}
}
}
推荐阅读
- c# - 如果调用者作为 Windows 服务运行,则包含 messagebox.show 的函数无法弹出
- r - 如何根据 R 中的特征名称将一个空间数据集子集为多个空间数据集
- css - Chrome 77 LayoutNG 打破了我的浮动布局
- apache-nifi - Apache Nifi 在 for 循环中执行任务
- python-3.x - Python slice all except the second to last row in an array
- python - 使用动画时如何在悬停时显示标记的标签?
- php - PHP中的Gmail API curl给出404(带有新令牌)
- angular - Angular 7 App 未在 Github 页面中正确显示项目,仅显示自述文件
- javascript - 打字稿装饰混乱
- linux - How to use sqlite command to import large CSV file?