Fresco图片加载工具

原文链接:http://www.fresco-cn.org/

关于 Fresco

Fresco 是一个强大的图片加载组件。

Fresco 中设计有一个叫做 image pipeline 的模块。它负责从网络,从本地文件系统,本地资源加载图片。为了最大限度节省空间和CPU时间,它含有3级缓存设计(2级内存,1级文件)。

Fresco 中设计有一个叫做 Drawees 模块,方便地显示loading图,当图片不再显示在屏幕上时,及时地释放内存和空间占用。

Fresco 支持 Android2.3(API level 9) 及其以上系统。

特性

内存管理

解压后的图片,即Android中的Bitmap,占用大量的内存。大的内存占用势必引发更加频繁的GC。在5.0以下,GC将会显著地引发界面卡顿。

在5.0以下系统,Fresco将图片放到一个特别的内存区域。当然,在图片不显示的时候,占用的内存会自动被释放。这会使得APP更加流畅,减少因图片内存占用而引发的OOM。

Fresco 在低端机器上表现一样出色,你再也不用因图片内存占用而思前想后。

图片的渐进式呈现

渐进式的JPEG图片格式已经流行数年了,渐进式图片格式先呈现大致的图片轮廓,然后随着图片下载的继续,呈现逐渐清晰的图片,这对于移动设备,尤其是慢网络有极大的利好,可带来更好的用户体验。

Android 本身的图片库不支持此格式,但是Fresco支持。使用时,和往常一样,仅仅需要提供一个图片的URI即可,剩下的事情,Fresco会处理。

Gif图和WebP格式

是的,支持加载Gif图,支持WebP格式。

图像的呈现

Fresco 的 Drawees 设计,带来一些有用的特性:

  • 自定义居中焦点(对人脸等图片显示非常有帮助)
  • 圆角图,当然圆圈也行。
  • 下载失败之后,点击重现下载
  • 自定义占位图,自定义overlay, 或者进度条
  • 指定用户按压时的overlay

图像的加载

Fresco 的 image pipeline 设计,允许用户在多方面控制图片的加载:

  • 为同一个图片指定不同的远程路径,或者使用已经存在本地缓存中的图片
  • 先显示一个低解析度的图片,等高清图下载完之后再显示高清图
  • 加载完成回调通知
  • 对于本地图,如有EXIF缩略图,在大图加载完成之前,可先显示缩略图
  • 缩放或者旋转图片
  • 处理已下载的图片
  • WebP 支持

开始使用 Fresco编辑和纠错

如果你仅仅是想简单下载一张网络图片,在下载完成之前,显示一张占位图,那么简单使用SimpleDraweeView 即可。

为了下载网络图片,请确保在 AndroidManifest.xml 中有以下权限:

<uses-permission android:name="android.permission.INTERNET"/>

在 Application 初始化时,在应用调用 setContentView() 之前,进行初始化:

Fresco.initialize(context);

在xml布局文件中, 加入命名空间:

<!-- 其他元素 -->
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:fresco="http://schemas.android.com/apk/res-auto">

加入SimpleDraweeView:

<com.facebook.drawee.view.SimpleDraweeView
    android:id="@+id/my_image_view"
    android:layout_width="20dp"
    android:layout_height="20dp"
    fresco:placeholderImage="@drawable/my_drawable"
  />

开始加载图片

Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/fresco-logo.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);

剩下的,Fresco会替你完成:

  • 显示占位图直到加载完成;
  • 下载图片;
  • 缓存图片;
  • 图片不再显示时,从内存中移除;

支持的URIs编辑和纠错

Fresco 支持许多URI格式。

特别注意:Fresco 不支持 相对路径的URI. 所有的URI都必须是绝对路径,并且带上该URI的scheme。

如下:

类型Scheme示例
远程图片http://, https://HttpURLConnection 或者参考 使用其他网络加载方案
本地文件file://FileInputStream
Content providercontent://ContentResolver
asset目录下的资源asset://AssetManager
res目录下的资源res://Resources.openRawResource

res 示例:

Uri uri = Uri.parse("res://包名(实际可以是任何字符串甚至留空)/" + R.drawable.ic_launcher);


