知乎 Matisse 原始碼解析,帶你探究高效圖片選擇庫的祕密

developerHaoz發表於2017-12-18

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

目錄

  • 基本介紹
  • 整體的設計和實現流程
  • 資原始檔夾的載入和展示
  • 主頁圖片牆的實現
  • 預覽介面的實現
  • 總結

一、基本介紹


Matisse 是「知乎」開源的一款十分精美的本地影象和視訊選擇庫。

Matisse

Matisse 的程式碼寫的相當的簡潔、規範,很有學習的價值。

講一下 Matisse 的一些優點:

  • 在 Activity 或 Fragment 都可以輕鬆的呼叫

  • 支援各種格式的圖片和視訊載入

  • 支援不同的樣式,包括兩種內建主題和自定義主題

  • 可以自定義檔案的過濾規則

可以看到 Matisse 的可擴充性是非常強的,不僅可以自定義我們需要的主題,而且還可以按照需求來過濾出我們想要的檔案,除此之外,Matisse 採用了建造者模式,使得我們可以通過鏈式呼叫的方式,配置各種各樣的屬性,使我們的圖片選擇更加靈活。

二、整體的設計和實現流程


在介紹 Matisse 的工作流程之前,我們先來看看幾個比較重要的類,有助於我們後面的理解

類名 功能
Matisse 通過外部傳入的 Activity 或 Fragment,以弱引用的形式進行儲存,同時通過 from() 方法返回 SelectionCreator 進行各個引數的配置
SelectionCreator 通過建造者模式,鏈式配置我們需要的各種屬性
MatisseActivity Matisse 首頁的 Activity,將圖片和視訊進行展示

我們先從 Matisse 的使用入手,看看 Matisse 的工作流程。

Matisse.from(MainActivity.this)
        .choose(MimeType.allOf()) // 1、獲取 SelectionCreator
        .countable(true)
        .maxSelectable(9)
        .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
        .gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))
        .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
        .thumbnailScale(0.85f)
        .imageEngine(new GlideEngine()) // 2、配置各種各樣的引數
        .forResult(REQUEST_CODE_CHOOSE); // 3、開啟 MatisseActivity
複製程式碼

上面的使用程式碼,我們以 Activity 為例,可以分成三部分來看

  • 將外部傳入的 Activity 以弱引用的形式進行儲存,然後呼叫 choose() 獲取 SelectionCreator

  • 通過鏈式呼叫的方式,配置 SelectionCreator 的各種屬性,如可選擇的數量、縮圖的大小、載入圖片的引擎等

  • 使用從第一步中傳入的 Activity 呼叫 startActivityForResult(),並從外部傳入請求碼,以便到時候返回所選擇圖片的 List

具體的流程圖如下:

Matisse 流程圖

以上便是 Matisse 的工作流程,接下來詳細的分析下相關的類。有一點要先說明一下,我下面貼出的所有類中的原始碼並不是完整的程式碼,而是將原始碼中與效能、相容性、擴充套件性有關的程式碼剔除後的「核心程式碼」。

Matisse

public final class Matisse {

    private final WeakReference<Activity> mContext;
    private final WeakReference<Fragment> mFragment;

    private Matisse(Activity activity, Fragment fragment) {
        mContext = new WeakReference<>(activity);
        mFragment = new WeakReference<>(fragment);
    }

    public static Matisse from(Activity activity) {
        return new Matisse(activity);
    }

    public static Matisse from(Fragment fragment) {
        return new Matisse(fragment);
    }

    /**
     *  在開啟 MatisseActivity 的 Activity 或 Fragment 中獲取使用者選擇的媒體 Uri 列表
     */
    public static List<Uri> obtainResult(Intent data) {
        return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION);
    }

    public SelectionCreator choose(Set<MimeType> mimeTypes, boolean mediaTypeExclusive) {
        return new SelectionCreator(this, mimeTypes, mediaTypeExclusive);
    }

}
複製程式碼

這個類的程式碼還是很簡單的,將外部傳入的 Activity 或 Fragment,用弱引用的形式儲存,防止記憶體洩露。然後通過 choose() 方法返回 SelectionCreator 用於之後引數的配置。等到圖片選擇完成後,我們可以在 Fragment 或 Activity 中的 onActivityResult() 中通過 obtainResult() 獲取我們所選擇媒體的 Uri 列表。

