Android 图片加载框架

Android 中有几个比较有名的图片加载框架,Universal ImageLoader、Picasso、Glide和Fresco。

 

Picasso

  • 处理Adapter 中ImageView的回收和取消下载。(在adapter中需要取消已经不在视野范围的ImageView图片资源的加载,否则会导致图片错位,Picasso已经解决了这个问题。)
  • 使用最小的内存来做复杂的图片变换,比如高斯模糊,圆角、圆形等处理。(使用复杂的图片压缩转换来尽可能的减少内存消耗
  • 自动帮我们缓存图片,内存和磁盘缓存。(自带内存和硬盘二级缓存功能)

1、添加依赖

要使用Picasso,首先我们要添加版本依赖,去官网或者Github 看一下当前的最新版本(截止本文最新版本为2.5.2),然后在build.gradle中添加依赖:

   compile 'com.squareup.picasso:picasso:2.5.2'

2、加载显示图片

将Picasso添加到项目之后,我们就可以用它来加载图片了,使用方法非常简单:

 Picasso.with(this)
        .load("http://ww3.sinaimg.cn/large/610dc034jw1fasakfvqe1j20u00mhgn2.jpg")
        .into(mImageView);

只需要一行代码就完成了加载图片到显示的整个过程,链式调用,非常简洁,其实有三步,一次调用了三个方法:

  • with(Context) 获取一个Picasso单例,参数是一个Context上下文
  • load(String) 调用load 方法加载图片
  • into (ImageView) 将图片显示在对应的View上,可以是ImageView,也可以是实现了Target j接口的自定义View。

上面演示了加载一张网络图片,它还支持其它形式的图片加载,加载文件图片,加载本地资源图片,加载一个Uri 路径给的图片,提供了几个重载的方法:

load(Uri uri) 加载一个以Uri路径给的图片:

Uri uri = Uri.parse(ANDROID_RESOURCE + context.getPackageName() + FOREWARD_SLASH + resourceId)

Picasso.with(this).load(uri).into(mImageView);

load(File file) 加载File中的图片:

 Picasso.with(this).load(file).into(mImageView);

load(int resourceId) 加载本地资源图片:

Picasso.with(this).load(R.mipmap.ic_launcher).into(mImageView);

以上是load的几个重载方法,加载不同资源的图片,另外提醒注意一下load(String path)接受String 参数的这个方法,参数String 可以是一个网络图片url,也可以是file 路径、content资源 和Android Resource。

要使用string 参数加载上面的几种资源,除了网络url,其它几种需要加上对应前缀,file文件路径前缀:file: , content 添加前缀:content: ,Android Resource 添加:android.resource:

3、占位图placeholder、error图、noPlaceholder、noFade

placeholder提供一张在网络请求还没有完成时显示的图片,它必须是本地图片,代码如下:

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .into(mImageView);

设置placeholder之后,在加载图片的时候,就可以显示设置的默认图了,提升用户体验。

和placeholder 的用法一样,error 提供一张在加载图片出错的情况下显示的默认图

          Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .into(mImageView);

noPlaceholder这个方法的意思就是:在调用into的时候明确告诉你没有占位图设置。

根据这个方法签名的解释是阻止View被回收的时候Picasso清空target或者设置一个应用的占位图。需要注意的是placeholder和noPlaceholder 不能同时应用在同一个请求上,会抛异常。

           Picasso.with(this).load(URL)
                .noPlaceholder()
                .error(R.drawable.error_iamge)
                .into(mImageView);

无论你是否设置了占位图,Picasso 从磁盘或者网络加载图片时,into 显示到ImageView 都会有一个简单的渐入过度效果,让你的UI视觉效果更柔顺丝滑一点,如果你不要这个渐入的效果(没有这么坑爹的需求吧!!!),就调用noFade方法。

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .noFade()
                .into(mImageView);

4、设置图片尺寸(Resize)、缩放(Scale)和裁剪(Crop)

在项目中,为了带宽、内存使用和下载速度等考虑,服务端给我们的图片的size 应该和我们View 实际的size一样的,但是实际情况并非如此,服务端可能给我们一些奇怪的尺寸的图片,我们可以使用resize(int w,int hei) 来重新设置尺寸。

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resize(400,200)
                .into(mImageView);

resize()方法接受的参数的单位是pixels,还有一个可以设置dp单位的方法,将你的尺寸写在dimens.xml文件中,然后用resizeDimen(int targetWidthResId, int targetHeightResId)方法

 <dimen name="image_width">300dp</dimen>
 <dimen name="image_height">200dp</dimen>
 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resizeDimen(R.dimen.image_width,R.dimen.image_height)
                .into(mImageView);

当调用了resize 方法重新设置图片尺寸的时候,调用onlyScaleDown 方法,只有当原始图片的尺寸大于我们指定的尺寸时,resize才起作用,如:

  Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resize(4000,2000)
                .onlyScaleDown()
                .into(mImageView);

图片裁剪 centerCrop()属性应该不陌生吧!ImageView 的ScaleType 就有这个属性。当我们使用resize 来重新设置图片的尺寸的时候,你会发现有些图片拉伸或者扭曲了(使用ImageView的时候碰到过吧),我要避免这种情况,Picasso 同样给我们提供了一个方法,centerCrop(充满ImageView 的边界,居中裁剪)。

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resize(400,200)
                .centerCrop()
                .into(mImageView);

上面的centerCrop是可能看不到全部图片的,如果你想让View将图片展示完全,可以用centerInside,但是如果图片尺寸小于View尺寸的话,是不能充满View边界的。

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resize(400,200)
                .centerInside()
                .into(mImageView);

fit 是干什么的呢?上面我们需要用resize()来指定我们需要的图片的尺寸,那就是说在程序中需要我们计算我们需要的尺寸(固定大小的除外),这样很麻烦,fit 方法就帮我们解决了这个问题。fit 它会自动测量我们的View的大小,然后内部调用reszie方法把图片裁剪到View的大小,这就帮我们做了计算size和调用resize 这2步。非常方便。代码如下:

Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .fit()
                .into(mImageView);

使用fit 还是会出现拉伸扭曲的情况,因此最好配合前面的centerCrop使用,代码如下:

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .fit()
                .centerCrop()
                .into(mImageView);

 

注意:特别注意,
1,fit 只对ImageView 有效
2,使用fit时,ImageView 宽和高不能为wrap_content,很好理解,因为它要测量宽高。

5、图片旋转Rotation()
在图片显示到ImageView 之前,还可以对图片做一些旋转操作,调用rotate(int degree)方法

Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .rotate(180)
                .into(mImageView);

这个方法它是以(0,0)点旋转,但是有些时候我们并不想以(0,0)点旋转,还提供了另外一个方法可以指定原点:

  • rotate(float degrees, float pivotX, float pivotY) 以(pivotX, pivotY)为原点旋转
 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .rotate(180,200,100)
                .into(mImageView);

6、转换器Transformation

Transformation 这就是Picasso的一个非常强大的功能了,它允许你在load图片 -> into ImageView 中间这个过程对图片做一系列的变换。比如你要做图片高斯模糊、添加圆角、做度灰处理、圆形图片等等都可以通过Transformation来完成。

来看一个高斯模糊的例子:

首先定义一个转换器继承 Transformation

 public static class BlurTransformation implements Transformation{

        RenderScript rs;

        public BlurTransformation(Context context) {
            super();
            rs = RenderScript.create(context);
        }

        @Override
        public Bitmap transform(Bitmap bitmap) {
            // Create another bitmap that will hold the results of the filter.
            Bitmap blurredBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);

            // Allocate memory for Renderscript to work with
            Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED);
            Allocation output = Allocation.createTyped(rs, input.getType());

            // Load up an instance of the specific script that we want to use.
            ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
            script.setInput(input);

            // Set the blur radius
            script.setRadius(25);

            // Start the ScriptIntrinisicBlur
            script.forEach(output);

            // Copy the output to the blurred bitmap
            output.copyTo(blurredBitmap);

            bitmap.recycle();

            return blurredBitmap;
        }

        @Override
        public String key() {
            return "blur";
        }
    }

