学而实习之 不亦乐乎

Android:图片加载框架 Glide 进阶

2022-06-15 21:12:58

环境:Android 11


一、回调与监听

1、into()方法

Glide 的 into() 方法中是可以传入 ImageView 的。那么 into() 方法还可以传入别的参数吗?我们可以让 Glide 加载出来的图片不显示到 ImageView 上吗?答案是肯定的,这就需要用到自定义 Target 功能,通常只需要在两种 Target 的基础上去自定义就可以了,一种是 SimpleTarget,一种是 ViewTarget。

SimpleTarget,它是一种极为简单的 Target,我们使用它可以将 Glide 加载出来的图片对象 resource获取到,而不是像之前那样只能将图片在 ImageView 上显示出来,虽然我们下面实现的也是将图片在 ImageView 上显示,但是我们能拿到图片对象resource,有了这个对象之后就可以使用它进行任意的逻辑操作了。

SimpleTarget<Drawable> simpleTarget = new SimpleTarget<Drawable>() {
    @Override
    public void onResourceReady(Drawable resource, Transition<? super Drawable> transition) {
        imageView.setImageDrawable(resource);
    }
};

public void loadImage(View view) {
    Glide.with(this)
         .load("https://www.baidu.com/img/bd_logo1.png")
         .into(simpleTarget);
}

ViewTarget要复杂一点,比如我创建了一个自定义布局MyLayout,代码如下所示:

public class MyLayout extends LinearLayout{
    private ViewTarget<MyLayout,Drawable> viewTarget;

    public MyLayout(Context context) {
        this(context,null);
    }
    public MyLayout(Context context, @Nullable AttributeSet attrs) {
        this(context,attrs,0);
    }
    public MyLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        viewTarget = new ViewTarget<MyLayout,Drawable>(this) {
            @Override
            public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
                MyLayout myLayout = getView();
                myLayout.setBackgroundDrawable(resource);
            }
        };
    }

    public ViewTarget<MyLayout,Drawable> getTarget() {
        return viewTarget;
    }
}

在MyLayout的构造函数中,创建了一个 ViewTarget 的实例,并将 Mylayout 当前的实例 this 传了进去。ViewTarget中需要指定两个泛型,一个是View的类型,一个图片的类型(Drawable或Bitmap)。然后在onResourceReady()方法中,我们就可以通过getView()方法获取到MyLayout的实例,并调用它的任意方法了。比如说这里我们调用了setBackgroundDrawable()方法来将加载出来的图片作为MyLayout布局的背景图。
接下来看一下怎么使用这个Target吧,由于MyLayout中已经提供了getTarget()接口,我们只需要在加载图片的地方这样写就可以了:

public class MainActivity extends AppCompatActivity {
    private MyLayout myLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myLayout = (MyLayout) findViewById(R.id.background);
    }

    public void loadImage(View view) {
        String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
        Glide.with(this)
             .load(url)
             .into(myLayout.getTarget());
    }
}

就是这么简单,在into()方法中传入myLayout.getTarget()即可。

2、preload()方法

如果我希望提前对图片进行预加载,等真正需要加载图片的时候就直接从缓存中读取。Glide专门给我们提供了预加载的接口,也就是 preload() 方法,我们只需要直接使用就可以了。

preload()方法有两个方法重载,一个不带参数,表示将会加载图片的原始尺寸,另一个可以通过参数指定加载图片的宽和高。

这里有一点值得关注的是在Glide v3时我们如果使用了preload()方法,要将diskCacheStrategy的缓存策略指定成加载原始图片尺寸大小。因为preload()方法默认是预加载的原始图片大小,而into()方法则默认会根据ImageView控件的大小来动态决定加载图片的大小,所以同样into()方法也要将diskCacheStrategy的缓存策略指定成加载原始图片尺寸大小。否则,如果两者(预加载和显示)diskCacheStrategy的缓存策略不一致的话,很容易会造成我们在预加载完成之后再使用into()方法加载图片,却仍然还是要从网络上去请求图片这种现象。但是到了Glide v4就不需要指定磁盘缓存策略,因为Glide v4的默认磁盘缓存策略是DiskCacheStrategy.AUTOMATIC,Glide会根据图片资源智能地选择使用哪一种缓存策略,智能帮我们把这些工作做了,所以我们就不需要指定了。

