Picasso图片加载框架 —— 源码解析(二)

Picasso图片加载框架 —— 源码解析(一)中,我们从头到尾梳理了一遍Picasso网络加载图片的过程,但对于其中很多细节并没有展开介绍。本篇将深入细节,以实际情景为出发点,带领大家看看Picasso中丰富的图片功能及其代码实现,进而学习下Picasso框架的设计思想。

一、在不同目标View上显示图片

在实际开发中,我们不一定每次都用ImageView来显示下载的图片。有些时候,我们需要把图片显示到自定义View或RemoteViews上。要想实现这点,一个最朴素的想法就是先获取图片,然后在调用自定义View或RemoteViews的API。想法虽糙,但很实用。Picasso框架也是这么处理的,只不过作为一个图片加载框架,它对这一过程又做了进一步的抽象。

仔细看RequestCreator类的源码,我们可以发现,into()方法共有五种重载:

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框架感兴趣,欢迎关注本系列文章。如对本文有疑问,欢迎留言交流。如需要转载,则请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容