注意,只有图片资源才能使用在Image pipeline中,比如(PNG)。其他资源类型,比如字符串,或者XML Drawable在Image pipeline中没有意义。所以加载的资源不支持这些类型。

像ShapeDrawable这样声明在XML中的drawable可能引起困惑。注意到这毕竟不是图片,如果想把这样的drawable作为图像显示。

那么把这个drawable设置为占位图,然后把URI设置为null。

圆角和圆圈编辑和纠错

Drawee 轻松支持圆角显示,并且显示圆角时,并不复制和修改Bitmap对象,那样太耗费内存。

圆角

圆角实际有2种呈现方式:

  1. 圆圈 - 设置roundAsCircle为true
  2. 圆角 - 设置roundedCornerRadius

设置圆角时,支持4个角不同的半径。XML中无法配置,但可在Java代码中配置。

设置圆角

可使用以下两种方式:

  1. 默认使用一个 shader 绘制圆角,但是仅仅占位图和所要显示的图有圆角效果。失败示意图和重下载示意图无圆角效果,且这种圆角方式不支持动画。
  2. 叠加一个solid color来绘制圆角。但是背景需要固定成指定的颜色。 在XML中指定roundWithOverlayColor, 或者通过调用setOverlayColor来完成此设定。

XML中配置

SimpleDraweeView 支持如下几种圆角配置:

<com.facebook.drawee.view.SimpleDraweeView
   ...
   fresco:roundedCornerRadius="5dp"
   fresco:roundBottomLeft="false"
   fresco:roundBottomRight="false"
   fresco:roundWithOverlayColor="@color/blue"
   fresco:roundingBorderWidth="1dp"
   fresco:roundingBorderColor="@color/red"

代码中配置

在创建 DraweeHierarchy 时,可以给 GenericDraweeHierarchyBuilder 指定一个RoundingParams 用来绘制圆角效果。

RoundingParams roundingParams = RoundingParams.fromCornersRadius(7f);
roundingParams.setOverlayColor(R.color.green);
// 或用 fromCornersRadii 以及 asCircle 方法
genericDraweeHierarchyBuilder
    .setRoundingParams(roundingParams);

你也可以在运行时,改变圆角效果

RoundingParams roundingParams = 
    mSimpleDraweeView.getHierarchy().getRoundingParams();
roundingParams.setBorder(R.color.red, 1.0);
roundingParams.setRoundAsCircle(true);
mSimpleDraweeView.getHierarchy().setRoundingParams(roundingParams);

在运行时,不能改变呈现方式: 原本是圆角,不能改为圆圈。

说明

当使用BITMAP_ONLY(默认)模式时的限制:

  • 并非所有的图片分支部分都可以实现圆角,目前只有占位图片和实际图片可以实现圆角,我们正在努力为背景图片实现圆角功能。
  • 只有BitmapDrawable 和 ColorDrawable类的图片可以实现圆角。我们目前不支持包括NinePatchDrawable和 ShapeDrawable在内的其他类型图片。(无论他们是在XML或是程序中声明的)
  • 动画不能被圆角。
  • 由于Android的BitmapShader的限制,当一个图片不能覆盖全部的View的时候,边缘部分会被重复显示,而非留白。对这种情况可以使用不同的缩放类型(比如centerCrop)来保证图片覆盖了全部的View。

OVERLAY_COLOR模式没有上述限制,但由于这个模式使用在图片上覆盖一个纯色图层的方式来模拟圆角效果,因此只有在图标背景是静止的并且与图层同色的情况下才能获得较好的效果。

Drawee 内部实现了一个CLIPPING模式。但由于有些Canvas的实现并不支持路径剪裁(Path Clipping),这个模式被禁用了且不对外开放。并且由于路径剪裁不支持反锯齿,会导致圆角的边缘呈现像素化的效果。

总之,如果生成临时bitmap的方法,所有的上述问题都可以避免。但是这个方法并不被支持因为这会导致很严重的内存问题。

综上所述,在 Android 中实现圆角效果,没有一个绝对好的方案,你必须在上述的方案中进行选择。

渐进式JPEG图编辑和纠错

注意: 本页提及的API仅是初步设计,后续可能变动

Fresco 支持渐进式的网络JPEG图。在开始加载之后,图会从模糊到清晰渐渐呈现。

