带你全方位学习Picasso(二)

xuanzhiqiang 2018-07-03

目录

  1. 图像请求排序
  2. tag() 分组请求
  3. 回调, RemoteView与Notifications
  4. 图像旋转与转换
  5. 图像缓存
  6. 缓存指示器,日志,统计
  7. 使用 Picasso.Builder 自定义Picasso
  8. 自定义请求处理
  9. picasso2-okhttp3-downloader 与 picasso-transformations

图像请求排序

优先级:HIGH,MEDIUM,LOW

假设有一种场景,就是图片加载需要优先级显示,这时候就会用到 priority()。这个方法需要三个参数之一: HIGH,MEDIUM,LOW,默认情况下,所有的请求是 MEDIUM。

//HIGH
Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[0])
 .fit()
 .priority(Picasso.Priority.HIGH)
 .into(imageViewHero);
//LOW
Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[2])
 .fit()
 .priority(Picasso.Priority.LOW)
 .into(imageViewLowPrioRight);

:使用 priority() 并不能百分百保证图片显示的顺序,只是请求的优先级。


tag() 分组请求

您已经了解了图片的请求优先级,可能这还不够,假设我们需要同时取消,暂停,恢复多个图片请求,这将会用到 tag()

tag() 可以以任何java对象作为参数,因为,基于这些我们可以构建图像组。图像组具有以下选项:

  • 暂停请求 pauseTag()
  • 恢复请求 resumeTag()
  • 取消请求 cancelTag()

或许你到现在也不能理解这究竟是什么,不妨我们看个小例子。

实例:pauseTag()和resumeTag()

假设我们有有个邮箱应用,内容主要用过 ListView 显示。

带你全方位学习Picasso(二)

现在,让我们想象一个情形:用户要寻找某条信息,然后会快速的滑动,ListView本来会迅速的回收和再利用每个 container,然而,Picasso将会尝试为每个 item去做一次请求,然后立即取消,因为用户快速的滑动。

为了减少更多的请求,我们可以等 ListView 滚动结束后去加载图片,同时用户不会注意到任何差异,但是这样已经减少了请求量。

实现很容易,首先,向你的Picasso请求添加标签。

Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[0])
 .tag("Profile ListView") // can be any Java object
 .into(imageViewWithTag);
//接着,实现一个 `AbsListView.OnScrollListener` 和重写 `onScrollStateChanged()`
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
final Picasso picasso = Picasso.with(context);
if (scrollState == SCROLL_STATE_IDLE || scrollState == SCROLL_STATE_TOUCH_SCROLL) {
 picasso.resumeTag(context);
} else {
 picasso.pauseTag(context);
}
}
//最后一步
listView.setOnScrollListener(onScrollListener);

当ListView处于非常快的滚动状态,它会暂停所有请求,当正常滚动或者停止,它就会恢复请求。

官网示例代码

注: 这篇主要使用了 String 作为 tag,或许你想使用不同的对象,或许你会想到 Context or Activity, Fragment,但是官方发出了警告 如果用户离开了 Activity,垃圾收集器可能因为 Picasso无法销毁Activity对象,会出现能存泄露


回调, RemoteView与Notifications

fetch(), get()和Target之间的区别

fetch(): fetch() 将异步加载图像在后台线程,但是不显示在 ImageView 中,也不返回 Bitmat,此方法只会映射保存到磁盘和内存的高速缓存中。可以用之后要加载当前图片。

get(): get()同步加载图像,返回一个 Bitmap 对象。 Targets: into选项除了传入ImageView对象,还可以转入 Target,这是一个回调。

使用 Target作为回调机制

到目前为止,.into() 我们一直传入的是ImageView对象,当然,这个方法还有重载。我们直接看一个例子,你或许会直接明白。

Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[0])
 .into(target);
