代码之家  ›  专栏  ›  技术社区  ›  sadeghpro

JavaFX listview使用线程在自定义单元格中加载错误的图像

  •  0
  • sadeghpro  · 技术社区  · 6 年前

    我从事我的第一个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个图像,因为在线程中,下载完图像后,将其添加到单元格图像视图中,甚至单元格会消失,不再显示。例如,如果我快速滚动一段时间,则索引10中的单元格X和索引25中的单元格Y将显示在Y中单间牢房

    如果因为这是我的第一个JavaFX项目,所以我没有解释清楚

    1 回复  |  直到 6 年前
        1
  •  1
  •   fabian    6 年前
    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);
    }
    

    缓存图像是一个好主意,但这样做是错误的。加载(&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
            }
        }
    }