Android图库视图使用延迟加载适配器时出现"卡顿"现象

5
我想创建一个延迟加载适配器,用于与Gallery小部件一起使用。
也就是说,getView()立即返回一个ImageView,稍后某些机制会异步调用其setImageBitmap()方法。我通过创建一个“懒惰”的ImageView来实现这一点,它扩展了ImageView
public class GalleryImageView extends ImageView {

    // ... other stuff here ...

    public void setImage(final Looper looper, final int position) {

    final Uri uri = looper.get(position);
    final String path = looper.sharePath(position);

    new Thread(new Runnable() {

        @Override
        public void run() {
            GalleryBitmap gbmp = new GalleryBitmap(context, uri, path);
            final Bitmap bmp = gbmp.getBitmap(); // all the work is here
            handler.post(new Runnable() {

                @Override
                public void run() {
                    if (GalleryImageView.this.getTag().equals(uri)) {
                        setImageBitmap(bmp);
                    }
                }
            });
        }
    }).start();
}

}

当我在 Gallery 中缓慢滚动时,中心图像会一直跳到中心位置。很难准确地解释,但真的很烦人。我也尝试了同样的方法用于 spinner 适配器,在那里它完美地工作。
你有什么想法吗?

我有同样的问题。滚动是粘性的,整个页面在左右抖动后才会回到原来的位置。你找到解决方法了吗? - i_am_jorf
你使用的是哪个版本的Android?如果不使用自定义ImageView,这个问题是否会出现? 当所有图片都加载完毕后,问题是否停止出现? - E.Z. Hart
请详细描述一下您的“画廊”中的主要问题是什么。 - Tanmay Mandal
3个回答

12

我有一个答案!

当在 ImageView 上调用任何 setImage... 方法时,在内部会请求布局传递,例如,setImageBitmap() 如上所示被定义为这样。

public void setImageBitmap(Bitmap bm) {
    setImageDrawable(new BitmapDrawable(mContext.getResources(), bm));
}

调用的是

public void setImageDrawable(Drawable drawable) {
    if (mDrawable != drawable) {
        mResource = 0;
        mUri = null;
        updateDrawable(drawable);
        requestLayout(); //layout requested here!
        invalidate();
    }
}

这会使得画廊“捕捉”到当前最接近中央的图片,并将其居中显示。

为了避免这种情况,我让加载到画廊的视图具有显式的高度和宽度(以 dip 为单位),并使用一个忽略布局请求的 ImageView 子类。这样做的效果是,画廊仍然会进行一次布局传递,但不会在每次画廊中的图片更改时都执行此操作。我想这只有在画廊视图的高度和宽度设置为WRAP_CONTENT时才需要发生。请注意,由于setImageDrawable() 中仍然调用了 invalidate() 方法,因此设置时图像仍将被绘制。

下面是我的非常简单的 ImageView 子类!

/**
 * This class is useful when loading images (say via a url or file cache) into
 * ImageView that are contained in dynamic views (Gallerys and ListViews for
 * example) The width and height should be set explicitly instead of using
 * wrap_content as any wrapping of content will not be triggered by the image
 * drawable or bitmap being set (which is normal behaviour for an ImageView)
 * 
 */
