如何在 RecyclerView 中显示“重”图像?

Posted

技术标签:

【中文标题】如何在 RecyclerView 中显示“重”图像?【英文标题】:How to display "heavy" images in a RecyclerView? 【发布时间】:2020-12-06 01:35:52 【问题描述】:

我需要在 RecyclerView 中显示一些图像。 当用户滚动列表时,这些图像应该“即时”呈现。渲染每张图片需要很长时间:50-500ms。在图像显示给用户之前,会显示一个进度条。

由于渲染时间长,这部分被放置到 AsyncTask 中。

请看下面的代码:


class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> 

    class ViewHolder extends RecyclerView.ViewHolder 
        ImageView myImg;
        ProgressBar myProgressBar;

        public ViewHolder(View view) 
            super(itemView);
            myImg = view.findViewById(R.id.myImg);
            myProgressBar = view.findViewById(R.id.myProgressBar);
        
    

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) 
        holder.myProgressBar.setVisibility(View.VISIBLE);
        AsyncTask.execute(() ->   
            Bitmap bitmap = longRenderingFunction();
            holder.myImg.post(() -> 
                holder.myImg.setImageBitmap(bitmap);
                holder.myProgressBar.setVisibility(View.GONE);

如果用户正在缓慢滚动列表,例如每秒 1-2 个屏幕,图像加载正确。有时可以注意到进度条,它很快就消失了。

但如果用户滚动速度非常快,图像会出现然后被替换,有时会被替换两次:

总的来说,在快速滚动时很明显:

调用多个onBindViewHolder,并启动每个可回收物品ViewHolder多个AsyncTasks。 即使是针对同一项目触发了新的onBindViewHolder,旧的AsyncTask 也会继续运行 然后AsyncTasks 为同一个ViewHolder 一个接一个完成。 每个AsyncTask 将自己的生成位图放到ImageView

我的意图是:

最低:不要从过时的setImageBitmapAsyncTasks 最大:尽快停止已经过时的AsyncTasks以节省系统资源

我希望听到一些提示或解决此问题的方法。

【问题讨论】:

你在正确的轨道上。只需取消AsyncTask。棘手的部分是在哪里保存对AsyncTask 的引用以让您取消它。我认为您应该在适配器中创建一个HashMap&lt;int, AsyncTask&gt; 来存储每个位置的每个任务。 虽然您可以自行管理,但我强烈建议您查看诸如 Glide 之类的库,这些库旨在为您在加载图像时管理大量繁重的工作。 @CarsonH​​olzheimer 取消任务是个好主意。在这里,我仍然缺少在 android 中执行此操作的正确方法。我的渲染器被打包成一个非常紧凑的可运行文件。 AsyncTask.execute 也是一个紧凑的结构。我想保持紧凑。如果我创建一个正确 AsyncTask,那么它就会变成一个带有doInBackground 等的怪物。所以我在这里考虑ExecutorService,它可以返回Future&lt;&gt;,并且可以取消任何时间。你怎么看? 【参考方案1】:

这是我想出的解决方案。

停止闪烁

第一部分非常简单:

引入了一个简单的索引:

int loadingPosition;

onBindViewHolder的开头保存当前位置:

holder.loadingPosition = position;

在切换位图之前,会根据上次启动的位置检查当前位置。如果一个新的任务已经开始了,索引是事先改变的,那么图片就不会更新:

if (holder.loadingPosition == position) 
    holder.myImg.setImageBitmap(bitmap);

提高性能

为了防止任务在不需要时进一步运行,引入了Future 对象。它允许管理已启动的任务,而AsyncTask 不允许:

Future<?> future;

通过ExecutorService而不是AsyncTask启动异步任务:

holder.future = executor.submit(() ->  ...

停止正在运行的任务,如果它仍在运行:

if ((holder.future != null) && (!holder.future.isDone()))
    holder.future.cancel(true);

完整代码

整个代码如下:

class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> 

    private ExecutorService executor = Executors.newSingleThreadExecutor();

    class ViewHolder extends RecyclerView.ViewHolder 
        ImageView myImg;
        ProgressBar myProgressBar;
        int loadingPosition;
        Future<?> future;

        public ViewHolder(View view) 
            super(itemView);
            myImg = view.findViewById(R.id.myImg);
            myProgressBar = view.findViewById(R.id.myProgressBar);
        
    

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) 
        holder.loadingPosition = position;
        if ((holder.future != null) && (!holder.future.isDone()))
            holder.future.cancel(true);
        holder.myProgressBar.setVisibility(View.VISIBLE);
        holder.future = executor.submit(() -> 
            Bitmap bitmap = longRenderingFunction();
            holder.myImg.post(() -> 
                if (holder.loadingPosition == position) 
                    holder.myImg.setImageBitmap(bitmap);
                    holder.myProgressBar.setVisibility(View.GONE);

也许同时使用这两种方法有点矫枉过正,但它提供了一定程度的保险。

【讨论】:

以上是关于如何在 RecyclerView 中显示“重”图像?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Kotlin 中显示来自 RecyclerView 的全屏图像?

如何在 RecyclerView 中显示乘法布局 [重复]

无法在 Recyclerview 中显示图像 - 文本已成功显示

Android:在 Recyclerview 中更改图像旋转

Recyclerview:如何将一些帖子与图像和没有图像混合? [复制]

Recyclerview 不显示图像