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

本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布

目錄

  • 基本介紹
  • 整體的設(shè)計(jì)和實(shí)現(xiàn)流程
  • 資源文件夾的加載和展示
  • 主頁圖片墻的實(shí)現(xiàn)
  • 預(yù)覽界面的實(shí)現(xiàn)
  • 總結(jié)

一、基本介紹


Matisse 是「知乎」開源的一款十分精美的本地圖像和視頻選擇庫。

Matisse

Matisse 的代碼寫的相當(dāng)?shù)暮?jiǎn)潔、規(guī)范,很有學(xué)習(xí)的價(jià)值。

講一下 Matisse 的一些優(yōu)點(diǎn):

  • 在 Activity 或 Fragment 都可以輕松的調(diào)用

  • 支持各種格式的圖片和視頻加載

  • 支持不同的樣式,包括兩種內(nèi)置主題和自定義主題

  • 可以自定義文件的過濾規(guī)則

可以看到 Matisse 的可拓展性是非常強(qiáng)的,不僅可以自定義我們需要的主題,而且還可以按照需求來過濾出我們想要的文件,除此之外,Matisse 采用了建造者模式,使得我們可以通過鏈?zhǔn)秸{(diào)用的方式,配置各種各樣的屬性,使我們的圖片選擇更加靈活。

二、整體的設(shè)計(jì)和實(shí)現(xiàn)流程


在介紹 Matisse 的工作流程之前,我們先來看看幾個(gè)比較重要的類,有助于我們后面的理解

類名 功能
Matisse 通過外部傳入的 Activity 或 Fragment,以弱引用的形式進(jìn)行保存,同時(shí)通過 from() 方法返回 SelectionCreator 進(jìn)行各個(gè)參數(shù)的配置
SelectionCreator 通過建造者模式,鏈?zhǔn)脚渲梦覀冃枰母鞣N屬性
MatisseActivity Matisse 首頁的 Activity,將圖片和視頻進(jìn)行展示

我們先從 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、配置各種各樣的參數(shù)
        .forResult(REQUEST_CODE_CHOOSE); // 3、打開 MatisseActivity

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

  • 將外部傳入的 Activity 以弱引用的形式進(jìn)行保存,然后調(diào)用 choose() 獲取 SelectionCreator

  • 通過鏈?zhǔn)秸{(diào)用的方式,配置 SelectionCreator 的各種屬性,如可選擇的數(shù)量、縮略圖的大小、加載圖片的引擎等

  • 使用從第一步中傳入的 Activity 調(diào)用 startActivityForResult(),并從外部傳入請(qǐng)求碼,以便到時(shí)候返回所選擇圖片的 List<Uri>

具體的流程圖如下:


Matisse 流程圖

以上便是 Matisse 的工作流程,接下來詳細(xì)的分析下相關(guān)的類。有一點(diǎn)要先說明一下,我下面貼出的所有類中的源碼并不是完整的代碼,而是將源碼中與性能、兼容性、擴(kuò)展性有關(guān)的代碼剔除后的「核心代碼」。

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);
    }

}

這個(gè)類的代碼還是很簡(jiǎn)單的,將外部傳入的 Activity 或 Fragment,用弱引用的形式保存,防止內(nèi)存泄露。然后通過 choose() 方法返回 SelectionCreator 用于之后參數(shù)的配置。等到圖片選擇完成后,我們可以在 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;
    }
    // 其余方法都類似上面這兩個(gè),這里面就不貼出來了

    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 內(nèi)部保存了 Matisse 的實(shí)例,用于獲取外部調(diào)用的 Activity 或 Fragment,以及一個(gè) SelectionSpec 類的實(shí)例,這個(gè)類封裝了圖片加載類中常見的參數(shù),使得 SelectionCreator 的代碼更加簡(jiǎn)潔。SelectionCreator 內(nèi)部使用了建造者模式,讓我們能夠進(jìn)行鏈?zhǔn)秸{(diào)用,配置各種各樣的屬性。最后 forResult() 里面其實(shí)就是跳轉(zhuǎn)到 MatisseActivity,然后通過外部傳入的 requestCode 將用戶選擇的媒體 Uri 列表返回給相應(yīng)的 Activity 或 Fragment.

三、資源文件夾的加載和展示


Matisse 中所展示的資源都是用 Loader 機(jī)制進(jìn)行加載的,Loader 機(jī)制是 Android 3.0 之后官方推薦的加載 ContentProvider 中資源的最佳方式,不僅能極大地提高我們資源加載的速度,而且還能讓我們的代碼變得更加的簡(jiǎn)潔。對(duì)于 Loader 機(jī)制不熟悉的同學(xué),可以先看下這篇文章 Android Loader 機(jī)制,讓你的數(shù)據(jù)加載更加高效

先附上此項(xiàng)操作的流程圖:


繼承了 Cursor 的 AlbumLoader,作為資源的加載器,通過配置與資源相關(guān)的一些參數(shù),從而加載資源。AlbumCollection 實(shí)現(xiàn)了 LoaderManager.LoaderCallbacks 接口,將 AlbumLoader 作為加載器,其內(nèi)部定義了 AlbumCallbacks 接口,在加載資源完成后,將包含數(shù)據(jù)的 Cursor 回調(diào)給外部調(diào)用的 MatisseActivity,然后在 MatisseActivity 中進(jìn)行資源文件夾的展示。

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();
    }
}

