一开始没打算分析 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. iostream 操作符

    iostream 操作符 Input/output manipulators - cppreference.com

  2. 工厂模式--摆脱你日复一日new对象却依旧单身的苦恼!

    前言 每每谈及到Java,就不免会想到一个悲伤的事实:你是否每天都在new对象,却依然坚守在单身岗上屹立不倒.(所谓面向对象编程hhh),这篇来学一下工厂模式,摆脱new对象的苦恼! 知识点 传统工厂 ...

  3. netty中使用protobuf实现多协议的消息

    在我们使用 netty 的过程中,有时候为了高效的传输数据,经常使用 protobuf 进行数据的传输,netty默认情况下为我们实现的 protobuf 的编解码,但是默认的只能实现单个对象的编解码 ...

  4. 大厂面试题系列:重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分

    面试题:重载(Overload)和重写(Override)的区别.重载的方法能否根据返回类型进行区分 面试官考察点猜想 这道题纯粹只是考查基础理论知识,对实际开发工作中没有太多的指导意义,毕竟编辑器都 ...

  5. 2021.8.18 NKOJ周赛总结

    两个字总结:安详 T1: NKOJ-6179 NP问题 问题描述: p6pou在平面上画了n个点,并提出了一个问题,称为N-Points问题,简称NP问题. p6pou首先在建立的平面直角坐标系,并标 ...

  6. 21.6.29 test

    \(NOI\) 模拟赛 \(T1\) 正解是个题解难以理解的数论,结果是组合数相加.暴力分拿满了,尝试打了 \(20*20\) 的表,最后大概打出了个三角形的表,并且帮我找到了一些性质.\(45\)p ...

  7. MVC下垃框的使用

    --------------模型-------------------- /// <summary> /// 状态 /// =0 下架 =1 上架 /// </summary> ...

  8. js this指向汇总

    this指向 普通函数  window 定时器函数         window 事件函数 事件源 箭头函数 父function中的this,没有就是window 对象函数 对象本身 构造函数 实例化 ...

  9. kubernetes常见日志采集问题和解决方案分析

    传统日志与kubernetes日志对比 传统服务 目录固定 重启不会丢失 不用关注标准与错误日志输出 容器服务 节点不固定 重启服务会漂移 需要关注标准与错误日志输出 日志文件重启会丢失 日志目录不固 ...

  10. 自动化SQL注入工具 sqlmap 使用手册

    0x00 sqlmap介绍 什么是sqlmap? sqlmap是一个开源的渗透测试工具,它自动化了检测和利用SQL注入缺陷 以及接管数据库服务器的过程.它配备了一个强大的检测引擎 ,以及终极渗透测试仪 ...