一开始没打算分析 SubsamplingScaleImageView 这个开源的图片浏览器的,因为这个库在我们 App 中使用了,觉得自己对这个库还是比较熟悉的,结果某天再看看到源码介绍的时候,才发现自己对其了解并不够深入,所以这才打算再细细看看源码的实现,同时记录方便以后回顾。

那么 SubsamplingScaleImageView 有啥优点呢?

  1. 采用 GestureDetector 进行手势控制,支持图片的点击,双击,滑动等来控制的放大缩小;

  2. 使用了 BitmapRegionDecoder,具有分块加载功能;

  3. 支持查看长图,超大图

上面的优点简直就是非常实用,基本上拿来就可以直接用,简单省力。

下面就是要来分析,它是如何满足这些优点的。

源码分析

首先附上源码地址:subsampling-scale-image-view

使用说明

如果可以拿到图片的资源id,assert或者文件路径,直接使用下面方式进行使用:

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.resource(R.drawable.monkey));
// ... or ...
imageView.setImage(ImageSource.asset("map.png"))
// ... or ...
imageView.setImage(ImageSource.uri("/sdcard/DCIM/DSCM00123.JPG"));

如果可以拿到 bitmap 就可以这么使用:

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.bitmap(bitmap));

可以看到使用是非常简单的。

ImageSource

在上节使用的过程中,发现都是依赖  ImageSource 来进行控制的,下面看下

// 缩减之后的部分源码
public final class ImageSource { static final String FILE_SCHEME = "file:///";
static final String ASSET_SCHEME = "file:///android_asset/"; private final Uri uri;
private final Bitmap bitmap;
private final Integer resource;
private boolean tile;
private int sWidth;
private int sHeight;
private Rect sRegion;
private boolean cached; private ImageSource(int resource) {
this.bitmap = null;
this.uri = null;
this.resource = resource;
this.tile = true;
}
}

简单来说,ImageSource 的作用跟它的命名是一样的,用来处理图片地址来源,最后 SubsamplingScaleImageView 也是从它获取图片的。这个类有好几个属性, uri bitmap resource这几个就是图片的来源, 还有几个是图片的尺寸,而我们调用的构造方法里面主要是resource和tile这两个属性, tile = true说明支持局部加载属性。

这个也是我们需要借鉴的。当我们再写一个图片库的时候,除了支持网络图片,也要考虑其他场景,比如对本地图片和资源的支持。还有就是如果你不知道怎么去支持的时候,这时候就可以看看 ImageSource 的实现。这就是我们为啥需要读源码,学习源码。

接着我们往下看,setImage 方法

    public final void setImage(@NonNull ImageSource imageSource, ImageSource previewSource, ImageViewState state) {
//noinspection ConstantConditions 为空直接抛出异常
if (imageSource == null) {
throw new NullPointerException("imageSource must not be null");
} reset(true); // 新图片,一切重置
if (state != null) { restoreState(state); }
     // 一般情况下都是为 nuLl,这里就不看了
if (previewSource != null) {
if (imageSource.getBitmap() != null) {
throw new IllegalArgumentException("Preview image cannot be used when a bitmap is provided for the main image");
}
if (imageSource.getSWidth() <= 0 || imageSource.getSHeight() <= 0) {
throw new IllegalArgumentException("Preview image cannot be used unless dimensions are provided for the main image");
}
this.sWidth = imageSource.getSWidth();
this.sHeight = imageSource.getSHeight();
this.pRegion = previewSource.getSRegion();
if (previewSource.getBitmap() != null) {
this.bitmapIsCached = previewSource.isCached();
onPreviewLoaded(previewSource.getBitmap());
} else {
Uri uri = previewSource.getUri();
if (uri == null && previewSource.getResource() != null) {
uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + previewSource.getResource());
}
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, true);
execute(task);
}
}
     // 下面加载图片会分成好几种类型进行加载,比如是否设置了 region,bitmap,uri,不同的参数,会有不同的加载方式
if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) {
onImageLoaded(Bitmap.createBitmap(imageSource.getBitmap(), imageSource.getSRegion().left, imageSource.getSRegion().top, imageSource.getSRegion().width(), imageSource.getSRegion().height()), ORIENTATION_0, false);
} else if (imageSource.getBitmap() != null) {
onImageLoaded(imageSource.getBitmap(), ORIENTATION_0, imageSource.isCached());
} else {
sRegion = imageSource.getSRegion();
uri = imageSource.getUri();
if (uri == null && imageSource.getResource() != null) {
uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + imageSource.getResource());
}
       // 这里会判断是否要开启瓦片加载形式,或者设置了 region 就说明需要开启瓦片加载方式