加载图片的时候,在into 方法前面调用 transform方法 应用Transformation

  Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .transform(new BlurTransformation(this))
                .into(mBlurImage);

看一下效果图:

上面为原图,下面为高斯模糊图

是不是很强大,任何复杂的变换都可以通过Transformation 来做。

还不止于此,还有更强大的功能。可以在一个请求上应用多个Transformation

比如:我想先做个度灰处理然后在做一个高斯模糊图:

度灰的Transformation:

 public static class GrayTransformation implements Transformation{

        @Override
        public Bitmap transform(Bitmap source) {
            int width, height;
            height = source.getHeight();
            width = source.getWidth();

            Bitmap bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
            Canvas c = new Canvas(bmpGrayscale);
            Paint paint = new Paint();
            ColorMatrix cm = new ColorMatrix();
            cm.setSaturation(0);
            ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
            paint.setColorFilter(f);
            c.drawBitmap(source, 0, 0, paint);

            if(source!=null && source!=bmpGrayscale){
                source.recycle();
            }
            return bmpGrayscale;
        }

        @Override
        public String key() {
            return "gray";
        }
    }

 如果是多个Transformation操作,有2种方式应用:
方式一:直接调用多次transform 方法,不会覆盖的。它只是保存到了一个List 里面

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .fit()
                .centerCrop()
                .transform(new GrayTransformation())//度灰处理
                .transform(new BlurTransformation(this))//高斯模糊
                .into(mBlurImage);

需要注意调用的顺序
方式二:接受一个List,将Transformation 放大list 里

        List<Transformation> transformations = new ArrayList<>();
        transformations.add(new GrayTransformation());
        transformations.add(new BlurTransformation(this));

        Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .fit()
                .centerCrop()
                .transform(transformations)
                .into(mBlurImage);

效果图:

 

如上图,第一张为度灰操作,第二张为 度灰+高斯模糊

7、请求优先级

Picasso 为请求设置有优先级,有三种优先级,LOW、NORMAL、HIGH。默认情况下都是NORMAL,除了调用fetch 方法,fetch 方法的优先级是LOW。

public enum Priority {
    LOW,
    NORMAL,
    HIGH
  }

可以通过priority方法设置请求的优先级,这会影响请求的执行顺序,但是这是不能保证的,它只会往高的优先级靠拢。代码如下:

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .priority(Picasso.Priority.HIGH)
               // .priority(Picasso.Priority.LOW)
                .into(mImageView);

8、Tag管理请求

Picasso 允许我们为一个请求设置tag来管理请求,看一下对应的几个方法:
下面3个方法是Picasso这个类的:

  • cancelTag(Object tag) 取消设置了给定tag的所有请求
  • pauseTag(Object tag) 暂停设置了给定tag 的所有请求
  • resumeTag(Object tag) resume 被暂停的给定tag的所有请求

还有一个方法是RequestCreator的:

  • tag(Object tag) 为请求设置tag

几个方法的意思也很明确,就是我们可以暂停、resume、和取消请求,可以用在哪些场景呢?

场景一: 比如一个照片流列表,当我们快速滑动列表浏览照片的时候,后台会一直发起请求加载照片的,这可能会导致卡顿,那么我们就可以为每个请求设置一个相同的Tag,在快速滑动的时候,调用pauseTag暂停请求,当滑动停止的时候,调用resumeTag恢复请求,这样的体验是不是就会更好一些呢。

Adapter中添加如下代码:

Picasso.with(this).load(mData.get(position))
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .tag("PhotoTag")
                .into(holder.mImageView);

Activity中为RecyclerView添加滑动监听:

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                final Picasso picasso = Picasso.with(MainActivity.this);

                if (newState == SCROLL_STATE_IDLE) {
                    picasso.resumeTag("PhotoTag");
                } else {
                    picasso.pauseTag("PhotoTag");
                }
            }
        });

场景二: 比如一个照片流列表界面,在弱网环境下,加载很慢,退出这个界面时可能会有很多请求没有完成,这个时候我们就可 以通过tag 来取消请求了。

 @Override
    protected void onDestroy() {
        super.onDestroy();
        Picasso.with(this).cancelTag("PhotoTag");
    }

9、同步/异步加载图片

Picasso 加载图片也有同步/异步两种方式
①get() 同步
很简单,同步加载使用get() 方法,返回一个Bitmap 对象,代码如下:

 try {
           Bitmap bitmap =  Picasso.with(this).load(URL).get();
        } catch (IOException e) {
            e.printStackTrace();
        }

