在Picasso圖片加載框架 —— 源碼解析(一)中,我們從頭到尾梳理了一遍Picasso網(wǎng)絡加載圖片的過程,但對于其中很多細節(jié)并沒有展開介紹。本篇將深入細節(jié),以實際情景為出發(fā)點,帶領(lǐng)大家看看Picasso中豐富的圖片功能及其代碼實現(xiàn),進而學習下Picasso框架的設計思想。
一、在不同目標View上顯示圖片
在實際開發(fā)中,我們不一定每次都用ImageView來顯示下載的圖片。有些時候,我們需要把圖片顯示到自定義View或RemoteViews上。要想實現(xiàn)這點,一個最樸素的想法就是先獲取圖片,然后在調(diào)用自定義View或RemoteViews的API。想法雖糙,但很實用。Picasso框架也是這么處理的,只不過作為一個圖片加載框架,它對這一過程又做了進一步的抽象。
仔細看RequestCreator類的源碼,我們可以發(fā)現(xiàn),into()方法共有五種重載:
1)into(ImageView)和into(ImageView, Callback)用于ImageView;
2)into(RemoteViews, int, int, Notification)用于狀態(tài)欄通知;
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.");
}
//創(chuàng)建真實請求
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()方法類似,創(chuàng)建Request和Action,并調(diào)用Picasso.enqueueAndSubmit()入隊和提交,后續(xù)過程和ImageView的流程一樣,就不在此贅述了,不清楚的小伙伴可以去看Picasso圖片加載框架 —— 源碼解析(一)。
值得注意的一點是,該into()方法并沒有做主線程檢查,即不強制要求由主線程進行調(diào)用。但我們記得,在Picasso.enqueueAndSubmit()中有一個cancelExistingRequest()的動作,該動作會做主線程檢查。那么,如果從非主線程調(diào)用into()方法,不就直接拋出異常了嘛,感覺像是源碼的bug......
在獲取到圖片后,會回調(diào)Action.complete()方法,此處就是回調(diào)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()方法實現(xiàn)了Notification自身的特殊需求。這樣做的好處顯而易見:將共性的RemoteViews圖片設置工作放在基類中予以實現(xiàn),而子類只需關(guān)注自身的特殊邏輯即可。這一點將會在接下來的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更新圖片,子類實現(xiàn)自身邏輯。
3.into(Target)
接下來,我們來看into()的最后一個重載:into(Target)。從方法參數(shù)就可以看出,這個重載與之前兩個明顯不同,這里不是對RemoteViews進行設置,而是在處理Target,那這個Target是什么呢?
public interface Target {
/**
* 在圖片獲取成功時觸發(fā)
* 注意:不能在此方法中回收Bitmap,否則會拋異常
*/
void onBitmapLoaded(Bitmap bitmap, LoadedFrom from);
/**
* 在圖片獲取失敗時觸發(fā)
*/
void onBitmapFailed(Drawable errorDrawable);
/**
* 在圖片請求提交之前觸發(fā)
*/
void onPrepareLoad(Drawable placeHolderDrawable);
}
Target是Picasso框架提供的接口,任何實現(xiàn)了該接口的類都會在圖片獲取成功、圖片獲取失敗和提交圖片請求之前收到相應的方法回調(diào),進而可以在回調(diào)中完成圖片處理邏輯。接下來,我們看看into(Target)的實現(xiàn)。
/**
* 異步方法
*/
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,并回調(diào)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中獲取到了圖片,則回調(diào)target.onBitmapLoaded()
if (bitmap != null) {
picasso.cancelRequest(target);
target.onBitmapLoaded(bitmap, MEMORY);
return;
}
}
//在提交請求之前,回調(diào)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獲取圖片 ---> 創(chuàng)建TargetAction并提交入列。整體流程沒有太大變化,只是在相應位置增加了Target的回調(diào)。同樣的,我們繼續(xù)看TargetAction的實現(xiàn)。
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對象,回調(diào)Target.onBitmapLoaded()方法,并且還會對Bitmap的狀態(tài)進行檢查。若Bitmap已被回收掉,則拋出IllegalStateException異常。當獲取圖片失敗時,TargetAction.error()則會回調(diào)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接口,并在相應的回調(diào)中處理圖片。至于圖片是如何被下載的,我們無需關(guān)心,Picasso框架會自動幫我們搞定。
使用Target接口時有一點需要格外注意:在Picasso.enqueueAndSubmit()方法中,Target對象會被存儲到WeakHashMap中,因此繼承Target接口的類最好也重寫下equals()和hashCode()方法。
二、直接獲取圖片
既然into()方法可以將圖片顯示到指定View上,那么是不是也有個方法可以讓開發(fā)者直接拿到圖片,然后自己決定如何使用呢?當然有的,Picasso框架提供了RequestCreator.get()方法,該方法可以返回一個Bitmap供開發(fā)者使用。
public class RequestCreator {
/**
* 同步方法,不能在主線程調(diào)用
*/
public Bitmap get() throws IOException {
long started = System.nanoTime();
//線程檢查。如果是主線程調(diào)用,則會拋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());
//創(chuàng)建Action
Action action = new GetAction(picasso, finalData, memoryPolicy, networkPolicy, tag, key);
//創(chuàng)建BitmapHunter,并調(diào)用BitmapHunter.hunt()方法獲取圖片
return forRequest(picasso, picasso.dispatcher, picasso.cache, picasso.stats, action).hunt();
}
}
//空實現(xiàn)
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()創(chuàng)建BitmapHunter對象,并調(diào)用BitmapHunter.hunt()直接從外部資源(網(wǎng)絡、磁盤等)獲取圖片并返回結(jié)果。由于GetAction全程沒有參與其中,源碼中也是給予了空實現(xiàn)。
基于上述代碼和分析,可以看出:
1)get()為同步方法,其直接調(diào)用了BitmapHunter.hunt()從外部資源獲取圖片,屬于耗時操作,因此不能從主線程調(diào)用;
2)get()方法直接將獲取到的圖片返回,并沒有將其保存到cache中。因此,即使多次用get()方法獲取相同的圖片,每次調(diào)用還是都會從外部資源獲取一遍,沒辦法享受cache的好處。
三、預加載
預加載,顧名思義就是提前從網(wǎng)絡或磁盤將圖片加載至內(nèi)存中,然后在需要的時候直接從內(nèi)存中獲取圖片。這樣便可將圖片加載的耗時過程隱藏起來,避免了用戶長時間的等待,可大大提升用戶體驗。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操作默認為低優(yōu)先級
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中找到圖片,則回調(diào)callback.onSuccess()
if (picasso.loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
}
if (callback != null) {
callback.onSuccess();
}
} else { //如果cache沒有目標圖片,則創(chuàng)建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()的實現(xiàn)很相似,都是異步方法,創(chuàng)建Action并提交給線程池處理。根據(jù)前面的分析,在Picasso獲取到圖片后會回調(diào)到FetchAction.complete()方法,那我們直接看FetchAction.complete()。
class FetchAction extends Action<Object> {
@Override
void complete(Bitmap result, Picasso.LoadedFrom from) {
if (callback != null) {
callback.onSuccess();
}
}
}
咦,F(xiàn)etchAction.complete()好像也沒做什么,只是回調(diào)了Callback的onSuccess()方法,如果Callback為null,那fetch()豈不白做了?并且有無Callback和這預加載又有什么關(guān)系呢?
非也非也,其實fetch()方法的核心步驟隱藏在Dispatcher.performComplete()方法中。我們在Picasso圖片加載框架 —— 源碼解析(一)的7. 分發(fā)Response中講過,當圖片獲取成功后,會調(diào)用Dispatcher.dispatchComplete()進行分發(fā),進而調(diào)用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中,那么以后再調(diào)用Picasso.quickMemoryCacheCheck()的時候,就可以輕松從cache中找目標圖片,無需再走網(wǎng)絡請求。
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()方法沒有做主線程檢查,沒有調(diào)Picasso.enqueueAndSubmit(),也沒有做任何UI相關(guān)的工作,因此可以大膽從子線程調(diào)用。
四、圖片變換
圖片變換是Android里一個老生常談的話題,圖片的縮放、裁剪和旋轉(zhuǎn)等都可以視為圖片變換。Picasso框架對圖片變換提供了強大的支持,根據(jù)使用方式,本文將其分為基本圖片變換和自定義圖片變換。
1.基本圖片變換
基本圖片變換即Picasso內(nèi)置的圖片變換API,無需用戶繼承或?qū)崿F(xiàn),只需簡單調(diào)用即可,主要包括:
1)RequestCreator.resize()/RequestCreator.resizeDimen():改變圖片尺寸
2)RequestCreator.rotate():圖片旋轉(zhuǎn)
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();
//轉(zhuǎn)換為以像素為單位的值
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為旋轉(zhuǎn)中心點
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中相關(guān)變量進行賦值,沒有做實際的圖片變換工作。而真正進行圖片變換的時機是在BitmapHunter獲取到圖片后,Dispatcher分發(fā)圖片前,即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();
}
//若調(diào)用過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圖片的旋轉(zhuǎn)信息;
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;
//圖片旋轉(zhuǎn)
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.自定義圖片變換
利用基本圖片變換可完成圖片旋轉(zhuǎn)、裁剪和縮放等,但若想實現(xiàn)更復雜的圖片變換,例如:高斯模糊、圓角裁剪或度灰處理等,則就要依賴于自定義圖片變換功能了。Picasso框架通過Transformation接口對自定義圖片變換功能進行支持。
public interface Transformation {
//對原圖片進行自定義變換
//如果在該方法中創(chuàng)建了新的Bitmap實例,則必須對source進行手動recycle
Bitmap transform(Bitmap source);
//返回標識該變換的唯一key
String key();
}
Transformation接口提供了兩個方法:
1)transform():用于對原圖片進行自定義變換,圖片的變換邏輯應寫在該方法中。但要注意,如果在該方法中創(chuàng)建了新的Bitmap實例,則必須對原Bitmap手動回收;
2)key():返回標識該變換的唯一key。
在使用自定義圖片變換功能時,開發(fā)者需先編寫實現(xiàn)Transformation接口的類,然后在調(diào)用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 {
//執(zhí)行自定義的圖片變換邏輯
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()方法的實現(xiàn)很簡單,就是調(diào)用Transformation.transform()進行處理,之后再進行一系列的狀態(tài)檢查。
總結(jié)
Picasso作為圖片加載框架,不只是簡單的封裝了圖片下載和加載的過程,而是在更高的層面上對整個流程進行了抽象:將共性部分封裝成框架主線,將易變部分封裝成抽象接口/抽象類(Target接口、Transformation接口、Action及其子類)。在保證主體代碼穩(wěn)定的同時,又提高了框架的可擴展性。這種抽象和封裝的思想非常值得我們學習。
如對Picasso框架感興趣,歡迎關(guān)注本系列文章。如對本文有疑問,歡迎留言交流。如需要轉(zhuǎn)載,則請注明出處。