if (imageSource.getTile() || sRegion != null) {
// Load the bitmap using tile decoding.
TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri);
execute(task);
} else {
// Load the bitmap as a single image.
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task);
}
}
}
这里直接看到末尾的 if else,有两种不同的加载形式 TilesInitTask,BitmapLoadTask 作用就是一个是直接加载一个bitmap,另一个是只是对 tile 进行初始化,并不加载图片。下面看看两个task 的具体处理方式。
private static class TilesInitTask extends AsyncTask<Void, Void, int[]> {
@Override
protected int[] doInBackground(Void... params) {
try {
String sourceUri = source.toString();
Context context = contextRef.get();
DecoderFactory<? extends ImageRegionDecoder> decoderFactory = decoderFactoryRef.get();
SubsamplingScaleImageView view = viewRef.get();
if (context != null && decoderFactory != null && view != null) {
view.debug("TilesInitTask.doInBackground");
            // 获取decoder
decoder = decoderFactory.make();
Point dimensions = decoder.init(context, source);
int sWidth = dimensions.x;
int sHeight = dimensions.y;
int exifOrientation = view.getExifOrientation(context, sourceUri);
            // 获取 region,或者说修正 region
if (view.sRegion != null) {
view.sRegion.left = Math.max(0, view.sRegion.left);
view.sRegion.top = Math.max(0, view.sRegion.top);
view.sRegion.right = Math.min(sWidth, view.sRegion.right);
view.sRegion.bottom = Math.min(sHeight, view.sRegion.bottom);
sWidth = view.sRegion.width();
sHeight = view.sRegion.height();
}
return new int[] { sWidth, sHeight, exifOrientation };
}
} catch (Exception e) {
Log.e(TAG, "Failed to initialise bitmap decoder", e);
this.exception = e;
}
return null;
} @Override
protected void onPostExecute(int[] xyo) {
final SubsamplingScaleImageView view = viewRef.get();
if (view != null) {
if (decoder != null && xyo != null && xyo.length == 3) {
view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]);
} else if (exception != null && view.onImageEventListener != null) {
view.onImageEventListener.onImageLoadError(exception);
}
}
}
}

在后台执行的主要事情是调用了解码器decoder的初始化方法,获取图片的宽高信息,然后再回到主线程调用onTilesInited方法通知已经初始化完成。我们先看初始化方法做的事情,先找到解码器,内置的解码器工厂如下,

    private DecoderFactory<? extends ImageRegionDecoder> regionDecoderFactory = new CompatDecoderFactory<ImageRegionDecoder>(SkiaImageRegionDecoder.class);

所以我们只需看看 SkiaImageRegionDecoder 这个decoder 既可:

public class SkiaImageRegionDecoder implements ImageRegionDecoder {

    private BitmapRegionDecoder decoder;
private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true); private static final String FILE_PREFIX = "file://";
private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/";
private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; private final Bitmap.Config bitmapConfig; @Keep
@SuppressWarnings("unused")
public SkiaImageRegionDecoder() {
this(null);
} @SuppressWarnings({"WeakerAccess", "SameParameterValue"})
public SkiaImageRegionDecoder(@Nullable Bitmap.Config bitmapConfig) {
Bitmap.Config globalBitmapConfig = SubsamplingScaleImageView.getPreferredBitmapConfig();
if (bitmapConfig != null) {
this.bitmapConfig = bitmapConfig;
} else if (globalBitmapConfig != null) {
this.bitmapConfig = globalBitmapConfig;
} else {
       // 如果没有传配置,就会使用 565 的方式,这样一个像素占有2个字节,16位 = 5+6+5
this.bitmapConfig = Bitmap.Config.RGB_565;
}
} @Override
@NonNull
// 总结起来就是根据不同的图片资源类型来选择合适的 regiondecoder 进行解析,最终返回的是图片的宽高。
public Point init(Context context, @NonNull Uri uri) throws Exception {
String uriString = uri.toString();
if (uriString.startsWith(RESOURCE_PREFIX)) {
Resources res;
String packageName = uri.getAuthority();
if (context.getPackageName().equals(packageName)) {
res = context.getResources();
} else {
PackageManager pm = context.getPackageManager();
res = pm.getResourcesForApplication(packageName);
} int id = 0;
List<String> segments = uri.getPathSegments();
int size = segments.size();
if (size == 2 && segments.get(0).equals("drawable")) {
String resName = segments.get(1);
id = res.getIdentifier(resName, "drawable", packageName);
} else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) {
try {
id = Integer.parseInt(segments.get(0));
} catch (NumberFormatException ignored) {
}
} decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false);
} else if (uriString.startsWith(ASSET_PREFIX)) {
String assetName = uriString.substring(ASSET_PREFIX.length());
decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false);
} else if (uriString.startsWith(FILE_PREFIX)) {
decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false);
} else {
InputStream inputStream = null;
try {
ContentResolver contentResolver = context.getContentResolver();
inputStream = contentResolver.openInputStream(uri);
if (inputStream == null) {
throw new Exception("Content resolver returned null stream. Unable to initialise with uri.");
}
decoder = BitmapRegionDecoder.newInstance(inputStream, false);
} finally {
if (inputStream != null) {
try { inputStream.close(); } catch (Exception e) { /* Ignore */ }
}
}
}
return new Point(decoder.getWidth(), decoder.getHeight());
}