注意:使用同步方式加载,不能放在主线程来做。

异步的方式加载图片,fetch()
一般直接加载图片通过into显示到ImageView 是异步的方式,除此之外,还提供了2个异步的方法:

  • fetch() 异步方式加载图片
  • fetch(Callback callback) 异步方式加载图片并给一个回调接口。
  Picasso.with(this).load(URL).fetch(new Callback() {
            @Override
            public void onSuccess() {
                //加载成功
            }

            @Override
            public void onError() {
              //加载失败
            }
        });

这里就要吐槽一下接口设计了,回调并没有返回Bitmap, 不知道作者是怎么考虑的,只是一个通知效果,知道请求失败还是成功。
**fetch 方法异步加载图片并没有返回Bitmap,这个方法在请求成功之后,将结果存到了缓存,包括磁盘和内存缓存。所以使用这种方式加载图片适用于这种场景:知道稍后会加载图片,使用fetch 先加载缓存,起到一个预加载的效果。 **

10、缓存(Disk 和 Memory)

Picasso 有内存缓存(Memory)和磁盘缓存( Disk), 首先来看一下源码中对于缓存的介绍:

  • LRU memory cache of 15% the available application RAM
  • Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only
    available on API 14+ <em>or</em> if you are using a standalone library that provides a disk cache on all API levels like OkHttp)
  • Three download threads for disk and network access.

可以看出,内存缓存是使用的LRU 策略的缓存实现,它的大小是内存大小的15%,可以自定义它的大小,磁盘缓存是磁盘容量的2%但是不超过50M,不少于5M。处理一个请求的时候,按照这个顺讯检查:memory->disk->network 。先检查有木有内存缓存,如果命中,直接返回结果,否则检查磁盘缓存,命中则返回结果,没有命中则从网上获取。

默认情况下,Picasso 内存缓存和磁盘缓存都开启了的,也就是加载图片的时候,内存和磁盘都缓存了,但是有些时候,我们并不需要缓存,比如说:加载一张大图片的时候,如果在内存中保存一份,很容易造成OOM,这时候我们只希望有磁盘缓存,而不希望缓存到内存,因此就需要我们设置缓存策略了。Picasso 提供了这样的方法。

①memoryPolicy 设置内存缓存策略
就像上面所说的,有时候我们不希望有内存缓存,我们可以通过 memoryPolicy 来设置。MemoryPolicy是一个枚举,有两个值

NO_CACHE表示处理请求的时候跳过检查内存缓存
NO_STORE: 表示请求成功之后,不将最终的结果存到内存。

示例代码如下:

Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .memoryPolicy(MemoryPolicy.NO_CACHE,MemoryPolicy.NO_STORE) //静止内存缓存
                .into(mBlurImage);

②networkPolicy 设置磁盘缓存策略
和内存缓存一样,加载一张图片的时候,你也可以跳过磁盘缓存,和内存缓存策略的控制方式一样,磁盘缓存调用方法networkPolicy(NetworkPolicy policy, NetworkPolicy... additional) , NetworkPolicy是一个枚举类型,有三个值:

NO_CACHE: 表示处理请求的时候跳过处理磁盘缓存
NO_STORE: 表示请求成功后,不将结果缓存到Disk,但是这个只对OkHttp有效。
OFFLINE: 这个就跟 上面两个不一样了,如果networkPolicy方法用的是这个参数,那么Picasso会强制这次请求从缓存中获取结果,不会发起网络请求,不管缓存中能否获取到结果。

使用示例:

 with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .memoryPolicy(MemoryPolicy.NO_CACHE,MemoryPolicy.NO_STORE)//跳过内存缓存
                .networkPolicy(NetworkPolicy.NO_CACHE)//跳过磁盘缓存
                .into(mBlurImage);

强制从缓存获取:

 with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .networkPolicy(NetworkPolicy.OFFLINE)//强制从缓存获取结果
                .into(mBlurImage);

11、Debug 和日志

①缓存指示器

Picasso 有内存缓存和磁盘缓存,先从内存获取,没有再去磁盘缓存获取,都没有就从网络加载,网络加载是比较昂贵和耗时的。因此,作为一个开发者,我们往往需要加载的图片是从哪儿来的(内存、Disk还是网络),Picasso让我们很容易就实现了。只需要调用一个方法setIndicatorsEnabled(boolean)就可以了,它会在图片的左上角出现一个带色块的三角形标示,有3种颜色,绿色表示从内存加载、蓝色表示从磁盘加载、红色表示从网络加载。

 Picasso.with(this)
        .setIndicatorsEnabled(true);//显示指示器

效果图:

如上图所示,第一张图从网络获取,第二张从磁盘获取,第三张图从内存获取。

看一下源码中定义指示器的颜色:

 /** Describes where the image was loaded from. */
  public enum LoadedFrom {
    MEMORY(Color.GREEN),
    DISK(Color.BLUE),
    NETWORK(Color.RED);

    final int debugColor;

    private LoadedFrom(int debugColor) {
      this.debugColor = debugColor;
    }
  }

可以很清楚看出,对应三种颜色代表着图片的来源。

②日志
上面的指示器能够很好的帮助我们看出图片的来源,但是有时候我们需要更详细的信息,Picasso,可以打印一些日志,比如一些关键方法的执行时间等等,我们只需要调用setLoggingEnabled(true)方法,然后App在加载图片的过程中,我们就可以从logcat 看到一些关键的日志信息。

   Picasso.with(this)
          .setLoggingEnabled(true);//开启日志打印

12、Picasso 扩展

到目前为止,Picasso的基本使用已经讲得差不多了,但是在实际项目中我们这可能还满足不了我们的需求,我们需要对它做一些自己的扩展,比如我们需要换缓存的位置、我们需要扩大缓存、自定义线程池、自定义下载器等等。这些都是可以的,接下来我们来看一下可以做哪些方面的扩展。

①用Builder 自己构造一个Picasso Instance
我们来回顾一下前面是怎么用Picasso 加载图片的

Picasso.with(this)
       .load("http://ww3.sinaimg.cn/large/610dc034jw1fasakfvqe1j20u00mhgn2.jpg")
       .into(mImageView);

总共3步:
1,用with方法获取一个Picasso 示例
2,用load方法加载图片
3,用into 放法显示图片

