Picasso圖片加載框架 —— 源碼解析(二)

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()方法共有五種重載:

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)載,則請注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容