1. 程式人生 > >subsampling-scale-image-view載入長圖原始碼分析(二)

subsampling-scale-image-view載入長圖原始碼分析(二)

subsampling-scale-image-view原始碼分析

概要

subsampling-scale-image-view是一個支援部分載入大圖長圖的圖片庫,並且還支援縮放,在subsampling-scale-image-view載入長圖原始碼分析(一)已經介紹過它的用法和部分原始碼,沒有看過的朋友可以先移步看前面的分析。

分析

上回說到取樣率等於1的情況下,因為不需要縮放和部分載入,所以直接呼叫了BitmapFactory進行解碼,那麼接下來我就來分析取樣率大於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也是如此,編碼麻煩,但是效能更加。