本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
目錄
- 基本介紹
- 整體的設(shè)計(jì)和實(shí)現(xiàn)流程
- 資源文件夾的加載和展示
- 主頁圖片墻的實(shí)現(xiàn)
- 預(yù)覽界面的實(shí)現(xiàn)
- 總結(jié)
一、基本介紹
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 的工作流程,接下來詳細(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ì)更加輕松,也更加有成效。