//下面是重点
private Target target = new Target() { 
 @Override
 public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from){
 // 图片加载成功
 }
 @Override
 public void onBitmapFailed(Drawable errorDrawable) {
 // 图片加载失败
 }
 @Override
 public void onPrepareLoad(Drawable placeHolderDrawable) {
 //图片加载中
 }
};

注: 这里不推荐使用匿名,垃圾回收机制可能会回收你的目标,将得不到 Bitmap

使用 RemoteView将图像加载到自定义Notifications

RemoteView 用于 Widgets和自定义notification

我们先来看一个自定义notification。

// create RemoteViews
final RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.remoteview_notification);
remoteViews.setImageViewResource(R.id.remoteview_notification_icon, R.mipmap.future_studio_launcher);
remoteViews.setTextViewText(R.id.remoteview_notification_headline, "Headline"); 
remoteViews.setTextViewText(R.id.remoteview_notification_short_message, "Short Message");
remoteViews.setTextColor(R.id.remoteview_notification_headline, getResources().getColor(android.R.color.black)); 
remoteViews.setTextColor(R.id.remoteview_notification_short_message, getResources().getColor(android.R.color.black));
// build notification
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(UsageExampleTargetsAndRemoteViews.this) 
 .setSmallIcon(R.mipmap.future_studio_launcher)
 .setContentTitle("Content Title")
 .setContentText("Content Text")
 .setContent(remoteViews)
 .setPriority(NotificationCompat.PRIORITY_MIN);
final Notification notification = mBuilder.build();
// set big content view for newer androids
if (android.os.Build.VERSION.SDK_INT >= 16) { 
 notification.bigContentView = remoteViews;
}
NotificationManager mNotificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); 
mNotificationManager.notify(NOTIFICATION_ID, notification);

上面一些代码都是创建一个自定义布局的通知,但是不打算深入细节,因为这不是我们要说的重点。我们去使用Picasso去实现这一需求,它是非常的容易: .into(android.widget.RemoteViews remoteViews, int viewId, int notificationId, android.app.Notification notification)

Picasso 
 .with(UsageExampleTargetsAndRemoteViews.this)
 .load(UsageExampleListViewAdapter.eatFoodyImages[0])
 .into(remoteViews, R.id.remoteview_notification_icon, NOTIFICATION_ID, notification);

如果你不知道每个变量是什么,可以常考上面的代码,我们先来看看效果图

带你全方位学习Picasso(二)


图像旋转与转换

旋转

Picasso内置旋转功能,我们直接来看代码,因为它并不难理解。

Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[0])
 .rotate(90f)
 .into(imageViewSimpleRotate);

除了默认以中心点旋转有的时候我们或许需要其他轴点进行选装,我们可以调用 rotate(float degrees, float pivotX, float pivotY)。

Picasso 
 .with(context)
 .load(R.drawable.floorplan)
 .rotate(45f, 200f, 100f)
 .into(imageViewComplexRotate);

转换

旋转只是图像处理技术的一小部分,Picasso还提供了 Transformation 用于图像处理。只需要实现 Transformation 接口的一个主要方法 transform(android.graphics.Bitmap source),此方法传入 Bitmap,并返回转换后的。 实现这个接口后,只需要在Picasso中调用 transform(Transformation transformation) 就完成了图像的转换,说了这么多,不妨我们看两个例子。

模糊图像

首先,我们需要去实现 Transformation 接口,通过使用 RenderScript。但是这篇文章并不打算介绍 RenderScript,但是我们要明白,他可以用于模糊图像。

public class BlurTransformation implements Transformation {
 RenderScript rs;
 public BlurTransformation(Context context) {
 super();
 rs = Renderscript.create(context);
 }
 @Override
 public Bitmap transform(Bitmap bitmap) {
 // 创建一个位图,
 Bitmap blurredBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
 // 分配内存
 Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED);
 Allocation output = Allocation.createTyped(rs, input.getType());
 // 加载一个特定的脚本
 ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
 script.setInput(input);
 // 设置模糊半径
 script.setRadius(10);
 // 启动ScriptIntrinisicBlur
 script.forEach(output);
 // 输出
 output.copyTo(blurredBitmap);
 bitmap.recycle();
 return blurredBitmap;
 }
 @Override
 public String key() {
 return "blur";
 }
}

