标签:

知乎的Android开源图片选择框架Matisse源码解析

Matisse中主要的模块有Matisse、SelectionCreator、SelectionSpec、MatisseActivity四个类,它们的工作流程如图:

4c9ce0c2a3dd561b5ff38f6a40f4d0b1.png

我们先看到Matisse的使用代码,通过使用的代码来解析源码

Matisse.from(MainActivity.this)

.choose(MimeType.allOf())

.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)// 缩略图的缩放尺寸,默认为0.5

.imageEngine(new GlideEngine())// 图片加载库

.forResult(REQUEST_CODE_CHOOSE);// 启动选择图片Activity

Matisse类

我们从使用时的入口,Matisse类看起。我们进入Matisse的源码,可以看到下面这一部分:

public final class Matisse {

private final WeakReference mContext;

private final WeakReference mFragment;

public static Matisse from(Activity activity) {

return new Matisse(activity);

}

public static Matisse from(Fragment fragment) {

return new Matisse(fragment);

}

...

@Nullable

Activity getActivity() {

return (Activity)this.mContext.get();

}

public SelectionCreator choose(Set mimeTypes) {

return this.choose(mimeTypes, true);

}

public SelectionCreator choose(Set mimeTypes, boolean mediaTypeExclusive) {

return new SelectionCreator(this, mimeTypes, mediaTypeExclusive);

}

@Nullable

Fragment getFragment() {

return this.mFragment != null ? (Fragment)this.mFragment.get() : null;

}

}

我们可以发现,Matisse中用弱引用保存了Activity及Fragment的引用。它的from方法有两个重载,一个是传入Activity,一个是传入Fragment。也就是它同时支持了Activity及Fragment。它的choose方法有两个重载,最后都创建了一个SelectionCreator类。

SelectionCreator类-配置部分

我们看到SelectionCreator类的源码

public final class SelectionCreator {

private final Matisse mMatisse;

private final SelectionSpec mSelectionSpec;

SelectionCreator(Matisse matisse, @NonNull Set mimeTypes,

boolean mediaTypeExclusive){

this.mMatisse = matisse;

this.mSelectionSpec = SelectionSpec.getCleanInstance();

this.mSelectionSpec.mimeTypeSet = mimeTypes;

this.mSelectionSpec.mediaTypeExclusive = mediaTypeExclusive;

this.mSelectionSpec.orientation = -1;

}

public SelectionCreator countable(boolean countable) {

mSelectionSpec.countable = countable;

return this;

}

public SelectionCreator maxSelectable(int maxSelectable) {

if (maxSelectable < 1)

throw new IllegalArgumentException("maxSelectable must be greater than or equal to one");

mSelectionSpec.maxSelectable = maxSelectable;

return this;

}

...

}

可以看到,它内部保存了刚刚创建的Matisse及一个SelectionSpec类。SelectionCreator类采用了一种Builder的设计,比较巧妙的是将它的配置属性都放到了SelectionSpec类中。

SelectionSpec类

我们可以查看SelectionSpec的源码

public final class SelectionSpec {

public Set mimeTypeSet;

public boolean mediaTypeExclusive;

... //一些配置属性

private SelectionSpec() {

}

public static SelectionSpec getInstance() {

return SelectionSpec.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();

private InstanceHolder() {

}

}

}

可以看到,SelectionSpec类采用了一种懒汉式单例模式的设计,使用的时候才会被加载。

看到刚刚获取实例的getCleanInstance方法,会发现它仍然是调用了getInstance方法,然后调用了其reset方法对数据进行清空。保证了每次调用时的配置都是初始配置。

SelectionCreator类-跳转部分

我们可以回到SelectionCreator。当我们对其进行了一系列配置之后,就会调用forResult方法来打开选择图片Activity。我们可以看看forResult的源码。

public void forResult(int requestCode) {

Activity activity = this.mMatisse.getActivity();

if (activity != null) {

Intent intent = new Intent(activity, MatisseActivity.class);

Fragment fragment = this.mMatisse.getFragment();

if (fragment != null) {

fragment.startActivityForResult(intent, requestCode);

} else {

activity.startActivityForResult(intent, requestCode);

}

}

}

可以看到,它构建了Intent,然后分别对Activity及Fragment进行不同的跳转处理。最后都是调用了startActivityForResult方法。也就是我们的选择结果会由onActivityResult方法返回。

同时可以看到,它在Intent中,启动了MatisseActivity。

MatisseActivity类

在MatisseActivity类的onCreate方法的开始,我们就可以看到这样一行代码:

this.mSpec = SelectionSpec.getInstance();

由于SelectionSpec是单例模式,所以我们可以通过getInstance方法拿到之前配置过的SelectionSpec。

获取资源及展示

Matisse 中所展示的资源都是用 Loader 机制进行加载的,Loader 机制是 Android 3.0 之后官方推荐的加载 ContentProvider 中资源的最佳方式,不仅能极大地提高我们资源加载的速度,而且还能让我们的代码变得更加的简洁。

下面是它的资源加载的流程图:

616ebca301971779346f64a5e0399418.png

public class MatisseActivity extends AppCompatActivity implements

