天天看點

從設計到實作,一步步教你實作Android-Universal-ImageLoader-解碼與顯示圖檔解碼圖檔顯示

轉載請标明出處,本文出自:chaossss的部落格

Android-Universal-ImageLoader Github 位址

在上一篇博文中我給大家剖析了 Android-Universal-ImageLoader 中緩存功能的設計和實作,希望大家可以在裡面學到一丢丢東西哈。今天呢,我将接着向下講解,介紹 AUI 核心類中的圖檔解碼與顯示功能,如果大家沒有看過上一篇博文的話,可以戳我進去看哈,廢話不多說,下面進入正題:

圖檔解碼

在考慮具體實作之前,我們不妨先想想一個解碼器的職責是什麼?我相信答案大家都能脫口而出:解碼。沒錯,解碼器的核心功能在于解碼,換言之,我們在設計 Decoder 類的時候,就應該遵循單一職責原則:

public interface ImageDecoder {

    Bitmap decode(ImageDecodingInfo imageDecodingInfo) throws IOException;
}
           

小夥伴們看到 decode() 方法裡的 ImageDecodingInfo 參數可能會很困惑,這是幹什麼的啊?我們不是隻要解碼圖檔麼。大家别急,我們先在腦海裡模拟一整個圖檔解碼的流程吧:

要解碼圖檔,我們得獲得圖檔的“來源”,圖檔可能來自網絡(那我們就得調用下載下傳子產品去下載下傳),圖檔可能來自本地的檔案(那我們就得通過 IO 流去讀取)。在獲得圖檔的來源之後,我們就要開始解碼了,我們進行解碼肯定不隻有一種方案,有時候可能能顯示出來就行了,有時候則要圖檔高清不失真地顯示出來,又或者我們需要縮放/放大圖檔,又甚至是需要定制圖檔的長和寬,這就意味着我們需要一個解碼輔助類存儲這些解碼所需的資訊,以便于解碼操作的完成。

是以大家現在應該能領會 ImageDecodingInfo 的作用了吧?我們一起來看看它的源碼吧:

public class ImageDecodingInfo {
    private final String imageKey;
    private final String imageUri;
    private final String originalImageUri;
    private final ImageSize targetSize;

    private final ImageScaleType imageScaleType;
    private final ViewScaleType viewScaleType;

    private final ImageDownloader downloader;
    private final Object extraForDownloader;

    private final boolean considerExifParams;
    private final Options decodingOptions;

    public ImageDecodingInfo(String imageKey, String imageUri, String originalImageUri, ImageSize targetSize, ViewScaleType viewScaleType,
                             ImageDownloader downloader, DisplayImageOptions displayOptions) {
        this.imageKey = imageKey;
        this.imageUri = imageUri;
        this.originalImageUri = originalImageUri;
        this.targetSize = targetSize;

        this.imageScaleType = displayOptions.getImageScaleType();
        this.viewScaleType = viewScaleType;

        this.downloader = downloader;
        this.extraForDownloader = displayOptions.getExtraForDownloader();

        considerExifParams = displayOptions.isConsiderExifParams();
        decodingOptions = new Options();
        copyOptions(displayOptions.getDecodingOptions(), decodingOptions);
    }

    private void copyOptions(Options srcOptions, Options destOptions) {
        destOptions.inDensity = srcOptions.inDensity;
        destOptions.inDither = srcOptions.inDither;
        destOptions.inInputShareable = srcOptions.inInputShareable;
        destOptions.inJustDecodeBounds = srcOptions.inJustDecodeBounds;
        destOptions.inPreferredConfig = srcOptions.inPreferredConfig;
        destOptions.inPurgeable = srcOptions.inPurgeable;
        destOptions.inSampleSize = srcOptions.inSampleSize;
        destOptions.inScaled = srcOptions.inScaled;
        destOptions.inScreenDensity = srcOptions.inScreenDensity;
        destOptions.inTargetDensity = srcOptions.inTargetDensity;
        destOptions.inTempStorage = srcOptions.inTempStorage;
        if (Build.VERSION.SDK_INT >= ) copyOptions10(srcOptions, destOptions);
        if (Build.VERSION.SDK_INT >= ) copyOptions11(srcOptions, destOptions);
    }

    //get方法略去
    …………

