本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
目錄
- 基本介紹
- 整體的設計和實現流程
- 資原始檔夾的載入和展示
- 主頁圖片牆的實現
- 預覽介面的實現
- 總結
一、基本介紹
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
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 機制,讓你的資料載入更加高效
先附上此項操作的流程圖:
繼承了 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
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 的話,那麼便描繪背景以及填入一個白色的 ✓
這部分主要是有關 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,減少程式碼的冗餘程度。
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 的架構設計和程式碼質量深感佩服。
在閱讀比較大型的開源專案的時候,由於這個專案你是完全陌生的,而且程式碼量通常都比較大,這時如果在閱讀原始碼的時候,深陷程式碼細節的話,很容易讓我們陷入到思維黑洞裡面。如果我們從功能點入手,一步一步分析功能點是如何實現的,分析主體的邏輯,這樣閱讀起來就會更加輕鬆,也更加有成效。