AlbumCollection.AlbumCallbacks, ... {

...

//用于保存资源以及资源的操作

private final AlbumCollection mAlbumCollection = new AlbumCollection();

//用于展示资源的 Adapter

private AlbumsAdapter mAlbumsAdapter;

...

@Override

protected void onCreate(@Nullable Bundle savedInstanceState) {

...

//获取资源的主要代码

mAlbumCollection.onCreate(this, this);

mAlbumCollection.onRestoreInstanceState(savedInstanceState);

mAlbumCollection.loadAlbums();

}

//拿到资源后回调方法

@Override

public void onAlbumLoad(final Cursor cursor) {

mAlbumsAdapter.swapCursor(cursor);

...

}

这里的数据加载使用到了Android的Loader API。详细可以看这篇文章:Android Loader 机制,让你的数据加载更加轻松

@Override

public Loader initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks callback) {

...

LoaderInfo info = mLoaders.get(id);

if (info == null) {

info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks)callback);

}

...

if (info.mHaveData && mStarted) {

// 创建并获取资源完成后调用该方法,执行到AlbumCollection 中重写的 onl oadingFinish() 方法,里面又 callbacks.onAlbumLoad()

info.callOnLoadFinished(info.mLoader, info.mData);

}

}

createAndInstallLoader方法如下:

private LoaderInfo createAndInstallLoader(int id, Bundle args,

LoaderManager.LoaderCallbacks callback) {

try {

mCreatingLoader = true;

//在 AlbumCollection 中重写了该方法,创建了指定好 query 语句的 AlbumLoader 对象

LoaderInfo info = createLoader(id, args, callback);

//调用 info.start(), 在 CursorLoader 中实现 onStartLoading()

installLoader(info);

return info;

} finally {

mCreatingLoader = false;

}

文件夹的选择

AlbumSpinner是一个自定义View,位于MainActivity左上角。主要包括了显示文件夹名称的TextView、显示文件夹列表的ListPopupWindow。

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()) {

this.mContainer.setVisibility(8);

this.mEmptyView.setVisibility(0);

} else {

this.mContainer.setVisibility(0);

this.mEmptyView.setVisibility(8);

Fragment fragment = MediaSelectionFragment.newInstance(album);

this.getSupportFragmentManager()

.beginTransaction()

.replace(id.container, fragment,

MediaSelectionFragment.class.getSimpleName())

.commitAllowingStateLoss();

}

}

可以看到这里做了一些处理,mContainer是有图片时图片列表的布局。而mEmptyView则是没有图片时的布局。在文件夹中没有图片时显示mEmpty。而显示具体图片列表的布局,则是MediaSelectionFragment这个Fragment。

首页照片墙的实现

首页的图片墙非常值得我们学习。图片墙的数据源是通过 Loader 机制来进行加载的 ,它会通过我们选择不同的资源文件夹而展示不同的图片。

因此我们在选择资源文件夹的时候,便将资源文件夹的 id,传给对应的 Loader,让它对相应的资源文件进行加载。

Item实体类

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")));

}

}

Item布局

图片墙是直接用一个 RecyclerView 通过 GridLayoutManager 进行展示的,Item 是一个继承了 SquareFrameLayout(正方形的 FrameLayout) 的自定义控件,主要包含三个部分

右上角的 CheckView

显示图片的 ImageView

显示视频时长的 TextView

dbe2e662924539563908b9caa5667be6.png

CheckView

CheckView是一个自定义的 CheckBox 。它重写了 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 的话,那么便描绘背景以及填入一个白色的 ✓

d81725b3131e889f1dfaca8abebc7b16.png

MediaGrid

我们接着来看看图片墙的 Item 布局「MediaGrid」的实现逻辑 。MediaGrid 是一个继承了 SquareFrameLayout(正方形的 FrameLayout)的自定义View。是一个拓展了复选功能(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 传给 MediaGrid,然后进行相应信息的设置。

MediaGrid 中自定义了回调的接口

public interface OnMediaGridClickListener {

void onThumbnailClicked(ImageView var1, Item var2, ViewHolder var3);

void onCheckViewClicked(CheckView var1, Item var2, ViewHolder var3);

}

点击图片的时候,将点击事件回调到 Adapter,再回调到 MediaSelectionFragment,再回调到 MatisseActivity。

当点击右上角的 CheckView 的时候,便将点击事件回调到 Adapter 中,然后根据 countable 的值,来进行相应的设置(显示数字或者显示 √),然后再将对应的 Item 信息保存在 SelectedItemCollection(Item 的容器) 中。

预览界面的实现

打开预览界面有两种方法

点击首页的某个图片

选择图片之后,点击首页左下角的预览(Preview)按钮

这两种方法打开的界面看起来似乎是一样的,但实际上他们两个的实现逻辑很不一样,因此用了两个不同的 Activity。

点击首页的某张图片之后,会跳转到一个包含 ViewPager 的界面,因为对应资源文件夹中可能会有很多的图片,这时候如果将包含该文件夹中所有的图片直接传给预览界面的 Activity,是非常不实际的。

比较好的实现方式便是将「包含对应文件夹的信息的 Album」传给界面,然后再用 Loader 机制进行加载。

而选择首页图片后,点击左下角的预览按钮,实现就不是很一样了。跳转到预览界面,因为我们选择的图片一般都比较少,所以这时候直接将「包含所有选择图片信息的 List」传给预览界面就行了。

虽然两个 Activity 的实现逻辑不太一样,但由于都是预览界面,所以有很多相同的地方。因此Matisse实现了一个 BasePreviewActivity。

7985fad5dca555464902aad07f64e6f2.png

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;

}

广告时间

我是N0tExpectErr0r,一名广东工业大学的大二学生

欢迎来到我的个人博客,所有文章均在个人博客中同步更新哦

http://blog.N0tExpectErr0r.cn

标签:

来源: https://blog.csdn.net/qq_21556263/article/details/82768358

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