SelectionCreator

public final class SelectionCreator {
    private final Matisse mMatisse;
    private final SelectionSpec mSelectionSpec;

    SelectionCreator(Matisse matisse, @NonNull Set<MimeType> mimeTypes) {
        mMatisse = matisse;
        mSelectionSpec = SelectionSpec.getCleanInstance();
        mSelectionSpec.mimeTypeSet = mimeTypes;
    }

    public SelectionCreator theme(@StyleRes int themeId) {
        mSelectionSpec.themeId = themeId;
        return this;
    }

    public SelectionCreator maxSelectable(int maxSelectable) {
        mSelectionSpec.maxSelectable = maxSelectable;
        return this;
    }
    // 其餘方法都類似上面這兩個,這裡面就不貼出來了

    public void forResult(int requestCode) {
        Activity activity = mMatisse.getActivity();
        Intent intent = new Intent(activity, MatisseActivity.class);
        Fragment fragment = mMatisse.getFragment();
        if (fragment != null) {
            fragment.startActivityForResult(intent, requestCode);
        } else {
            activity.startActivityForResult(intent, requestCode);
        }
    }

}
複製程式碼

可以看到 SelectionCreator 內部儲存了 Matisse 的例項,用於獲取外部呼叫的 Activity 或 Fragment,以及一個 SelectionSpec 類的例項,這個類封裝了圖片載入類中常見的引數,使得 SelectionCreator 的程式碼更加簡潔。SelectionCreator 內部使用了建造者模式,讓我們能夠進行鏈式呼叫,配置各種各樣的屬性。最後 forResult() 裡面其實就是跳轉到 MatisseActivity,然後通過外部傳入的 requestCode 將使用者選擇的媒體 Uri 列表返回給相應的 Activity 或 Fragment.

三、資原始檔夾的載入和展示


Matisse 中所展示的資源都是用 Loader 機制進行載入的,Loader 機制是 Android 3.0 之後官方推薦的載入 ContentProvider 中資源的最佳方式,不僅能極大地提高我們資源載入的速度,而且還能讓我們的程式碼變得更加的簡潔。對於 Loader 機制不熟悉的同學,可以先看下這篇文章 Android Loader 機制,讓你的資料載入更加高效

先附上此項操作的流程圖:

知乎 Matisse 原始碼解析,帶你探究高效圖片選擇庫的祕密

繼承了 Cursor 的 AlbumLoader,作為資源的載入器,通過配置與資源相關的一些引數,從而載入資源。AlbumCollection 實現了 LoaderManager.LoaderCallbacks 介面,將 AlbumLoader 作為載入器,其內部定義了 AlbumCallbacks 介面,在載入資源完成後,將包含資料的 Cursor 回撥給外部呼叫的 MatisseActivity,然後在 MatisseActivity 中進行資原始檔夾的展示。

AlbumsLoader

public class AlbumLoader extends CursorLoader {

    // content://media/external/file
    private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external");

    private static final String[] COLUMNS = {
            MediaStore.Files.FileColumns._ID,
            "bucket_id",
            "bucket_display_name",
            MediaStore.MediaColumns.DATA,
            COLUMN_COUNT};

    private static final String[] PROJECTION = {
            MediaStore.Files.FileColumns._ID,
            "bucket_id",
            "bucket_display_name",
            MediaStore.MediaColumns.DATA,
            "COUNT(*) AS " + COLUMN_COUNT};

    private static final String SELECTION =
            "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
                    + " OR "
                    + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)"
                    + " AND " + MediaStore.MediaColumns.SIZE + ">0"
                    + ") GROUP BY (bucket_id";

    private static final String[] SELECTION_ARGS = {
            String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
            String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
    };

    private static final String BUCKET_ORDER_BY = "datetaken DESC";

    private AlbumLoader(Context context, String selection, String[] selectionArgs) {
        super(context, QUERY_URI, PROJECTION, selection, selectionArgs, BUCKET_ORDER_BY);
    }

    public static CursorLoader newInstance(Context context) {
        return new AlbumLoader(context, SELECTION, SELECTION_ARGS);
    }

    @Override
    public Cursor loadInBackground() {
       return super.loadInBackground();
    }
}
複製程式碼

