前言

通过前面两篇的讲解,我们对于Glide的一些使用都有了基本了解,知道了使用Glide加载图片只需要一行代码即可:

Glide.with(this).load(url).into(imageView);

而在这一行代码的背后,Glide帮我们执行了成千上万行的逻辑。

项目需求:
1,将glide下载的缓存图片重新保存在本地系统相册
2,将Glide加载出来的图片对象获取到,在加载到ImageView上之前对该对象进行一些处理,比如宽高动态适配等,并且不是像之前那样只能将图片在ImageView上显示出来,任意的View都可以into。
3,Glide下载的图片缓存路径以及预加载

into()方法

使用了这么久的Glide,我们都知道into()方法中是可以传入ImageView的。那么into()方法还可以传入别的参数吗?我可以让Glide加载出来的图片不显示到ImageView上吗?答案是肯定的,这就需要用到自定义Target功能。

Target是什么?首先我们先看下 into(imageView)方法对应的源码:

Android Glide加载相册失败_加载


通过源码的最后一行我们看到,执行了into(imageview)方法之后其实在底层调用了into(glide.buildImageViewTarget(view, transcodeClass));方法,其中glide.buildImageViewTarget(view, transcodeClass)的作用就是创建一个Target对象,也就是说into()方法的执行其实是根据参数into()了一个Target,我们再看下glide.buildImageViewTarget()方法中的源码:

Android Glide加载相册失败_Android Glide加载相册失败_02


buildTarget()方法会根据传入的class参数来构建不同的Target对象,如果你在使用Glide加载图片的时候调用了asBitmap()方法,那么这里就会构建出BitmapImageViewTarget对象,否则的话构建的都是GlideDrawableImageViewTarget对象。至于上述代码中的DrawableImageViewTarget对象,这个通常都是用不到的,我们可以暂时不用管它。

通过上面的分析,我们已经知道了,into()方法还有一个接收Target参数的重载。即使我们传入的参数是ImageView,Glide也会在内部自动构建一个Target对象。而如果我们能够掌握自定义Target技术的话,就可以更加随心所欲地控制Glide的回调了。

我们先来看一下Glide中Target的继承结构图吧,如下所示:

Android Glide加载相册失败_Android Glide加载相册失败_03


可以看到,Target的继承结构还是相当复杂的,实现Target接口的子类非常多。不过你不用被这么多的子类所吓到,这些大多数都是Glide已经实现好的具备完整功能的Target子类,如果我们要进行自定义的话,通常只需要在两种Target的基础上去自定义就可以了,一种是SimpleTarget,一种是ViewTarget。

接下来我就分别以这两种Target来举例,学习一下自定义Target的功能。

SimpleTarget

顾名思义,它是一种极为简单的Target,我们使用它可以将Glide加载出来的图片对象获取到,而不是像之前那样只能将图片在ImageView上显示出来。

那么下面我们来看一下SimpleTarget的用法示例吧,其实非常简单:

SimpleTarget<GlideDrawable> simpleTarget = new SimpleTarget<GlideDrawable>() { 
    @Override 
    public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) {    
        imageView.setImageDrawable(resource); 
    } 
}; 
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(simpleTarget); 
}

不愧是SimpleTarget吧,短短几行代码就搞了,这里我们创建了一个SimpleTarget的实例,并且指定它的泛型是GlideDrawable,然后重写了onResourceReady()方法。在onResourceReady()方法中,我们就可以获取到Glide加载出来的图片对象resource了,有了这个对象之后你可以使用它进行任意的逻辑操作,这里我只是简单地把它显示到了ImageView上。
上面的代码执行的效果和直接into(ImageView imageView)的效果是一样的,但是通过上面的方式我们获得了图片对象的实例,然后就可以随意做更多的事情了。

当然,SimpleTarget中的泛型并不一定只能是GlideDrawable,如果你能确定你正在加载的是一张静态图而不是GIF图的话,我们还能直接拿到这张图的Bitmap对象,如下所示:

