前言
作为一名一年多的 Android 开发者,面对源码阅读一直是踯躅不前。毫不夸张的说内心一直是拒绝的,担心自己没能力去读懂那该死的源码。在和朋友的交流中,我向他请教了很多关于源码阅读的技巧。提及最多的就是 搞清楚每个类的职责,类与类之间的设计,最终明白源码的执行过程。
重要的是通过阅读源码,针对不会的知识点进行查漏补缺,这个过程就像是打怪升级,逐渐的提高自己的技术。在朋友不断的鼓励下,终于迈出了源码阅读第一步,希望在今后的工作学习中,能把这种学习方式坚持下去。好了,现在进入今天的主题 Matisse。
简介
Matisse 是知乎开源的一款,针对 Android 本地图像和视频的选择器。它主要有以下优点:
方便在 Activity 和 Fragment 中使用
支持各种格式的图片和视频
应用不同的主题,包括内置的两种主题和自定义主题‘
使用不同的图片加载器
自定义文件的过滤规则
-
方便拓展
针对 方便拓展,基佬 的 RxImagePicker 完美的诠释了这一优点。在 RxImagePicker 中,Matisse 被抽出来放入了 RxImagePicker_Support,成为了 UI 层的基础组件。
Matisse 的基本使用
参考 Matisse 的 SampleActivity 中的例子,我们来一步步探索 Matisse 的实现过程。
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);
调用 from() 获取 Matisse 的一个实例。
调用 choose() 方法获取 SelectionCreator,进行一些列的参数配置。如主题、最大选择数量。参数的保存是由全局单例 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); } /** * 获得选择的数据 */ 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 对象,进行参数配置。
获得用户选择的数据。
-
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); } } }
这个类的主要职责是:
通过链式调用,进行参数配置。
开启 MatisseActivity。
-
SelectionSpec
该类主要定义了一系列可配置的参数
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);
// 资源文件夹的数据源
mAlbumCollection.onCreate(this, this);
mAlbumCollection.onRestoreInstanceState(savedInstanceState);
mAlbumCollection.loadAlbums(); // 加载资源文件夹
updateBottomToolbar();
}
}
Matisse 进行数据的获取是通过 Loader 机制完成的。Loader 是官方在 3.0 之后推荐的加载 ContentProvider 资源的最佳使用方式。方便我们在 Activity 和 Fragment 异步加载数据,在这里安利一篇关于 Loader 的文章。
-
AlbumSpinner
AlbumSpinner 是对资源文件夹的 ListPopubWindow 和显示文件夹名称的 TextView 进行了一层封装。最终把点击事件的处理,通过接口的方式暴露给 MatisseActivity,展示该资源文件夹下所有的图片。
/** * 每次点击 ListPopuWindow 都会触发 * * @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 提供数据。
-
AlbumCollection
调用 loadAlbums(),去查询资源文件夹的数据,具体的查询过程是交给 AlbumLoader 执行。
最后将查询的结果通过 AlbumCallbacks 接口回调给 MatisseActivity,为 AlbumAdapter 提供数据并默认显示第一个文件夹下的图片。
/** * 资源文件夹,数据查询完成的回调 * * @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); } }); }
现在资源文件夹的数据已经获取到了,照片墙的数据又是如何获得的哪?答案就在 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();
}
}
可以看到照片墙的实现是通过 MediaSelectionFragment 进行展示的。
MediaSelectionFragment
显示照片墙的 Fragment,布局中只有一个 RecyclerView。
在初始化 MediaSelectionFragment 的时候,传入了一个 Album 对象,这个对象的作用又是做什么的呢?让我们继续追踪 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 是否选中的监听事件
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); // 加载媒体数据
}
原来 Album 对象最终传递到了 AlbumMediaCollection 中。
-
AlbumMediaCollection
和查询资源文件夹的套路一样,都是通过 Loader 机制完成。
根据传入的 Album 对象,获取该文件夹的 Id 作为查询参数,通过 AlbumMediaLoader 去查询数据。
将最终查询的结果,通过 AlbumMediaCallbacks 接口暴露给 MediaSelectionFragment 。
-
AlbumMediaAdapter
为 RecyclerView 提供数据。以下是绑定数据的代码。
@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); // 绑定数据 mediaViewHolder.mMediaGrid.setOnMediaGridClickListener(this); // 设置条目的点击事件 setCheckStatus(item, mediaViewHolder.mMediaGrid); } }
在这个类中,还声明了以下 3 个接口。具体的实现有 MatisseActivity 完成。
// 修改底部工具栏的状态,更改已选择条目的集合数据 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)
进行数据具体的绑定工作。并将点击事件通过接口暴露给 AlbumMediaAdapter。
public interface OnMediaGridClickListener { // ImageView 的点击事件 void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder); // CheckView 的点击事件 void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder); }
通过这么多接口的回调,减少了每个类的代码,使得每个类的逻辑更加清晰。这样的设计的确值得学习和借鉴。
至此照片墙的部分了解的差不多了,让我们去看下预览界面的设计。
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 ,携带的参数有 该图片对应的文件夹对象。在其内部通过 Loader 机制去加载预览图片的数据:
@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); // 加载数据
Item item = getIntent().getParcelableExtra(EXTRA_ITEM); // 点击的图片对象
if (mSpec.countable) {
mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
} else {
mCheckView.setChecked(mSelectedCollection.isSelected(item));
}
updateSize(item);
}
通过方式 2 进入SelectedPreviewActivity,直接把携带的数据,作为数据源进行展示
@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 这方面的知识,也非常值得我们学习。