因為在 Matisse 只需要獲取到手機中的圖片和視訊資源,所以直接將必要的引數配置在 AlbumLoader 中,然後提供 newInstance() 方法給外部呼叫,獲取 AlbumLoader 的例項。

AlbumCollection

public class AlbumCollection implements LoaderManager.LoaderCallbacks<Cursor> {
    private static final int LOADER_ID = 1;
    private static final String STATE_CURRENT_SELECTION = "state_current_selection";
    private WeakReference<Context> mContext;
    private LoaderManager mLoaderManager;
    private AlbumCallbacks mCallbacks;
    private int mCurrentSelection;

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Context context = mContext.get();
        return AlbumLoader.newInstance(context);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        Context context = mContext.get();
        mCallbacks.onAlbumLoad(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        Context context = mContext.get();
        mCallbacks.onAlbumReset();
    }

    public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) {
        mContext = new WeakReference<Context>(activity);
        mLoaderManager = activity.getSupportLoaderManager();
        mCallbacks = callbacks;
    }

    public void loadAlbums() {
        mLoaderManager.initLoader(LOADER_ID, null, this);
    }

    public interface AlbumCallbacks {
        void onAlbumLoad(Cursor cursor);

        void onAlbumReset();
    }
}
複製程式碼

Matisse 為了降低程式碼的耦合度,將一些客戶端與 LoaderManager 互動的一些操作封裝在 AlbumCollection 中。在 onCreate() 中,傳入 Activity 用於獲取 LoaderManager,載入資源完成後,在 onLoadFinished() 方法中,通過 AlbumCallbacks 的 onAlbumLoad(Cursor cursor) 方法將「包含資料的 Cursor」返回給外部呼叫的 MatisseActivity.

AlbumsSpinner

AlbumsSpinner 將 MatisseActivity 左上角的一組控制元件進行了封裝,主要包括顯示資料夾名稱的 TextView 以及顯示資料夾列表的 ListPopupWindow,相當於把一個相對完整的功能抽取出來,把邏輯操作寫在裡面,在 Activity 中當做一種控制元件來用,有點類似自定義 View.

public class AlbumsSpinner {

    private static final int MAX_SHOWN_COUNT = 6;
    private CursorAdapter mAdapter;
    private TextView mSelected;
    private ListPopupWindow mListPopupWindow;
    private AdapterView.OnItemSelectedListener mOnItemSelectedListener;
複製程式碼

在 AlbumCollection 中返回的 Cursor,作為 AlbumsSpinner 的資料來源,然後通過 AlbumsAdapter 將資原始檔夾顯示出來。當選中資料夾的時候,將所點選的資料夾的 position 回撥給 MatisseActivity 中的 onItemSelected() 方法。

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        mAlbumCollection.setStateCurrentSelection(position);
        mAlbumsAdapter.getCursor().moveToPosition(position);
        // Album 是資料夾的實體類,封裝了資料夾的名字、封面圖片等資訊
        Album album = Album.valueOf(mAlbumsAdapter.getCursor());
        onAlbumSelected(album);
    }
複製程式碼

通過 AlbumsSpinner 回撥出來的 position 拿到對應的資料夾的資訊,然後將當前的介面進行重新整理,使當前介面顯示所選擇的資料夾的圖片。

    private void onAlbumSelected(Album album) {
        if (album.isAll() && album.isEmpty()) {
            mContainer.setVisibility(View.GONE);
            mEmptyView.setVisibility(View.VISIBLE);
        } else {
            mContainer.setVisibility(View.VISIBLE);
            mEmptyView.setVisibility(View.GONE);
            // MediaSelectionFragment 中包含一個 RecyclerView,用於顯示資料夾中所有的圖片
            Fragment fragment = MediaSelectionFragment.newInstance(album);
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
                    .commitAllowingStateLoss();
        }
    }
複製程式碼

四、主頁圖片牆的實現