因?yàn)樵?Matisse 只需要獲取到手機(jī)中的圖片和視頻資源,所以直接將必要的參數(shù)配置在 AlbumLoader 中,然后提供 newInstance() 方法給外部調(diào)用,獲取 AlbumLoader 的實(shí)例。

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) 方法將「包含數(shù)據(jù)的 Cursor」返回給外部調(diào)用的 MatisseActivity.

AlbumsSpinner

AlbumsSpinner 將 MatisseActivity 左上角的一組控件進(jìn)行了封裝,主要包括顯示文件夾名稱的 TextView 以及顯示文件夾列表的 ListPopupWindow,相當(dāng)于把一個(gè)相對(duì)完整的功能抽取出來,把邏輯操作寫在里面,在 Activity 中當(dāng)做一種控件來用,有點(diǎn)類似自定義 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 的數(shù)據(jù)源,然后通過 AlbumsAdapter 將資源文件夾顯示出來。當(dāng)選中文件夾的時(shí)候,將所點(diǎn)擊的文件夾的 position 回調(diào)給 MatisseActivity 中的 onItemSelected() 方法。

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        mAlbumCollection.setStateCurrentSelection(position);
        mAlbumsAdapter.getCursor().moveToPosition(position);
        // Album 是文件夾的實(shí)體類,封裝了文件夾的名字、封面圖片等信息
        Album album = Album.valueOf(mAlbumsAdapter.getCursor());
        onAlbumSelected(album);
    }

通過 AlbumsSpinner 回調(diào)出來的 position 拿到對(duì)應(yīng)的文件夾的信息,然后將當(dāng)前的界面進(jìn)行刷新,使當(dāng)前界面顯示所選擇的文件夾的圖片。

    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 中包含一個(gè) RecyclerView,用于顯示文件夾中所有的圖片
            Fragment fragment = MediaSelectionFragment.newInstance(album);
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
                    .commitAllowingStateLoss();
        }
    }

四、主頁圖片墻的實(shí)現(xiàn)


主頁的照片墻可以說是 Matisse 中最有意思的模塊了,而且學(xué)習(xí)價(jià)值也是最高的。圖片墻的數(shù)據(jù)源同樣是通過 Loader 機(jī)制來進(jìn)行加載的,實(shí)現(xiàn)思路也跟上一節(jié)講的「資源文件夾的加載和展示」差不多,這里簡(jiǎn)單講一下就好。

主頁的照片墻會(huì)通過我們選擇不同的資源文件夾而展示不同的圖片,所以我們?cè)谶x擇資源文件夾的時(shí)候,便將資源文件夾的 id,傳給對(duì)應(yīng)的 Loader,讓它對(duì)相應(yīng)的資源文件進(jìn)行加載。

Matisse 把圖片和音頻的信息封裝成了實(shí)體類,并實(shí)現(xiàn)了 Parcelable 接口,讓其序列化,通過外部傳入的 Cursor,拿到對(duì)應(yīng)的 Uri、媒體類型、文件大小,如果是視頻的話,就獲取視頻播放的時(shí)長(zhǎng)。

/**
 * 圖片或音頻的實(shí)體類
 */
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 {
            // 如果不是圖片也不是音頻就直接當(dāng)文件存儲(chǔ)
            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")));
    }

}

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

  • 右上角的 CheckView

  • 顯示圖片的 ImageView

  • 顯示視頻時(shí)長(zhǎng)的 TextView

CheckView 就是右上角那個(gè)白色的小圓圈,可以理解為是一個(gè)自定義的 CheckBox,或者說是一個(gè)比較好看的復(fù)選框。我在前文中說 Matisse 的學(xué)習(xí)價(jià)值比較高,一個(gè)很重要的原因就是 Matisse 中有很多的自定義 View,能夠讓我們學(xué)習(xí)圖片選擇庫的同時(shí),學(xué)習(xí)自定義 View 的一些好的思路和做法。