首先Picasso是一个单例模式,我们每一次获取的示例都是默认提供给我们的实例。但是也可以不用它给的Instance,我们直接用builder来构造一个Picasso:

       Picasso.Builder builder = new Picasso.Builder(this);
        //构造一个Picasso
        Picasso picasso = builder.build();
        //加载图片
        picasso.load(URL)
                .into(mImageView);

这样我们就构造了一个局部的Picasso实例,当然了,我们直接用new 了一个builder,然后build()生成了一个Picasso。这跟默认的通过with方法获取的实例是一样的。那么现在我们就可以配置一些自定义的功能了。

②配置自定义下载器 downLoader
如果我们不想用默认提供的Downloader,那么我们可以自定义一个下载器然后配置进去。举个例子:

(1) 先自定义一个Downloader(只是举个例子,并没有实现):

public class CustomDownloader implements Downloader {

    @Override
    public Response load(Uri uri, int networkPolicy) throws IOException {
        return null;
    }

    @Override
    public void shutdown() {

    }
}

(2) 然后通过builder 配置:

        //配置下载器
        builder.downloader(new CustomDownloader());
        //构造一个Picasso
        Picasso picasso = builder.build();

这样配置后,我们用build()生成的Picasso 实例来加载图片就会使用自定义的下载器来下载图片了。

③配置缓存
前面说过,内存缓存是用的LRU Cahce ,大小是手机内存的15% ,如果你想缓存大小更大一点或者更小一点,可以自定义,然后配置。

        //配置缓存
        LruCache cache = new LruCache(5*1024*1024);// 设置缓存大小
        builder.memoryCache(cache);

上面只是一个简单的举例,当然了你可以自定义,也可以使用LRUCache,改变大小,改变存储路径等等。

④ 配置线程池
Picasso 默认的线程池的核心线程数为3,如果你觉得不够用的话,可以配置自己需要的线程池,举个列子:

        //配置线程池
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        builder.executor(executorService);

⑤配置全局的 Picasso Instance 
上面说的这些自定义配置项目都是应用在一个局部的Picasso instance 上的,我们不可能每一次使用都要重新配置一下,这样就太麻烦了。我们希望我们的这些自定义配置能在整个项目都应用上,并且只配置一次。其实Picasso 给我们提供了这样的方法。可以调用setSingletonInstance(Picasso picasso)就可以了,看一下这个方法的源码:

 /**
   * Set the global instance returned from {@link #with}.
   * <p>
   * This method must be called before any calls to {@link #with} and may only be called once.
   */
  public static void setSingletonInstance(Picasso picasso) {
    synchronized (Picasso.class) {
      if (singleton != null) {
        throw new IllegalStateException("Singleton instance already exists.");
      }
      singleton = picasso;
    }
  }

设置一个通过with方法返回的全局instance。我们只希望配置一次,所以,我们应该在Application 的onCreate方法中做全局配置就可以了。app一启动就配置好,然后直接和前面的使用方法一样,调用with方法获取Picasso instance 加载图片就OK了。

因此在Application 中添加如下代码:

 @Override
    public void onCreate() {
        super.onCreate();
        
        // 配置全局的Picasso instance 
        
        Picasso.Builder builder = new Picasso.Builder(this);
        //配置下载器
        builder.downloader(new CustomDownloader());
        //配置缓存
        LruCache cache = new LruCache(5*1024*1024);// 设置缓存大小
        builder.memoryCache(cache);
        //配置线程池
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        builder.executor(executorService);

        //构造一个Picasso
        Picasso picasso = builder.build();
        // 设置全局单例instance 
        Picasso.setSingletonInstance(picasso);
        
        
    }

然后应用这些自定义配置加载图片。

Picasso.with(this).load("http://ww3.sinaimg.cn/large/610dc034jw1fasakfvqe1j20u00mhgn2.jpg").into(mImageView);

用法和以前的一样,但是我们已经将我们的自定义配置应用上了。

Glide

Glide和Picasso对比,首先这两个框架的用法非常相似,但其实它们各有特色。Picasso比Glide更加简洁和轻量,Glide比Picasso功能更为丰富。

要想使用Glide,首先需要将这个库引入到我们的项目当中。在app/build.gradle文件当中添加如下依赖:

dependencies {
    compile 'com.github.bumptech.glide:glide:3.7.0'
}

1、基本用法:

 Glide.with(this).load(“http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg”).into(imageView);

千万不要小看这一行代码,实际上仅仅就这一行代码,你已经可以做非常非常多的事情了,包括加载网络上的图片、加载手机本地的图片、加载应用资源中的图片等等。

首先,调用Glide.with()方法用于创建一个加载图片的实例。with()方法可以接收Context、Activity或者Fragment类型的参数。也就是说我们选择的范围非常广,不管是在Activity还是Fragment中调用with()方法,都可以直接传this。那如果调用的地方既不在Activity中也不在Fragment中呢?也没关系,我们可以获取当前应用程序的ApplicationContext,传入到with()方法当中。注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。

接下来看一下load()方法,这个方法用于指定待加载的图片资源。Glide支持加载各种各样的图片资源,包括网络图片、本地图片、应用资源、二进制流、Uri对象等等。因此load()方法也有很多个方法重载,除了我们刚才使用的加载一个字符串网址之外,你还可以这样使用load()方法:

// 加载本地图片
File file = new File(getExternalCacheDir() + "/image.jpg");
Glide.with(this).load(file).into(imageView);

// 加载应用资源
int resource = R.drawable.image;
Glide.with(this).load(resource).into(imageView);

// 加载二进制流
byte[] image = getImageBytes();
Glide.with(this).load(image).into(imageView);

// 加载Uri对象
Uri imageUri = getImageUri();
Glide.with(this).load(imageUri).into(imageView);

最后看一下into()方法,这个方法就很简单了,我们希望让图片显示在哪个ImageView上,把这个ImageView的实例传进去就可以了。当然,into()方法不仅仅是只能接收ImageView类型的参数,还支持很多更丰富的用法。

2、占位图

Glide.with(this)
     .load(url)
     .placeholder(R.drawable.loading)
     .into(imageView);

不过如果你现在重新运行一下代码,很可能是根本看不到占位图效果的。因为Glide有非常强大的缓存机制,我们刚才加载那张必应美图的时候Glide自动就已经将它缓存下来了,下次加载的时候将会直接从缓存中读取,不会再去网络下载了,因而加载的速度非常快,所以占位图可能根本来不及显示。