你可以设置一个清晰度标准,在未达到这个清晰度之前,会一直显示占位图。

渐进式JPEG图仅仅支持网络图。

初始化

配置Image pipeline时 需要传递一个 ProgressiveJpegConfig. 的实例。

这个实例需要完成两个事情: 1. 返回下一个需要解码的扫描次数 2. 确定多少个扫描次数之后的图片才能开始显示。

下面的实例中,为了实现节省CPU,并不是每个扫描都进行解码。

注意:

  • 每次解码完之后,调用getNextScanNumberToDecode, 等待扫描值大于返回值,才有可能进行解码。

假设,随着下载的进行,下载完的扫描序列如下: 1, 4, 5, 10。那么:

  1. 首次调用getNextScanNumberToDecode返回为2, 因为初始时,解码的扫描数为0。
  2. 那么1将不会解码,下载完成4个扫描时,解码一次。下个解码为扫描数为6
  3. 5不会解码,10才会解码
ProgressiveJpegConfig pjpegConfig = new ProgressiveJpegConfig() {
  @Override
  public int getNextScanNumberToDecode(int scanNumber) {
    return scanNumber + 2;
  }    

  public QualityInfo getQualityInfo(int scanNumber) {
    boolean isGoodEnough = (scanNumber >= 5);
    return ImmutableQualityInfo.of(scanNumber, isGoodEnough, false);
  }
}

ImagePipelineConfig config = ImagePipelineConfig.newBuilder()
    .setProgressiveJpegConfig(pjpeg)
    .build();

除了自己实现ProgressiveJpegConfig, 也可以直接使用SimpleProgressiveJpegConfig.

At Request Time

目前,我们必须显式地在加载时,允许渐进式JPEG图片加载。

Uri uri;
ImageRequest request = ImageRequestBuilder
    .newBuilderWithSource(uri)
    .setProgressiveRenderingEnabled(true)
    .build();
PipelineDraweeController controller = Fresco.newControllerBuilder()
    .setImageRequest(requests)
    .setOldController(mSimpleDraweeView.getController())
    .build();

mSimpleDraweeView.setController(controller);

我们希望在后续的版本中,在setImageURI方法中可以直接支持渐进式图片加载。

动画图支持编辑和纠错

Fresco 支持 GIF 和 WebP 格式的动画图片。对于 WebP 格式的动画图的支持包括扩展的 WebP 格式,即使 Android 2.3及其以后那些没有原生 WebP 支持的系统。

设置动画图自动播放

如果你希望图片下载完之后自动播放,同时,当View从屏幕移除时,停止播放,只需要在image request 中简单设置,如下:

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setAutoPlayAnimations(true)
    . // other setters
    .build();
mSimpleDraweeView.setController(controller);

手动控制动画图播放

也许你希望在代码中直接控制动画的播放。这种情况下,你需要监听图片是否加载完毕,然后才能控制动画的播放:

ControllerListener controllerListener = new BaseControllerListener<ImageInfo>() {
    @Override
    public void onFinalImageSet(
        String id,
        @Nullable ImageInfo imageInfo,
        @Nullable Animatable anim) {
    if (anim != null) {
      // app-specific logic to enable animation starting
      anim.start();
    }
};

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setControllerListener(controllerListener)
    // other setters
    .build();
mSimpleDraweeView.setController(controller);

另外,controller提供对Animatable 的访问。

如果有可用动画的话,可对动画进行灵活的控制:

Animatable animation = mSimpleDraweeView.getController().getAnimatable();
if (animation != null) {
  // 开始播放
  animation.start();
  // 一段时间之后,根据业务逻辑,停止播放
  animation.stop();
}

限制#

动画现在还不支持 postprocessors 。

多图请求及图片复用编辑和纠错

多图请求需 自定义ImageRequest.

先显示低分辨率的图,然后是高分辨率的图

假设你要显示一张高分辨率的图,但是这张图下载比较耗时。与其一直显示占位图,你可能想要先下载一个较小的缩略图。

这时,你可以设置两个图片的URI,一个是低分辨率的缩略图,一个是高分辨率的图。

Uri lowResUri, highResUri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setLowResImageRequest(ImageRequest.fromUri(lowResUri))
    .setImageRequest(ImageRequest.fromUri(highResUri))
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

缩略图预览

本功能仅支持本地URI,并且是JPEG图片格式

如果本地JPEG图,有EXIF的缩略图,image pipeline 可以立刻返回它作为一个缩略图。Drawee 会先显示缩略图,完整的清晰大图在 decode 完之后再显示。

Uri uri;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setLocalThumbnailPreviewsEnabled(true)
    .build();

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

加载最先可用的图片

大部分时候,一张图片只有一个 URI。加载它,然后工作完成~

但是假设同一张图片有多个 URI 的情况。比如,你可能上传过一张拍摄的照片。原始图片太大而不能上传,所以图片首先经过了压缩。在这种情况下,首先尝试获取本地压缩后的图片 URI,如果失败的话,尝试获取本地原始图片 URI,如果还是失败的话,尝试获取上传到网络的图片 URI。直接下载我们本地可能已经有了的图片不是一件光彩的事。

Image pipeline 会首先从内存中搜寻图片,然后是磁盘缓存,再然后是网络或其他来源。对于多张图片,不是一张一张按上面的过程去做,而是 pipeline 先检查所有图片是否在内存。只有没在内存被搜寻到的才会寻找磁盘缓存。还没有被搜寻到的,才会进行一个外部请求。

使用时,创建一个image request 数组,然后传给 ControllerBuilder :

Uri uri1, uri2;
ImageRequest request = ImageRequest.fromUri(uri1);
ImageRequest request2 = ImageRequest.fromUri(uri2);
ImageRequest[] requests = { request1, request2 };

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setFirstAvailableImageRequests(requests)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

这些请求中只有一个会被展示。第一个被发现的,无论是在内存,磁盘或者网络,都会是被返回的那个。pipeline 认为数组中请求的顺序即为优先顺序。

自定义 DataSource Supplier

为了更好的灵活性,你可以在创建 Drawee controller 时自定义 DataSource Supplier。你可以以 FirstAvailiableDataSourceSupplier,IncreasingQualityDataSourceSupplier为例自己实现DataSource Supplier,或者以AbstractDraweeControllerBuilder为例将多个 DataSource Supplier 根据需求组合在一起。

监听下载事件编辑和纠错

动机

你也许想在图片下载完成后执行一些动作,比如使某个别的 View 可见,或者显示一些文字。你也许还想在下载失败后做一些事,比如向用户显示一条失败信息。

图片是后台线程异步加载的,所以你需要某一方式来监听 DraweeController 传递的事件。我们可以使用一个 ControllerListener 实现事件的监听。

在监听事件回调时,无法修改图片,如果需要修改图片,可使用后处理器(Postprocessor)

使用方法

简单定义一个ControllerListener即可,推荐继承BaseControllerListener:

ControllerListener controllerListener = new BaseControllerListener<ImageInfo>() {
    @Override
    public void onFinalImageSet(
        String id,
        @Nullable ImageInfo imageInfo,
        @Nullable Animatable anim) {
      if (imageInfo == null) {
        return;
      }
      QualityInfo qualityInfo = imageInfo.getQualityInfo();
      FLog.d("Final image received! " + 
          "Size %d x %d",
          "Quality level %d, good enough: %s, full quality: %s",
          imageInfo.getWidth(),
          imageInfo.getHeight(),
          qualityInfo.getQuality(),
          qualityInfo.isOfGoodEnoughQuality(),
          qualityInfo.isOfFullQuality());
    }

    @Override 
    public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
      FLog.d("Intermediate image received");
    }

    @Override
    public void onFailure(String id, Throwable throwable) {
      FLog.e(getClass(), throwable, "Error loading %s", id)
    }
};

Uri uri;
DraweeController controller = Fresco.newControllerBuilder()
    .setControllerListener(controllerListener)
    .setUri(uri);
    // other setters
    .build();
mSimpleDraweeView.setController(controller);

对所有的图片加载,onFinalImageSet 或者 onFailure 都会被触发。前者在成功时,后者在失败时。

如果允许呈现渐进式JPEG,同时图片也是渐进式图片,onIntermediateImageSet会在每个扫描被解码后回调。具体图片的那个扫描会被解码,参见渐进式JPEG图