那我們就來看看 CheckView 究竟是怎樣實(shí)現(xiàn)的。

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

    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、畫出外在和內(nèi)在的陰影
        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、畫出圓里面的內(nèi)容
        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() 方法主要分為三個(gè)部分

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

  • 畫出白色的空心圓
    這個(gè)真沒什么好講的

  • 描繪出里面的內(nèi)容
    通過我們外部配置的 mCountable 參數(shù),來決定 CheckView 的顯示方式,如果 mCountable 的值為 true 的話,便在內(nèi)部描繪一層主題顏色的背景,以及代表所選擇圖片數(shù)量的數(shù)字,如果 mCount 的值為 false 的話,那么便描繪背景以及填入一個(gè)白色的 ?

這部分主要是有關(guān) Paint 的知識(shí),以及數(shù)學(xué)方面的計(jì)算,如果對(duì)于 Paint 不是很熟悉的讀者,可以看看這篇文章 HenCoder Android 開發(fā)進(jìn)階: 自定義 View 1-2 Paint 詳解,順便安利一波,凱哥的 HenCoder 教程,寫得是真的好,強(qiáng)烈建議去好好看看。

看完了 CheckView 的實(shí)現(xiàn)邏輯,我們接著來看看圖片墻的 Item 布局「MediaGrid」的實(shí)現(xiàn)邏輯,MediaGrid 是一個(gè)繼承了 SquareFrameLayout(正方形的 FrameLayout)的自定義控件,可以理解為是一個(gè)拓展了復(fù)選功能(CheckView)和顯示視頻時(shí)長(zhǎng)(TextView)功能的 ImageView.

我們從 MediaGrid 在 Adapter 中的使用入手,進(jìn)一步看看 MediaGrid 的代碼實(shí)現(xiàn)

mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo(
        getImageResize(mediaViewHolder.mMediaGrid.getContext()),
        mPlaceholder,
        mSelectionSpec.countable,
        holder
        ));
       mediaViewHolder.mMediaGrid.bindMedia(item);

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

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

  • 將圖片對(duì)應(yīng)的信息進(jìn)行綁定(MediaGrid.bindMedia(Item) )

PreBindInfo 是 MediaGrid 的一個(gè)靜態(tài)內(nèi)部類,封裝了一些圖片的一些公用的屬性

    public static class PreBindInfo {
        int mResize; // 圖片的大小
        Drawable mPlaceholder; // ImageView 的占位符
        boolean mCheckViewCountable; // √ 的圖標(biāo)
        RecyclerView.ViewHolder mViewHolder; // 對(duì)應(yīng)的 ViewHolder

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

Item 在上文已經(jīng)介紹了,是圖片或音頻的實(shí)體類。第二步便是將一個(gè)包含圖片信息的 Item 傳給 MediaGrid,然后進(jìn)行相應(yīng)信息的設(shè)置。

MediaGrid 中自定義了回調(diào)的接口

    public interface OnMediaGridClickListener {

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

        void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
    }

當(dāng)用戶點(diǎn)擊圖片的時(shí)候,將點(diǎn)擊事件回調(diào)到 Adapter,再回調(diào)到 MediaSelectionFragment,再回調(diào)到 MatisseActivity,然后打開圖片的大圖預(yù)覽界面,你沒看錯(cuò),真的回調(diào)了三層,我也是一臉蒙蔽。一遇到這種情況,我就覺得 EventBus 還是挺好用的。

當(dāng)點(diǎn)擊右上角的 CheckView 的時(shí)候,便將點(diǎn)擊事件回調(diào)到 Adapter 中,然后根據(jù) countable 的值,來進(jìn)行相應(yīng)的設(shè)置(顯示數(shù)字或者顯示 √),然后再將對(duì)應(yīng)的 Item 信息保存在 SelectedItemCollection(Item 的容器) 中。

五、預(yù)覽界面的實(shí)現(xiàn)


打開預(yù)覽界面有兩種方法

  • 點(diǎn)擊首頁的某個(gè)圖片

  • 選擇圖片之后,點(diǎn)擊首頁左下角的預(yù)覽(Preview)按鈕

這兩種方法打開的界面看起來似乎是一樣的,但實(shí)際上他們兩個(gè)的實(shí)現(xiàn)邏輯很不一樣,因此用了兩個(gè)不同的 Activity.

點(diǎn)擊首頁的某張圖片之后,會(huì)跳轉(zhuǎn)到一個(gè)包含 ViewPager 的界面,因?yàn)閷?duì)應(yīng)資源文件夾中可能會(huì)有很多的圖片,這時(shí)候如果將包含該文件夾中所有的圖片直接傳給預(yù)覽界面的 Activity,這是非常不實(shí)際的。比較好的實(shí)現(xiàn)方式便是將「包含對(duì)應(yīng)文件夾的信息的 Album」傳給界面,然后再用 Loader 機(jī)制進(jìn)行加載。