主頁的照片牆可以說是 Matisse 中最有意思的模組了,而且學習價值也是最高的。圖片牆的資料來源同樣是通過 Loader 機制來進行載入的,實現思路也跟上一節講的「資原始檔夾的載入和展示」差不多,這裡簡單講一下就好。

主頁的照片牆會通過我們選擇不同的資原始檔夾而展示不同的圖片,所以我們在選擇資原始檔夾的時候,便將資原始檔夾的 id,傳給對應的 Loader,讓它對相應的資原始檔進行載入。

Matisse 把圖片和音訊的資訊封裝成了實體類,並實現了 Parcelable 介面,讓其序列化,通過外部傳入的 Cursor,拿到對應的 Uri、媒體型別、檔案大小,如果是視訊的話,就獲取視訊播放的時長。

/**
 * 圖片或音訊的實體類
 */
public class Item implements Parcelable {

    public final long id;
    public final String mimeType;
    public final Uri uri;
    public final long size;
    public final long duration; // only for video, in ms

    private Item(long id, String mimeType, long size, long duration) {
        this.id = id;
        this.mimeType = mimeType;
        Uri contentUri;
        if (isImage()) {
            contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        } else if (isVideo()) {
            contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
        } else {
            // 如果不是圖片也不是音訊就直接當檔案儲存
            contentUri = MediaStore.Files.getContentUri("external");
        }
        this.uri = ContentUris.withAppendedId(contentUri, id);
        this.size = size;
        this.duration = duration;
    }

    public static Item valueOf(Cursor cursor) {
        return new Item(cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)),
                cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)),
                cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns.SIZE)),
                cursor.getLong(cursor.getColumnIndex("duration")));
    }

}
複製程式碼

圖片牆是直接用一個 RecyclerView 進行展示的,Item 是一個繼承了 SquareFrameLayout(正方形的 FrameLayout) 的自定義控制元件,主要包含三個部分

  • 右上角的 CheckView

  • 顯示圖片的 ImageView

  • 顯示視訊時長的 TextView

知乎 Matisse 原始碼解析,帶你探究高效圖片選擇庫的祕密

CheckView 就是右上角那個白色的小圓圈,可以理解為是一個自定義的 CheckBox,或者說是一個比較好看的核取方塊。我在前文中說 Matisse 的學習價值比較高,一個很重要的原因就是 Matisse 中有很多的自定義 View,能夠讓我們學習圖片選擇庫的同時,學習自定義 View 的一些好的思路和做法。

那我們就來看看 CheckView 究竟是怎樣實現的。

首先,CheckView 重寫了 onMeasure() 方法,將寬和高都定為 48,而且為了螢幕適配性,將 48dp 乘以 density,將 dp 單位轉換為畫素單位。

    private static final int SIZE = 48; // dp

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int sizeSpec = MeasureSpec.makeMeasureSpec((int) (SIZE * mDensity), MeasureSpec.EXACTLY);
        super.onMeasure(sizeSpec, sizeSpec);
    }
複製程式碼

接下來就看重頭戲的 onDraw() 方法了

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 1、畫出外在和內在的陰影
        initShadowPaint();
        canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                (STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint);

        // 2、畫出白色的空心圓
        canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                STROKE_RADIUS * mDensity, mStrokePaint);

        // 3、畫出圓裡面的內容
        if (mCountable) {
                initBackgroundPaint();
                canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                        BG_RADIUS * mDensity, mBackgroundPaint);
                initTextPaint();
                String text = String.valueOf(mCheckedNum);
                int baseX = (int) (canvas.getWidth() - mTextPaint.measureText(text)) / 2;
                int baseY = (int) (canvas.getHeight() - mTextPaint.descent() - mTextPaint.ascent()) / 2;
                canvas.drawText(text, baseX, baseY, mTextPaint);
        } else {
            if (mChecked) {
                initBackgroundPaint();
                canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                        BG_RADIUS * mDensity, mBackgroundPaint);

                mCheckDrawable.setBounds(getCheckRect());
                mCheckDrawable.draw(canvas);
            }
        }
    }
複製程式碼