SkiaImageRegionDecoder 主要就是根据图片资源类型选择一个合适的 RegionDecoder。接下去再看看 onTilesInited 都做了啥:

// overrides for the dimensions of the generated tiles 省略无关的代码
public static final int TILE_SIZE_AUTO = Integer.MAX_VALUE;
private int maxTileWidth = TILE_SIZE_AUTO;
private int maxTileHeight = TILE_SIZE_AUTO; this.decoder = decoder;
this.sWidth = sWidth;
this.sHeight = sHeight;
this.sOrientation = sOrientation;
checkReady();
if (!checkImageLoaded() && maxTileWidth > 0 && maxTileWidth != TILE_SIZE_AUTO && maxTileHeight > 0 && maxTileHeight != TILE_SIZE_AUTO && getWidth() > 0 && getHeight() > 0) {
initialiseBaseLayer(new Point(maxTileWidth, maxTileHeight));
}
invalidate();
requestLayout();

这里就将相关参数都传给 SubsamplingScaleImageView 了,后续就可以直接用了。可以看到最后调用了invalidate 和 requestLayout,也就说最终会触发重绘操作。

绘制流程

onMeasure

比较简单,这块就直接略过了。

ondraw

下面直接看 ondraw 方法。ondraw 的方法很长,我们主要看一些关键逻辑:

  protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
createPaints(); // When using tiles, on first render with no tile map ready, initialise it and kick off async base image loading.
if (tileMap == null && decoder != null) {
initialiseBaseLayer(getMaxBitmapDimensions(canvas));
} preDraw(); if (tileMap != null && isBaseLayerReady()) {
// Optimum sample size for current scale
int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize(scale));
// First check for missing tiles - if there are any we need the base layer underneath to avoid gaps
boolean hasMissingTiles = false;
for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
if (tileMapEntry.getKey() == sampleSize) {
for (Tile tile : tileMapEntry.getValue()) {
if (tile.visible && (tile.loading || tile.bitmap == null)) {
hasMissingTiles = true;
}
}
}
} // Render all loaded tiles. LinkedHashMap used for bottom up rendering - lower res tiles underneath.
for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) {
for (Tile tile : tileMapEntry.getValue()) {
sourceToViewRect(tile.sRect, tile.vRect);
if (!tile.loading && tile.bitmap != null) {
if (tileBgPaint != null) {
canvas.drawRect(tile.vRect, tileBgPaint);
}
if (matrix == null) { matrix = new Matrix(); }
matrix.reset();
setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight());
matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4);
canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint); }
}
}
} else if (bitmap != null) { float xScale = scale, yScale = scale;
if (bitmapIsPreview) {
xScale = scale * ((float)sWidth/bitmap.getWidth());
yScale = scale * ((float)sHeight/bitmap.getHeight());
} if (matrix == null) { matrix = new Matrix(); }
matrix.reset();
matrix.postScale(xScale, yScale);
matrix.postRotate(getRequiredRotation());
matrix.postTranslate(vTranslate.x, vTranslate.y);
if (tileBgPaint != null) {
if (sRect == null) { sRect = new RectF(); }
sRect.set(0f, 0f, bitmapIsPreview ? bitmap.getWidth() : sWidth, bitmapIsPreview ? bitmap.getHeight() : sHeight);
matrix.mapRect(sRect);
canvas.drawRect(sRect, tileBgPaint);
}
canvas.drawBitmap(bitmap, matrix, bitmapPaint); }
}