public class ImageViewNoLayoutRefresh extends ImageView
{
    public ImageViewNoLayoutRefresh(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    public ImageViewNoLayoutRefresh(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    public ImageViewNoLayoutRefresh(Context context)
    {
        super(context);
    }

    @Override
    public void requestLayout()
    {
        // do nothing - for this to work well this image view should have its dims
        // set explicitly
    }
}

编辑:我应该提到使用 onItemSelected 的方法也可以工作,但是由于我需要在 flinging 正在进行时钩入它,所以我想出了上面的方法,我认为这是更灵活的方法。

编辑:我应该提到使用 onItemSelected 的方法也可以工作,但是由于我需要在flinging正进行时就插入钩子,所以我想出了上述方法,我认为这是一种更加灵活的方法。

1
太棒了。我尝试实现自己的ImageView,当设置一个可绘制对象时不调用requestLayout(),但你可以简单地重写requestLayout并什么也不做。谢谢。 - weakwire

12
解决方案是实现一种更智能的缩略图获取方法 - 在用户浏览列表时提取缩略图是无意义的。你需要像Romain Guy的Shelves应用程序中实现的那样。
为了获得最响应的相册,你需要实现某种形式的内存缓存并执行以下操作:
  • 仅当图片存在于内存缓存中时,才从getView中设置图片。设置一个标志指示该图片是否被设置或者是否需要下载。你还可以在SD卡和内部存储器中维护一个内存缓存,如果当前没有滑动,则显示低分辨率(inSampleSize设置为16或8)版本,这将在仅仅滚动时可见-高分辨率版本将在用户松开并停留在图片上时加载。
  • 添加 OnItemSelectedListener(初始化时确保调用 setCallbackDuringFling(false)),仅当用户手指离开时,下载所有需要下载的可见条目的新缩略图(通过使用 getFirstVisiblePositiongetLastVisiblePosition 找到视图范围)。
  • 当用户放开手指时,还要检查所选位置自手指按下以来是否发生变化,如果发生变化,是否由于你的OnItemSelectedListener发起了下载 - 如果没有启动下载,则启动。这是为了捕捉不发生突然移动的情况,因此在这种情况下始终使用手指按下调用OnItemSelected并不会做任何事情。我会使用一个处理程序来延迟开始下载你的画廊动画时间(确保在每次调用onItemSelected或收到ACTION_DOWN 事件时清除该处理程序发布的任何延迟消息)。
  • 在下载图像后,检查是否有任何可见视图请求此图像,然后更新这些视图。

请注意,默认的相册组件未正确实现视图回收(它假设适配器中的每个位置都有一个唯一的视图,并且在这些项目离开屏幕时也清除了回收器,使其变得不太有意义)。编辑:经过更多的查看,它并不是毫无意义 - 但从下一个/上一个视图的角度来看,它不是一个“回收器”,而是为了避免在布局更改期间对当前视图调用getView

这意味着传递给getView方法的convertView参数很可能为空,这意味着您将会充气大量的视图(这很昂贵) - 参见我在这里对“是否存在带有视图回收的Gallery替代品?”的问题的回答,了解一些提示。(PS:自那以后,我已修改了那段代码 - 我会为布局阶段和滚动阶段使用不同的回收站,在布局阶段,根据它们的位置将视图放置和检索到布局回收站中,并且如果从回收站获取的视图非空,则不要调用getView,因为它将是完全相同的视图;还要在布局阶段之后清除布局回收站 - 这使事情更加流畅)

PS:请在OnItemSelected中非常小心您所做的操作 - 除非是上面提到的地方,否则尽可能少地执行。例如,我在OnItemSelected中设置了一个TextView上面的一些文本。将此调用移动到更新缩略图的相同位置会产生明显的差异。


啊,谢谢。这似乎会让我朝着一个合理的方向前进。虽然此时此刻我可能会放弃这个工作项,只选择一定数量的静态图像,并完全取消滚动。至少对于这个版本来说。 :) - i_am_jorf
所以它不会加载任何图像,直到用户在画廊中选择了某些内容?如果他们看不到它们,用户怎么知道他们想要选择什么?我一定是漏掉了什么。似乎你希望在甩动手势停止时开始下载/加载图像任务,而不是在选择项目时。如何知道画廊滚动完成?画廊应用采取的方法是最初加载缩小的图像,这样更快,然后稍后加载完整分辨率版本。但如果问题是网络下载,则无法使用此方法。 - Jeffrey Blattman
在这种情况下,显示加载旋转器,直到图像下载完成。此外,画廊确实会创建不可见的视图,因此在onItemSelected中下载缩略图将为左右几个视图下载图像。如果您想知道画廊何时完成滚动 - 即当调用您的OnItemSelected侦听器且没有手指按下时 - 侦听器每次移动项目进入中心时都会被调用。 - Joseph Earl
是的,您需要在开始时调用 setSelection 来触发缩略图下载,或者手动启动缩略图下载。 - Joseph Earl
很棒的答案,我不明白这如何解决我在此页面上描述的布局传递问题,这正是导致我(以及我相信OP也是)出现卡顿的原因。 - Dori

2

虽然这是相同的问题,但我不认为这个解决方案会起作用。虽然它会在滚动期间(以及之后的250毫秒内)停止所有布局请求,但它不会在快速滑动期间停止布局请求(请参见Gallery中的onFling)。据我所见,由于所有相关方法和字段都是私有的(在Gallery中),所以没有办法告诉何时完成快速滑动,因此我认为你能做的最好的事情(除了遵循我在此页面上的答案)是从传递到此方法中的速度的某些函数中计算出来,但是看着FlingRunnable和Scroller类,这并不简单 :) - Dori

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接