https://github.com/davemorrissey/subsampling-scale-image-view
原理:将大图分成多个小图(tile),先加载低清晰度的整体图,放大时加载可见区域高清晰度的小图。
步骤:
1、 添加依赖
dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
}
2、 XML中增加view
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
3、 代码中设置图片,可来源于asset 、路径、资源图片
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"));
解析加载整体图代码解读:
1、sample ImageDisplayActivity 页面加载card.png 7800x6240
nexus 6p 运行情况下getHeight 1890 getWidth 1440
SubsamplingScaleImageView.java
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.createPaints();
if (this.sWidth != 0 && this.sHeight != 0 && this.getWidth() != 0 && this.getHeight() != 0) {
if (this.tileMap == null && this.decoder != null) {
this.initialiseBaseLayer(this.getMaxBitmapDimensions(canvas));//
}
}
2、初始化整体图
/**
* Called on first draw when the view has dimensions. Calculates the initial sample size and starts async loading of
* the base layer image - the whole source subsampled as necessary.
*/
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);
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.
decoder.recycle();
decoder = null;
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task);
} else {
initialiseTileMap(maxTileDimensions);
List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
for (Tile baseTile : baseGrid) {
TileLoadTask task = new TileLoadTask(this, decoder, baseTile);
execute(task);
}
refreshRequiredTiles(true);
}
}
3、 initialiseBaseLayer 开始计算后整体图SampleSize
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);
if (fullImageSampleSize > 1) {
fullImageSampleSize /= 2;
}
计算后整体图sample
fullImageSampleSize = 4
4、 initialiseBaseLayer 中 initialiseTileMap 初始化 tile
/**
* Once source image and view dimensions are known, creates a map of sample size to tile grid.
*/
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;
while (subTileWidth + xTiles + 1 > maxTileDimensions.x || (subTileWidth > getWidth() * 1.25 && sampleSize < fullImageSampleSize)) {
xTiles += 1;
sTileWidth = sWidth()/xTiles;
subTileWidth = sTileWidth/sampleSize;
}
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);
}
}
tileMap.put(sampleSize, tileGrid);
if (sampleSize == 1) {
break;
} else {
sampleSize /= 2;
}
}
}
不同抽样尺寸下的tile大小
sampleSize = 4
xTiles 1
yTiles 1
sampleSize = 2
xTiles 3
yTiles 2
sampleSize = 1
xTiles 5
yTiles 3
5、 initialiseBaseLayer 最后TileLoadTask加载整体图
List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
for (Tile baseTile : baseGrid) {
TileLoadTask task = new TileLoadTask(this, decoder, baseTile);
execute(task);
}
6、TileLoadTask 主要是decoder.decodeRegion(tile.fileSRect, tile.sampleSize);
@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
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;
}
7、 设置图片时 setImage 使用SkiaImageRegionDecoder
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); }
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);
}
}
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());
}
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);
}
}
}
private DecoderFactory<? extends ImageRegionDecoder> regionDecoderFactory = new CompatDecoderFactory<ImageRegionDecoder>(SkiaImageRegionDecoder.class);
8、 SkiaImageRegionDecoder decodeRegion 加载整体图生成bitmap,配置inSampleSize,Rect
public Bitmap decodeRegion(@NonNull Rect sRect, int sampleSize) {
getDecodeLock().lock();
try {
if (decoder != null && !decoder.isRecycled()) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
options.inPreferredConfig = bitmapConfig;
Bitmap bitmap = decoder.decodeRegion(sRect, options);
if (bitmap == null) {
throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported");
}
return bitmap;
} else {
throw new IllegalStateException("Cannot decode region after decoder has been recycled");
}
} finally {
getDecodeLock().unlock();
}
}