定义完成之后,Picasso中的调用就会非常的简单了。

Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[0])
 .transform(new BlurTransformation(context))
 .into(imageViewTransformationBlur);

模糊和灰色缩放 不仅如此,Picasso的 transform方法中还提供了 transform(List<? extends Transformation> transformations),这意味着我们可以对图像使用一连串的转换。在这里,我们再实现一个灰色缩放。

public class GrayscaleTransformation implements Transformation {
 private final Picasso picasso;
 public GrayscaleTransformation(Picasso picasso) {
 this.picasso = picasso;
 }
 @Override
 public Bitmap transform(Bitmap source) {
 Bitmap result = createBitmap(source.getWidth(), source.getHeight(), source.getConfig());
 Bitmap noise;
 try {
 noise = picasso.load(R.drawable.noise).get();
 } catch (IOException e) {
 throw new RuntimeException("Failed to apply transformation! Missing resource.");
 }
 BitmapShader shader = new BitmapShader(noise, REPEAT, REPEAT);
 ColorMatrix colorMatrix = new ColorMatrix();
 colorMatrix.setSaturation(0);
 ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
 Paint paint = new Paint(ANTI_ALIAS_FLAG);
 paint.setColorFilter(filter);
 Canvas canvas = new Canvas(result);
 canvas.drawBitmap(source, 0, 0, paint);
 paint.setColorFilter(null);
 paint.setShader(shader);
 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
 canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
 source.recycle();
 noise.recycle();
 return result;
 }
 @Override
 public String key() {
 return "grayscaleTransformation()";
 }
}

完成之后就是又到了Picasso中引用,这次只需要建立一个List集合。

List<Transformation> transformations = new ArrayList<>();
transformations.add(new GrayscaleTransformation(Picasso.with(context))); 
transformations.add(new BlurTransformation(context));
Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[0])
 .transform(transformations)
 .into(imageViewTransformationsMultiple);

图像缓存

默认Picasso的缓存配置。

LRU内存缓存15%的可用RAM 磁盘缓存2%的存储空间,高达50mb,但是不能小于5mb 三个下载线程用于磁盘和网络访问

我们可以更改缓存大小,但是超出了本博客的范围,回到图像缓存的主题,Picasso优先尝试从内存缓存中加载图像,如果没有则在检索磁盘缓存,如果磁盘上也没有, 那么就会启动网络请求(内存->磁盘->网络请求)。此外,所有请求的图像都存储在两个缓存中(知道他们必须被删除才释放空间)。 如果你认为这种缓存策略无法满足自己,我们可以看看 MemoryPolicy

内存策略

首先,Picasso优先会从内存中获取所请求图像,如果你想跳过这一步,可以用使用 memoryPolicy(MemoryPolicy policy, MemoryPolicy... additional)。 MemoryPolicy 是一个有两个值的简单枚举 NO_CACHE和NO_STORE。

Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[1])
 .memoryPolicy(MemoryPolicy.NO_CACHE)
 .into(imageViewFromDisk);

如果你想知道 NO_STORE 用于什么情况:就是请求的图片不缓存到内存中。 我们再来看下下面的例子。

Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[1])
 .memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)
 .into(imageViewFromDisk);

这样做就是使请求的图片既不缓存到内存中,Picasso也不去内存中寻找。这样做的好处可以加速内存回收,因为现有API无法清理内存缓存。

网络策略

如果你想跳过磁盘缓存,可以调用 .networkPolicy(NetworkPolicy policy, NetworkPolicy... additional) 与 NetworkPolicy.NO_CACHE 作为参数。

Picasso 
 .with(context)
 .load(UsageExampleListViewAdapter.eatFoodyImages[2])
 .networkPolicy(NetworkPolicy.NO_CACHE)
 .into(imageViewFromNetwork);

