Android 当不再可见时停止异步加载列表元素

Android stop async loading list element when no more visible

如许多教程和 Android developers pages too 中所述,我正在使用异步任务将图像作为缩略图加载到 ListView 中。该任务从 SD 卡加载全尺寸图像,调整其大小并将其放入列表项布局的 ImageView 中。

一切正常,除了在快速上下滚动列表后,单个可见元素的图像在获得正确的图像之前用不同的图像更新了两到三次。

在我看来,此行为与 recycling views in ListView 有关:当异步任务准备好在引用的视图中注入列表的元素 X 图像时,视图本身可能已经被回收和分配列出元素-Y.

我意识到我的代码有些 丑陋 ,例如,我既没有为缩略图实现易失缓存也没有实现持久缓存(针对下一个版本),但是这个问题只会被部分隐藏。

我找到了 possible solution using libraries for loading image,但我正在调查如何在我的代码中修复,因为这个问题更普遍地与将异步代码与列表结合使用有关,今天我处理图像,但明天我'在加载文本或任何其他类型的数据时可能会遇到同样的问题。

我正在研究的可能解决方案是:

  1. 通知 asynctask 关于它正在处理的列表的项目,一旦加载图像仅当项目可见时更新它
  2. 当列表从元素中分离视图时(我如何检测到这一点?),stop the asynctask
  3. 重写列表的 OnScrollListener 以检查 OnScroll 事件何时从可见项目列表中退出并停止其异步任务(如果存在)。

这些解决方案中的一种是否可行,或者您建议使用不同的解决方案?

这是我的列表适配器(我在片段中使用可扩展列表):

@Override
public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {

    Log.i(TAG, "ExpandableListAdapter.getChildView entered, getting view n. " + groupPosition + "-" + childPosition + ", convertview = " + convertView);

    ViewHolder holder;

    if (convertView == null) {

        convertView = inf.inflate(R.layout.selfie_list_item_layout, parent, false);
        holder = new ViewHolder();

        holder.date = (TextView) convertView.findViewById(R.id.selfieListItemDateView);
        holder.place = (TextView) convertView.findViewById(R.id.selfieListItemPlaceView);
        holder.thumb = (ImageView) convertView.findViewById(R.id.selfieListItemThumbView);
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }

    Integer mChildIndex = (Integer) getChild(groupPosition, childPosition);
    SelfieItem mChildObj = selfies.get(mChildIndex);
    String mText = mChildObj.getDate().toString();
    holder.date.setText(mText);
    holder.thumb.setImageBitmap(BitmapFactory.decodeResource(convertView.getResources(), R.drawable.selfie_place_holder));


    File selfieFile = mChildObj.getFile();

    new LoadSelfieTask(mFragmentContext).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, selfieFile, holder.thumb);

    return convertView;
}

下面是异步代码:

   @Override
    protected Bitmap doInBackground(Object... params) {

        File selfieFile = (File)params[0];
        Bitmap mySrcBitmap = null;
        Bitmap myDestBitmap = null;

        if (selfieFile.exists()) {

            mySrcBitmap = BitmapFactory.decodeFile(selfieFile.getAbsolutePath());

        }

        if (mySrcBitmap != null) {

            // Get info about view to be updated
            mImageViewToBeUpdated = (ImageView) params[1];
            mImageHeight = mImageViewToBeUpdated.getHeight();
            mImageWidth = mImageViewToBeUpdated.getWidth();

            if (mySrcBitmap.getWidth() >= mySrcBitmap.getHeight()){

                myDestBitmap = Bitmap.createBitmap(
                        mySrcBitmap,
                        mySrcBitmap.getWidth()/2 - mySrcBitmap.getHeight()/2,
                        0,
                        mySrcBitmap.getHeight(),
                        mySrcBitmap.getHeight()
                );

            }else{

                myDestBitmap = Bitmap.createBitmap(
                        mySrcBitmap,
                        0,
                        mySrcBitmap.getHeight()/2 - mySrcBitmap.getWidth()/2,
                        mySrcBitmap.getWidth(),
                        mySrcBitmap.getWidth()
                );
            }

            mySrcBitmap = Bitmap.createScaledBitmap(myDestBitmap, mImageWidth, mImageHeight, true);

        }

        return mySrcBitmap;

    }

提前感谢您的回答。

如果你需要快速的东西,试试 Picasso http://square.github.io/picasso/

您的代码是为每一行创建一个 AsyncTask 以从外部存储中获取图像。当您滚动回行项目时,您将创建另一个 AsynTask 以再次获取相同的图像。我建议你创建一个缓存来存储 AsynTask 的结果,并有适当的缓存替换策略。

正在取消 AsyncTasks?这是一个好主意吗?我发现它多次不起作用并且 postExecute() 总是被调用,因此您的图像可能仍会放置在列表视图中,可能是一个错误的图像会进一步弄乱您的场景..

我在this Android Developers Training Lesson的示例代码中找到了问题的答案。

在ImageWorker.java中我们可以找到启动后台任务并加载图片的方法:

/**
 * Load an image specified by the data parameter into an ImageView (override
 * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and
 * disk cache will be used if an {@link ImageCache} has been added using
 * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the
 * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask}
 * will be created to asynchronously load the bitmap.
 *
 * @param data The URL of the image to download.
 * @param imageView The ImageView to bind the downloaded image to.
 */
public void loadImage(Object data, ImageView imageView) {
    if (data == null) {
        return;
    }

    BitmapDrawable value = null;

    if (mImageCache != null) {
        value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
    }

    if (value != null) {
        // Bitmap found in memory cache
        imageView.setImageDrawable(value);
    } else if (cancelPotentialWork(data, imageView)) {
        //BEGIN_INCLUDE(execute_background_task)
        final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(mResources, mLoadingBitmap, task);
        imageView.setImageDrawable(asyncDrawable);

        // NOTE: This uses a custom version of AsyncTask that has been pulled from the
        // framework and slightly modified. Refer to the docs at the top of the class
        // for more info on what was changed.
        task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR);
        //END_INCLUDE(execute_background_task)
    }
}

对 AsyncTask 实例的引用保存在 AsyncDrawable 中 class:

/**
 * A custom Drawable that will be attached to the imageView while the work is in progress.
 * Contains a reference to the actual worker task, so that it can be stopped if a new binding is
 * required, and makes sure that only the last started worker process can bind its result,
 * independently of the finish order.
 */
private static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

在后台 activity 结束时,AsyncTask 验证它是否是最后一个 "attached" 它必须更新的视图,并且仅当没有其他任务 [=] 时才执行更新22=] 到视图

    /**
     * Returns the ImageView associated with this task as long as the ImageView's task still
     * points to this task as well. Returns null otherwise.
     */
    private ImageView getAttachedImageView() {
        final ImageView imageView = imageViewReference.get();
        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

        if (this == bitmapWorkerTask) {
            return imageView;
        }

        return null;
    }


   /**
     * Once the image is processed, associates it to the imageView
     */
    @Override
    protected void onPostExecute(BitmapDrawable value) {
        //BEGIN_INCLUDE(complete_background_work)
        // if cancel was called on this task or the "exit early" flag is set then we're done
        if (isCancelled() || mExitTasksEarly) {
            value = null;
        }

        final ImageView imageView = getAttachedImageView();
        if (value != null && imageView != null) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "onPostExecute - setting bitmap");
            }
            setImageDrawable(imageView, value);
        }
        //END_INCLUDE(complete_background_work)
    }