对于安卓开发人员,最头疼的问题就是内存问题了,而内存问题又当属bitmap最头疼,虽说如今市面上已经有愈来愈多成熟的图片加载框架,像Fresco,Glide,它们也确实帮我更好地管理了图片的内存,生命周期等,可是仍是有一个比较棘手的问题,那就是大图长图的加载,动辄750 * 30000的长图,若是一次性不压缩加载出来,内存就暴涨,以下图: android
首先是引入依赖ios
dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
}
复制代码
布局文件引入控件git
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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/image_big"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
</RelativeLayout>
复制代码
代码调用github
image_big.setImage(ImageSource.resource(R.mipmap.ic_long))
复制代码
拭目以待看看内存占用吧 canvas
上面就是SubsamplingScaleImageView的最简单用法,咱们接下来再看看其余一些API,首先是获取图片来源,固然ImageSourece.bitmap不推荐使用,毕竟已经解了码,就没有起到节约内存的做用了bash
image_big.setImage(ImageSource.resource(R.mipmap.ic_long))
image_big.setImage(ImageSource.asset("ic_long.jpg"))
image_big.setImage(ImageSource.bitmap(BitmapFactory.decodeResource(resources, R.mipmap.ic_long)))
复制代码
接下来是缩放的API,这里须要注意的是须要设置ScaleType为SCALE_TYPE_CUSTOM,否则maxScale,mixScale设置不会生效。框架
image_big.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
image_big.maxScale = 5f
image_big.minScale = 0.1f
image_big.setImage(ImageSource.resource(R.mipmap.ic_long))
复制代码
还有旋转的API,以下,特别地,ORIENTATION_USE_EXIF是跟随照片的exifOrientation属性来进行角度的适应。async
image_big.orientation = SubsamplingScaleImageView.ORIENTATION_0
image_big.orientation = SubsamplingScaleImageView.ORIENTATION_90
image_big.orientation = SubsamplingScaleImageView.ORIENTATION_180
image_big.orientation = SubsamplingScaleImageView.ORIENTATION_270
image_big.orientation = SubsamplingScaleImageView.ORIENTATION_USE_EXIF
image_big.setImage(ImageSource.resource(R.mipmap.ic_long))
复制代码
除此以外,还有一些禁止缩放啊,快速缩放等一些基础API,你们能够自行探索。ide
上面就是SubsamplingScaleImageView的用法,用法很简单,接下来我就从入口开始分析它的源码。源码分析
在调用setImage的时候,会建立一个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;
}
}
复制代码
这个类有好几个属性, uri bitmap resource这几个就是图片的来源, 还有几个是图片的尺寸,而咱们调用的构造方法里面主要是resource和tile这两个属性, tile = true说明支持局部加载属性。 接着咱们往下看,setImage方法
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);
}
}
复制代码
这里主要是根据imagesource的属性进行一些初始化工做,结合上文的构造方法,这里进入了一个初始化任务的调用,即
TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri);
execute(task);
复制代码
话很少说,咱们进入TilesInitTask 一窥究竟。
TilesInitTask 是一个AsyncTask, 主要的代码逻辑以下
@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 = decoderFactory.make();
Point dimensions = decoder.init(context, source);
int sWidth = dimensions.x;
int sHeight = dimensions.y;
int exifOrientation = view.getExifOrientation(context, sourceUri);
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既可,查看init方法
@Override
@NonNull
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);
decoder = BitmapRegionDecoder.newInstance(inputStream, false);
} finally {
if (inputStream != null) {
try { inputStream.close(); } catch (Exception e) { /* Ignore */ }
}
}
}
return new Point(decoder.getWidth(), decoder.getHeight());
}
复制代码
先是一堆的资源和uri解析判断,这个咱们不用管,关键代码是BitmapRegionDecoder.newInstance(inputStream, false); 而后最后返回了decoder解析的宽高信息,BitmapRegionDecoder就是上文提到的部分加载bitmap的类,因此分析到这里咱们就知道了,初始化工做就是调用BitmapRegionDecoder获取bitmap宽高。 解析宽高以后,咱们再回过头看看初始化完成的回调:
// 代码通过整理 为了更方便看
// 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();
复制代码
回调完成主要作了一些赋值操做,还有进行判断是否初始化baseLayer,因为咱们事先并无覆盖尺寸大小,因此直接进入重绘操做。快马加鞭,等等,先停一下蹄。
对于图片的解码,分采样率等于1和大于1两种状况,采样率等于1,直接解码,大于1,则须要使用局部解码。
好的,咱们继续看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作了什么。
private synchronized void initialiseBaseLayer(@NonNull Point maxTileDimensions) {
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); } } 复制代码
ScaleAndTranslate是存储了绘制的时候的偏移量和缩放级别,调用fitToBounds其实就是先对基本的偏移位置等设置好,咱们先重点关注ScaleAndTranslate的scale,先看看scale的计算,
private float minScale() {
int vPadding = getPaddingBottom() + getPaddingTop();
int hPadding = getPaddingLeft() + getPaddingRight();
if (minimumScaleType == SCALE_TYPE_CENTER_CROP || minimumScaleType == SCALE_TYPE_START) {
return Math.max((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight());
} else if (minimumScaleType == SCALE_TYPE_CUSTOM && minScale > 0) {
return minScale;
} else {
return Math.min((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight());
}
}
复制代码
sWidth,sHeight是刚刚获取的图片大小,getWidth,getHeight是控件的大小,因此scale的值其实就是,控件大小占图片大小的比例,这样一来就能够把图片缩放到合适的比例大小。 计算scale以后,接着是计算bitmap的采样率, 对应代码的fullImageSampleSize,
private int calculateInSampleSize(float scale) {
if (minimumTileDpi > 0) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
float averageDpi = (metrics.xdpi + metrics.ydpi)/2;
scale = (minimumTileDpi/averageDpi) * scale;
}
int reqWidth = (int)(sWidth() * scale);
int reqHeight = (int)(sHeight() * scale);
// Raw height and width of image
int inSampleSize = 1;
if (reqWidth == 0 || reqHeight == 0) {
return 32;
}
if (sHeight() > reqHeight || sWidth() > reqWidth) {
// Calculate ratios of height and width to requested height and width
final int heightRatio = Math.round((float) sHeight() / (float) reqHeight);
final int widthRatio = Math.round((float) sWidth() / (float) reqWidth);
// Choose the smallest ratio as inSampleSize value, this will guarantee
// a final image with both dimensions larger than or equal to the
// requested height and width.
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
// We want the actual sample size that will be used, so round down to nearest power of 2.
int power = 1;
while (power * 2 < inSampleSize) {
power = power * 2;
}
return power;
}
复制代码
参数scale是上文计算得来的,这里会根据设置的目标dpi进行调整,接着再根据图片实际大小与请求绘制的大小比例就获得了相应的采样率,即对bitmap的缩放。 特别地,若是计算获得的fullImageSampleSize 等于1,即图片大小可以显示彻底,就会调用BitmapLoadTask 这个任务,咱们进去这个任务一窥究竟。
@Override
protected Integer doInBackground(Void... params) {
try {
String sourceUri = source.toString();
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");
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;
}
复制代码
和上面提到的TileLoadTask大同小异,这里是调用了解码方法,再看看解码方法
@Override
@NonNull
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)) {
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) {
}
}
bitmap = BitmapFactory.decodeResource(context.getResources(), id, options);
} 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)) {
bitmap = BitmapFactory.decodeFile(uriString.substring(FILE_PREFIX.length()), options);
} else {
InputStream inputStream = null;
try {
ContentResolver contentResolver = context.getContentResolver();
inputStream = contentResolver.openInputStream(uri);
bitmap = BitmapFactory.decodeStream(inputStream, null, options);
} finally {
if (inputStream != null) {
try { inputStream.close(); } catch (Exception e) { /* Ignore */ }
}
}
}
if (bitmap == null) {
throw new RuntimeException("Skia image region decoder returned null bitmap - image format may not be supported");
}
return bitmap;
}
复制代码
调用了BitmapFactory进行解码,以后主线程回调对bitmap进行赋值。 而后从新刷新ui,由于此时bitmap不为null,那么就把解码获得的bitmap进行绘制。此时,就完成了图片的绘制过程。这就是采样率等于1的直接解码,无需调用局部解码,简单粗暴。
接下来分析采样率大于1的状况。上代码:
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); } } 复制代码
else里面就是采样率大于1的状况,先进行了tileMap的初始化,接着是TilLoadTask的执行,那么咱们先看一下initialiseTileMap。
private void initialiseTileMap(Point maxTileDimensions) {
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,由于是进行局部加载,因此在放大的时候,要取出对应的采样率的图片,继而取出对应的区域,试想一下,若是放大几倍,仍然用的16的采样率,那么图片放大以后确定很模糊,因此缩放级别不一样,要使用不一样的采样率解码图片。这里的tileMap是一个Map,key是采样率,value是一个列表,列表存储的是对应key采样率的全部切片集合,以下图
fileSRect是一个切片的矩阵大小,每个切片的矩阵大小要确保在对应的缩放级别和采样率下可以显示正常。 初始化切片以后,就执行当前采样率下的TileLoadTask。
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; } @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); } } } 复制代码
能够看到了调用了图片解码器的decodeRegion方法,传入了当前的采样率和切片矩阵大小,进入解码器代码,
@Override
@NonNull
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();
}
}
复制代码
超级简单有没有,就是设置好inSampleSize,而后调用BitmapRegionDecoder的decodeRegion方法,传入的矩阵是切片的大小。解码成功以后,从新刷新UI,咱们继续看到onDraw方法。
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事件,才会去从新绘制对应采样率下的图片。因此在缩放的过程当中,会看到一个模糊的图像,其实就是高采样率下的图片进行放大致使的。等到缩放结束,会从新绘制,图片就显示正常了。 流程图以下:
经过这篇博客,我分别介绍了subsampling-scale-image-view的初始化过程,缩放级别,采样率等,经过不一样的采样率进行不一样方法的解码。在部分解码图片的时候,又会根据当前缩放级别从新去获取采样率,解码新的图片,缩放越大,须要的图片就越清晰,越小就不须要太过清晰,这样子能够起到节约内存的做用。 对应内存使用这一块,其实要想节约,就要省着来用,不可见的先不加载,subsampling-scale-image-view是如此,viewstub也是如此,编码麻烦,可是性能更加。