最后还提供了第三个选项 NetworkPolicy:OFFLINE。如果使用这个作为参数,那么Picasso只会从缓存中寻找图像,即使有网络也不会发送请求。


缓存指示器,日志,统计

缓存指示器

作为称职的开发人员,有的时候我们需要去分析图像来自何处,最简单的方法就是激活缓存指示器,只需要使用 .setIndicatorsEnabled(true) 。

Picasso 
 .with(context)
 .setIndicatorsEnabled(true);

之后所有的图片在左上角都会有一个小指标。

带你全方位学习Picasso(二)

  • 绿色(图片来自内存)
  • 蓝色(图片来自磁盘)
  • 红色(图片来自网络)

日志

有的时候我们或许需要更多的信息,我们可以激活日志,.setLoggingEnabled(true)。

Picasso 
 .with(context)
 .setLoggingEnabled(true);

这将导致所有的图片加载都将打印日志。

D/Picasso﹕ Main created [R0] Request{http://i.imgur.com/rT5vXE1.jpg} 
D/Picasso﹕ Dispatcher enqueued [R0]+21ms 
D/Picasso﹕ Hunter executing [R0]+26ms 
D/Picasso﹕ Hunter decoded [R0]+575ms 
D/Picasso﹕ Dispatcher batched [R0]+576ms for completion 
D/Picasso﹕ Main completed [R0]+807ms from NETWORK 
D/Picasso﹕ Dispatcher delivered [R0]+809ms

统计

我们可以得到请求的平均结果。

StatsSnapshot picassoStats = Picasso.with(context).getSnapshot();
Log.d("Picasso Stats", picassoStats.toString());

控制台将会输出

D/Picasso Stats﹕ StatsSnapshot{
maxSize=28760941, 
size=26567204, 
cacheHits=30, 
cacheMisses=58, 
downloadCount=0, 
totalDownloadSize=0, 
averageDownloadSize=0, 
totalOriginalBitmapSize=118399432, 
totalTransformedBitmapSize=96928004, 
averageOriginalBitmapSize=2466654, 
averageTransformedBitmapSize=2019333, 
originalBitmapCount=48, 
transformedBitmapCount=41, 
timeStamp=1432576918067}

使用 Picasso.Builder 自定义Picasso

Picasso有着直接的方法来修改Picasso的实例 Picasso.Builder类。可以使用它自定义Picasso。

局部自定义Picasso

在进入自定义Picasso之前,我们需要简单的回顾下获取标准的Picasso实例。

Picasso picasso = Picasso.with(Context);

这个方法返回的是标准的Picasso,如果你需要建立一个自定义实例,有一种方法就是建立 Picasso.Builder对象来做调整。

// 创建 Picasso.Builder 对象
Picasso.Builder picassoBuilder = new Picasso.Builder(context);
// TODO
Picasso picasso = picassoBuilder.build()

接下来我们就可以使用自定义的picasso去加载图片

picasso 
 .load(UsageExampleListViewAdapter.eatFoodyImages[0])
 .into(imageView1)

全局使用自定义Picasso

创建和修改Picasso实例方法保持不变

// 创建 Picasso.Builder 对象
Picasso.Builder picassoBuilder = new Picasso.Builder(context);
//TODO
Picasso picasso = picassoBuilder.build();

为了让这个Picasso变成全局的,只需要调用 Picasso.setSingletonInstance(picasso);,通常情况下,这个应该在App启动的时候初始化。

try { 
 Picasso.setSingletonInstance(picasso);
} catch (IllegalStateException ignored) {
}

一旦你自定义的全局Picasso,接下来所有的 Picasso.with(Context context) 调用都将返回自定义的Picasso。

既然我们学会了如何自定义Picasso,但是我们并不明白自定义Picasso能做些什么?

自定义Picasso:更改下载器

Picasso默认使用最佳可用缓存和网络组件。如果你想更换默认的下载器,你可以通过 Picasso.Builder 调用 downloader(Downloader downloader),比如说我们的 Okhttp。

Picasso.Builder picassoBuilder = new Picasso.Builder(context);
//更改下载器。
picassoBuilder.downloader(new OkHttpDownloader(new OkHttpClient()));
Picasso picasso = picassoBuilder.build();

但是有一个需要注意的地方,如果你加载的图片是从自己服务器上获取的,而且服务器用的是HTTPS,而且需要一个签名,这个时候OKHttp就会因为SSL的问题而导致图片无法被加载,这时你可以选择使用UnsafeOkHttpClient,就可以避免该问题的产生;

picassoBuilder.downloader( 
 new OkHttpDownloader(
 UnsafeOkHttpClient.getUnsafeOkHttpClient()
 )
);

自定义请求处理

依旧是自定义Picasso,我们先来看一段代码。

Picasso.Builder picassoBuilder = new Picasso.Builder(context);
picassoBuilder.addRequestHandler(new EatFoodyRequestHandler());
Picasso picasso = picassoBuilder.build();

唯一有疑惑的应该是 EatFoodyRequestHandler ,这是一个我们继承 RequestHandler 的自定义(请求处理)类,所以我们来看看什么是 RequestHandler。

RequestHandler实现

这是一个抽象类,有两个方法要实现。

boolean canHandleRequest(Request data) Result load(Request request, int networkPolicy)

第一个方法让Picasso知道这个请求处理程序是否处理当前请求,如果可以的话请求将被传递给 load() 方法。

不妨我们来看下这个类整体是如何实现的。

public class EatFoodyRequestHandler extends RequestHandler { 
 private static final String EAT_FOODY_RECIPE_SCHEME = "eatfoody";
 @Override
 public boolean canHandleRequest(Request data) {
 return EAT_FOODY_RECIPE_SCHEME.equals(data.uri.getScheme());
 }
 @Override
 public Result load(Request request, int networkPolicy) throws IOException {
 //获取请求的host
 String imageKey = request.uri.getHost(); 
 Bitmap bitmap;
 if (imageKey.contentEquals("cupcake")) {
 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.cupcake);
 }else if (imageKey.contentEquals("full_cake")) {
 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.full_cake);
 }else {
 bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
 }
 return new Result(bitmap, Picasso.LoadedFrom.DISK);
 }
}

