You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaFX ListView自定义单元格异步加载图片显示错误问题

解决JavaFX ListView异步加载图片时的单元格复用问题

这个问题我太熟悉了——这是ListView单元格复用机制和异步任务生命周期不匹配导致的经典坑!快速滚动时,单元格被重新分配给新的列表项,但之前启动的图片加载线程还在后台跑,等线程完成后,它依然会把图片塞给那个已经“换主人”的ImageView,就出现了你看到的张冠李戴的情况。

核心原因分析

当你调用updateItem时,如果单元格被复用,原来的ImageView已经属于新的Comment对象,但之前为旧Comment启动的异步线程还没结束。线程执行完Platform.runLater时,它不知道这个ImageView已经绑定了新数据,依然会把旧图片设置上去。

具体解决方案

咱们可以从两个层面修复这个问题:

  1. 跟踪并取消未完成的异步任务:确保单元格复用后,旧的加载任务不会再干扰新的列表项。
  2. 设置图片前校验当前数据匹配性:即使任务没被取消,也要确认当前单元格的item和任务对应的item一致,再设置图片。

另外,你的images HashMap不是线程安全的,多线程操作可能出问题,建议换成ConcurrentHashMap。

修改后的代码示例

我把你的代码关键部分修改一下,用JavaFX的Task来代替普通Thread,这样可以方便地取消任务:

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.concurrent.Task;
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.Map;
import java.util.concurrent.ConcurrentHashMap;

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;
    // 换成线程安全的ConcurrentHashMap
    private static Map<String, Image> images = new ConcurrentHashMap<>();
    // 跟踪当前单元格的加载任务
    private Task<Image> currentImageTask;

    @Override
    protected void updateItem(Comment item, boolean empty) {
        super.updateItem(item, empty);

        // 先取消当前可能正在运行的任务
        if (currentImageTask != null && !currentImageTask.isDone()) {
            currentImageTask.cancel(true);
            currentImageTask = null;
        }

        if (empty || item == null) {
            setText(null);
            setGraphic(null);
            // 清空ImageView,避免残留旧图片
            if (img != null) {
                img.setImage(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 cachedImage = images.get(item.getOwnerId());
            if (cachedImage != null) {
                img.setImage(cachedImage);
            } else {
                // 先清空ImageView,显示默认状态(比如占位图)
                img.setImage(null);

                // 创建新的加载Task
                currentImageTask = new Task<>() {
                    @Override
                    protected Image call() throws Exception {
                        // 如果任务被取消,直接返回null
                        if (isCancelled()) {
                            return null;
                        }
                        return new Image(item.getOwnerProfilePicUrl());
                    }
                };

                // 任务成功完成后的处理
                currentImageTask.setOnSucceeded(event -> {
                    // 首先检查任务是否被取消,以及当前单元格的item是否还是原来的那个
                    if (!currentImageTask.isCancelled() && getItem() != null && getItem().getOwnerId().equals(item.getOwnerId())) {
                        Image loadedImage = currentImageTask.getValue();
                        if (loadedImage != null) {
                            images.put(item.getOwnerId(), loadedImage);
                            img.setImage(loadedImage);
                        }
                    }
                    // 任务完成后清空引用
                    currentImageTask = null;
                });

                // 任务失败或取消的处理
                currentImageTask.setOnFailed(event -> {
                    currentImageTask.getException().printStackTrace();
                    currentImageTask = null;
                });

                // 启动任务
                new Thread(currentImageTask).start();
            }

            // 圆形裁剪逻辑保留
            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);
        }
    }
}

关键修改点说明

  • 用Task替代Thread:Task支持取消操作,当单元格复用后,我们可以取消旧的加载任务,避免它后续干扰新数据。
  • 任务完成前校验数据匹配:在onSucceeded里,我们检查当前单元格的item是否和任务对应的Comment是同一个(通过ownerId判断),只有匹配时才设置图片。
  • 替换为ConcurrentHashMap:保证多线程下缓存的安全性,避免并发修改异常。
  • 清空旧状态:当单元格变为空或绑定新数据时,先清空ImageView的内容,避免残留旧图片。

这样修改后,快速滚动列表时,旧的加载任务会被取消,即使有漏网的任务,也会因为数据不匹配而不会错误设置图片,完美解决你的问题!

内容的提问来源于stack exchange,提问作者sadeghpro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 04:01:33