Picasso基本使用
0、导入依赖
implementation 'com.squareup.picasso:picasso:2.5.2'
1、加载显示图片
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。
重载方法
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);
占位图
Picasso.with(MainActivity.this)
.load(url)
.placeholder(R.mipmap.ic_launcher)
.into(headerImage);
异常图
Picasso.with(MainActivity.this)
.load(url)
.error(R.drawable.error)
.into(headerImage);
noPlaceholder
这个方法的意思就是:在调用into的时候明确告诉你没有占位图设置。根据这个方法签名的解释是阻止View被回收的时候Picasso清空target或者设置一个应用的占位图。需要注意的是placeholder和noPlaceholder 不能同时应用在同一个请求上,会抛异常。
noFade
无论你是否设置了占位图,Picasso 从磁盘或者网络加载图片时,into 显示到ImageView 都会有一个简单的渐入过度效果,让你的UI视觉效果更柔顺丝滑一点,如果你不要这个渐入的效果(没有这么坑爹的需求吧!!!),就调用noFade方法。
设置图片的尺寸(Resize)、缩放(Scale)和裁剪(Crop)
Resize(int w,int h用于重新设置尺寸
Picasso.with(this).load(URL)
.placeholder(R.drawable.default_bg)
.error(R.drawable.error_iamge)
.resize(400,200)
.into(mImageView);
将尺寸写在imens.xml文件中,设置dp单位
// <dimen name="image_width">300dp</dimen><br>
// <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);
onlyScaleDown
当调用了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() 使用resize 来重新设置图片的尺寸的时候,会发现有些图片拉伸或者扭曲了(使用ImageView的时候碰到过吧),为了避免这种情况,Picasso 同样给我们提供了一个方法,centerCrop,充满ImageView 的边界,居中裁剪。
centerInside
上面的centerCrop是可能看不到全部图片的,如果你想让View将图片展示完全,可以用centerInside,但是如果图片尺寸小于View尺寸的话,是不能充满View边界的。
fit
fit 它会自动测量我们的View的大小,然后内部调用reszie方法把图片裁剪到View的大小,这就帮我们做了计算size和调用resize 这2步。 使用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,很好理解,因为它要测量宽高。
Rotation()
图片旋转
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);
转换器Transformation!!!
Transformation 这就是Picasso的一个非常强大的功能了,它允许你在load图片 -> into ImageView 中间这个过成对图片做一系列的变换。比如你要做图片高斯模糊、添加圆角、做度灰处理、圆形图片等等都可以通过Transformation来完成。
首先自定义一个Transform
private class customTransformer implements Transformation{
@Override
public Bitmap transform(Bitmap source) {
//在这里可以对Bitmap进行操作,比如改变大小,形状等
return source;
}
@Override
public String key() {
return null;
}
}
然后在transform方法中进行处理:
Picasso.with(MainActivity.this)
.load(url)
.placeholder(R.mipmap.ic_launcher)
.transform(new customTransformer())
.error(R.drawable.error)
.into(headerImage);
多个Transformation操作的2种应用方式
直接调用多次transform 方法,不会覆盖的。它只是保存到了一个List 里面 接受一个List,将Transformation 放大list 里
请求的优先级
Picasso 为请求设置有优先级,有三种优先级,LOW、NORMAL、HIGH。默认情况下都是NORMAL
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);
Tag管理请求
Picasso允许我们为一个请求设置tag来管理请求,相应的一些处理如下:
1) cancelTag(Object tag) 取消设置了给定tag的所有请求
2) pauseTag(Object tag) 暂停设置了给定tag 的所有请求
3) resumeTag(Object tag) resume 被暂停的给定tag的所有请求
4) tag(Object tag) 为请求设置tag
Picasso.with(this).load(mData.get(position))
.placeholder(R.drawable.default_bg)
.error(R.drawable.error_iamge)
.tag("PhotoTag")
.into(holder.mImageView);
Tag管理常见的应用场景
图片列表滑动的时候不加载图片,滑动停止后开始加载图片
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");
}
}
});
在Activity finish之后用于停止未完成的任务,防止内存泄漏
@Override
protected void onDestroy() {
super.onDestroy();
Picasso.with(this).cancelTag("PhotoTag");
}
同步/异步加载图片
同步加载使用get方法,返回一个Bitmap
try {
Bitmap bitmap = Picasso.with(this).load(URL).get();
} catch (IOException e) {
e.printStackTrace();
}
异步加载
一般直接加载图片通过into显示到imageView中也是异步的,除此之外还提供了2个异步的方法
1、fetch() 异步方式加载图片
2、fetch(Callback callback) 异步方式加载图片并给一个回调接口。
Picasso.with(this).load(URL).fetch(new Callback() {
@Override
public void onSuccess() {
//加载成功
}
@Override
public void onError() {
//加载失败
}
});
fetch方法在请求成功之后并不会返回一个bitmap,而是将结果存到了缓存,包括磁盘和内存缓存,可以用于预加载。
缓存(Disk和Memory)
默认情况下,Picasso 内存缓存和磁盘缓存都开启了的,也就是加载图片的时候,内存和磁盘都缓存了。
memoryPolicy设置内存缓存策略
NO_CACHE:表示处理请求的时候跳过检查内存缓存
NO_STORE: 请求成功之后,不将最存到内存。
with(this).load(URL)
.placeholder(R.drawable.default_bg)
.error(R.drawable.error_iamge)
.memoryPolicy(MemoryPolicy.NO_CACHE,MemoryPolicy.NO_STORE) //静止内存缓存
.into(mBlurImage);
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);
Debug和日志
Picasso也是有三级缓存策略的,Picasso可以通过setIndicatorsEnabled(boolean)展示图片的来源,调用这个方法之后在图片的左上角出现一个带色块的三角形标示,有3种颜色,绿色表示从内存加载、蓝色表示从磁盘加载、红色表示从网络加载。
Picasso.with(this)
.setIndicatorsEnabled(true);//显示指示器
Picasso源码解析
Picasso的工作流程
分析一些上面提及到的常见的方法
Picasso.with()
public static Picasso with(Context context) {
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}
这个方法维护一个Picasso的单例,如果还未实例化就通过new Builder(context).build()创建一个singleton并返回,下面来看一下Builder类的实现:
public static class Builder {
public Builder(Context context) {
if (context == null) {
throw new IllegalArgumentException("Context must not be null.");
}
this.context = context.getApplicationContext();
}
/** Create the {@link Picasso} instance. */
public Picasso build() {
Context context = this.context;
if (downloader == null) {
//创建默认下载器
downloader = Utils.createDefaultDownloader(context);
}
if (cache == null) {
//创建Lru内存缓存
cache = new LruCache(context);
}
if (service == null) {
//创建线程池,默认有3个执行线程,会根据网络状况自动切换线程数
service = new PicassoExecutorService();
}
if (transformer == null) {
//创建默认的transformer,并无实际作用
transformer = RequestTransformer.IDENTITY;
}
//创建stats用于统计缓存,以及缓存命中率,下载数量等等
Stats stats = new Stats(cache);
//创建dispatcher对象用于任务的调度
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}
}
我们使用Picasso默认配置的时候(当然也可以自定义),最后会调用build()方法并配置好我们需要的各种对象,最后实例化一个Picasso对象并返回。下面就看一下load方法的实现
public RequestCreator load(Uri uri) {
return new RequestCreator(this, uri, 0);
}
通过load函数,我们最终得到了一个RequestCreator对象,通过这个对象我们就可以定制一些对图片的特殊处理了,再看看RequestCreator的构造方法
equestCreator(Picasso picasso, Uri uri, int resourceId) {
if (picasso.shutdown) {
throw new IllegalStateException(
"Picasso instance already shut down. Cannot submit new requests.");
}
this.picasso = picasso;
this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
}
首先是持有一个Picasso的对象,然后构建一个Request的Builder对象,将我们需要加载的图片的信息都保存在data里,在我们通过.centerCrop()或者.transform()等等方法的时候实际上也就是改变data内的对应的变量标识,再到处理的阶段根据这些参数来进行对应的操作,所以在我们调用into()方法之前,所有的操作都是在设定我们需要处理的参数,真正的操作都是有into()方法引起的。
关键来了:into方法
public void into(ImageView target) {
//传入空的callback
into(target, null);
}
public void into(ImageView target, Callback callback) {
long started = System.nanoTime();
//检查调用是否在主线程
checkMain();
if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
}
//如果没有设置需要加载的uri,或者resourceId
if (!data.hasImage()) {
picasso.cancelRequest(target);
//如果设置占位图片,直接加载并返回
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
}
//如果是延时加载,也就是选择了fit()模式
if (deferred) {
//fit()模式是适应target的宽高加载,所以并不能手动设置resize,如果设置就抛出异常
if (data.hasSize()) {
throw new IllegalStateException("Fit cannot be used with resize.");
}
int width = target.getWidth();
int height = target.getHeight();
//如果目标ImageView的宽或高现在为0
if (width == 0 || height == 0) {
//先设置占位符
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
//监听ImageView的ViewTreeObserver.OnPreDrawListener接口,一旦ImageView
//的宽高被赋值,就按照ImageView的宽高继续加载.
picasso.defer(target, new DeferredRequestCreator(this, target, callback));
return;
}
//如果ImageView有宽高就设置设置
data.resize(width, height);
}
//构建Request
Request request = createRequest(started);
//构建requestKey
String requestKey = createKey(request);
//根据memoryPolicy来决定是否可以从内存里读取
if (shouldReadFromMemoryCache(memoryPolicy)) {
//通过LruCache来读取内存里的缓存图片
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
//如果读取到
if (bitmap != null) {
//取消target的request
picasso.cancelRequest(target);
//设置图片
setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
if (picasso.loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
}
//如果设置了回调接口就回调接口的方法.
if (callback != null) {
callback.onSuccess();
}
return;
}
}
//如果缓存里没读到,先根据是否设置了占位图并设置占位
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
//构建一个Action对象,由于我们是往ImageView里加载图片,所以这里创建的是一个ImageViewAction对象.
Action action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
//将Action对象入列提交
picasso.enqueueAndSubmit(action);
}
into方法中的处理流程
- into会检查当前是否是在主线程上执行。
- 如果我们没有提供一个图片资源并且有设置placeholder,那么就会把我们设置的placeholder显示出来,并中断执行。
- defered属性我们一般情况下不需要关注,只有当我们调用了RequestCreator的fit方法时defered才为true,而fit()模式是适应target的宽高加载,所以并不能手动设置resize,如果设置就抛出异常。
- 接下来就是创建了一个Request对象,我们在前面做得一些设置都会被封装到这个Request对象里面。
- 检查我们要显示的图片是否可以直接在缓存中获取,如果有就直接显示出来好了。
- 缓存没命中,那就只能费点事把源图片down下来了。这个过程是异步的,并且通过一个Action来完成请求前后的衔接工作。
小结
至此,Picasso在主线程中的工作就结束了。通过上面的分析,我们看到Picasso的思想还是很清晰的:首先通过Picasso创建了一个RequestCreator对象,通过这个对象我们可以针对不同的场景来设置一些属性,之后创建出Request对象,最后通过Action来确定异步请求并对请求结果做处理。
Picasso的内部构造以及设计思想
Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
RequestTransformer requestTransformer, List<RequestHandler> extraRequestHandlers, Stats stats,
Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled, boolean loggingEnabled) {
...
}
Picasso是不允许外部创建的,而是通过单例和构建者模式去进行对象的构建,在Builder中实例化了Picasso所需要的这些组件,这样就可以个性化地去管理需要用到的各个组件。
上面的工作流程中也简介了Builder的各部分重要的组件,其中有一个就是我们的调度器Dispatcher,图片要不要开始下载以及下载后Bitmap的返回都是通过这个调度器来执行的,而通过上面的工作流程也得知到,其实在into的最后调用Picasso的enqueueAndSubmit方法,代码如下:
void enqueueAndSubmit(Action action) {
Object target = action.getTarget();
//取消这个target已经有的action.
if (target != null && targetToAction.get(target) != action) {
// This will also check we are on the main thread.
cancelExistingRequest(target);
targetToAction.put(target, action);
}
//提交action
submit(action);
}
//调用dispatcher来派发action
void submit(Action action) {
dispatcher.dispatchSubmit(action);
}
可见在enqueueAndSubmit方法的最后是调用了Dispatcher的dispatchSubmit方法,代码如下:
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
附带:Dispatcher的内部构造
Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
Downloader downloader, Cache cache, Stats stats) {
this.dispatcherThread = new DispatcherThread();
this.dispatcherThread.start();
...
this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
...
}
通过上面的代码可见,在Dispatcher内部,Dispatcher定义了DispatcherThread和DispatcherHandler两个内部类,并在Dispatcher的构造函数中对他们经行了实例化,所有的调度也都是通过handler异步的执行的。
handler的最终其实调用了performSubmit方法来触发一个图片的加载
void performSubmit(Action action, boolean dismissFailed) {
if (pausedTags.contains(action.getTag())) {
pausedActions.put(action.getTarget(), action);
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
"because tag '" + action.getTag() + "' is paused");
}
return;
}
BitmapHunter hunter = hunterMap.get(action.getKey());
if (hunter != null) {
hunter.attach(action);
return;
}
if (service.isShutdown()) {
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");
}
return;
}
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
if (dismissFailed) {
failedActions.remove(action.getTarget());
}
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
}
}
简单分析一下方法的逻辑:这里主要是获取到一个BitmapHunter实例,当然在这里做了一些操作:(通过action的key来在hunterMap查找是否有相同的hunter,这个key里保存的是我们的uri或者resourceId和一些参数,如果都是一样就将这些action合并到一个BitmapHunter里去。)然后就交给Picasso的线程池去执行,而接下来加载图片成功或失败的结果也是由调度器去更新UI。
@Override public void run() {
try {
updateThreadName(data);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
}
result = hunt();
// 通过Dispatcher来处理结果
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}
} catch() {
//各种异常处理
}
下载成功的时候,也是像上面调度图片加载一样通过handler去发送消息,再到息处理函数进行实际逻辑。
void dispatchComplete(BitmapHunter hunter) {
handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));
}
@Override public void handleMessage(final Message msg) {
switch (msg.what) {
...
case HUNTER_COMPLETE: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
dispatcher.performComplete(hunter);
break;
}
...
}
}
void performComplete(BitmapHunter hunter) {
if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
cache.set(hunter.getKey(), hunter.getResult());
}
hunterMap.remove(hunter.getKey());
batch(hunter);
if (hunter.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
}
}
批处理:在这里Picasso并不是立即将图片显示出来,而是用到而是用到了一个批处理,其实就是把操作先暂存在一个list中,等空闲的时候再拿出来处理,这样做得好处也是尽量减少主线程的执行时间,一方面防止ANR,另一方面快速返回,响应页面的其他渲染操作,防止卡顿用户界面。
private void batch(BitmapHunter hunter) {
if (hunter.isCancelled()) {
return;
}
batch.add(hunter);
if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
}
}
@Override public void handleMessage(final Message msg) {
switch (msg.what) {
...
case HUNTER_DELAY_NEXT_BATCH: {
dispatcher.performBatchComplete();
break;
}
...
}
}
void performBatchComplete() {
List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);
batch.clear();
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
logBatch(copy);
}
由上面的三个方法可见,其实最后还是调用了Dispatcher的方法来处理,而又上面Dispatcher的构建过程也知道,其实在Dispatcher的构建过程中会创建一个handlerThread和handler,而handler肯定就是用于不断地派送消息,而这个过程在handlerThread里面去执行的用意也是很明显,因为handlerThread适用于会长时间在后台运行,并且间隔时间内(或适当情况下)会调用的情况(有点扯远了)反正就是目前不在主线程,所以这也是为什么在构建的时候要在外部传进来一个主线程的handler,就是用来更新UI的。然后通过这个发送消息的过程,处理又回到了Picasso,它的其中有一个静态的handler,就可以接收到Dispatcher发来的消息然后进行图片显示的操作
static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
@Override public void handleMessage(Message msg) {
switch (msg.what) {
case HUNTER_BATCH_COMPLETE: {
@SuppressWarnings("unchecked") List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj;
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = batch.size(); i < n; i++) {
BitmapHunter hunter = batch.get(i);
hunter.picasso.complete(hunter);
}
break;
}
...
}
}
@Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
if (result == null) {
throw new AssertionError(
String.format("Attempted to complete action with no result!\n%s", this));
}
//得到target也就是ImageView
ImageView target = this.target.get();
if (target == null) {
return;
}
Context context = picasso.context;
boolean indicatorsEnabled = picasso.indicatorsEnabled;
//通过PicassoDrawable来将bitmap设置到ImageView上
PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);
//回调callback接口
if (callback != null) {
callback.onSuccess();
}
}
来到这里图片显示就完成了,很显然这里通过了PicassoDrawable.setBitmap()将我们的Bitmap设置到了我们的ImageView上就可以带一点渐变加载动画的效果。