preload()方法的用法也非常简单,直接使用它来替换into()方法即可,如下所示:

Glide.with(this)
     .load("https://www.domain.com/img/logo1.png")
     .preload();

调用了预加载之后,我们以后想再去加载这张图片就会非常快了,因为Glide会直接从缓存当中去读取图片并显示出来,代码如下所示:

Glide.with(this)
     .load("https://www.domain.com/img/logo1.png")
     .into(imageView);

3、submit()方法

如果想要去访问图片的缓存文件就需要用到submit()方法。和preload()方法类似,submit()方法也是可以替换into()方法的,不过submit()方法的用法明显要比preload()方法复杂不少。这个方法只会下载图片,而不会对图片进行加载。当图片下载完成之后,我们可以得到图片的存储路径,以便后续进行操作。
submit()方法有两个方法重载:
submit():下载原始尺寸的图片。
submit(int width, int height):下载指定尺寸的图片。

当调用了submit()方法后会立即返回一个FutureTarget对象,然后Glide会在后台开始下载图片文件。接下来我们调用FutureTarget的get()方法就可以去获取下载好的图片文件了,如果此时图片还没有下载完,那么get()方法就会阻塞住,一直等到图片下载完成才会有值返回,所以get()方法必须在子线程中执行。

演示代码如下所示:

private void downloadImage() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                String url = "http://p1.pstatp.com/large/166200019850062839d3";
                final Context context = getApplicationContext();
                FutureTarget<File> target = Glide.with(context)
                        .asFile()
                        .load(url)
                        .submit();
                final File imageFile = target.get();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this, imageFile.getPath(), Toast.LENGTH_LONG).show();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

首先,submit()方法必须要用在子线程当中,因为刚才说了FutureTarget的get()方法是会阻塞线程的,因此这里的第一步就是new了一个Thread。在子线程当中,我们先获取了一个Application Context,这个时候不能再用Activity作为Context了,因为会有Activity销毁了但子线程下载还没执行完这种可能出现。
接下来就是Glide的基本用法,只不过将into()方法替换成了submit()方法,并且还使用了一个asFile()方法来指定加载格式。submit()方法会返回一个FutureTarget对象,这个时候其实Glide已经开始在后台下载图片了,我们随时都可以调用FutureTarget的get()方法来获取下载的图片文件,只不过如果图片还没下载好线程会暂时阻塞住,等下载完成了才会把图片的File对象返回。
最后,我们使用runOnUiThread()切回到主线程,然后使用Toast将下载好的图片文件路径显示出来。

4、下载图片并保存到指定路径

submit方法可以下载图片并获取到图片缓存路径路径,但是不知道大家有没有这样的需要就是想让图片下载到指定的路径,因为这样我们之后快速使用这部分图片,也方便对这部分图片进行管理,同时不需要受限于Glide的磁盘缓存机制,因为如果由Glide自动管理缓存的话,当下载的图片超过设定的缓存大小,一些比如长时间不使用的图片就会被Glide删除,但是其实这张图片我们之后还是需要使用的,这就很尴尬了,所以我们需要把图片下载到我们指定的位置,由我们自己来进行管理。代码如下:

添加读写数据权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

编写下载图片线程类

public class DownloadImage implements Runnable{
    //要下载图片的url
    private String url;
    //Glide下载所需的Context,最好用ApplicationContext,
    //如果再用Activity作为Context了,可能会有Activity销毁了但子线程下载还没执行完这种情况出现。
    private Context context;
    //指定下载宽度,如果想下载原始宽带指定为0
    private int width;
    //指定下载高度,如果想下载原始高带指定为0
    private int height;
    //指定下载位置
    private File mFile;
    //下载完之后的回调
    private ImagedownLoadCallBack callBack;
    public interface ImagedownLoadCallBack{
        void onDownLoadSuccess(Bitmap bitmap);
        void onDownLoadFailed();
    }

