subsampling-scale-image-view加載長圖源碼分析總結

背景

對於安卓開發人員,最頭疼的問題就是內存問題了,而內存問題又當屬bitmap最頭疼,雖說如今市面上已經有愈來愈多成熟的圖片加載框架,像Fresco,Glide,它們也確實幫我更好地管理了圖片的內存,生命週期等,可是仍是有一個比較棘手的問題,那就是大圖長圖的加載,動輒750 * 30000的長圖,若是一次性不壓縮加載出來,內存就暴漲,以下圖: android

在這裏插入圖片描述
看着這接近90度的走勢,嚇得我腎上腺也飆升。那既然一次性加載太耗內存,那就局部加載不就等了,系統還真的提供了這樣的類,那就是 BitmapRegionDecoder,鴻洋大神也寫過一篇加載巨圖的博客,就是利用了BitmapRegionDecoder,傳送門 Android 高清加載巨圖方案 拒絕壓縮圖片。可是今天的主角是subsampling-scale-image-view它,它不只提供了局部加載長圖,還提供縮放支持,這是它的 github主頁,接下來咱們就來看看它的用法。

介紹使用

首先是引入依賴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

在調用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也是如此,編碼麻煩,可是性能更加。

參考

Android 高清加載巨圖方案 拒絕壓縮圖片

相關文章
相關標籤/搜索