onDraw() 方法主要分為三個部分

  • 畫出空心圓內外的陰影 不得不說,Matisse 的細節處理真的做得特別好,為了圖片選擇庫看起來更加美觀,在空心圓的內外增加了一層輻射漸變的陰影

  • 畫出白色的空心圓 這個真沒什麼好講的

  • 描繪出裡面的內容 通過我們外部配置的 mCountable 引數,來決定 CheckView 的顯示方式,如果 mCountable 的值為 true 的話,便在內部描繪一層主題顏色的背景,以及代表所選擇圖片數量的數字,如果 mCount 的值為 false 的話,那麼便描繪背景以及填入一個白色的 ✓

知乎 Matisse 原始碼解析,帶你探究高效圖片選擇庫的祕密

這部分主要是有關 Paint 的知識,以及數學方面的計算,如果對於 Paint 不是很熟悉的讀者,可以看看這篇文章 HenCoder Android 開發進階: 自定義 View 1-2 Paint 詳解,順便安利一波,凱哥的 HenCoder 教程,寫得是真的好,強烈建議去好好看看。

看完了 CheckView 的實現邏輯,我們接著來看看圖片牆的 Item 佈局「MediaGrid」的實現邏輯,MediaGrid 是一個繼承了 SquareFrameLayout(正方形的 FrameLayout)的自定義控制元件,可以理解為是一個擴充了複選功能(CheckView)和顯示視訊時長(TextView)功能的 ImageView.

我們從 MediaGrid 在 Adapter 中的使用入手,進一步看看 MediaGrid 的程式碼實現

mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo(
        getImageResize(mediaViewHolder.mMediaGrid.getContext()),
        mPlaceholder,
        mSelectionSpec.countable,
        holder
        ));
       mediaViewHolder.mMediaGrid.bindMedia(item);
複製程式碼

可以看到 MediaGrid 的使用主要分兩步

  • 初始化圖片的公有屬性(MediaGrid.preBindMedia(new MediaGrid.PreBindInfo()))

  • 將圖片對應的資訊進行繫結(MediaGrid.bindMedia(Item) )

PreBindInfo 是 MediaGrid 的一個靜態內部類,封裝了一些圖片的一些公用的屬性

    public static class PreBindInfo {
        int mResize; // 圖片的大小
        Drawable mPlaceholder; // ImageView 的佔位符
        boolean mCheckViewCountable; // √ 的圖示
        RecyclerView.ViewHolder mViewHolder; // 對應的 ViewHolder

        public PreBindInfo(int resize, Drawable placeholder, boolean checkViewCountable,
                           RecyclerView.ViewHolder viewHolder) {
            mResize = resize;
            mPlaceholder = placeholder;
            mCheckViewCountable = checkViewCountable;
            mViewHolder = viewHolder;
        }
    }
複製程式碼

Item 在上文已經介紹了,是圖片或音訊的實體類。第二步便是將一個包含圖片資訊的 Item 傳給 MediaGrid,然後進行相應資訊的設定。

MediaGrid 中自定義了回撥的介面

    public interface OnMediaGridClickListener {

        void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder);

        void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
    }
複製程式碼

當使用者點選圖片的時候,將點選事件回撥到 Adapter,再回撥到 MediaSelectionFragment,再回撥到 MatisseActivity,然後開啟圖片的大圖預覽介面,你沒看錯,真的回撥了三層,我也是一臉矇蔽。一遇到這種情況,我就覺得 EventBus 還是挺好用的。

當點選右上角的 CheckView 的時候,便將點選事件回撥到 Adapter 中,然後根據 countable 的值,來進行相應的設定(顯示數字或者顯示 √),然後再將對應的 Item 資訊儲存在 SelectedItemCollection(Item 的容器) 中。

五、預覽介面的實現


開啟預覽介面有兩種方法

  • 點選首頁的某個圖片

  • 選擇圖片之後,點選首頁左下角的預覽(Preview)按鈕

這兩種方法開啟的介面看起來似乎是一樣的,但實際上他們兩個的實現邏輯很不一樣,因此用了兩個不同的 Activity.