onDraw主要做了几件事,initialiseBaseLayer,设置tileMap,最后就是先优先tileMap进行drawBitmap,再取bitmap绘制,我们先看看initialiseBaseLayer做了什么。

initialiseBaseLayer

private synchronized void initialiseBaseLayer(@NonNull Point maxTileDimensions) {
debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y); satTemp = new ScaleAndTranslate(0f, new PointF(0, 0)); // 先给定一个初始值
fitToBounds(true, satTemp); // 居中 // Load double resolution - next level will be split into four tiles and at the center all four are required,
// so don't bother with tiling until the next level 16 tiles are needed.
fullImageSampleSize = calculateInSampleSize(satTemp.scale); // 计算采样率,要不要samplesize
if (fullImageSampleSize > 1) {
fullImageSampleSize /= 2;
} if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) { // Whole image is required at native resolution, and is smaller than the canvas max bitmap size.
// Use BitmapDecoder for better image support. 不需要regiondecoder ,直接加载图片
decoder.recycle();
decoder = null;
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task); } else {
       // 需要进行瓦片化加载
initialiseTileMap(maxTileDimensions);
       // 首先取出当前屏幕需要的采样率, fullImageSampleSIze 就是当前屏幕所需要的采样率,并不是对map所有的数据都进行解压
List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
for (Tile baseTile : baseGrid) {
TileLoadTask task = new TileLoadTask(this, decoder, baseTile);
execute(task);
}
       // 按照要求来加载展示图片,同时对不是该采样率的 bitmap 进行回收
refreshRequiredTiles(true); } }

ScaleAndTranslate是存储了绘制的时候的偏移量和缩放级别,调用 fitToBounds 其实就是先对基本的偏移位置等设置好。然后计算采用率来决定要不要进行 regiondecoder。