因此这里我们还需要稍微做一点修改,来让占位图能有机会显示出来,修改代码如下所示:

Glide.with(this)
     .load(url)
     .placeholder(R.drawable.loading)
     .diskCacheStrategy(DiskCacheStrategy.NONE)
     .into(imageView);

可以看到,这里串接了一个diskCacheStrategy()方法,并传入DiskCacheStrategy.NONE参数,这样就可以禁用掉Glide的缓存功能。

3、error图

Glide.with(this)
     .load(url)
     .placeholder(R.drawable.loading)
     .error(R.drawable.error)
     .diskCacheStrategy(DiskCacheStrategy.NONE)
     .into(imageView);

如果因为某些异常情况导致图片加载失败,比如说手机网络信号不好,这个时候就显示这张error图。

4、指定图片格式

我们还需要再了解一下Glide另外一个强大的功能,那就是Glide是支持加载GIF图片的。这一点确实非常牛逼,因为相比之下,Picasso是不会支持加载GIF图片的。

而使用Glide加载GIF图并不需要编写什么额外的代码,Glide内部会自动判断图片格式。比如这是一张GIF图片的URL地址:

http://p1.pstatp.com/large/166200019850062839d3

我们只需要将刚才那段加载图片代码中的URL地址替换成上面的地址就可以了。

也就是说,不管我们传入的是一张普通图片,还是一张GIF图片,Glide都会自动进行判断,并且可以正确地把它解析并展示出来。

但是如果我想指定图片的格式该怎么办呢?就比如说,我希望加载的这张图必须是一张静态图片,我不需要Glide自动帮我判断它到底是静图还是GIF图。

想实现这个功能仍然非常简单,我们只需要再串接一个新的方法就可以了,如下所示:

Glide.with(this)
     .load(url)
     .asBitmap()
     .placeholder(R.drawable.loading)
     .error(R.drawable.error)
     .diskCacheStrategy(DiskCacheStrategy.NONE)
     .into(imageView);

可以看到,这里在load()方法的后面加入了一个asBitmap()方法,这个方法的意思就是说这里只允许加载静态图片,不需要Glide去帮我们自动进行图片格式的判断了。

由于调用了asBitmap()方法,现在GIF图就无法正常播放了,而是会在界面上显示第一帧的图片。

那么类似地,既然我们能强制指定加载静态图片,就也能强制指定加载动态图片。比如说我们想要实现必须加载动态图片的功能,就可以这样写:

Glide.with(this)
     .load(url)
     .asGif()
     .placeholder(R.drawable.loading)
     .error(R.drawable.error)
     .diskCacheStrategy(DiskCacheStrategy.NONE)
     .into(imageView);

那么既然指定了只允许加载动态图片,如果我们传入了一张静态图片的URL地址又会怎么样呢?试一下就知道了,将图片的URL地址改成刚才的必应美图,然后重新运行代码,会显示error图。

没错,如果指定了只能加载动态图片,而传入的图片却是一张静图的话,那么结果自然就只有加载失败喽。

5、指定图片大小

实际上,使用Glide在绝大多数情况下我们都是不需要指定图片大小的。

使用Glide,我们就完全不用担心图片内存浪费,甚至是内存溢出的问题。因为Glide从来都不会直接将图片的完整尺寸全部加载到内存中,而是用多少加载多少。Glide会自动判断ImageView的大小,然后只将这么大的图片像素加载到内存当中,帮助我们节省内存开支。

也正是因为Glide是如此的智能,所以说,在绝大多数情况下我们都是不需要指定图片大小的,因为Glide会自动根据ImageView的大小来决定图片的大小。

不过,如果你真的有这样的需求,必须给图片指定一个固定的大小,Glide仍然是支持这个功能的:

Glide.with(this)
     .load(url)
     .placeholder(R.drawable.loading)
     .error(R.drawable.error)
     .diskCacheStrategy(DiskCacheStrategy.NONE)
     .override(100, 100)
     .into(imageView);

这里使用override()方法指定了一个图片的尺寸,也就是说,Glide现在只会将图片加载成100*100像素的尺寸,而不会管你的ImageView的大小是多少了。

6、glide生命周期原理

如果在Glide.with()方法中传入的是一个Application对象,那么这里就会调用带有Context参数的get()方法重载,然后调用getApplicationManager()方法来获取一个RequestManager对象。其实这是最简单的一种情况,因为Application对象的生命周期即应用程序的生命周期,因此Glide并不需要做什么特殊的处理,它自动就是和应用程序的生命周期是同步的,如果应用程序关闭的话,Glide的加载也会同时终止。

接下来我们看传入非Application参数的情况。不管你在Glide.with()方法中传入的是Activity、FragmentActivity、v4包下的Fragment、还是app包下的Fragment,最终的流程都是一样的,那就是会向当前的Activity当中添加一个隐藏的Fragment。那么这里为什么要添加一个隐藏的Fragment呢?因为Glide需要知道加载的生命周期。很简单的一个道理,如果你在某个Activity上正在加载着一张图片,结果图片还没加载出来,Activity就被用户关掉了,那么图片还应该继续加载吗?当然不应该。可是Glide并没有办法知道Activity的生命周期,于是Glide就使用了添加隐藏Fragment的这种小技巧,因为Fragment的生命周期和Activity是同步的,如果Activity被销毁了,Fragment是可以监听到的,这样Glide就可以捕获这个事件并停止图片加载了。

这里额外再提一句,如果我们是在非主线程当中使用的Glide,那么不管你是传入的Activity还是Fragment,都会被强制当成Application来处理。

9、glide缓存机制

Glide的缓存设计可以说是非常先进的,考虑的场景也很周全。在缓存这一功能上,Glide又将它分成了两个模块,一个是内存缓存,一个是硬盘缓存。

这两个缓存模块的作用各不相同,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。

内存缓存和硬盘缓存的相互结合才构成了Glide极佳的图片缓存效果。

默认情况下,Glide自动就是开启内存缓存的。也就是说,当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个RecyclerView当中反复上下滑动,RecyclerView中只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来,从而大大提升了用户体验。

而Glide最为人性化的是,你甚至不需要编写任何额外的代码就能自动享受到这个极为便利的内存缓存功能,因为Glide默认就已经将它开启了。