SimpleTarget<Bitmap> simpleTarget = new SimpleTarget<Bitmap>() { 
    @Override 
    public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { 

        // 比如获取到图片的Bitmap对象之后就可以拿到原始图片的宽高,然后进行动态适配
        int imageWidth = resource.getWidth(); // 获取到图片的宽度 
        int imageHeight = resource.getHeight(); // 获取到图片的高度 
        Log.d("test","原始图片的宽度为:"+imageWidth); 
        Log.d("test","原始图片的高度为:"+imageHeight);  
         // 如果宽高相等,拿到ImageView控件父容器的para对象
         RelativeLayout.LayoutParams para = (RelativeLayout.LayoutParams) holder.rl.getLayoutParams(); 
         // 动态设置imageview的宽高,然后再加载图片 
         // 获取到屏幕的宽度 
         DisplayMetrics dm = new DisplayMetrics(); 
         mActivity.getWindowManager().getDefaultDisplay().getMetrics(dm); // 获取手机屏幕的大小 
         para.height = (int)(dm.widthPixels/3*1.7); 
         para.width = para.height*imageWidth/imageHeight;
         // 加载图片
        imageView.setImageBitmap(resource); 
    } 
}; 
public void loadImage(View view) { 
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg"; 
    Glide.with(this) .
          load(url) .
          asBitmap() .
          into(simpleTarget); 
}

可以看到,这里我们将SimpleTarget的泛型指定成Bitmap,然后在加载图片的时候调用了asBitmap()方法强制指定这是一张静态图,这样就能在onResourceReady()方法中获取到这张图的Bitmap对象了。

ViewTarget

从刚才的继承结构图上就能看出,Glide在内部自动帮我们创建的GlideDrawableImageViewTarget就是ViewTarget的子类。只不过GlideDrawableImageViewTarget被限定只能作用在ImageView上,而ViewTarget的功能更加广泛,它可以作用在任意的View上。
那么如何实现作用在任意View上的功能呢?案例如下:
1.比如我创建了一个自定义布局MyLayout,如下所示:

public class MyLayout extends LinearLayout { 

    private ViewTarget<MyLayout, GlideDrawable> viewTarget; 
    
    public MyLayout(Context context, AttributeSet attrs) { 
        super(context, attrs); 
        viewTarget = new ViewTarget<MyLayout, GlideDrawable>(this) { 
            @Override 
            public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) { 
                MyLayout myLayout = getView(); 
                myLayout.setImageAsBackground(resource); 
            } 
        }; 
    } 
    
    public ViewTarget<MyLayout, GlideDrawable> getTarget() { 
        return viewTarget; 
    } 
    
    public void setImageAsBackground(GlideDrawable resource) { 
        setBackground(resource); 
    } 
}

在MyLayout的构造函数中,我们创建了一个ViewTarget的实例,并将Mylayout当前的实例this传了进去。ViewTarget中需要指定两个泛型,一个是View的类型,一个图片的类型(GlideDrawable或Bitmap)。然后在onResourceReady()方法中,我们就可以通过getView()方法获取到MyLayout的实例,并调用它的任意接口了。比如说这里我们调用了setImageAsBackground()方法来将加载出来的图片作为MyLayout布局的背景图。

接下来看一下怎么使用这个Target吧,由于MyLayout中已经提供了getTarget()接口,我们只需要在加载图片的地方这样写就可以了:

public class MainActivity extends AppCompatActivity { 

    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()即可。现在重新运行一下程序,效果如下图所示。

Android Glide加载相册失败_Android Glide加载相册失败_04

preload()方法:预加载

Glide加载图片虽说非常智能,它会自动判断该图片是否已经有缓存了,如果有的话就直接从缓存中读取,没有的话再从网络去下载。但是如果我希望提前对图片进行一个预加载,等真正需要加载图片的时候就直接从缓存中读取,不想再等待慢长的网络加载时间了,这该怎么办呢?
事实上,Glide专门给我们提供了预加载的接口,也就是preload()方法,我们只需要直接使用就可以了。

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

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

Glide.with(this)
     .load(url)
     .diskCacheStrategy(DiskCacheStrategy.SOURCE)
     .preload();

需要注意的是,我们如果使用了preload()方法,最好要将diskCacheStrategy的缓存策略指定成DiskCacheStrategy.SOURCE。因为preload()方法默认是预加载的原始图片大小,而into()方法则默认会根据ImageView控件的大小来动态决定加载图片的大小。因此,如果不将diskCacheStrategy的缓存策略指定成DiskCacheStrategy.SOURCE的话,很容易会造成我们在预加载完成之后再使用into()方法加载图片,却仍然还是要从网络上去请求图片这种现象。

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

Glide.with(this)
     .load(url)
     .diskCacheStrategy(DiskCacheStrategy.SOURCE)
     .into(imageView);

注意,这里我们仍然需要使用diskCacheStrategy()方法将硬盘缓存策略指定成DiskCacheStrategy.SOURCE,以保证Glide一定会去读取刚才预加载的图片缓存。

正如刚才所说,preload()方法有两个方法重载,你可以调用带参数的preload()方法来明确指定图片的宽和高,也可以调用不带参数的preload()方法,它会在内部自动将图片的宽和高都指定成Target.SIZE_ORIGINAL,也就是图片的原始尺寸。

downloadOnly()方法:只下载图片不加载图片

一直以来,我们使用Glide都是为了将图片显示到界面上。虽然我们知道Glide会在图片的加载过程中对图片进行缓存,但是缓存文件到底是存在哪里的,以及如何去直接访问这些缓存文件?我们都还不知道。

其实Glide将图片加载接口设计成这样也是希望我们使用起来更加的方便,不用过多去考虑底层的实现细节。但如果我现在就是想要去访问图片的缓存文件该怎么办呢?这就需要用到downloadOnly()方法了。

和preload()方法类似,downloadOnly()方法也是可以替换into()方法的,不过downloadOnly()方法的用法明显要比preload()方法复杂不少。顾名思义,downloadOnly()方法表示只会下载图片,而不会对图片进行加载。当图片下载完成之后,我们可以得到图片的存储路径,以便后续进行操作。

那么首先我们还是先来看下基本用法。downloadOnly()方法是定义在DrawableTypeRequest类当中的,它有两个方法重载,一个接收图片的宽度和高度,另一个接收一个泛型对象,如下所示:

downloadOnly(int width, int height)
downloadOnly(Y target)

这两个方法各自有各自的应用场景,其中downloadOnly(int width, int height)是用于在子线程中下载图片的,而downloadOnly(Y target)是用于在主线程中下载图片的。

1,downloadOnly(int width, int height)的用法

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

下面我们通过一个例子来演示一下吧,代码如下所示:

public void downloadImage(View view) { 
    new Thread(new Runnable() { 
        @Override 
        public void run() { 
            try { 
                String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg"; 
                final Context context = getApplicationContext(); 
				FutureTarget<File> target = Glide.with(context) 
												 .load(url) 
												 .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); 
				final File imageFile = target.get(); 
 				runOnUiThread(new Runnable() { 
                    @Override 
                    public void run() { 
					    Toast.makeText(context, imageFile.getPath(), Toast.LENGTH_LONG).show(); 
                    } 
                }); 
           } 
           catch (Exception e) { 
               e.printStackTrace(); 
           } 
       } 
    }).start(); 
}

这段代码稍微有一点点长,我带着大家解读一下。首先刚才说了,downloadOnly(int width, int height)方法必须要用在子线程当中,因此这里的第一步就是new了一个Thread。在子线程当中,我们先获取了一个Application Context,这个时候不能再用Activity作为Context了,因为会有Activity销毁了但子线程还没执行完这种可能出现。

接下来就是Glide的基本用法,只不过将into()方法替换成了downloadOnly()方法。downloadOnly()方法会返回一个FutureTarget对象,这个时候其实Glide已经开始在后台下载图片了,我们随时都可以调用FutureTarget的get()方法来获取下载的图片文件,只不过如果图片还没下载好线程会暂时阻塞住,等下载完成了才会把图片的File对象返回。

最后,我们使用runOnUiThread()切回到主线程,然后使用Toast将下载好的图片文件路径显示出来。

