正文
目录
-
基本介绍
-
整体的设计和实现流程
-
资源文件夹的加载和展示
-
主页图片墙的实现
-
预览界面的实现
-
总结
一、基本介绍
Matisse
是「知乎」开源的一款十分精美的本地图像和视频选择库。
Matisse
Matisse 的代码写的相当的简洁、规范,很有学习的价值。
讲一下 Matisse 的一些优点:
可以看到 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<Uri>
具体的流程图如下:
Matisse 流程图
以上便是 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 的使用主要分两步
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,然后进行相应信息的设置。