代码看起来并不理解,首先 canHandleRequest 方法中判断uri的协议是否等于 eatfoody。如果不是,Picasso会回复到标准的下载流程。如果是,那么就会进入 load 方法,在 load 方法中,我们判断uri的 host 部分,随后通过判断返回相应的 bitmap。

//这会按照标准的流程加载图片
picasso 
 .load("http://i.imgur.com/DvpvklR.png")
 .into(imageView1);
//他会进入 load 方法中的逻辑判断去加载 `R.drawable.full_cake`
picasso 
 .load("eatfoody://cupcake")
 .into(imageView2)

picasso2-okhttp3-downloader 与 picasso-transformations

如果你仔细看过前面的内容,我想你已经明白了下载器以及转换。他的可扩展性是不可否认的,那么自然会有很多优秀的开发者会去为我们提供相应的框架。

  • picasso-transformations
  • picasso2-okhttp3-downloader

但是我们需要添加相关的依赖,这个并不困难,可以自己去翻阅github,同时也给出了实例。


总结

Picasso的使用教程暂时到这里,有机会我们还会去带你阅读他的源码,但是博主承认目前为止还不够成熟,所以我们放在后面带你去阅读,因为他的设计理念,可扩展真的非常出色,即便是博主现在一直使用的Glide也是基于累死的设计模式,只是在策略上略有出入。同时,我们还要学习Glide,虽然两者差不多,但是还是要给有需要的人..... 当然,少不了博主对于他们两者之间的对比分析,这或许会更加的有意思。

相关推荐

狂草 / 0评论 2014-12-06