subsampling-scale-image-view載入長圖原始碼分析
subsampling-scale-image-view原始碼分析
背景
對於安卓開發人員,最頭疼的問題就是記憶體問題了,而記憶體問題又當屬bitmap最頭疼,雖然說現在市面上已經有越來越多成熟的圖片載入框架,像Fresco,Glide,它們也確實幫我更好地管理了圖片的記憶體,生命週期等,但是還是有一個比較棘手的問題,那就是大圖長圖的載入,動輒750 * 30000的長圖,如果一次性不壓縮加載出來,記憶體就暴漲,如下圖: 看著這接近90度的走勢,嚇得我腎上腺也飆升。那既然一次性載入太耗記憶體,那就區域性載入不就等了,系統還真的提供了這樣的類,那就是BitmapRegionDecoder
介紹使用
首先是引入依賴
dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
}
佈局檔案引入控制元件
<?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>
程式碼呼叫
image_big.setImage(ImageSource.resource(R.mipmap.ic_long))
拭目以待看看記憶體佔用吧
上面就是SubsamplingScaleImageView的最簡單用法,我們接下來再看看其他一些API,首先是獲取圖片來源,當然ImageSourece.bitmap不推薦使用,畢竟已經解了碼,就沒有起到節約記憶體的作用了
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屬性來進行角度的適應。
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,大家可以自行探索。
上面就是SubsamplingScaleImageView的用法,用法很簡單,接下來我就從入口開始分析它的原始碼。
原始碼分析
- setImage
在呼叫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方法。
- 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) {
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也是如此,編碼麻煩,但是效能更加。