下面直接看 regiondecoder 相关逻辑。首先是要对 TileMap 进行初始化。

    private void initialiseTileMap(Point maxTileDimensions) {
debug("initialiseTileMap maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y);
this.tileMap = new LinkedHashMap<>();
int sampleSize = fullImageSampleSize; // 采样率
int xTiles = 1;
int yTiles = 1;
while (true) { // 死循环
int sTileWidth = sWidth()/xTiles; // 即将被采样的图片大小
int sTileHeight = sHeight()/yTiles;
int subTileWidth = sTileWidth/sampleSize; // 采样率下的图片大小
int subTileHeight = sTileHeight/sampleSize;
       // maxTileDimensions 本质上就是 cavas 可以支持的最大宽高,这里调整 subtileWidth 的宽度,使得其可以显示在屏幕上,这里需要注意的是,一块tile 其实还包含1/4的不可见区域(屏幕外)
while (subTileWidth + xTiles + 1 > maxTileDimensions.x || (subTileWidth > getWidth() * 1.25 && sampleSize < fullImageSampleSize)) {
xTiles += 1;
sTileWidth = sWidth()/xTiles;
subTileWidth = sTileWidth/sampleSize;
          // 当采样率为1的时候,由于此时采样后图片依旧远远大于屏幕宽度,因此,会被分割成块数也会更多
}
while (subTileHeight + yTiles + 1 > maxTileDimensions.y || (subTileHeight > getHeight() * 1.25 && sampleSize < fullImageSampleSize)) {
yTiles += 1;
sTileHeight = sHeight()/yTiles;
subTileHeight = sTileHeight/sampleSize;
}
       // 最终划分的块数
List<Tile> tileGrid = new ArrayList<>(xTiles * yTiles);
for (int x = 0; x < xTiles; x++) {
for (int y = 0; y < yTiles; y++) {
Tile tile = new Tile();
tile.sampleSize = sampleSize;
tile.visible = sampleSize == fullImageSampleSize; // 当前是否可见
tile.sRect = new Rect(
x * sTileWidth,
y * sTileHeight,
x == xTiles - 1 ? sWidth() : (x + 1) * sTileWidth,
y == yTiles - 1 ? sHeight() : (y + 1) * sTileHeight
);
tile.vRect = new Rect(0, 0, 0, 0);
tile.fileSRect = new Rect(tile.sRect);
tileGrid.add(tile);
}
}
       // 以采样率当做key 值,对应的 list 分块当做value
tileMap.put(sampleSize, tileGrid);
       // 采样率为1 就退出
if (sampleSize == 1) {
break;
} else {
sampleSize /= 2;
}
}
}
这里顾名思义就是切片,在不同的采样率的情况下切成一个个的tile,因为是进行局部加载,所以在放大的时候,要取出对应的采样率的图片,继而取出对应的区域,试想一下,如果放大几倍,仍然用的16的采样率,那么图片放大之后肯定很模糊,所以缩放级别不同,要使用不同的采样率解码图片。这里的tileMap是一个Map,key是采样率,value是一个列表,列表存储的是对应key采样率的所有切片集合,如下图

fileSRect是一个切片的矩阵大小,每一个切片的矩阵大小要确保在对应的缩放级别和采样率下能够显示正常。 初始化切片之后,就执行当前采样率下的TileLoadTask。

    /**
* Async task used to load images without blocking the UI thread.
*/
private static class TileLoadTask extends AsyncTask<Void, Void, Bitmap> {
private final WeakReference<SubsamplingScaleImageView> viewRef;
private final WeakReference<ImageRegionDecoder> decoderRef;
private final WeakReference<Tile> tileRef;
private Exception exception; TileLoadTask(SubsamplingScaleImageView view, ImageRegionDecoder decoder, Tile tile) {
this.viewRef = new WeakReference<>(view);
this.decoderRef = new WeakReference<>(decoder);
this.tileRef = new WeakReference<>(tile);
tile.loading = true;
} @Override
protected Bitmap doInBackground(Void... params) {
try {
SubsamplingScaleImageView view = viewRef.get();
ImageRegionDecoder decoder = decoderRef.get();
Tile tile = tileRef.get();
if (decoder != null && tile != null && view != null && decoder.isReady() && tile.visible) {
view.debug("TileLoadTask.doInBackground, tile.sRect=%s, tile.sampleSize=%d", tile.sRect, tile.sampleSize);
view.decoderLock.readLock().lock();
try {
if (decoder.isReady()) {
// Update tile's file sRect according to rotation 如果用户有过操作,需要对 rect 进行调整
view.fileSRect(tile.sRect, tile.fileSRect);
if (view.sRegion != null) {
tile.fileSRect.offset(view.sRegion.left, view.sRegion.top);
}
return decoder.decodeRegion(tile.fileSRect, tile.sampleSize);
} else {
tile.loading = false;
}
} finally {
view.decoderLock.readLock().unlock();
}
} else if (tile != null) {
tile.loading = false;
}
} catch (Exception e) {
Log.e(TAG, "Failed to decode tile", e);
this.exception = e;
} catch (OutOfMemoryError e) {
Log.e(TAG, "Failed to decode tile - OutOfMemoryError", e);
this.exception = new RuntimeException(e);
}
return null;
} @Override
protected void onPostExecute(Bitmap bitmap) {
final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get();
final Tile tile = tileRef.get();
if (subsamplingScaleImageView != null && tile != null) {
if (bitmap != null) {
tile.bitmap = bitmap;
tile.loading = false;
subsamplingScaleImageView.onTileLoaded();
} else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) {
subsamplingScaleImageView.onImageEventListener.onTileLoadError(exception);
}
}
}
} /**
* Called by worker task when a tile has loaded. Redraws the view.
*/
private synchronized void onTileLoaded() {
debug("onTileLoaded");
checkReady();
checkImageLoaded();
if (isBaseLayerReady() && bitmap != null) {
if (!bitmapIsCached) {
bitmap.recycle();
}
bitmap = null;
if (onImageEventListener != null && bitmapIsCached) {
onImageEventListener.onPreviewReleased();
}
bitmapIsPreview = false;
bitmapIsCached = false;
}
invalidate(); // 进行重绘
}

整体而言,没太多复杂逻辑,这里采用异步加载来获取bitmap,中间会调整 filerect,bitmap 解压完成后,就会重新绘制。

preDraw

没有太多逻辑,主要就是绘制前一些准备工作,包括缩放,位置等等。

isBaseLayerReady

主要就是看 tileMap 里面的 bitmap 是否准备好了。

 for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) {
for (Tile tile : tileMapEntry.getValue()) {
sourceToViewRect(tile.sRect, tile.vRect);
if (!tile.loading && tile.bitmap != null) {
if (tileBgPaint != null) {
canvas.drawRect(tile.vRect, tileBgPaint);
} matrix.reset();
setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight());
setMatrixArray(dstArray, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom); matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4);
canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint);
}
}
}
}