    //用于回调到主线程的Handler,便于在回调回去的方法中执行UI操作
    private Handler mHandler;

    public DownloadImage(String url, Context context, int width, int height, File mFile, ImagedownLoadCallBack callBack) {
        this.url = url;
        this.context = context;
        this.width = width;
        this.height = height;
        this.mFile = mFile;
        this.callBack = callBack;
        mHandler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void run() {
        Bitmap bitmap = null;
        FileOutputStream fos = null;
        try {
            if (width==0){
                width = Target.SIZE_ORIGINAL;
            }
            if (height==0){
                height = Target.SIZE_ORIGINAL;
            }
            bitmap = Glide.with(context)
                    .asBitmap()
                    .load(url)
                    .submit(width,height)
                    .get();
            if (bitmap != null){
                //上级文件夹不存在则创建
                if (!mFile.getParentFile().exists()){
                    mFile.getParentFile().mkdirs();
                }
                //文件不存在则创建
                if (!mFile.exists()){
                    mFile.createNewFile();
                }
                fos = new FileOutputStream(mFile);
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
                fos.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bitmap != null && mFile.exists()) {
                final Bitmap finalBitmap = bitmap;
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callBack.onDownLoadSuccess(finalBitmap);
                    }
                });
            } else {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callBack.onDownLoadFailed();
                    }
                });
            }
        }
    }
}

在按钮点击方法中开启线程调用DownloadImage去下载图片到指定路径中

public void doClick(View view) {
        String url = "http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg";
        File mFile = new File(Environment.getExternalStorageDirectory()+File.separator+"Glide","glideDownload.png");
        DownloadImage downloadImage = new DownloadImage(url, getApplicationContext(), 600, 600, mFile, new DownloadImage.ImagedownLoadCallBack() {
            @Override
            public void onDownLoadSuccess(Bitmap bitmap) {
                Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onDownLoadFailed() {
                Toast.makeText(MainActivity.this, "下载失败", Toast.LENGTH_SHORT).show();
            }
        });
        new Thread(downloadImage).start();
    }

注意:这里submit(width,height)方法指定宽高后下载下来的图片尺寸并不是完全按我们指定尺寸来的,Glide是会做处理的,会保留原始尺寸的宽高比,以缩小比例较小边取我们指定的值的大小。比如上面我们下载的那张图片原始尺寸是1920 * 1080,我们指定尺寸为600 * 600,最后下载下来的图片尺寸是1067 * 600(宽是缩小比例较小边(1080/600<1920/600)取指定值600,长则按长宽比不变进行计算求得,计算过程:长=600 * (1920/1080)=1067)。除此之外我还试了下override()方法和preload()方法指定尺寸Glide是否也做了同样的处理,结果是是的。其实这如果源码读的很细应该是可以看出来的,不过这也确实有点太细节了,毕竟源码篇幅还是很多的,这种细节还是挺难注意到的,如果有精力可以找这部分去精读下,理解会更深。

5、listener()方法

其实listener()方法的作用非常普遍,它可以用来监听Glide加载图片的状态。举个例子,比如说我们刚才使用了preload()方法来对图片进行预加载,但是我怎样确定预加载有没有完成呢?还有如果Glide加载图片失败了,我该怎样调试错误的原因呢?答案都在listener()方法当中。
下面来看下listener()方法的基本用法吧,不同于刚才几个方法都是要替换into()方法的,listener()是结合into()方法一起使用的,当然也可以结合preload()方法一起使用。最基本的用法如下所示:

Glide.with(this)
    .load("http://domain.com/test.jpg")
    .listener(new RequestListener<Drawable>() {
        @Override
        public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
            return false;
        }
        @Override
        public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
            return false;
        }
    })
    .into(imageView);