那么既然已经默认开启了这个功能,还有什么可讲的用法呢?只有一点,如果你有什么特殊的原因需要禁用内存缓存功能,Glide对此提供了接口:

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

可以看到,只需要调用skipMemoryCache()方法并传入true,就表示禁用掉Glide的内存缓存功能。

其实说到内存缓存的实现,非常容易就让人想到LruCache算法(Least Recently Used),也叫近期最少使用算法。它的主要算法原理就是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

Glide内存缓存的实现自然也是使用的LruCache算法。不过除了LruCache算法之外,Glide还结合了一种弱引用的机制,共同完成了内存缓存功能。

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

调用diskCacheStrategy()方法并传入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盘缓存功能了。

这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收四种参数:

  • DiskCacheStrategy.NONE: 表示不缓存任何内容。
  • DiskCacheStrategy.SOURCE: 表示只缓存原始图片。
  • DiskCacheStrategy.RESULT: 表示只缓存转换过后的图片(默认选项)。
  • DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。

上面四种参数的解释本身并没有什么难理解的地方,但是有一个概念大家需要了解,就是当我们使用Glide去加载一张图片的时候,Glide默认并不会将原始图片展示出来,而是会对图片进行压缩和转换。总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。而Glide默认情况下在硬盘缓存的就是转换过后的图片,我们通过调用diskCacheStrategy()方法则可以改变这一默认行为。

10、延伸

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

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

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

可以看到,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加载出来的图片对象了,也就是方法参数中传过来的GlideDrawable对象。有了这个对象之后你可以使用它进行任意的逻辑操作,这里我只是简单地把它显示到了ImageView上。

SimpleTarget的实现创建好了,那么只需要在加载图片的时候将它传入到into()方法中就可以了,现在运行一下程序,效果如下图所示。

虽然目前这个效果和直接在into()方法中传入ImageView并没有什么区别,但是我们已经拿到了图片对象的实例,然后就可以随意做更多的事情了。

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

SimpleTarget<Bitmap> simpleTarget = new SimpleTarget<Bitmap>() {
    @Override
    public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) {
        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对象了。

好了,SimpleTarget的用法就是这么简单,接下来我们学习一下ViewTarget的用法。

事实上,从刚才的继承结构图上就能看出,Glide在内部自动帮我们创建的GlideDrawableImageViewTarget就是ViewTarget的子类。只不过GlideDrawableImageViewTarget被限定只能作用在ImageView上,而ViewTarget的功能更加广泛,它可以作用在任意的View上。

这里我们还是通过一个例子来演示一下吧,比如我创建了一个自定义布局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()即可。现在重新运行一下程序,效果如下图所示。

好的,关于自定义Target的功能我们就介绍这么多,这些虽说都是自定义Target最基本的用法,但掌握了这些用法之后,你就能应对各种各样复杂的逻辑了。

11、preload()方法

Glide加载图片虽说非常智能,它会自动判断该图片是否已经有缓存了,如果有的话就直接从缓存中读取,没有的话再从网络去下载。但是如果我希望提前对图片进行一个预加载,等真正需要加载图片的时候就直接从缓存中读取,不想再等待慢长的网络加载时间了,这该怎么办呢?

对于很多Glide新手来说这确实是一个烦恼的问题,因为在没有学习本篇文章之前,into()方法中必须传入一个ImageView呀,而传了ImageView之后图片就显示出来了,这还怎么预加载呢?

不过在学习了本篇文章之后,相信你已经能够想到解决方案了。因为into()方法中除了传入ImageView之后还可以传入Target对象,如果我们在Target对象的onResourceReady()方法中做一个空实现,也就是不做任何逻辑处理,那么图片自然也就显示不出来了,而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一定会去读取刚才预加载的图片缓存。

12、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)是用于在主线程中下载图片的

那么我们先来看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将下载好的图片文件路径显示出来。

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

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

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

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将无法使用我们刚才下载好的图片缓存文件。

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

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

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在开始加载图片之前会先计算图片的大小,然后回调到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按钮,然后观察控制台日志的输出,结果如下图所示。

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

13、listener()方法

其实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()方法了。

14、图片变换功能

Glide给我们提供了专门的API来添加和取消图片变换,想要取消图片变换只需要使用如下代码即可:

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

图片变换的意思就是说,Glide从加载了原始图片到最终展示给用户之前,又进行了一些变换处理,从而能够实现一些更加丰富的图片效果,如图片圆角化、圆形化、模糊化等等。

添加图片变换的用法非常简单,我们只需要调用transform()方法,并将想要执行的图片变换操作作为参数传入transform()方法即可,如下所示:

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

至于具体要进行什么样的图片变换操作,这个通常都是需要我们自己来写的。不过Glide已经内置了两种图片变换操作,我们可以直接拿来使用,一个是CenterCrop,一个是FitCenter。

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

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

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

FitCenter的效果就是会将图片按照原始的长宽比充满全屏。那么CenterCrop是对原图的中心区域进行裁剪后得到的图片。

自定义图片变换

Glide给我们定制好了一个图片变换的框架,大致的流程是我们可以获取到原始的图片,然后对图片进行变换,再将变换完成后的图片返回给Glide,最终由Glide将图片显示出来。理论上,在对图片进行变换这个步骤中我们可以进行任何的操作,你想对图片怎么样都可以。包括圆角化、圆形化、黑白化、模糊化等等,甚至你将原图片完全替换成另外一张图都是可以的。

这里我们就选择一种常用的图片变换效果来进行自定义吧——对图片进行圆形化变换。

图片圆形化的功能现在在手机应用中非常常见,比如手机QQ就会将用户的头像进行圆形化变换,从而使得界面变得更加好看。

自定义图片变换功能的实现逻辑比较固定,其实就是自定义一个类让它继承自BitmapTransformation ,然后重写transform()方法,并在这里去实现具体的图片变换逻辑就可以了。一个空的图片变换实现大概如下所示:

public class CircleCrop extends BitmapTransformation {

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

    public CircleCrop(BitmapPool bitmapPool) {
        super(bitmapPool);
    }

    @Override
    public String getId() {
        return "com.example.glidetest.CircleCrop";
    }

    @Override
    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        return null;
    }
}

