SubsamplingScaleImageView解析(上)

3,728 阅读7分钟

SubsamplingScaleImageView可用于超大图片的显示,显示图片时有如下几个特点

  • 在放大后只显示图片一部分时,使用官方提供的BitmapRegionDecoder类解码局部图片
  • 根据当前缩放比选择合适的采样率进行采样,并根据当前可显示范围将解码后的图片拆分为多个tile存放在tilemap里,tile里存储着拆分后的其中一个bitmap
  • 当缩放比发生改变时,判断当前tilemap里的tile是否还有用,如果无用就回收bitmap重新解析原图片然后拆分为多个tile

使用

使用起来很简单,在xml里

<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 
  android:width="match_parent"
  android:height="match_parent"
  android:id="@+id/imageView"/>                                                          

在java代码里设置图片资源

//使用assets目录下的图片
view.setImage(ImageSource.asset("sanmartino.jpg"));
//使用res目录下的图片
imageView.setImage(ImageSource.resource(R.drawable.monkey));
//使用绝对路径
imageView.setImage(ImageSource.uri("/sdcard/DCIM/DSCM00123.JPG"));

图片加载流程

先来看看它是如何显示图片的,以 view.setImage(ImageSource.asset("sanmartino.jpg")); 为例(其他的类似)

设置基本参数

先看 ImageSource.asset("sanmartino.jpg")

public static ImageSource asset(@NonNull String assetName) {
    //noinspection ConstantConditions
    if (assetName == null) {
        throw new NullPointerException("Asset name must not be null");
    }
    return uri(ASSET_SCHEME + assetName);
}
public static ImageSource uri(@NonNull String uri) {
    //noinspection ConstantConditions
    //省略无关代码
    return new ImageSource(Uri.parse(uri));
}
private ImageSource(@NonNull Uri uri) {
    // #114 If file doesn't exist, attempt to url decode the URI and try again
    String uriString = uri.toString();
    //省略无关代码
    this.bitmap = null;
    this.uri = uri;
    this.resource = null;
    this.tile = true;
}

可以看到最终返回了一个ImageSource,这个ImageSource里包含了图片的地址信息,并且默认开启tile(即会采用tilemap将图片拆分为多个包含局部bitmap的tile)

接下来看看setImage这个方法

public final void setImage(@NonNull ImageSource imageSource) {
    setImage(imageSource, null, null);
}
public final void setImage(@NonNull ImageSource imageSource, ImageSource previewSource, ImageViewState state) {
    //noinspection ConstantConditions
    if (imageSource == null) {
        throw new NullPointerException("imageSource must not be null");
    }
    
    //这个方法主要将各种参数(缩放,显示范围,matrix等)置为初始值(或默认值)
    //并且这里传递了true,表示这是一张新的图片,所以还会将bitmap,tilemap,decoder等回收掉
    reset(true);
    if (state != null) { restoreState(state); }
    
    if (previewSource != null) {
        //省略,没用到预览图
    }

    if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) {
        //省略,从面可知这里bitmap为null
    } else if (imageSource.getBitmap() != null) {
        //省略,从面可知这里bitmap为null
    } else {
        //这里sRegion为null
        sRegion = imageSource.getSRegion();
        uri = imageSource.getUri();
        if (uri == null && imageSource.getResource() != null) {
            //省略,这里uri不为null
        }
        if (imageSource.getTile() || sRegion != null) {
            // Load the bitmap using tile decoding.
            //省略,这里sRegion为null
        } else {
            // Load the bitmap as a single image.
            //bitmapDecoderFactory可以理解为一个用于获得图片解码器的工厂类
            BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
            execute(task);
        }
    }
}

Bitmap加载

接下来就是BitmapLoadTask,这是一个继承自AsyncTask的后台任务类

看看它的doInBackground方法

protected Integer doInBackground(Void... params) {
    try {
        String sourceUri = source.toString();
        //这个类用虚引用来保存context,decoderFactory和view,防止内存泄漏
        Context context = contextRef.get();
        DecoderFactory<? extends ImageDecoder> decoderFactory = decoderFactoryRef.get();
        SubsamplingScaleImageView view = viewRef.get();
        if (context != null && decoderFactory != null && view != null) {
            view.debug("BitmapLoadTask.doInBackground");
            //decoderFactory.make()即使用该工厂获得一个解码器类
            bitmap = decoderFactory.make().decode(context, source);
            return view.getExifOrientation(context, sourceUri);
        }
    } catch (Exception e) {
        Log.e(TAG, "Failed to load bitmap", e);
        this.exception = e;
    } catch (OutOfMemoryError e) {
        Log.e(TAG, "Failed to load bitmap - OutOfMemoryError", e);
        this.exception = new RuntimeException(e);
    }
    return null;
}

decoderFactory.make()会获得了一个解码器类SkiaImageDecoder,看看SkiaImageDecoder#decode方法

public Bitmap decode(Context context, @NonNull Uri uri) throws Exception {
    String uriString = uri.toString();
    BitmapFactory.Options options = new BitmapFactory.Options();
    Bitmap bitmap;
    options.inPreferredConfig = bitmapConfig;
    if (uriString.startsWith(RESOURCE_PREFIX)) {
        //如果使用的是res目录下的图片,省略
    } else if (uriString.startsWith(ASSET_PREFIX)) {
        String assetName = uriString.substring(ASSET_PREFIX.length());
        bitmap = BitmapFactory.decodeStream(context.getAssets().open(assetName), null, options);
    } else if (uriString.startsWith(FILE_PREFIX)) {
        //如果使用的是手机目录下的图片,省略
    } else {
        //其他情况,省略
    }
    if (bitmap == null) {
        //异常情况处理,省略
    }
    return bitmap;
}

继续看BitmapLoadTask#onPostExecute方法

protected void onPostExecute(Integer orientation) {
    SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get();
    if (subsamplingScaleImageView != null) {
        if (bitmap != null && orientation != null) {
            if (preview) {
                subsamplingScaleImageView.onPreviewLoaded(bitmap);
            } else {
                //bitmap成功加载
                subsamplingScaleImageView.onImageLoaded(bitmap, orientation, false);
            }
        } else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) {
            //异常处理,省略
        }
    }
}

字数限制,下篇看这里 SubsamplingScaleImageView解析(下)