这里我们在into()方法之前串接了一个listener()方法,然后实现了一个RequestListener的实例。其中RequestListener需要实现两个方法,一个onResourceReady()方法,一个onLoadFailed()方法。从方法名上就可以看出来了,当图片加载完成的时候就会回调onResourceReady()方法,而当图片加载失败的时候就会回调onLoadFailed()方法,onLoadFailed()方法中会将失败的GlideException参数传进来,这样我们就可以定位具体失败的原因了。
没错,listener()方法就是这么简单。不过还有一点需要处理,onResourceReady()方法和onLoadFailed()方法都有一个布尔值的返回值,返回false就表示这个事件没有被处理,还会继续向下传递,返回true就表示这个事件已经被处理掉了,从而不会再继续向下传递。举个简单点的例子,如果我们在RequestListener的onResourceReady()方法中返回了true,那么就不会再回调Target的onResourceReady()方法了。

二、图片变换功能

Glide从加载了原始图片到最终展示给用户之前,又进行了一些变换处理,从而能够实现一些更加丰富的图片效果,如图片圆角化、圆形化、模糊化等等。
添加图片变换的用法非常简单,我们只需要在RequestOptions中串接transforms()方法,并将想要执行的图片变换操作作为参数传入transforms()方法即可,如下所示:

RequestOptions options = new RequestOptions()
        .transforms(...);
Glide.with(this)
     .load(url)
     .apply(options)
     .into(imageView);

至于具体要进行什么样的图片变换操作,这个通常都是需要我们自己来写的。Glide已经内置了几种图片变换操作,比如CenterCrop、FitCenter、CircleCrop等。

ImageView默认的scaleType是FIT_CENTER,因此加载图片时会自动添加一个FitCenter的图片变换,而在这个图片变换过程中做了某些操作,会导致图片充满整个布局,可是有时我们不想要让它充满整个布局,怎么办呢?实际上,Glide给我们提供了专门的API来取消图片变换,使所有的图片变换(默认的和我们自己添加的)操作就全部失效了。取消API如下:

RequestOptions options = new RequestOptions()
                .dontTransform();

但所有的内置图片变换操作其实都不需要使用transform()方法,Glide为了方便我们使用直接提供了现成的API:

RequestOptions options = new RequestOptions()
        .centerCrop();

RequestOptions options = new RequestOptions()
        .fitCenter();

RequestOptions options = new RequestOptions()
        .circleCrop();

当然,这些内置的图片变换API其实也只是对transform()方法进行了一层封装而已,它们背后的源码仍然还是借助transform()方法来实现的。

如:对图片进行圆形化裁剪的,代码如下所示:

String url = "http://domain.com/img.png";
RequestOptions options = new RequestOptions()
        .circleCrop();
Glide.with(this)
     .load(url)
     .apply(options)
     .into(imageView);

可以看到,现在展示的图片是对原图进行圆形化裁剪后得到的图片。

当然,除了使用内置的图片变换操作之外,我们完全可以自定义自己的图片变换操作。理论上,在对图片进行变换这个步骤中我们可以进行任何的操作,你想对图片怎么样都可以。包括圆角化、圆形化、黑白化、模糊化等等,甚至你将原图片完全替换成另外一张图都是可以的。

如:glide-transformations。它实现了很多通用的图片变换效果,如裁剪变换、颜色变换、模糊变换等等,使得我们可以非常轻松地进行各种各样的图片变换。

glide-transformations的项目主页地址是 https://github.com/wasabeef/glide-transformations

下面我们就来体验一下这个库的强大功能吧。首先需要将这个库引入到我们的项目当中,在app/build.gradle文件当中添加如下依赖:

dependencies {
    implementation 'jp.wasabeef:glide-transformations:3.0.1'
}

可以对图片进行单个变换处理,也可以将多种图片变换叠加在一起使用。比如我想同时对图片进行模糊化和黑白化处理,代码如下:

String url = "http://guolin.tech/book.png";
RequestOptions options = new RequestOptions()
        .transforms(new BlurTransformation(), new GrayscaleTransformation());
Glide.with(this)
     .load(url)
     .apply(options)
     .into(imageView);

可以看到,同时执行多种图片变换的时候,只需要将它们都传入到transforms()方法中即可。