这里有一点需要注意,就是getId()方法中要求返回一个唯一的字符串来作为id,以和其他的图片变换做区分。通常情况下,我们直接返回当前类的完整类名就可以了。

另外,这里我们选择继承BitmapTransformation还有一个限制,就是只能对静态图进行图片变换。当然,这已经足够覆盖日常95%以上的开发需求了。如果你有特殊的需求要对GIF图进行图片变换,那就得去自己实现Transformation接口才可以了。

我们继续实现对图片进行圆形化变换的功能,接下来只需要在transform()方法中去做具体的逻辑实现就可以了,代码如下所示:

public class CircleCrop extends BitmapTransformation {

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

    public CircleCrop(BitmapPool bitmapPool) {
        super(bitmapPool);
    }

    @Override
    public String getId() {
        return "com.example.glidetest.CircleCrop";
    }

    @Override
    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        int diameter = Math.min(toTransform.getWidth(), toTransform.getHeight());

        final Bitmap toReuse = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888);
        final Bitmap result;
        if (toReuse != null) {
            result = toReuse;
        } else {
            result = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
        }

        int dx = (toTransform.getWidth() - diameter) / 2;
        int dy = (toTransform.getHeight() - diameter) / 2;
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        BitmapShader shader = new BitmapShader(toTransform, BitmapShader.TileMode.CLAMP, 
                                            BitmapShader.TileMode.CLAMP);
        if (dx != 0 || dy != 0) {
            Matrix matrix = new Matrix();
            matrix.setTranslate(-dx, -dy);
            shader.setLocalMatrix(matrix);
        }
        paint.setShader(shader);
        paint.setAntiAlias(true);
        float radius = diameter / 2f;
        canvas.drawCircle(radius, radius, radius, paint);

        if (toReuse != null && !pool.put(toReuse)) {
            toReuse.recycle();
        }
        return result;
    }
}

下面我来对transform()方法中的逻辑做下简单的解释。首先第18行先算出原图宽度和高度中较小的值,因为对图片进行圆形化变换肯定要以较小的那个值作为直径来进行裁剪。第20-26行则和刚才一样,从Bitmap缓存池中尝试获取一个Bitmap对象来进行重用,如果没有可重用的Bitmap对象的话就创建一个。第28-41行是具体进行圆形化变换的部分,这里算出了画布的偏移值,并且根据刚才得到的直径算出半径来进行画圆。最后,尝试将复用的Bitmap对象重新放回到缓存池当中,并将圆形化变换后的Bitmap对象进行返回。

这样,一个自定义图片变换的功能就写好了,那么现在我们就来尝试使用一下它吧。使用方法非常简单,刚才已经介绍过了,就是把这个自定义图片变换的实例传入到transform()方法中即可,如下所示:

Glide.with(this)
     .load(url)
     .transform(new CircleCrop(this))
     .into(imageView);

现在我们重新运行一下程序,效果如下图所示。

 

虽说Glide的图片变换功能框架已经很强大了,使得我们可以轻松地自定义图片变换效果,但是如果每一种图片变换都要我们自己去写还是蛮吃力的。事实上,确实也没有必要完全靠自己去实现各种各样的图片变换效果,因为大多数的图片变换都是比较通用的,各个项目会用到的效果都差不多,我们每一个都自己去重新实现无异于重复造轮子。

也正是因此,网上出现了很多Glide的图片变换开源库,其中做的最出色的应该要数glide-transformations这个库了。它实现了很多通用的图片变换效果,如裁剪变换、颜色变换、模糊变换等等,使得我们可以非常轻松地进行各种各样的图片变换。

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

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

dependencies {
    compile 'jp.wasabeef:glide-transformations:2.0.2'
}

现在如果我想对图片进行模糊化处理,那么就可以使用glide-transformations库中的BlurTransformation这个类,代码如下所示:

Glide.with(this)
     .load(url)
     .bitmapTransform(new BlurTransformation(this))
     .into(imageView);

注意这里我们调用的是bitmapTransform()方法而不是transform()方法,因为glide-transformations库都是专门针对静态图片变换来进行设计的。现在重新运行一下程度,效果如下图所示。

没错,我们就这样轻松地实现模糊化的效果了。

接下来我们再试一下图片黑白化的效果,使用的是GrayscaleTransformation这个类,代码如下所示:

Glide.with(this)
     .load(url)
     .bitmapTransform(new GrayscaleTransformation(this))
     .into(imageView);

现在重新运行一下程度,效果如下图所示。

而且我们还可以将多个图片变换效果组合在一起使用,比如同时执行模糊化和黑白化的变换:

Glide.with(this)
     .load(url)
     .bitmapTransform(new BlurTransformation(this), new GrayscaleTransformation(this))
     .into(imageView);

可以看到,同时执行多种图片变换的时候,只需要将它们都传入到bitmapTransform()方法中即可。现在重新运行一下程序,效果如下图所示。

当然,这些只是glide-transformations库的一小部分功能而已,更多的图片变换效果你可以到它的GitHub项目主页去学习,所有变换的用法都是这么简单哦。

15、自定义模块功能

学到这里相信你已经知道,Glide的用法是非常非常简单的,大多数情况下,我们想要实现的图片加载效果只需要一行代码就能解决了。但是Glide过于简洁的API也造成了一个问题,就是如果我们想要更改Glide的某些默认配置项应该怎么操作呢?很难想象如何将更改Glide配置项的操作串联到一行经典的Glide图片加载语句中当中吧?没错,这个时候就需要用到自定义模块功能了。

自定义模块功能可以将更改Glide配置,替换Glide组件等操作独立出来,使得我们能轻松地对Glide的各种配置进行自定义,并且又和Glide的图片加载逻辑没有任何交集,这也是一种低耦合编程方式的体现。那么接下来我们就学习一下自定义模块的基本用法。

首先需要定义一个我们自己的模块类,并让它实现GlideModule接口,如下所示:

public class MyGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
    }
}

可以看到,在MyGlideModule类当中,我们重写了applyOptions()和registerComponents()方法,这两个方法分别就是用来更改Glide和配置以及替换Glide组件的。我们待会儿只需要在这两个方法中加入具体的逻辑,就能实现更改Glide配置或者替换Glide组件的功能了。

不过,目前Glide还无法识别我们自定义的MyGlideModule,如果想要让它生效,还得在AndroidManifest.xml文件当中加入如下配置才行:

<manifest>

    ...

    <application>

        <meta-data
            android:name="com.example.glidetest.MyGlideModule"
            android:value="GlideModule" />

        ...

    </application>
</manifest>  

在<application>标签中加入一个meta-data配置项,其中android:name指定成我们自定义的MyGlideModule的完整路径,android:value必须指定成GlideModule,这个是固定值。

这样的话,我们就将Glide自定义模块的功能完成了,是不是非常简单?现在Glide已经能够识别我们自定义的这个MyGlideModule了。

如果想要更改Glide的默认配置,其实只需要在applyOptions()方法中提前将Glide的配置项进行初始化就可以了。那么Glide一共有哪些配置项呢?这里我给大家做了一个列举:

  • setMemoryCache() 
    用于配置Glide的内存缓存策略,默认配置是LruResourceCache。

  • setBitmapPool() 
    用于配置Glide的Bitmap缓存池,默认配置是LruBitmapPool。

  • setDiskCache() 
    用于配置Glide的硬盘缓存策略,默认配置是InternalCacheDiskCacheFactory。

  • setDiskCacheService() 
    用于配置Glide读取缓存中图片的异步执行器,默认配置是FifoPriorityThreadPoolExecutor,也就是先入先出原则。

  • setResizeService() 
    用于配置Glide读取非缓存中图片的异步执行器,默认配置也是FifoPriorityThreadPoolExecutor。

  • setDecodeFormat() 
    用于配置Glide加载图片的解码模式,默认配置是RGB_565。

其实Glide的这些默认配置都非常科学且合理,使用的缓存算法也都是效率极高的,因此在绝大多数情况下我们并不需要去修改这些默认配置,这也是Glide用法能如此简洁的一个原因。

但是Glide科学的默认配置并不影响我们去学习自定义Glide模块的功能,因为总有某些情况下,默认的配置可能将无法满足你,这个时候就需要我们自己动手来修改默认配置了。

下面就通过具体的实例来看一下吧。刚才说到,Glide默认的硬盘缓存策略使用的是InternalCacheDiskCacheFactory,这种缓存会将所有Glide加载的图片都存储到当前应用的私有目录下。这是一种非常安全的做法,但同时这种做法也造成了一些不便,因为私有目录下即使是开发者自己也是无法查看的,如果我想要去验证一下图片到底有没有成功缓存下来,这就有点不太好办了。

这种情况下,就非常适合使用自定义模块来更改Glide的默认配置。我们完全可以自己去实现DiskCache.Factory接口来自定义一个硬盘缓存策略,不过却大大没有必要这么做,因为Glide本身就内置了一个ExternalCacheDiskCacheFactory,可以允许将加载的图片都缓存到SD卡。

那么接下来,我们就尝试使用这个ExternalCacheDiskCacheFactory来替换默认的InternalCacheDiskCacheFactory,从而将所有Glide加载的图片都缓存到SD卡上。

由于在前面我们已经创建好了一个自定义模块MyGlideModule,那么现在就可以直接在这里编写逻辑了,代码如下所示:

public class MyGlideModule implements GlideModule {

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        builder.setDiskCache(new ExternalCacheDiskCacheFactory(context));
    }

    @Override
    public void registerComponents(Context context, Glide glide) {

    }

}

没错,就是这么简单,现在所有Glide加载的图片都会缓存到SD卡上了。

另外,InternalCacheDiskCacheFactory和ExternalCacheDiskCacheFactory的默认硬盘缓存大小都是250M。也就是说,如果你的应用缓存的图片总大小超出了250M,那么Glide就会按照DiskLruCache算法的原则来清理缓存的图片。

当然,我们是可以对这个默认的缓存大小进行修改的,而且修改方式非常简单,如下所示:

public class MyGlideModule implements GlideModule {

    public static final int DISK_CACHE_SIZE = 500 * 1024 * 1024;

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, DISK_CACHE_SIZE));
    }

    @Override
    public void registerComponents(Context context, Glide glide) {

    }

}

只需要向ExternalCacheDiskCacheFactory或者InternalCacheDiskCacheFactory再传入一个参数就可以了,现在我们就将Glide硬盘缓存的大小调整成了500M。

好了,更改Glide配置的功能就是这么简单,那么接下来我们就来验证一下更改的配置到底有没有生效吧。

这里还是使用最基本的Glide加载语句来去加载一张网络图片:

String url = "http://guolin.tech/book.png";
Glide.with(this)
     .load(url)
     .into(imageView);

运行一下程序,效果如下图所示:

 

 

OK,现在图片已经加载出现了,那么我们去找一找它的缓存吧。

ExternalCacheDiskCacheFactory的默认缓存路径是在sdcard/Android/包名/cache/image_manager_disk_cache目录当中,我们使用文件浏览器进入到这个目录,结果如下图所示。

可以看到,这里有两个文件,其中journal文件是DiskLruCache算法的日志文件,这个文件必不可少,且只会有一个。

而另外一个文件就是那张缓存的图片了,它的文件名虽然看上去很奇怪,但是我们只需要把这个文件的后缀改成.png,然后用图片浏览器打开,结果就一目了然了,如下图所示。

由此证明,我们已经成功将Glide的硬盘缓存路径修改到SD卡上了。

另外这里再提一点,我们都知道Glide和Picasso的用法是非常相似的,但是有一点差别却很大。Glide加载图片的默认格式是RGB_565,而Picasso加载图片的默认格式是ARGB_8888。ARGB_8888格式的图片效果会更加细腻,但是内存开销会比较大。而RGB_565格式的图片则更加节省内存,但是图片效果上会差一些。

Glide和Picasso各自采取的默认图片格式谈不上熟优熟劣,只能说各自的取舍不一样。但是如果你希望Glide也能使用ARGB_8888的图片格式,这当然也是可以的。我们只需要在MyGlideModule中更改一下默认配置即可,如下所示:

public class MyGlideModule implements GlideModule {

    public static final int DISK_CACHE_SIZE = 500 * 1024 * 1024;

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, DISK_CACHE_SIZE));
        builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
    }

    @Override
    public void registerComponents(Context context, Glide glide) {

    }

}

通过这样配置之后,使用Glide加载的所有图片都将会使用ARGB_8888的格式,虽然图片质量变好了,但同时内存开销也会明显增大,所以你要做好心理准备哦。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值