现在重新运行一下代码,效果如下图所示:

Android Glide加载相册失败_ide_05


这样我们就能清晰地看出来图片完整的缓存路径是什么了。

之后我们可以使用如下代码去加载这张图片,图片就会立即显示出来,而不用再去网络上请求了:

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

需要注意的是,这里必须将硬盘缓存策略指定成DiskCacheStrategy.SOURCE或者DiskCacheStrategy.ALL,否则Glide将无法使用我们刚才下载好的图片缓存文件。

现在重新运行一下代码,效果如下图所示:

Android Glide加载相册失败_加载_06


可以看到,图片的加载和显示是非常快的,因为Glide直接使用的是刚才下载好的缓存文件。

2,downloadOnly(Y target)方法的用法

其实downloadOnly(int width, int height)方法必须使用在子线程当中,最主要还是因为它在内部帮我们自动创建了一个RequestFutureTarget,是这个RequestFutureTarget要求必须在子线程当中执行。而downloadOnly(Y target)方法则要求我们传入一个自己创建的Target,因此就不受RequestFutureTarget的限制了。

但是downloadOnly(Y target)方法的用法也会相对更复杂一些,因为我们又要自己创建一个Target了,而且这次必须直接去实现最顶层的Target接口,比之前的SimpleTarget和ViewTarget都要复杂不少。

那么下面我们就来实现一个最简单的DownloadImageTarget吧,注意Target接口的泛型必须指定成File对象,这是downloadOnly(Y target)方法要求的,代码如下所示:

public class DownloadImageTarget implements Target<File> { 

    private static final String TAG = "DownloadImageTarget"; 
    
    @Override 
    public void onStart() { 
    
    } 
    
    @Override 
    public void onStop() { 
    } 
    
    @Override 
    public void onDestroy() { 
    } 
    
    @Override 
    public void onLoadStarted(Drawable placeholder) { 
    } 
    
    @Override 
    public void onLoadFailed(Exception e, Drawable errorDrawable) { 
    } 
    
    @Override 
    public void onResourceReady(File resource, GlideAnimation<? super File> glideAnimation) { 
        Log.d(TAG, resource.getPath()); 
    } 
    
    @Override 
    public void onLoadCleared(Drawable placeholder) { 
    } 
    
    @Override
    public void getSize(SizeReadyCallback cb) { 
        cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); 
    } 
    
    @Override 
    public void setRequest(Request request) { 
    } 
    
    @Override 
    public Request getRequest() { 
        return null; 
    } 
}

由于是要直接实现Target接口,因此需要重写的方法非常多。这些方法大多是数Glide加载图片生命周期的一些回调,我们可以不用管它们,其中只有两个方法是必须实现的,一个是getSize()方法,一个是onResourceReady()方法。

通过对Glide的源码进行解析,我们知道Glide在开始加载图片之前会先计算图片的大小,然后回调到onSizeReady()方法当中,之后才会开始执行图片加载。而这里,计算图片大小的任务就交给我们了。只不过这是一个最简单的Target实现,我在getSize()方法中就直接回调了Target.SIZE_ORIGINAL,表示图片的原始尺寸。

然后onResourceReady()方法我们就非常熟悉了,图片下载完成之后就会回调到这里,我在这个方法中只是打印了一下下载的图片文件的路径。

这样一个最简单的DownloadImageTarget就定义好了,使用它也非常的简单,我们不用再考虑什么线程的问题了,而是直接把它的实例传入downloadOnly(Y target)方法中即可,如下所示:

public void downloadImage(View view) { 
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg"; 
    Glide.with(this) 
         .load(url) 
         .downloadOnly(new DownloadImageTarget()); 
}

现在重新运行一下代码并点击Download Image按钮,然后观察控制台日志的输出,结果如下图所示:

Android Glide加载相册失败_安卓项目实战系列_07


这样我们就使用了downloadOnly(Y target)方法同样获取到下载的图片文件的缓存路径了。

将glide下载的缓存图片保存在本地

