前言
作為一名一年多的 Android 開發(fā)者,面對源碼閱讀一直是躑躅不前。毫不夸張的說內(nèi)心一直是拒絕的,擔心自己沒能力去讀懂那該死的源碼。在和朋友的交流中,我向他請教了很多關于源碼閱讀的技巧。提及最多的就是 搞清楚每個類的職責,類與類之間的設計,最終明白源碼的執(zhí)行過程。
重要的是通過閱讀源碼,針對不會的知識點進行查漏補缺,這個過程就像是打怪升級,逐漸的提高自己的技術。在朋友不斷的鼓勵下,終于邁出了源碼閱讀第一步,希望在今后的工作學習中,能把這種學習方式堅持下去。好了,現(xiàn)在進入今天的主題 Matisse。
簡介
Matisse 是知乎開源的一款,針對 Android 本地圖像和視頻的選擇器。它主要有以下優(yōu)點:
方便在 Activity 和 Fragment 中使用
支持各種格式的圖片和視頻
應用不同的主題,包括內(nèi)置的兩種主題和自定義主題‘
使用不同的圖片加載器
自定義文件的過濾規(guī)則
-
方便拓展
針對 方便拓展,基佬 的 RxImagePicker 完美的詮釋了這一優(yōu)點。在 RxImagePicker 中,Matisse 被抽出來放入了 RxImagePicker_Support,成為了 UI 層的基礎組件。
Matisse 的基本使用
參考 Matisse 的 SampleActivity 中的例子,我們來一步步探索 Matisse 的實現(xiàn)過程。
Matisse.from(SampleActivity.this)
.choose(MimeType.ofImage())
.theme(R.style.Matisse_Dracula)
.countable(true)
.addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
.maxSelectable(9)
.originalEnable(true)
.maxOriginalSize(10)
.imageEngine(new Glide4Engine())
.forResult(REQUEST_CODE_CHOOSE);
調(diào)用 from() 獲取 Matisse 的一個實例。
調(diào)用 choose() 方法獲取 SelectionCreator,進行一些列的參數(shù)配置。如主題、最大選擇數(shù)量。參數(shù)的保存是由全局單例 SelectionSpec 完成的。
通過 forResult() 去打開 MatisseActivity。
接下來我們看下每個類的設計
-
Matisse
public final class Matisse { private final WeakReference<Activity> mContext; private final WeakReference<Fragment> mFragment; private Matisse(Activity activity) { this(activity, null); } private Matisse(Fragment fragment) { this(fragment.getActivity(), fragment); } private Matisse(Activity activity, Fragment fragment) { mContext = new WeakReference<>(activity); mFragment = new WeakReference<>(fragment); } /** * 獲取 Matisse 對象 */ public static Matisse from(Activity activity) { return new Matisse(activity); } public static Matisse from(Fragment fragment) { return new Matisse(fragment); } /** * 獲得選擇的數(shù)據(jù) */ public static List<Uri> obtainResult(Intent data) { return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION); } public static List<String> obtainPathResult(Intent data) { return data.getStringArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION_PATH); } public SelectionCreator choose(Set<MimeType> mimeTypes) { return this.choose(mimeTypes, true); } /** * 獲取 SelectionCreator 對象 */ public SelectionCreator choose(Set<MimeType> mimeTypes, boolean mediaTypeExclusive) { return new SelectionCreator(this, mimeTypes, mediaTypeExclusive); } @Nullable Activity getActivity() { return mContext.get(); } @Nullable Fragment getFragment() { return mFragment != null ? mFragment.get() : null; } }可以看到,這個類的代碼還是很少的。這個類的主要職責是:
獲取上下文對象,以弱引用的方式進行保存。
獲取 SelectionCreator 對象,進行參數(shù)配置。
獲得用戶選擇的數(shù)據(jù)。
-
SelectionCreator,這是一個配置類,所以省略大部分相同的代碼:
public final class SelectionCreator { private final Matisse mMatisse; private final SelectionSpec mSelectionSpec; SelectionCreator(Matisse matisse, @NonNull Set<MimeType> mimeTypes, boolean mediaTypeExclusive) { mMatisse = matisse; mSelectionSpec = SelectionSpec.getCleanInstance(); mSelectionSpec.mimeTypeSet = mimeTypes; mSelectionSpec.mediaTypeExclusive = mediaTypeExclusive; mSelectionSpec.orientation = SCREEN_ORIENTATION_UNSPECIFIED; } /** * 是否顯示單媒體類型 */ public SelectionCreator showSingleMediaType(boolean showSingleMediaType) { mSelectionSpec.showSingleMediaType = showSingleMediaType; return this; } ...... 省略部分代碼 public void forResult(int requestCode) { Activity activity = mMatisse.getActivity(); if (activity == null) { return; } Intent intent = new Intent(activity, MatisseActivity.class); Fragment fragment = mMatisse.getFragment(); if (fragment != null) { fragment.startActivityForResult(intent, requestCode); } else { activity.startActivityForResult(intent, requestCode); } } }這個類的主要職責是:
通過鏈式調(diào)用,進行參數(shù)配置。
開啟 MatisseActivity。
-
SelectionSpec
該類主要定義了一系列可配置的參數(shù)
public final class SelectionSpec { public List<Item> selectItems; public Set<MimeType> mimeTypeSet; public boolean mediaTypeExclusive; public boolean showSingleMediaType; @StyleRes public int themeId; public int orientation; public boolean countable; public int maxSelectable; public int maxImageSelectable; public int maxVideoSelectable; public List<Filter> filters; public boolean capture; public CaptureStrategy captureStrategy; public int spanCount; public int gridExpectedSize; public float thumbnailScale; public ImageEngine imageEngine; public boolean hasInited; public OnSelectedListener onSelectedListener; public boolean originalable; public int originalMaxSize; public OnCheckedListener onCheckedListener; private SelectionSpec() { } public static SelectionSpec getInstance() { return InstanceHolder.INSTANCE; } public static SelectionSpec getCleanInstance() { SelectionSpec selectionSpec = getInstance(); selectionSpec.reset(); return selectionSpec; } ...... 省略部分代碼 private static final class InstanceHolder { private static final SelectionSpec INSTANCE = new SelectionSpec(); } }
到此,我們應進入 MatisseActivity 中一看究竟,讓我們思考下展示的圖片是如何獲取的,帶著這樣的疑問向 MatisseActivity 邁進吧。
MatisseActivity
public class MatisseActivity extends AppCompatActivity implements
AlbumCollection.AlbumCallbacks,
AdapterView.OnItemSelectedListener,
MediaSelectionFragment.SelectionProvider,
View.OnClickListener,
AlbumMediaAdapter.CheckStateListener,
AlbumMediaAdapter.OnMediaClickListener,
AlbumMediaAdapter.OnPhotoCapture {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...... 省略部分代碼
// 拍照功能
if (mSpec.capture) {
mMediaStoreCompat = new MediaStoreCompat(this);
if (mSpec.captureStrategy == null)
throw new RuntimeException("Don't forget to set CaptureStrategy.");
mMediaStoreCompat.setCaptureStrategy(mSpec.captureStrategy);
}
// 資源文件夾的適配器
mAlbumsAdapter = new AlbumsAdapter(this, null, false);
// 資源文件夾的 Spinner
mAlbumsSpinner = new AlbumsSpinner(this);
mAlbumsSpinner.setOnItemSelectedListener(this);
mAlbumsSpinner.setSelectedTextView((TextView) findViewById(R.id.selected_album));
mAlbumsSpinner.setPopupAnchorView(findViewById(R.id.toolbar));
mAlbumsSpinner.setAdapter(mAlbumsAdapter);
// 資源文件夾的數(shù)據(jù)源
mAlbumCollection.onCreate(this, this);
mAlbumCollection.onRestoreInstanceState(savedInstanceState);
mAlbumCollection.loadAlbums(); // 加載資源文件夾
updateBottomToolbar();
}
}
Matisse 進行數(shù)據(jù)的獲取是通過 Loader 機制完成的。Loader 是官方在 3.0 之后推薦的加載 ContentProvider 資源的最佳使用方式。方便我們在 Activity 和 Fragment 異步加載數(shù)據(jù),在這里安利一篇關于 Loader 的文章。
-
AlbumSpinner
AlbumSpinner 是對資源文件夾的 ListPopubWindow 和顯示文件夾名稱的 TextView 進行了一層封裝。最終把點擊事件的處理,通過接口的方式暴露給 MatisseActivity,展示該資源文件夾下所有的圖片。
/** * 每次點擊 ListPopuWindow 都會觸發(fā) * * @param parent * @param view * @param position * @param id */ @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { mAlbumCollection.setStateCurrentSelection(position); mAlbumsAdapter.getCursor().moveToPosition(position); Album album = Album.valueOf(mAlbumsAdapter.getCursor()); if (album.isAll() && SelectionSpec.getInstance().capture) { album.addCaptureCount(); } onAlbumSelected(album); } -
AlbumAdapter
繼承自 CursorAdapter ,為 ListPopubWindow 提供數(shù)據(jù)。
-
AlbumCollection
調(diào)用 loadAlbums(),去查詢資源文件夾的數(shù)據(jù),具體的查詢過程是交給 AlbumLoader 執(zhí)行。
最后將查詢的結果通過 AlbumCallbacks 接口回調(diào)給 MatisseActivity,為 AlbumAdapter 提供數(shù)據(jù)并默認顯示第一個文件夾下的圖片。
/** * 資源文件夾,數(shù)據(jù)查詢完成的回調(diào) * * @param cursor */ @Override public void onAlbumLoad(final Cursor cursor) { mAlbumsAdapter.swapCursor(cursor); // select default album. Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { cursor.moveToPosition(mAlbumCollection.getCurrentSelection()); mAlbumsSpinner.setSelection(MatisseActivity.this, mAlbumCollection.getCurrentSelection()); Album album = Album.valueOf(cursor); if (album.isAll() && SelectionSpec.getInstance().capture) { album.addCaptureCount(); } onAlbumSelected(album); } }); }
現(xiàn)在資源文件夾的數(shù)據(jù)已經(jīng)獲取到了,照片墻的數(shù)據(jù)又是如何獲得的哪?答案就在 onAlbumSelected() 中。
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);
Fragment fragment = MediaSelectionFragment.newInstance(album);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
.commitAllowingStateLoss();
}
}
可以看到照片墻的實現(xiàn)是通過 MediaSelectionFragment 進行展示的。
MediaSelectionFragment
顯示照片墻的 Fragment,布局中只有一個 RecyclerView。
在初始化 MediaSelectionFragment 的時候,傳入了一個 Album 對象,這個對象的作用又是做什么的呢?讓我們繼續(xù)追蹤 MediaSelectionFragment 中的代碼。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Album album = getArguments().getParcelable(EXTRA_ALBUM);
mAdapter = new AlbumMediaAdapter(getContext(),
mSelectionProvider.provideSelectedItemCollection(), mRecyclerView);
mAdapter.registerCheckStateListener(this); // 注冊 CheckView 是否選中的監(jiān)聽事件
mAdapter.registerOnMediaClickListener(this); // 注冊圖片的點擊事件
mRecyclerView.setHasFixedSize(true);
int spanCount;
SelectionSpec selectionSpec = SelectionSpec.getInstance();
if (selectionSpec.gridExpectedSize > 0) {
spanCount = UIUtils.spanCount(getContext(), selectionSpec.gridExpectedSize);
} else {
spanCount = selectionSpec.spanCount;
}
mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), spanCount));
int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
mRecyclerView.addItemDecoration(new MediaGridInset(spanCount, spacing, false));
mRecyclerView.setAdapter(mAdapter);
mAlbumMediaCollection.onCreate(getActivity(), this);
mAlbumMediaCollection.load(album, selectionSpec.capture); // 加載媒體數(shù)據(jù)
}
原來 Album 對象最終傳遞到了 AlbumMediaCollection 中。
-
AlbumMediaCollection
和查詢資源文件夾的套路一樣,都是通過 Loader 機制完成。
根據(jù)傳入的 Album 對象,獲取該文件夾的 Id 作為查詢參數(shù),通過 AlbumMediaLoader 去查詢數(shù)據(jù)。
將最終查詢的結果,通過 AlbumMediaCallbacks 接口暴露給 MediaSelectionFragment 。
-
AlbumMediaAdapter
為 RecyclerView 提供數(shù)據(jù)。以下是綁定數(shù)據(jù)的代碼。
@Override protected void onBindViewHolder(final RecyclerView.ViewHolder holder, Cursor cursor) { if (holder instanceof CaptureViewHolder) { CaptureViewHolder captureViewHolder = (CaptureViewHolder) holder; Drawable[] drawables = captureViewHolder.mHint.getCompoundDrawables(); TypedArray ta = holder.itemView.getContext().getTheme().obtainStyledAttributes( new int[]{R.attr.capture_textColor}); int color = ta.getColor(0, 0); ta.recycle(); for (int i = 0; i < drawables.length; i++) { Drawable drawable = drawables[i]; if (drawable != null) { final Drawable.ConstantState state = drawable.getConstantState(); if (state == null) { continue; } Drawable newDrawable = state.newDrawable().mutate(); newDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); newDrawable.setBounds(drawable.getBounds()); drawables[i] = newDrawable; } } captureViewHolder.mHint.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3]); } else if (holder instanceof MediaViewHolder) { MediaViewHolder mediaViewHolder = (MediaViewHolder) holder; final Item item = Item.valueOf(cursor); mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo( getImageResize(mediaViewHolder.mMediaGrid.getContext()), mPlaceholder, mSelectionSpec.countable, holder )); mediaViewHolder.mMediaGrid.bindMedia(item); // 綁定數(shù)據(jù) mediaViewHolder.mMediaGrid.setOnMediaGridClickListener(this); // 設置條目的點擊事件 setCheckStatus(item, mediaViewHolder.mMediaGrid); } }在這個類中,還聲明了以下 3 個接口。具體的實現(xiàn)有 MatisseActivity 完成。
// 修改底部工具欄的狀態(tài),更改已選擇條目的集合數(shù)據(jù) public interface CheckStateListener { void onUpdate(); } // 處理點擊條目的跳轉 public interface OnMediaClickListener { void onMediaClick(Album album, Item item, int adapterPosition); } // 相機按鈕的點擊事件 public interface OnPhotoCapture { void capture(); } -
MediaGrid
每個條目顯示的View。自定義的 ViewGroup(包含 ImageView,CheckView,顯示視頻時長的 TextView,是否是 Gif 標志的 ImageView)
進行數(shù)據(jù)具體的綁定工作。并將點擊事件通過接口暴露給 AlbumMediaAdapter。
public interface OnMediaGridClickListener { // ImageView 的點擊事件 void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder); // CheckView 的點擊事件 void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder); }
通過這么多接口的回調(diào),減少了每個類的代碼,使得每個類的邏輯更加清晰。這樣的設計的確值得學習和借鑒。
至此照片墻的部分了解的差不多了,讓我們?nèi)タ聪骂A覽界面的設計。
BasePreviewActivity
打開預覽界面有兩種方式:
-
點擊圖片,直接進入的預覽界面 AlbumPreviewActivity
// 點擊圖片 @Override public void onMediaClick(Album album, Item item, int adapterPosition) { Intent intent = new Intent(this, AlbumPreviewActivity.class); intent.putExtra(AlbumPreviewActivity.EXTRA_ALBUM, album); // 該圖片對應的文件夾對象 intent.putExtra(AlbumPreviewActivity.EXTRA_ITEM, item); intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); startActivityForResult(intent, REQUEST_CODE_PREVIEW); } -
點擊 CheckView,選中照片,再點擊底部的預覽按鈕 SelectedPreviewActivity
@Override public void onClick(View v) { // 點擊預覽按鈕 if (v.getId() == R.id.button_preview) { Intent intent = new Intent(this, SelectedPreviewActivity.class); intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); startActivityForResult(intent, REQUEST_CODE_PREVIEW); } }
通過方式 1 進入的 AlbumPreviewActivity ,攜帶的參數(shù)有 該圖片對應的文件夾對象。在其內(nèi)部通過 Loader 機制去加載預覽圖片的數(shù)據(jù):
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!SelectionSpec.getInstance().hasInited) {
setResult(RESULT_CANCELED);
finish();
return;
}
mCollection.onCreate(this, this);
// 點擊的圖片,所屬的文件夾對象
Album album = getIntent().getParcelableExtra(EXTRA_ALBUM);
mCollection.load(album); // 加載數(shù)據(jù)
Item item = getIntent().getParcelableExtra(EXTRA_ITEM); // 點擊的圖片對象
if (mSpec.countable) {
mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
} else {
mCheckView.setChecked(mSelectedCollection.isSelected(item));
}
updateSize(item);
}
通過方式 2 進入SelectedPreviewActivity,直接把攜帶的數(shù)據(jù),作為數(shù)據(jù)源進行展示
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!SelectionSpec.getInstance().hasInited) {
setResult(RESULT_CANCELED);
finish();
return;
}
Bundle bundle = getIntent().getBundleExtra(EXTRA_DEFAULT_BUNDLE);
List<Item> selected = bundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION);
mAdapter.addAll(selected);
mAdapter.notifyDataSetChanged();
if (mSpec.countable) {
mCheckView.setCheckedNum(1);
} else {
mCheckView.setChecked(true);
}
mPreviousPos = 0;
updateSize(selected.get(0));
}
由于同樣都是預覽界面,代碼的重復性比較多,便抽取了一個 BasePreviewActivity。該主要是由 ViewPager 和 Fragment 組成。這部分的代碼就比較容易閱讀了。
總結
在 Matisse 中,有許多關于自定義 View 的知識,如自定義的 CheckView,CheckRadioView。關于自定義 View 這方面的知識,也非常值得我們學習。