選擇首頁圖片后,點(diǎn)擊左下角的預(yù)覽按鈕,跳轉(zhuǎn)到預(yù)覽界面,因?yàn)槲覀冞x擇的圖片一般都比較少,所以這時(shí)候直接將「包含所有選擇圖片信息的 List<Item>」傳給預(yù)覽界面就行了。

雖然,兩個(gè) Activity 的實(shí)現(xiàn)邏輯不太一樣,但由于都是預(yù)覽界面,所以有很多相同的地方。因此,Matisse 便實(shí)現(xiàn)了一個(gè) BasePreviewActivity,減少代碼的冗余程度。

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

  • 右上角的 CheckView

  • 自定義的 ViewPager

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

主要的代碼邏輯也基本上是圍繞這三個(gè)部分進(jìn)行展開的。

當(dāng)點(diǎn)擊 CheckView 的時(shí)候,根據(jù)該圖片是否已經(jīng)被選擇以及圖片的類型,對(duì) CheckView 進(jìn)行相應(yīng)的設(shè)置以及更新底部欄。

        mCheckView.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Item item = mAdapter.getMediaItem(mPager.getCurrentItem());
                // 如果當(dāng)前的圖片已經(jīng)被選擇
                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();
            }
        });

當(dāng)用戶對(duì) ViewPager 進(jìn)行左右滑動(dòng)的時(shí)候,根據(jù)當(dāng)前的 position 拿到對(duì)應(yīng)的 Item 信息,然后對(duì) CheckView 進(jìn)行相應(yīng)的設(shè)置以及切換圖片。

    @Override
    public void onPageSelected(int position) {
        PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter();
        if (mPreviousPos != -1 && mPreviousPos != position) {
            ((PreviewItemFragment) adapter.instantiateItem(mPager, mPreviousPos)).resetView();
            // 獲取對(duì)應(yīng)的 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 的實(shí)現(xiàn)邏輯,至于它的子類 AlbumPreviewActivity(包含所有圖片的預(yù)覽界面)和 SelectedPreviewActivity(所選擇圖片的預(yù)覽界面)就很簡(jiǎn)單了,大家自己看下源碼就能明白了。

總結(jié)


Matisse 應(yīng)該是我第一個(gè)完整啃下來的開源項(xiàng)目了,從一開始被 MatisseActivity 實(shí)現(xiàn)的一堆接口嚇蒙。到后來的一步一步抽絲剝繭,從各個(gè)功能點(diǎn)入手,慢慢的理解了其中的代碼設(shè)計(jì)以及實(shí)現(xiàn)思路,看完整個(gè)項(xiàng)目之后,對(duì)于 Matisse 的架構(gòu)設(shè)計(jì)和代碼質(zhì)量深感佩服。

在閱讀比較大型的開源項(xiàng)目的時(shí)候,由于這個(gè)項(xiàng)目你是完全陌生的,而且代碼量通常都比較大,這時(shí)如果在閱讀源碼的時(shí)候,深陷代碼細(xì)節(jié)的話,很容易讓我們陷入到思維黑洞里面。如果我們從功能點(diǎn)入手,一步一步分析功能點(diǎn)是如何實(shí)現(xiàn)的,分析主體的邏輯,這樣閱讀起來就會(huì)更加輕松,也更加有成效。


猜你喜歡

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,034評(píng)論 25 709
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,329評(píng)論 0 17
  • 原文鏈接:https://github.com/opendigg/awesome-github-android-u...
    IM魂影閱讀 33,165評(píng)論 6 472
  • Singapore,一個(gè)東南亞小島國(guó),南中國(guó),700年的短暫歷史,人種雜,有什么好去的? 有時(shí)候旅行就是不看地方,...
    莉歷閱讀 761評(píng)論 0 6
  • 我知道我的目的不單純,我總是想的太多,有太多雜念,所以拎不清。 不能再進(jìn)一步,每天都是折磨,都在耗著,明顯我是耗不...
    F從你的全世界路過閱讀 241評(píng)論 1 0

友情鏈接更多精彩內(nèi)容