最近有个群友提了一个挺有意思的问题,当使用 Glide 加载图片时,假如不设置 ImageView 的宽高,会出现显示不可控的情况。

什么意思呢?我们来复现一下他的情况。

这位群友为我们提供了一张表情包图片:

Sticker

要不是我们是正儿八经的学术讨论群,可能这位群友已经被锤死了…

当然,这个表情包是使用一个 URL 提供的,实际上只是相当于模拟从后端获取图片时的场景,因为一般这种情况后端是不会特意将图片尺寸提供给我们的,其实你也可以将图片放在 Assets Folder 中,同样能够复现,但别放在 /res/drawable 或者 /res/mipmap 文件夹内,因为系统会根据屏幕分辨率对其进行缩放。

我先把这张图片下载下来,看一下它的实际尺寸:

图片尺寸信息

可以看到这张图片的尺寸为 100 × 97 像素,并不大,我们在项目中实际加载看看。

首先随便写一个简单的布局用于加载这张图片:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/background_dark"
    tools:context=".MainActivity">
    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

因为『Android Studio』新建项目时默认就采用了约束布局,那我这里就不做修改了,让 ImageView 置于屏幕的中央,因为群友提到不设置宽高,所以这里宽高我都定为 wrap_content,另外由于表情包是白底的,所以我给了一个深色的背景以更好的看出其显示边界。

为了对比,我要采用两种加载方式,一种是原生 API 的加载,另一种就是 Glide

原生加载方式不复杂,通过 URL 获取流并将其解码为 Bitmap 即可:

public class MainActivity extends AppCompatActivity {

    String url = "http://wx4.sinaimg.cn/bmiddle/b64da6adly1gjbwvv4es7j202s02p0sk.jpg";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        final ImageView imageView = findViewById(R.id.sticker);
        new Thread(() -> {
            try {
                final Bitmap bitmap = BitmapFactory.decodeStream(new URL(url).openStream());
                runOnUiThread(() -> imageView.setImageBitmap(bitmap));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

需要注意的是由于我们是加载网络图片,尽管我们并没有显式的写出网络请求的代码,它实际上是做了网络请求的,所以我们不能在主线程中执行,而将图片加载到 ImageView 时是 UI 操作,又需要切回到主线程。

Glide 的加载方式更加简单,之前『一次实战爱上「Glide」』一文也做了详细的介绍,而且因为 Glide 内部已经帮我们做了处理,所以我们不再需要关心线程的问题了,一行代码搞定:

public class MainActivity extends AppCompatActivity {

    String url = "http://wx4.sinaimg.cn/bmiddle/b64da6adly1gjbwvv4es7j202s02p0sk.jpg";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        final ImageView imageView = findViewById(R.id.sticker);
        Glide.with(this).load(url).into(imageView);
    }
}

结果的确如群友所说,Glide 加载的图片显示的并不是实际尺寸:

图片实际大小 Glide 加载出来的大小

我们监听一下 Glide 加载出来的尺寸:

Glide.with(this)
        .asBitmap()
        .load(url)
        .listener(new RequestListener<Bitmap>() {
            @Override
            public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
                return false;
            }
            @Override
            public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
                int width = resource.getWidth();
                int height = resource.getHeight();
                Log.d(TAG, "Width: " + width);
                Log.d(TAG, "Height: " + height);
                return false;
            }
        })
        .into(imageView);

好家伙,加载的尺寸为 2139 × 2075 像素,这明显不对劲呀。其实这并不是图片的尺寸大小,而是加载图片后的 ImageView 的大小,也就是图片显示时的大小。

那我们能否获取到图片的实际尺寸呢?

当然可以。

Glide.with(this)
        .asBitmap()
        .load(url)
        .into(new SimpleTarget<Bitmap>() {
            @Override
            public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                int realWidth = resource.getWidth();
                int realHeight = resource.getHeight();
                Log.d(TAG, "Real Width: " + realWidth);
                Log.d(TAG, "Real Height: " + realHeight);
            }
        });

这里获取到的尺寸就是实际的 100 × 97 像素了。

然后你可能会发现,这个获取实际尺寸的方法是在 into() 里面的,那没有把 ImageView 实例传进去,它不能够加载我们的图片啊。

其实我们只需要用回最原始的方法即可:

Glide.with(this)
        .asBitmap()
        .load(url)
        .into(new SimpleTarget<Bitmap>() {
            @Override
            public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                imageView.setImageBitmap(resource);
            }
        });

当然你不嫌麻烦的话也可以用 Glide 重新加载一遍:

Glide.with(this)
        .asBitmap()
        .load(url)
        .into(new SimpleTarget<Bitmap>() {
            @Override
            public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                Glide.with(imageView)
                        .load(resource)
                        .override(resource.getWidth(), resource.getHeight())
                        .into(imageView);
            }
        });

说实话这种应用场景我至今还没有遇到过,总感觉 ImageView 不确定尺寸的话很容易会产生屏幕适配问题。

但转念一想,在微信聊天界面的表情包似乎是可以根据图片尺寸显示的,应该存在一个阈值,当表情包的尺寸大于等于这个阈值时,按照这个阈值缩放显示,当表情包的尺寸小于这个阈值时,就按照实际大小显示。

由于上面的场景我们只处理了小图加载的问题,因此上面的方法不能够直接应用到项目中,否则加载大图的时候也会产生不可控,更甚的是大图还容易造成 OOM。

而另一种场景,一些 App 有省流模式的功能,可以只显示文字而不加载图片,但是其显示的占位图和实际图片的宽高是一致的,这种就不能使用上面的方法来处理了,因为使用了上面的方法就会去请求图片,也就达不到省流的目的,理论上应由后端将图片的尺寸返回,再由客户端生成占位图。