Android 模仿手机 QQ 聊天页面图片列表——Picasso 实现

1,576 阅读4分钟
原文链接: www.jianshu.com

本文将通过模仿手机QQ聊天页面的图片列表,来学习如何使用Picasso展示图片,以及一些图片列表中一些问题的解决方案,完整示例代码地址,赠送一个 BaseRecyclerViewAdapter,不用谢!

先看一下手机QQ的效果


qq.gif

数据源

简单起见我们从相册中查询出一百张本地图片

new Thread(new Runnable() {
    @Override
    public void run() {
        Cursor mCursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA,
                        MediaStore.Images.Media.WIDTH, MediaStore.Images.Media.HEIGHT},
                MediaStore.Images.Media.MIME_TYPE + "=? OR " + MediaStore.Images.Media.MIME_TYPE + "=?",
                new String[] { "image/jpeg", "image/png" }, MediaStore.Images.Media._ID + " DESC");

        if (mCursor == null) return;
        // Take 100 images
        while (mCursor.moveToNext() && mImageList.size() < MAX_IMAGE) {
            long id = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Images.Media._ID));
            Log.i(TAG, "MediaStore.Images.Media_ID=" + id + "");

            String path = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DATA));
            int width = mCursor.getInt(mCursor.getColumnIndex(MediaStore.Images.Media.WIDTH));
            int height = mCursor.getInt(mCursor.getColumnIndex(MediaStore.Images.Media.HEIGHT));
            Image image = new Image(Uri.fromFile(new File(path)), width, height);
            mImageList.add(image);
        }
        mCursor.close();
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mImageListAdapter.addAllData(mImageList);
                mImageListAdapter.notifyDataSetChanged();
            }
        });
    }
}).start();

上面的代码从本地获取了最多100张图片,然后将图片信息存储到 ImageInfo 类中,最后通过一个list传递给adapter
其中ImageInfo类的主要属性如下

private final Uri mUri;
private int mWidth;
private int mHeight;
private boolean mNeedResize;

这里的属性 mNeedResize,用来表示是否需要重新计算图片宽高,下面会说到如何使用
到这里,我们的数据源就准备完毕了

解决图片乱跳问题

这是一个比较常见的问题,问题原因是加载图片是一个异步的过程,如果异步回来Bitmap无法对应正确ImageView,就会出现图片跳来跳去的情况。解决方法有很多,这里只说一种(选择困难症的福音)
通过给ImageView设置tag,绑定图片信息,Adapter中具体代码如下:

public void onBindViewHolder(final ImageListAdapter.ImageHolder holder, int position) {
    final ImageInfo imageInfo = mDataList.get(position);
    holder.mImageIv.setTag(imageInfo.getUri().getPath());

    mPicasso.load(imageInfo.getUri())
            .resize(imageInfo.getWidth(), imageInfo.getHeight())
            .config(Bitmap.Config.RGB_565)
            .centerCrop()
            .into(new Target() {
                @Override
                public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
                    if (holder.mImageIv.getTag().equals(imageInfo.getUri().getPath())) {
                        holder.mImageIv.setImageBitmap(bitmap);
                    }
                }
                ...
            });
}

在load图片之前,给ImageView设置一个tag,在回调回来之后通过tag判断是否为正确的ImageView,再进行图片的设置,这样就解决了图片乱跳的问题

解决图片尺寸问题

获取到图片的尺寸是高效显示图片的一个前提

从数据源获取

  1. 图片从服务器中获取,服务器应该同时返回图片的宽高等信息
  2. 图片从本地获取,例如上面的代码,可以看一下是否有相应的API可以查询到图片的宽高

然而很多时候,我们事先并不知道图片的尺寸

预先读取图片信息得到宽高

这种方式是在拿到图片的 Uri 之后,通过预加载,得到图片的宽高信息。

Picasso 举例来说(这个方法需要在非主线程中执行)

try {
    Bitmap bitmap = Picasso.with(context).load(uri).get();
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();
} catch (IOException e) {
    e.printStackTrace();
}

一般情况下,我们不会使用这种方式去获取图片的宽高。对于网络图片来说,预加载会把图片下载下来,但是我们并不知道用户是否会查看这张图片,这样就造成了流量的浪费。同时,对于本地图片来说图片不需要下载,不过读取图片和decode图片也会耗费CPU和内存。另一方面来说,如果使用的是Glide或者Fresco,会更加的麻烦,这两个库是通过异步的方式返回Bitmap,处理起来更加麻烦

动态计算图片信息得到宽高

个人比较推荐这种做法
具体思路如下:

  1. 给ImageView设置一个固定的宽高
  2. 在得到Bitmap之后,根据Bitmap的宽高重新计算ImageView的宽高

Adapter中代码如下:

public void onBindViewHolder(final ImageListAdapter.ImageHolder holder, int position) {
    if (mHeight == 0) return;

    final ImageInfo imageInfo = mDataList.get(position);
    holder.mImageIv.setImageResource(R.color.defaultImageSource);
    if (imageInfo.getHeight() != mHeight) {
        if (imageInfo.getHeight() == 0) {
            // set default size
            imageInfo.setHeight(mHeight);
            imageInfo.setWidth(mHeight);
            imageInfo.setNeedResize(true);
        } else {
            int width = mHeight * imageInfo.getWidth() / imageInfo.getHeight();
            imageInfo.setWidth(Math.min(width, mMaxWidth));
            imageInfo.setHeight(mHeight);
        }
    }

    resizeImageView(holder.mImageIv, imageInfo);
    holder.mImageIv.setTag(imageInfo.getUri().getPath());

    Target target = new Target() {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            if (holder.mImageIv.getTag().equals(imageInfo.getUri().getPath())) {
                if (imageInfo.isNeedResize()) {
                    // resize imageView after get bitmap info
                    imageInfo.setHeight(bitmap.getHeight());
                    imageInfo.setWidth(bitmap.getWidth());
                    resizeImageView(holder.mImageIv, imageInfo);
                    imageInfo.setNeedResize(false);
                }
                holder.mImageIv.setImageBitmap(bitmap);
            }
        }
        ...
    };
    mTargetMap.put(imageInfo.getUri().toString(), target);

    mPicasso.load(imageInfo.getUri())
            .resize(imageInfo.getWidth(), imageInfo.getHeight())
            .config(Bitmap.Config.RGB_565)
            .centerCrop()
            .into(mTargetMap.get(imageInfo.getUri().toString()));
}

private static void resizeImageView(ImageView imageView, ImageInfo imageInfo) {
    ViewGroup.LayoutParams layoutParams = imageView.getLayoutParams();
    layoutParams.height = imageInfo.getHeight();
    layoutParams.width = imageInfo.getWidth();
    imageView.setLayoutParams(layoutParams);
}

首先在发现ImageInfo中存储的宽高异常之后,给ImageView设置一个默认的宽高,并且标记当前的图片需要重新计算宽高。在 onBitmapLoaded 之后判断如果需要计算宽高,将bitmap中的宽高取出,重新配置ImageView的宽高并且设置对应的ImageInfo

最终效果


bitmap.gif

如果你在使用Picasso的时候碰到一些问题,这里获取能找到一些答案Picasso中一些问题的解决方法