    @TargetApi()
    private void copyOptions10(Options srcOptions, Options destOptions) {
        destOptions.inPreferQualityOverSpeed = srcOptions.inPreferQualityOverSpeed;
    }

    @TargetApi()
    private void copyOptions11(Options srcOptions, Options destOptions) {
        destOptions.inBitmap = srcOptions.inBitmap;
        destOptions.inMutable = srcOptions.inMutable;
    }
}
           

BaseImageDecoder

我們在獲得了圖檔解碼器的基類 ImageDecoder 後,就得完成我們的具體實作了。那麼我們現在就得想想,基于 ImageDecoder 的抽象:decode() 方法,我們會衍生出哪些 ImageDecoder 的實作細節。為了完成圖檔解碼操作,我們會按照下面的步驟實作:

獲得圖檔的“來源” —> 獲得圖檔輸入流并判斷圖檔的實際大小、是否需要被裁減以及是否需要旋轉 —> 獲得圖檔 —> 根據圖檔資訊以及解碼設定處理圖檔 —> 得到符合要求的圖檔

既然實作思路已經定下來了,那具體實作肯定也不難拉:

@Override
    public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
        Bitmap decodedBitmap;
        ImageFileInfo imageInfo;

        InputStream imageStream = getImageStream(decodingInfo);
        if (imageStream == null) {
            L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
            return null;
        }
        try {
            imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
            imageStream = resetStream(imageStream, decodingInfo);
            Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
            decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
        } finally {
            IoUtils.closeSilently(imageStream);
        }

        if (decodedBitmap == null) {
            L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
        } else {
            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                    imageInfo.exif.flipHorizontal);
        }
        return decodedBitmap;
    }
           

代碼我就不全放出來了,大家可以根據剛剛的思路順着這裡的實作進入相應的方法看就行了。而 ExifInfo、ImageFileInfo 兩個類隻是解碼過程需要的輔助類,提供處理圖檔需要的一些資訊,例如:圖檔檔案的大小、圖檔是否需要裁減、旋轉:

protected static class ExifInfo {

    public final int rotation;
    public final boolean flipHorizontal;

    protected ExifInfo() {
        this.rotation = ;
        this.flipHorizontal = false;
    }

    protected ExifInfo(int rotation, boolean flipHorizontal) {
        this.rotation = rotation;
        this.flipHorizontal = flipHorizontal;
    }
}

protected static class ImageFileInfo {

    public final ImageSize imageSize;
    public final ExifInfo exif;

    protected ImageFileInfo(ImageSize imageSize, ExifInfo exif) {
        this.imageSize = imageSize;
        this.exif = exif;
    }
}
           

圖檔顯示

事實上大家會發現,圖檔顯示和圖檔解碼兩個功能在實作上會很相似:抽象單一(圖檔顯示/圖檔解碼),具體實作隻要根據相應的需求依據抽象實作細節就可以了。

public interface BitmapDisplayer {

    void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom);
}
           

我們顯示圖檔的時候可能需要顯示圓形/矩形/圓角圖檔,或者設定相應的圖檔動畫等等等等……實際上我們隻需要依據相應的圖檔形狀、動畫設定/實作相應的 Drawable 和 Animation 就可以了。我覺得這裡沒有啥好說的……但在這裡引入了一個接口 ImageAware,我倒覺得值得我們注意一下:

public interface ImageAware {
    int getWidth();

    int getHeight();

    ViewScaleType getScaleType();

    View getWrappedView();

    boolean isCollected();

    int getId();

    boolean setImageDrawable(Drawable drawable);

    boolean setImageBitmap(Bitmap bitmap);
}
           

通過這個接口我們在圖檔顯示類内獲得顯示圖檔的 View 的資訊,還可以修改 View 顯示的圖檔,圖檔的形狀,以及顯示圖檔的 ID。可能有人會說,然而這并沒有什麼卵用……但你仔細想想,真的是這樣麼?如果沒有這個接口,我們要怎麼在 Display 類裡面直接讓圖檔顯示為圓形,抑或是添加相應的動畫呢?要知道這些效果的最終實作都是應用到 View 上面的。

當然了,顯示圓形圖檔和動畫肯定能實作,例如自定義 View,或者是把 View 作為參數傳入 Display,在 Display 類裡對每一個顯示圖檔的 View 進行處理或者是獲得 View 的屬性。AUI 中的做法也是值得參考的,因為它把這一塊邏輯剝離,避免耦合。