缩放和旋转图片编辑和纠错

使用这个功能需要直接创建 image request

术语

Scaling 是一种画布操作,通常是由硬件加速的。图片实际大小保持不变,它只不过在绘制时被放大或缩小。

Resizing 是一种软件执行的管道操作。它返回一张新的,尺寸不同的图片。

Downsampling 同样是软件实现的管道操作。它不是创建一张新的图片,而是在解码时改变图片的大小。

应该 resize 还是 scale ?

Resize 很少是必要的。Scale 是大部分情况下的优先选择,即使在用 resize 时。

Resize 有以下几个限制:

  • 修改尺寸是受限的,它不能返回一张更大的图片,只能让图片变小。
  • 目前,只有 JPEG 图片可以修改尺寸。
  • 对于产生的图片的尺寸,只能粗略地控制。图片不能修改为确定的尺寸,只能通过支持的修改选项来变化。这意味着即使是修改后的图片,也需要在展示前进行 scale 操作。
  • 只支持以下的修改选项: N / 8,1 <= N <= 8
  • Resize 是由软件执行的,相比硬件加速的 scale 操作较慢。

Scale 并没有以上的限制,它使用 Android 内置的功能使图片和显示边界相符。在 Android 4.0 及之后,它可以通过 GPU 进行加速。这在大部分情况下是最快,同时也是最高效地将图片显示为你想要的尺寸的方式。唯一的缺点是当图片远大于显示大小时,会浪费内存。

那么什么时候该使用 resize 呢?你应该只在需要展示一张远大于显示大小的图片时使用 resize 以节省内存。一个例子是当你想要在 1280*720(大约 1MP) 的 view 中显示一张 8MP 的照片时。一张 8MP 的图片当解码为 4字节/像素的 ARGB 图片时大约占 32MB 的内存。如果 resize 为显示大小,它只占用少于 4MB 内存。

对于网络图片,在考虑 resize 之前,先尝试请求大小合适的图片。如果服务器能返回一张较小的图,就不要请求一张 8MP 的高解析度图片。你应该考虑用户的流量。同时,获取较小的图片可以减少你的 APP 的存储空间和 CPU 占用。

只有当服务器不提供可选的较小图片,或者你在使用本地图片时,你才应该采取 resize。在任何其他情况下,包括放大图片,都该使用 scale。对于 scale,只需要指定 SimpleDraweeView中 layout_width 和 layout_height 的大小,就像在其他 Android View 中做的那样。然后指定缩放类型

Resizing

Resize 并不改变原始图片,它只在解码前修改内存中的图片大小。

相比 Android 内置的功能,这个方法可以进行更大范围的调整。尤其是通过相机拍摄的照片,对于 scale 来说通常太大,需要在显示前进行 resize。

目前仅支持 JPEG 格式图片的 resize 操作,但它是最常用的图片格式,且大多数安卓设备的相机照片存储为该格式。

如果要 resize,创建ImageRequest时,提供一个 ResizeOptions :

Uri uri = "file:///mnt/sdcard/MyApp/myfile.jpg";
int width = 50, height = 50;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setResizeOptions(new ResizeOptions(width, height))
    .build();
PipelineDraweeController controller = Fresco.newDraweeControllerBuilder()
    .setOldController(mDraweeView.getController())
    .setImageRequest(request)
    .build();
mSimpleDraweeView.setController(controller);

向下采样(Downsampling)

向下采样是一个最近添加到 Fresco 的特性。使用的话需要在设置 image pipeline 时进行设置:

   .setDownsampleEnabled(true)

如果开启该选项,pipeline 会向下采样你的图片,代替 resize 操作。你仍然需要像上面那样在每个图片请求中调用 setResizeOptions 。

向下采样在大部分情况下比 resize 更快。除了支持 JPEG 图片,它还支持 PNG 和 WebP(除动画外) 图片。

我们希望在将来的版本中默认开启此选项。

自动旋转

如果看到的图片是侧着的,用户会非常难受。许多设备会在 JPEG 文件的 metadata 中记录下照片的方向。如果你想图片呈现的方向和设备屏幕的方向一致,你可以简单地这样做到:

ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setAutoRotateEnabled(true)
    .build();
