在Picasso图片加载框架 —— 源码解析(一)中,我们从头到尾梳理了一遍Picasso网络加载图片的过程,但对于其中很多细节并没有展开介绍。本篇将深入细节,以实际情景为出发点,带领大家看看Picasso中丰富的图片功能及其代码实现,进而学习下Picasso框架的设计思想。
一、在不同目标View上显示图片
在实际开发中,我们不一定每次都用ImageView来显示下载的图片。有些时候,我们需要把图片显示到自定义View或RemoteViews上。要想实现这点,一个最朴素的想法就是先获取图片,然后在调用自定义View或RemoteViews的API。想法虽糙,但很实用。Picasso框架也是这么处理的,只不过作为一个图片加载框架,它对这一过程又做了进一步的抽象。
仔细看RequestCreator类的源码,我们可以发现,into()方法共有五种重载:
1)into(ImageView)和into(ImageView, Callback)用于ImageView;
2)into(RemoteViews, int, int, Notification)用于状态栏通知;
3)into(RemoteViews, int, int[])用于桌面小部件;
4)into(Target)用于继承Target接口的类。
1.into(RemoteViews, int, int, Notification)
/**
* 异步方法
*/
public void into(RemoteViews remoteViews, int viewId, int notificationId,
Notification notification) {
long started = System.nanoTime();
if (remoteViews == null) {
throw new IllegalArgumentException("RemoteViews must not be null.");
}
if (notification == null) {
throw new IllegalArgumentException("Notification must not be null.");
}
//不支持图片变换
if (deferred) {
throw new IllegalStateException("Fit cannot be used with RemoteViews.");
}
if (placeholderDrawable != null || placeholderResId != 0 || errorDrawable != null) {
throw new IllegalArgumentException(
"Cannot use placeholder or error drawables with remote views.");
}
//创建真实请求
Request request = createRequest(started);
String key = createKey(request, new StringBuilder());
//处理Notification图片加载的action
RemoteViewsAction action =
new NotificationAction(picasso, request, remoteViews, viewId, notificationId, notification,
memoryPolicy, networkPolicy, key, tag, errorResId);
performRemoteViewInto(action);
}
private void performRemoteViewInto(RemoteViewsAction action) {
//是否从缓存获取图片
if (shouldReadFromMemoryCache(memoryPolicy)) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(action.getKey());
if (bitmap != null) {
action.complete(bitmap, MEMORY);
return;
}
}
if (placeholderResId != 0) {
action.setImageResource(placeholderResId);
}
//入队并提交
picasso.enqueueAndSubmit(action);
}
与ImageView的into()方法类似,创建Request和Action,并调用Picasso.enqueueAndSubmit()入队和提交,后续过程和ImageView的流程一样,就不在此赘述了,不清楚的小伙伴可以去看Picasso图片加载框架 —— 源码解析(一)。
值得注意的一点是,该into()方法并没有做主线程检查,即不强制要求由主线程进行调用。但我们记得,在Picasso.enqueueAndSubmit()中有一个cancelExistingRequest()的动作,该动作会做主线程检查。那么,如果从非主线程调用into()方法,不就直接抛出异常了嘛,感觉像是源码的bug......
在获取到图片后,会回调Action.complete()方法,此处就是回调NotificationAction.complete()方法。
abstract class RemoteViewsAction extends Action<RemoteViewsAction.RemoteViewsTarget> {
@Override
void complete(Bitmap result, Picasso.LoadedFrom from) {
//为remoteViews设置图片
remoteViews.setImageViewBitmap(viewId, result);
update();
}
abstract void update();
static class NotificationAction extends RemoteViewsAction {
@Override
void update() {
//刷新Notification
NotificationManager manager = getService(picasso.context, NOTIFICATION_SERVICE);
manager.notify(notificationId, notification);
}
}
}
从源码可以看到,NotificationAction是RemoteViewsAction的子类,并且没有重写RemoteViewsAction的complete()方法。这里是通过在RemoteViewsAction.complete()方法中提供抽象的update()方法实现了Notification自身的特殊需求。这样做的好处显而易见:将共性的RemoteViews图片设置工作放在基类中予以实现,而子类只需关注自身的特殊逻辑即可。这一点将会在接下来的into(RemoteViews, int, int[])方法中得到进一步的实践。
2.into(RemoteViews, int, int[])
/**
* 异步方法
*/
public void into(RemoteViews remoteViews, int viewId, int[] appWidgetIds) {
long started = System.nanoTime();
if (remoteViews == null) {
throw new IllegalArgumentException("remoteViews must not be null.");
}
if (appWidgetIds == null) {
throw new IllegalArgumentException("appWidgetIds must not be null.");
}
if (deferred) {
throw new IllegalStateException("Fit cannot be used with remote views.");
}
if (placeholderDrawable != null || placeholderResId != 0 || errorDrawable != null) {
throw new IllegalArgumentException(
"Cannot use placeholder or error drawables with remote views.");
}
Request request = createRequest(started);
String key = createKey(request, new StringBuilder()); // Non-main thread needs own builder.
RemoteViewsAction action =
new AppWidgetAction(picasso, request, remoteViews, viewId, appWidgetIds, memoryPolicy,
networkPolicy, key, tag, errorResId);
performRemoteViewInto(action);
}
与Notification的into()方法十分相似,主要不同是Action对象不一样,这里的Action是AppWidgetAction,并且这个AppWidgetAction也是RemoteViewsAction的子类。
abstract class RemoteViewsAction extends Action<RemoteViewsAction.RemoteViewsTarget> {
static class AppWidgetAction extends RemoteViewsAction {
@Override
void update() {
AppWidgetManager manager = AppWidgetManager.getInstance(picasso.context);
manager.updateAppWidget(appWidgetIds, remoteViews);
}
}
}
AppWidgetAction与NotificationAction如出一辙,基类为RemoteViews更新图片,子类实现自身逻辑。
3.into(Target)
接下来,我们来看into()的最后一个重载:into(Target)。从方法参数就可以看出,这个重载与之前两个明显不同,这里不是对RemoteViews进行设置,而是在处理Target,那这个Target是什么呢?
public interface Target {
/**
* 在图片获取成功时触发
* 注意:不能在此方法中回收Bitmap,否则会抛异常
*/
void onBitmapLoaded(Bitmap bitmap, LoadedFrom from);
/**
* 在图片获取失败时触发
*/
void onBitmapFailed(Drawable errorDrawable);
/**
* 在图片请求提交之前触发
*/
void onPrepareLoad(Drawable placeHolderDrawable);
}
Target是Picasso框架提供的接口,任何实现了该接口的类都会在图片获取成功、图片获取失败和提交图片请求之前收到相应的方法回调,进而可以在回调中完成图片处理逻辑。接下来,我们看看into(Target)的实现。
/**
* 异步方法
*/
public void into(Target target) {
long started = System.nanoTime();
//主线程检查
checkMain();
if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
}
if (deferred) {
throw new IllegalStateException("Fit cannot be used with a Target.");
}
//若请求中没有设置uri和resourceId,则视其为无效请求,取消这次request,并回调target.onPrepareLoad()
if (!data.hasImage()) {
picasso.cancelRequest(target);
target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);
return;
}
Request request = createRequest(started);
String requestKey = createKey(request);
if (shouldReadFromMemoryCache(memoryPolicy)) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
//若从cache中获取到了图片,则回调target.onBitmapLoaded()
if (bitmap != null) {
picasso.cancelRequest(target);
target.onBitmapLoaded(bitmap, MEMORY);
return;
}
}
//在提交请求之前,回调target.onPrepareLoad()
target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);
Action action =
new TargetAction(picasso, target, request, memoryPolicy, networkPolicy, errorDrawable,
requestKey, tag, errorResId);
picasso.enqueueAndSubmit(action);
}
主线程检查 ---> request有效性检查 ---> 从cache获取图片 ---> 创建TargetAction并提交入列。整体流程没有太大变化,只是在相应位置增加了Target的回调。同样的,我们继续看TargetAction的实现。
final class TargetAction extends Action<Target> {
@Override
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 target = getTarget();
if (target != null) {
target.onBitmapLoaded(result, from);
if (result.isRecycled()) {
throw new IllegalStateException("Target callback must not recycle bitmap!");
}
}
}
@Override
void error() {
Target target = getTarget();
if (target != null) {
if (errorResId != 0) {
target.onBitmapFailed(picasso.context.getResources().getDrawable(errorResId));
} else {
target.onBitmapFailed(errorDrawable);
}
}
}
}
当获取图片成功后,TargetAction.complete()会取出Target对象,回调Target.onBitmapLoaded()方法,并且还会对Bitmap的状态进行检查。若Bitmap已被回收掉,则抛出IllegalStateException异常。当获取图片失败时,TargetAction.error()则会回调Target.onBitmapFailed()方法。
看完into(Target)方法的源码,那么into(Target)具体要怎么使用呢?请看源码中的例子:
//在自定义View中使用Target接口
public class ProfileView extends FrameLayout implements Target {
@Override
public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
setBackgroundDrawable(new BitmapDrawable(bitmap));
}
@Override
public void onBitmapFailed() {
setBackgroundResource(R.drawable.profile_error);
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
setBackgroundDrawable(placeHolderDrawable);
}
}
//在其他类中使用Target接口
public class ViewHolder implements Target {
public FrameLayout frame;
@Override
public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
frame.setBackgroundDrawable(new BitmapDrawable(bitmap));
}
@Override
public void onBitmapFailed() {
frame.setBackgroundResource(R.drawable.profile_error);
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
frame.setBackgroundDrawable(placeHolderDrawable);
}
}
Target就是Picasso暴露给外部的接口,任何想要图片的地方,都需要继承Target接口,并在相应的回调中处理图片。至于图片是如何被下载的,我们无需关心,Picasso框架会自动帮我们搞定。
使用Target接口时有一点需要格外注意:在Picasso.enqueueAndSubmit()方法中,Target对象会被存储到WeakHashMap中,因此继承Target接口的类最好也重写下equals()和hashCode()方法。
二、直接获取图片
既然into()方法可以将图片显示到指定View上,那么是不是也有个方法可以让开发者直接拿到图片,然后自己决定如何使用呢?当然有的,Picasso框架提供了RequestCreator.get()方法,该方法可以返回一个Bitmap供开发者使用。
public class RequestCreator {
/**
* 同步方法,不能在主线程调用
*/
public Bitmap get() throws IOException {
long started = System.nanoTime();
//线程检查。如果是主线程调用,则会抛IllegalStateException
checkNotMain();
if (deferred) {
throw new IllegalStateException("Fit cannot be used with get.");
}
if (!data.hasImage()) {
return null;
}
Request finalData = createRequest(started);
String key = createKey(finalData, new StringBuilder());
//创建Action
Action action = new GetAction(picasso, finalData, memoryPolicy, networkPolicy, tag, key);
//创建BitmapHunter,并调用BitmapHunter.hunt()方法获取图片
return forRequest(picasso, picasso.dispatcher, picasso.cache, picasso.stats, action).hunt();
}
}
//空实现
class GetAction extends Action<Void> {
GetAction(Picasso picasso, Request data, int memoryPolicy, int networkPolicy, Object tag,
String key) {
super(picasso, null, data, memoryPolicy, networkPolicy, 0, null, key, tag, false);
}
@Override void complete(Bitmap result, Picasso.LoadedFrom from) {
}
@Override public void error() {
}
}
get()方法不同于into()方法将Action提交并入队,而是直接通过BitmapHunter.forRequest()创建BitmapHunter对象,并调用BitmapHunter.hunt()直接从外部资源(网络、磁盘等)获取图片并返回结果。由于GetAction全程没有参与其中,源码中也是给予了空实现。
基于上述代码和分析,可以看出:
1)get()为同步方法,其直接调用了BitmapHunter.hunt()从外部资源获取图片,属于耗时操作,因此不能从主线程调用;
2)get()方法直接将获取到的图片返回,并没有将其保存到cache中。因此,即使多次用get()方法获取相同的图片,每次调用还是都会从外部资源获取一遍,没办法享受cache的好处。
三、预加载
预加载,顾名思义就是提前从网络或磁盘将图片加载至内存中,然后在需要的时候直接从内存中获取图片。这样便可将图片加载的耗时过程隐藏起来,避免了用户长时间的等待,可大大提升用户体验。Picasso框架通过RequestCreator.fetch()方法提供对预加载的支持,废话不多说,直接上源码。
public class RequestCreator {
/**
* 异步方法
*/
public void fetch() {
fetch(null);
}
/**
* 异步方法
*/
public void fetch(Callback callback) {
long started = System.nanoTime();
if (deferred) {
throw new IllegalStateException("Fit cannot be used with fetch.");
}
//检查请求有效性
if (data.hasImage()) {
//fetch操作默认为低优先级
if (!data.hasPriority()) {
data.priority(Priority.LOW);
}
Request request = createRequest(started);
String key = createKey(request, new StringBuilder());
Bitmap bitmap = picasso.quickMemoryCacheCheck(key);
if (bitmap != null) { //如果从cache中找到图片,则回调callback.onSuccess()
if (picasso.loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
}
if (callback != null) {
callback.onSuccess();
}
} else { //如果cache没有目标图片,则创建Action并提交
Action action =
new FetchAction(picasso, request, memoryPolicy, networkPolicy, tag, key, callback);
picasso.submit(action);
}
}
}
}
public class Picasso {
void submit(Action action) {
dispatcher.dispatchSubmit(action);
}
}
是不是很眼熟,其实fetch()和into()的实现很相似,都是异步方法,创建Action并提交给线程池处理。根据前面的分析,在Picasso获取到图片后会回调到FetchAction.complete()方法,那我们直接看FetchAction.complete()。
class FetchAction extends Action<Object> {
@Override
void complete(Bitmap result, Picasso.LoadedFrom from) {
if (callback != null) {
callback.onSuccess();
}
}
}
咦,FetchAction.complete()好像也没做什么,只是回调了Callback的onSuccess()方法,如果Callback为null,那fetch()岂不白做了?并且有无Callback和这预加载又有什么关系呢?
非也非也,其实fetch()方法的核心步骤隐藏在Dispatcher.performComplete()方法中。我们在Picasso图片加载框架 —— 源码解析(一)的7. 分发Response中讲过,当图片获取成功后,会调用Dispatcher.dispatchComplete()进行分发,进而调用Dispatcher.performComplete()。在Dispatcher.performComplete()中有一个将图片保存到cache的过程,这便是我们要找到预加载代码。
void performComplete(BitmapHunter hunter) {
//是否将目标图片缓存起来
if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
cache.set(hunter.getKey(), hunter.getResult());
}
//省略其他代码
}
而Dispatcher持有的这个cache对象便是Picasso中的cache对象。一旦fetch()方法将图片保存到cache中,那么以后再调用Picasso.quickMemoryCacheCheck()的时候,就可以轻松从cache中找目标图片,无需再走网络请求。
public class Picasso {
public static class Builder {
public Picasso build() {
//省略其他代码
//Dispatcher持有的cache对象,便是Picasso中的cache对象
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
//省略其他代码
}
}
//任何从cache获取图片的地方,都会走到quickMemoryCacheCheck()方法
Bitmap quickMemoryCacheCheck(String key) {
Bitmap cached = cache.get(key);
if (cached != null) {
stats.dispatchCacheHit();
} else {
stats.dispatchCacheMiss();
}
return cached;
}
}
另外多提一点,fetch()方法没有做主线程检查,没有调Picasso.enqueueAndSubmit(),也没有做任何UI相关的工作,因此可以大胆从子线程调用。
四、图片变换
图片变换是Android里一个老生常谈的话题,图片的缩放、裁剪和旋转等都可以视为图片变换。Picasso框架对图片变换提供了强大的支持,根据使用方式,本文将其分为基本图片变换和自定义图片变换。
1.基本图片变换
基本图片变换即Picasso内置的图片变换API,无需用户继承或实现,只需简单调用即可,主要包括:
1)RequestCreator.resize()/RequestCreator.resizeDimen():改变图片尺寸
2)RequestCreator.rotate():图片旋转
3)RequestCreator.centerCrop()/RequestCreator.centerInside():图片裁剪
public class RequestCreator {
//data为真实请求
private final Request.Builder data;
//targetWidthResId和targetHeightResId为资源Id
public RequestCreator resizeDimen(int targetWidthResId, int targetHeightResId) {
Resources resources = picasso.context.getResources();
//转换为以像素为单位的值
int targetWidth = resources.getDimensionPixelSize(targetWidthResId);
int targetHeight = resources.getDimensionPixelSize(targetHeightResId);
return resize(targetWidth, targetHeight);
}
//targetWidth和targetHeight的单位是像素
public RequestCreator resize(int targetWidth, int targetHeight) {
data.resize(targetWidth, targetHeight);
return this;
}
//degrees的单位是角度
public RequestCreator rotate(float degrees) {
data.rotate(degrees);
return this;
}
//pivotX和pivotX为旋转中心点
public RequestCreator rotate(float degrees, float pivotX, float pivotY) {
data.rotate(degrees, pivotX, pivotY);
return this;
}
//按比例裁减,使图片居中显示,需配合resize()使用
public RequestCreator centerCrop() {
data.centerCrop();
return this;
}
//按比例裁减,使图片完全显示,需配合resize()使用
public RequestCreator centerInside() {
data.centerInside();
return this;
}
}
public final class Request {
public static final class Builder {
public Builder resize(int targetWidth, int targetHeight) {
if (targetWidth < 0) {
throw new IllegalArgumentException("Width must be positive number or 0.");
}
if (targetHeight < 0) {
throw new IllegalArgumentException("Height must be positive number or 0.");
}
if (targetHeight == 0 && targetWidth == 0) {
throw new IllegalArgumentException("At least one dimension has to be positive number.");
}
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
return this;
}
public Builder rotate(float degrees) {
rotationDegrees = degrees;
return this;
}
public Builder rotate(float degrees, float pivotX, float pivotY) {
rotationDegrees = degrees;
rotationPivotX = pivotX;
rotationPivotY = pivotY;
hasRotationPivot = true;
return this;
}
//与centerInside为互斥操作
public Builder centerCrop() {
if (centerInside) {
throw new IllegalStateException("Center crop can not be used after calling centerInside");
}
centerCrop = true;
return this;
}
//与centerCrop为互斥操作
public Builder centerInside() {
if (centerCrop) {
throw new IllegalStateException("Center inside can not be used after calling centerCrop");
}
centerInside = true;
return this;
}
}
}
从源码可以看到,上述API只是对Request中相关变量进行赋值,没有做实际的图片变换工作。而真正进行图片变换的时机是在BitmapHunter获取到图片后,Dispatcher分发图片前,即BitmapHunter.hunt()方法中。
class BitmapHunter implements Runnable {
//运行在子线程中
Bitmap hunt() throws IOException {
//省略部分代码
RequestHandler.Result result = requestHandler.load(data, networkPolicy);
if (result != null) {
//省略部分代码
exifRotation = result.getExifOrientation();
bitmap = result.getBitmap();
}
//省略部分代码
//重点
if (bitmap != null) {
//检查是否需要对图片进行变换
if (data.needsTransformation() || exifRotation != 0) {
synchronized (DECODE_LOCK) {
//基本图片变换
if (data.needsMatrixTransform() || exifRotation != 0) {
//在transformResult()方法中对图片进行变换
bitmap = transformResult(data, bitmap, exifRotation);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
}
}
//自定义图片变换
if (data.hasCustomTransformations()) {
//此处暂时略过,下面会细讲
}
}
}
}
return bitmap;
}
}
public final class Request {
//needsMatrixTransform()返回true,表示需要基本图片变换
//hasCustomTransformations()返回true,表示需要自定义图片变换
boolean needsTransformation() {
return needsMatrixTransform() || hasCustomTransformations();
}
//若调用过resize()或rotate(),则返回true
boolean needsMatrixTransform() {
return hasSize() || rotationDegrees != 0;
}
public boolean hasSize() {
return targetWidth != 0 || targetHeight != 0;
}
boolean hasCustomTransformations() {
return transformations != null;
}
}
hunt()方法获取到图片后,针对图片变换,做了以下几件事:
1)检查Request.needsTransformation()和exifRotation,看看是否需要进行图片变换。其中,exifOrientation为EXIF图片的旋转信息;
2)若需要变换,则先检查是否要做基本图片变换;
3)在基本图片变换之后,再检查是否要做自定义图片变换。
具体的基本图片变换工作,是由BitmapHunter.transformResult()方法完成的:
static Bitmap transformResult(Request data, Bitmap result, int exifRotation) {
int inWidth = result.getWidth();
int inHeight = result.getHeight();
boolean onlyScaleDown = data.onlyScaleDown;
int drawX = 0;
int drawY = 0;
int drawWidth = inWidth;
int drawHeight = inHeight;
Matrix matrix = new Matrix();
if (data.needsMatrixTransform()) {
int targetWidth = data.targetWidth;
int targetHeight = data.targetHeight;
//图片旋转
float targetRotation = data.rotationDegrees;
if (targetRotation != 0) {
if (data.hasRotationPivot) {
matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY);
} else {
matrix.setRotate(targetRotation);
}
}
//图片裁剪
if (data.centerCrop) {
float widthRatio = targetWidth / (float) inWidth;
float heightRatio = targetHeight / (float) inHeight;
float scaleX, scaleY;
if (widthRatio > heightRatio) {
int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio));
drawY = (inHeight - newSize) / 2;
drawHeight = newSize;
scaleX = widthRatio;
scaleY = targetHeight / (float) drawHeight;
} else {
int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio));
drawX = (inWidth - newSize) / 2;
drawWidth = newSize;
scaleX = targetWidth / (float) drawWidth;
scaleY = heightRatio;
}
if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
matrix.preScale(scaleX, scaleY);
}
} else if (data.centerInside) {
float widthRatio = targetWidth / (float) inWidth;
float heightRatio = targetHeight / (float) inHeight;
float scale = widthRatio < heightRatio ? widthRatio : heightRatio;
if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
matrix.preScale(scale, scale);
}
} else if ((targetWidth != 0 || targetHeight != 0)
&& (targetWidth != inWidth || targetHeight != inHeight)) {
float sx =
targetWidth != 0 ? targetWidth / (float) inWidth : targetHeight / (float) inHeight;
float sy =
targetHeight != 0 ? targetHeight / (float) inHeight : targetWidth / (float) inWidth;
if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
matrix.preScale(sx, sy);
}
}
}
if (exifRotation != 0) {
matrix.preRotate(exifRotation);
}
Bitmap newResult =
Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true);
if (newResult != result) {
result.recycle();
result = newResult;
}
return result;
}
可以看到,基本图片变换就是利用Android的Matrix类,对Bitmap进行处理的过程。
2.自定义图片变换
利用基本图片变换可完成图片旋转、裁剪和缩放等,但若想实现更复杂的图片变换,例如:高斯模糊、圆角裁剪或度灰处理等,则就要依赖于自定义图片变换功能了。Picasso框架通过Transformation接口对自定义图片变换功能进行支持。
public interface Transformation {
//对原图片进行自定义变换
//如果在该方法中创建了新的Bitmap实例,则必须对source进行手动recycle
Bitmap transform(Bitmap source);
//返回标识该变换的唯一key
String key();
}
Transformation接口提供了两个方法:
1)transform():用于对原图片进行自定义变换,图片的变换逻辑应写在该方法中。但要注意,如果在该方法中创建了新的Bitmap实例,则必须对原Bitmap手动回收;
2)key():返回标识该变换的唯一key。
在使用自定义图片变换功能时,开发者需先编写实现Transformation接口的类,然后在调用RequestCreator.transform()方法将自定义的Transformation添加到Request中。当BitmapHunter获取到图片后,会在hunt()方法中先进行基本图片变换,然后在处理自定义图片变换。
Bitmap hunt() throws IOException {
//省略部分代码
RequestHandler.Result result = requestHandler.load(data, networkPolicy);
if (result != null) {
//省略部分代码
exifRotation = result.getExifOrientation();
bitmap = result.getBitmap();
}
//省略部分代码
//重点
if (bitmap != null) {
//检查是否需要对图片进行变换
if (data.needsTransformation() || exifRotation != 0) {
//基本图片变换
if (data.needsMatrixTransform() || exifRotation != 0) {
//省略部分代码
}
//自定义图片变换
if (data.hasCustomTransformations()) {
bitmap = applyCustomTransformations(data.transformations, bitmap);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
}
}
//省略部分代码
}
}
return bitmap;
}
static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) {
for (int i = 0, count = transformations.size(); i < count; i++) {
final Transformation transformation = transformations.get(i);
Bitmap newResult;
try {
//执行自定义的图片变换逻辑
newResult = transformation.transform(result);
} catch (final RuntimeException e) {
Picasso.HANDLER.post(new Runnable() {
@Override public void run() {
throw new RuntimeException(
"Transformation " + transformation.key() + " crashed with exception.", e);
}
});
return null;
}
//若transformation.transform()返回null,则抛异常
if (newResult == null) {
final StringBuilder builder = new StringBuilder() //
.append("Transformation ")
.append(transformation.key())
.append(" returned null after ")
.append(i)
.append(" previous transformation(s).\n\nTransformation list:\n");
for (Transformation t : transformations) {
builder.append(t.key()).append('\n');
}
Picasso.HANDLER.post(new Runnable() {
@Override public void run() {
throw new NullPointerException(builder.toString());
}
});
return null;
}
//若transformation.transform()返回原Bitmap,且原Bitmap已被回收,则抛异常
if (newResult == result && result.isRecycled()) {
Picasso.HANDLER.post(new Runnable() {
@Override public void run() {
throw new IllegalStateException("Transformation "
+ transformation.key()
+ " returned input Bitmap but recycled it.");
}
});
return null;
}
//若transformation.transform()返回新Bitmap,且原Bitmap未被回收,则抛异常
if (newResult != result && !result.isRecycled()) {
Picasso.HANDLER.post(new Runnable() {
@Override public void run() {
throw new IllegalStateException("Transformation "
+ transformation.key()
+ " mutated input Bitmap but failed to recycle the original.");
}
});
return null;
}
result = newResult;
}
return result;
}
applyCustomTransformations()方法的实现很简单,就是调用Transformation.transform()进行处理,之后再进行一系列的状态检查。
总结
Picasso作为图片加载框架,不只是简单的封装了图片下载和加载的过程,而是在更高的层面上对整个流程进行了抽象:将共性部分封装成框架主线,将易变部分封装成抽象接口/抽象类(Target接口、Transformation接口、Action及其子类)。在保证主体代码稳定的同时,又提高了框架的可扩展性。这种抽象和封装的思想非常值得我们学习。
如对Picasso框架感兴趣,欢迎关注本系列文章。如对本文有疑问,欢迎留言交流。如需要转载,则请注明出处。