點選首頁的某張圖片之後,會跳轉到一個包含 ViewPager 的介面,因為對應資原始檔夾中可能會有很多的圖片,這時候如果將包含該資料夾中所有的圖片直接傳給預覽介面的 Activity,這是非常不實際的。比較好的實現方式便是將「包含對應資料夾的資訊的 Album」傳給介面,然後再用 Loader 機制進行載入。

選擇首頁圖片後,點選左下角的預覽按鈕,跳轉到預覽介面,因為我們選擇的圖片一般都比較少,所以這時候直接將「包含所有選擇圖片資訊的 List」傳給預覽介面就行了。

雖然,兩個 Activity 的實現邏輯不太一樣,但由於都是預覽介面,所以有很多相同的地方。因此,Matisse 便實現了一個 BasePreviewActivity,減少程式碼的冗餘程度。

知乎 Matisse 原始碼解析,帶你探究高效圖片選擇庫的祕密

BasePreviewActivity 的佈局主要由三部分組成

  • 右上角的 CheckView

  • 自定義的 ViewPager

  • 底部欄(包括預覽(Preview)和使用按鈕(Apply))

主要的程式碼邏輯也基本上是圍繞這三個部分進行展開的。

當點選 CheckView 的時候,根據該圖片是否已經被選擇以及圖片的型別,對 CheckView 進行相應的設定以及更新底部欄。

        mCheckView.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Item item = mAdapter.getMediaItem(mPager.getCurrentItem());
                // 如果當前的圖片已經被選擇
                if (mSelectedCollection.isSelected(item)) {
                    mSelectedCollection.remove(item);
                    if (mSpec.countable) {
                        mCheckView.setCheckedNum(CheckView.UNCHECKED);
                    } else {
                        mCheckView.setChecked(false);
                    }
                } else {
                    // 判斷能否新增該圖片
                    if (assertAddSelection(item)) {
                        mSelectedCollection.add(item);
                        if (mSpec.countable) {
                            mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
                        } else {
                            mCheckView.setChecked(true);
                        }
                    }
                }
                // 更新底部欄
                updateApplyButton();
            }
        });
複製程式碼

當使用者對 ViewPager 進行左右滑動的時候,根據當前的 position 拿到對應的 Item 資訊,然後對 CheckView 進行相應的設定以及切換圖片。

    @Override
    public void onPageSelected(int position) {
        PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter();
        if (mPreviousPos != -1 && mPreviousPos != position) {
            ((PreviewItemFragment) adapter.instantiateItem(mPager, mPreviousPos)).resetView();
            // 獲取對應的 Item 
            Item item = adapter.getMediaItem(position);
            if (mSpec.countable) {
                int checkedNum = mSelectedCollection.checkedNumOf(item);
                mCheckView.setCheckedNum(checkedNum);
                if (checkedNum > 0) {
                    mCheckView.setEnabled(true);
                } else {
                    mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
                }
            } else {
                boolean checked = mSelectedCollection.isSelected(item);
                mCheckView.setChecked(checked);
                if (checked) {
                    mCheckView.setEnabled(true);
                } else {
                    mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
                }
            }
            updateSize(item);
        }
        mPreviousPos = position;
    }
複製程式碼

以上便是 BasePreviewActivity 的實現邏輯,至於它的子類 AlbumPreviewActivity(包含所有圖片的預覽介面)和 SelectedPreviewActivity(所選擇圖片的預覽介面)就很簡單了,大家自己看下原始碼就能明白了。

總結


Matisse 應該是我第一個完整啃下來的開源專案了,從一開始被 MatisseActivity 實現的一堆介面嚇蒙。到後來的一步一步抽絲剝繭,從各個功能點入手,慢慢的理解了其中的程式碼設計以及實現思路,看完整個專案之後,對於 Matisse 的架構設計和程式碼質量深感佩服。

在閱讀比較大型的開源專案的時候,由於這個專案你是完全陌生的,而且程式碼量通常都比較大,這時如果在閱讀原始碼的時候,深陷程式碼細節的話,很容易讓我們陷入到思維黑洞裡面。如果我們從功能點入手,一步一步分析功能點是如何實現的,分析主體的邏輯,這樣閱讀起來就會更加輕鬆,也更加有成效。


猜你喜歡

相關文章