// as above

修改图片编辑和纠错

动机

有时,我们想对从服务器下载,或者本地获取的图片做些修改,比如在某个坐标统一加个网格什么的。你可以使用 Postprocessor,最好的方式是继承 BasePostprocessor

例子

下面的例子给图片加了红色网格:

Uri uri;
Postprocessor redMeshPostprocessor = new BasePostprocessor() { 
  @Override
  public String getName() {
    return "redMeshPostprocessor";
  }

  @Override
  public void process(Bitmap bitmap) {
    for (int x = 0; x < bitmap.getWidth(); x+=2) {
      for (int y = 0; y < bitmap.getHeight(); y+=2) {
        bitmap.setPixel(x, y, Color.RED);
      }
    }
  }
}

ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setPostprocessor(redMeshPostprocessor)
    .build();

PipelineDraweeController controller = (PipelineDraweeController) 
    Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    // other setters as you need
    .build();
mSimpleDraweeView.setController(controller);

注意点

图片在进入后处理器(postprocessor)的图片是原图的一个完整拷贝,原来的图片不受修改的影响。在5.0以前的机器上,拷贝后的图片也在native内存中。

在开始一个图片显示时,即使是反复显示同一个图片,在每次进行显示时,都需要指定后处理器。对于同一个图片,每次显示可以使用不同的后处理器。

后处理器现在不支持动画图片。

复制 bitmap

可能会出现即时的后处理无法实现的情况。如果出现该情况,BasePostprocessor 还有一个接收两个参数的 process 方法。下面的例子实现了水平翻转图片:

@Override
public void process(Bitmap destBitmap, Bitmap sourceBitmap) {
  for (int x = 0; x < destBitmap.getWidth(); x++) {
    for (int y = 0; y < destBitmap.getHeight(); y++) {
      destBitmap.setPixel(destBitmap.getWidth() - x, y, sourceBitmap.getPixel(x, y));
    }
  }
}

源图片和目标图片具有相同的大小。

  • 不要修改源图片。在未来的版本中这会抛出一个异常。
  • 不要保存对任何一个图片的引用。它们的内存会由 image pipeline 进行管理,目标图片会在 Drawww 或 DataSource 中正常地销毁。

复制成不同大小

如果处理后的图片大小需要和原图片不同,我们有第三个 process 方法。你可以使用PlatformBitmapFactory 类以指定的大小安全地创建一张图片,在 Java Heap 之外。

下面的例子将源图片复制为 1 / 4 大小。

@Override
public CloseableReference<Bitmap> process(
    Bitmap sourceBitmap,
    PlatformBitmapFactory bitmapFactory) {
  CloseableReference<Bitmap> bitmapRef = bitmapFactory.createBitmap(
      sourceBitmap.getWidth() / 2,
      sourceBitmap.getHeight() / 2);
  try {
    Bitmap destBitmap = bitmapRef.get();
     for (int x = 0; x < destBitmap.getWidth(); x+=2) {
       for (int y = 0; y < destBitmap.getHeight(); y+=2) {
         destBitmap.setPixel(x, y, sourceBitmap.getPixel(x, y));
       }
     }
     return CloseableReference.cloneOrNull(bitmapRef);
  } finally {
    CloseableReference.closeSafely(bitmapRef);
  } 
}

你必须遵循 closeable references 的规则。

不要使用 Android 中 Bitmap.createBitmap() 方法,它会在 Java 堆内存中产生一个 bitmap 对象。

应该 Override 哪个方法?

不要重写多于 1 个的 process 方法。这么做可能造成无法预测的结果。

缓存处理后的图片

你可以选择性地缓存后处理器的输出结果。它会和原始图片一起放在缓存里。

如果要这样做,你的后处理器必须实现 getPostprocessorCacheKey 方法,并返回一个非空的结果。

为实现缓存命中,随后的请求中使用的后处理器必须是同一个类并返回同样的键。否则,它的返回结果将会覆盖之前缓存的条目。

例子:

public class OperationPostprocessor extends BasePostprocessor {
  private int myParameter;

  public OperationPostprocessor(int param) {
    myParameter = param;
  }

  public void process(Bitmap bitmap) { 
    doSomething(myParameter);
  }