在这里插入代码片项目需求:使用glide加载显示网络图片,然后在图片上长按出现保存到本地选项,点击可以保存到本地。
解决方案:因为是网络图片,所以保存到本地是否会有下载的过程??
Glide加载显示图片已经将图片下载并且缓存到了本地,我们只需将该缓存重新保存在手机系统相册即可,那么如何将glide的缓存
保存到本地呢?
          实现的方法如下:(需要写入SD卡的权限)
          
    // 思路:从glide中获取到指定url的bitmap对象,然后将其转换成二进制再写入本地,获取bitmap必须放在子线程中来完成,
    @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
    void saveImage(final String s1) {
        //将ImageView中的图片转换成Bitmap
        /*iv.buildDrawingCache();
        Bitmap bitmap = iv.getDrawingCache();*/
        new Thread(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = null;
                try {
                    bitmap = Glide.with(ImageActivity.this)
                            .load(image)
                            .asBitmap()
                            .into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
                            .get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
                if(bitmap != null){
                    //将Bitmap 转换成二进制,写入本地
                    ByteArrayOutputStream stream = new ByteArrayOutputStream();
                    bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
                    byte[] byteArray = stream.toByteArray();
                    File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/yiyou");
                    Log.d("test","dir的路径地址为:"+dir.getPath());
                    if (!dir.isFile()) {
                        dir.mkdir();
                    }
                /*// 获取当前时间戳字符串,精确到毫秒
                SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMddHHmm"); //精确到毫秒
                String suffix = fmt.format(new Date());*/
                    String s = s1;
                    File file = new File(dir, s);
                    Message msg = new Message();
                    msg.what = 0x22;
                    msg.obj = file.getPath();
                    mHandler.sendMessage(msg);
                    try {
                        FileOutputStream fos = new FileOutputStream(file);
                        fos.write(byteArray, 0, byteArray.length);
                        fos.flush();
                        //用广播通知相册进行更新相册
                        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                        Uri uri = Uri.fromFile(file);
                        intent.setData(uri);
                        sendBroadcast(intent);
//                    Toast.makeText(ImageActivity.this, "图片保存成功,保存路径为"+, Toast.LENGTH_SHORT).show();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

listener()方法:监听Glide加载图片的状态

其实listener()方法的作用非常普遍,它可以用来监听Glide加载图片的状态。举个例子,比如说我们刚才使用了preload()方法来对图片进行预加载,但是我怎样确定预加载有没有完成呢?还有如果Glide加载图片失败了,我该怎样调试错误的原因呢?答案都在listener()方法当中。

首先来看下listener()方法的基本用法吧,不同于刚才几个方法都是要替换into()方法的,listener()是结合into()方法一起使用的,当然也可以结合preload()方法一起使用。最基本的用法如下所示:

public void loadImage(View view) { 
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg"; 
    Glide.with(this) 
         .load(url) 
         .listener(new RequestListener<String, GlideDrawable>() { 
             @Override 
             public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) { 
                 return false; 
             } 
             @Override 
             public boolean onResourceReady(GlideDrawable resource, String model, 
                 Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) { 
                 return false; 
             } 
         }) 
         .into(imageView); 
}

这里我们在into()方法之前串接了一个listener()方法,然后实现了一个RequestListener的实例。其中RequestListener需要实现两个方法,一个onResourceReady()方法,一个onException()方法。从方法名上就可以看出来了,当图片加载完成的时候就会回调onResourceReady()方法,而当图片加载失败的时候就会回调onException()方法,onException()方法中会将失败的Exception参数传进来,这样我们就可以定位具体失败的原因了。

没错,listener()方法就是这么简单。不过还有一点需要处理,onResourceReady()方法和onException()方法都有一个布尔值的返回值,返回false就表示这个事件没有被处理,还会继续向下传递,返回true就表示这个事件已经被处理掉了,从而不会再继续向下传递。举个简单点的例子,如果我们在RequestListener的onResourceReady()方法中返回了true,那么就不会再回调Target的onResourceReady()方法了。

如果我们在onException()方法中返回了true,那么Glide请求中使用error(int resourceId)方法设置的异常占位图就失效了。