这就是切片绘制的关键代码,在Tile这个类中,sRect负责保存切片的原始大小,vRect则负责保存切片的绘制大小,所以 sourceToViewRect(tile.sRect, tile.vRect) 这里进行了矩阵的缩放,其实就是根据之前计算得到的scale对图片原始大小进行缩放。 接着再通过矩阵变换,将图片大小变换为绘制大小进行绘制。分析到这里,其实整个的加载过程和逻辑已经是了解得七七八八了。 还有另外的就是手势缩放的处理,通过监听move等触摸事件,然后重新计算scale的大小,接着通过scale的大小去重新得到对应的采样率,继续通过tileMap取出采样率下对应的切片,对切片请求解码。值得一提的是,在move事件的时候,这里做了优化,解码的图片并没有进行绘制,而是对原先采样率下的图片进行缩放,直到监听到up事件,才会去重新绘制对应采样率下的图片。所以在缩放的过程中,会看到一个模糊的图像,其实就是高采样率下的图片进行放大导致的。等到缩放结束,会重新绘制,图片就显示正常了。 流程图如下:

到这里,SubsamplingScaleImageView 的关键逻辑就讲完了,希望对大家有帮助。

参考文章

subsampling-scale-image-view加载长图源码分析总结

https://juejin.cn/post/6955427322291814431

https://juejin.cn/post/6844903910088392712

SubsamplingScaleImageView 源码解析的更多相关文章

  1. 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新

    本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...

  2. 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    [原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...

  3. 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新

    上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...

  4. 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例

    前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...

  5. jQuery2.x源码解析(缓存篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...

  6. Spring IoC源码解析——Bean的创建和初始化

    Spring介绍 Spring(http://spring.io/)是一个轻量级的Java 开发框架,同时也是轻量级的IoC和AOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器 ...

  7. jQuery2.x源码解析(构建篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...

  8. jQuery2.x源码解析(设计篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...

  9. jQuery2.x源码解析(回调篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...

随机推荐

  1. Kubernetes Job Controller 原理和源码分析(一)

    概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...

  2. spring提供的可拓展接口

    接口:SmartLifecycle(https://www.jianshu.com/p/7b8f2a97c8f5)

  3. python3去除行号

    问题:在复制一些代码时会同时复制每行的行号,删除比较麻烦,所以利用python3本身的代码进行一键删除. # 导入re 模块来使用正则表达式 import re """去 ...

  4. SpringCloud微服务实战——搭建企业级开发框架(六):使用knife4j集成Swagger2接口文档

    knife4j是为集成Swagger生成api文档的增强解决方案,前后端Java代码以及前端Ui模块进行分离,在微服务架构下使用更加灵活, 提供专注于Swagger的增强解决方案,不同于只是改善增强前 ...

  5. Sequence Model-week3编程题2-Trigger Word Detection

    1. Trigger Word Detection 我们的触发词将是 "Activate.".每当它听到你说 "Activate.",它就会发出 "c ...

  6. Jupyter Notebook配置多个kernel

    Jupyter Notebook配置多个kernel 前言: 在anaconda下配置了多个环境,而Jupiter Notebook只是安装在base环境下,为了能在Jupiter Notebook中 ...

  7. 无网络下,配置yum本地源

    1. 新建一个没有iso镜像文件的虚拟机: 2. 本地上传一个镜像文件(CentOS7的镜像),到虚拟机已创建的目录: 例如:上传一个镜像文件CentOS-7-x86_64-Everything-17 ...

  8. 梦开始的地方(Noip模拟3) 2021.5.24

    T1 景区路线规划(期望dp/记忆化搜索) 一看题目发现肯定是概率期望题,再仔细想想这三天做的题,就知道是个期望dp. 考试思路(错): 因为聪聪与可可的10分打法根深蒂固,导致在考试时想到了用深搜( ...

  9. Hash算法:双重散列

    双重散列是线性开型寻址散列(开放寻址法)中的冲突解决技术.双重散列使用在发生冲突时将第二个散列函数应用于键的想法. 此算法使用: (hash1(key) + i * hash2(key)) % TAB ...

  10. 编译内核错误:Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at kernel/timeconst.pl line 373

    最近在编译一个新的rk sdk的时候,编译内核报错 CHK include/linux/version.h CHK include/generated/utsrelease.h make[1]: 'i ...