  public CacheKey getPostprocessorCacheKey() {
    return new MyCacheKey(myParameter);
  }
}

如果你想要缓存总是命中,只需在 getPostprocessorCacheKey 中返回一个常量值。如果你的postprocessor 总是返回不同的结果,而你也不想要缓存命中,返回 null。

重复的后处理

如果想对同一个图片进行多次后处理,那么继承BaseRepeatedPostprocessor即可。该类有一个update方法,需要执行后处理时,调用该方法即可。

下面的例子展示了在运行时,后处理改变图片网格的颜色:

public class MeshPostprocessor extends BaseRepeatedPostprocessor { 
  private int mColor = Color.TRANSPARENT;

  public void setColor(int color) {
    mColor = color;
    update();
  }

  @Override
  public String getName() {
    return "meshPostprocessor";
  }

  @Override
  public void process(Bitmap bitmap) {
    for (int x = 0; x < bitmap.getWidth(); x+=2) {
      for (int y = 0; y < bitmap.getHeight(); y+=2) {
        bitmap.setPixel(x, y, mColor);
      }
    }
  }
}
MeshPostprocessor meshPostprocessor = new MeshPostprocessor();

// setPostprocessor as in above example

// 改变颜色
meshPostprocessor.setColor(Color.RED);
meshPostprocessor.setColor(Color.BLUE);

每个 image request, 应该只有一个Postprocessor,但是这个后处理器是状态相关了。

透明的图片

根据 postprocessor 的性质,目标图片不会永远是完全不透明的。由于 Bitmap.hasAlpha 方法的返回值,这有时会导致问题。也就是说如果该方法返回 false(默认值),Android 会选择不进行混合地快速绘制。这会导致出现一张用黑色代替透明像素的半透明图片。为了解决这一问题,将目标图片中该值设为 true。

destinationBitmap.setHasAlpha(true);

(图片请求)Image Requests编辑和纠错

如果你需要的ImageRequest仅仅是一个URI,那么ImageRequest.fromURI就足够了,在多图请求及图片复用中,有这样的用法。

否则,你需要ImageRequestBuilder来做更多的事情。

Uri uri;

ImageDecodeOptions decodeOptions = ImageDecodeOptions.newBuilder()
    .setBackgroundColor(Color.GREEN)
    .build();

ImageRequest request = ImageRequestBuilder
    .newBuilderWithSource(uri)
    .setImageDecodeOptions(decodeOptions)
    .setAutoRotateEnabled(true)
    .setLocalThumbnailPreviewsEnabled(true)
    .setLowestPermittedRequestLevel(RequestLevel.FULL_FETCH)
    .setProgressiveRenderingEnabled(false)
    .setResizeOptions(new ResizeOptions(width, height))
    .build();
ImageRequest 的属性和成员
最低请求级别

Image pipeline 加载图片时有一套明确的请求流程

  1. 检查内存缓存,有如,立刻返回。这个操作是实时的。
  2. 检查未解码的图片缓存,如有,解码并返回。
  3. 检查磁盘缓存,如果有加载,解码,返回。
  4. 下载或者加载本地文件。调整大小和旋转(如有),解码并返回。对于网络图来说,这一套流程下来是最耗时的。

setLowestPermittedRequestLevel允许设置一个最低请求级别,请求级别和上面对应地有以下几个取值:

  • BITMAP_MEMORY_CACHE
  • ENCODED_MEMORY_CACHE
  • DISK_CACHE
  • FULL_FETCH

如果你需要立即取到一个图片,或者在相对比较短时间内取到图片,否则就不显示的情况下,这非常有用。

自定义View编辑和纠错

DraweeHolders

总有一些时候,DraweeViews是满足不了需求的,在展示图片的时候,我们还需要展示一些其他的内容,或者支持一些其他的操作。在同一个View里,我们可能会想显示一张或者多张图。

在自定义View中,Fresco 提供了两个类来负责图片的展现:

  • DraweeHolder 单图情况下用。
  • MultiDraweeHolder 多图情况下用。

自定义View需要完成的事情

Android 呈现View对象,只有View对象才能得到一些系统事件的通知。DraweeViews 处理这些事件通知,高效地管理内存。使用DraweeHolder时,你需要自己实现这几个方法。

处理 attach/detach 事件

如果没按照以下步骤实现的话,很可能会引起内存泄露

当图片不再在View上显示时,比如滑动时View滑动到屏幕外,或者不再绘制,图片就不应该再存在在内存中。Drawees 监听这些事情,并负责释放内存。当图片又需要显示时,重新加载。

这些在DraweeView中是自动的,但是在自定义View中,需要我们自己去操作,如下:

DraweeHolder mDraweeHolder;

@Override
public void onDetachedFromWindow() {
  super.onDetachedFromWindow();
  mDraweeHolder.onDetach();
}

@Override
public void onStartTemporaryDetach() {
  super.onStartTemporaryDetach();
  mDraweeHolder.onDetach();
}

@Override
public void onAttachedToWindow() {
  super.onAttachedToWindow();
  mDraweeHolder.onAttach();
}

@Override
public void onFinishTemporaryDetach() {
  super.onFinishTemporaryDetach();
  mDraweeHolder.onAttach();
}
处理触摸事件

如果你启用了点击重新加载,在自定义View中,需要这样:

@Override
public boolean onTouchEvent(MotionEvent event) {
  return mDraweeHolder.onTouchEvent(event) || super.onTouchEvent(event);
}
自定义onDraw

你必须调用

Drawable drawable = mDraweeHolder.getHierarchy().getTopLevelDrawable();
drawable.setBounds(...);

否则图片将不会出现。

  • 不要向下转换这个Drawable
  • 不要变换这个Drawable
其他应该做的
  • 设置 Drawable.Callback
// When a holder is set to the view for the first time,
// don't forget to set the callback to its top-level drawable:
mDraweeHolder = ...
mDraweeHolder.getTopLevelDrawable().setCallback(this);

// In case the old holder is no longer needed,
// don't forget to clear the callback from its top-level drawable:
mDraweeHolder.getTopLevelDrawable().setCallback(null);
mDraweeHolder = ...
  • 重写 verifyDrawable:
@Override
protected boolean verifyDrawable(Drawable who) {
  if (who == mDraweeHolder.getHierarchy().getTopLevelDrawable()) {
    return true;
  }
  // 对其他Drawable的验证逻辑
}
  • 确保 invalidateDrawable 处理了图片占用的那块区域。

创建 DraweeHolder

这同样需要非常小心和细致

构造函数

我们推荐如下实现构造函数:

  • 重写所有构造函数,共 3 个
  • 在每个构造函数中调用同等签名的父类构造函数,和一个私有的 init 方法。
  • 在 init 方法中执行初始化操作。

即,不要在构造函数中用 this 来调用另外一个构造。

这样可以保证,不管调用哪个构造,都可以正确地执行初始化流程。holder 在 init 方法中被创建。

创建 Holder

如果有可能,只在 View 创建时,创建 Drawees。创建 DraweeHierarchy 开销较大,最好只做一次。

class CustomView extends View {
  DraweeHolder<GenericDraweeHierarchy> mDraweeHolder;

  // constructors following above pattern

  private void init() {
    GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources());
      .set...
      .set...
      .build();
    mDraweeHolder = DraweeHolder.create(hierarchy, context);
  }
}
设置要显示的图片

使用 controller builder 创建DraweeController,然后调用holder的 setController 方法,而不是设置给自定义 View。

DraweeController controller = Fresco.newControllerBuilder()
    .setUri(uri)
    .setOldController(mDraweeHolder.getController())
    .build();
mDraweeHolder.setController(controller);

MultiDraweeHolder

和 DraweeHolder 相比,MultiDraweeHolder 有 addremoveclear 等方法可以操作 Drawees。如下:

MultiDraweeHolder<GenericDraweeHierarchy> mMultiDraweeHolder;

private void init() {
  GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources());
    .set...
    .build();
  mMultiDraweeHolder = new MultiDraweeHolder<GenericDraweeHierarchy>();
  mMultiDraweeHolder.add(new DraweeHolder<GenericDraweeHierarchy>(hierarchy, context));
  // repeat for more hierarchies
}

同样,也需要处理系统事件,设置声音等等,就像处理单个DraweeHolder那样。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值