Matisse 是一款知乎开源的设计精美的图片选择器,这里是源码地址。先放两张官方图来看一下。
这么俏皮可爱的界面怎么让人不喜爱呢?下面就来了解一下它吧。
基本用法:
Matisse.from(SampleActivity.this)
.choose(MimeType.ofAll(), false) //参数1 显示资源类型 参数2 是否可以同时选择不同的资源类型 true表示不可以 false表示可以
// .theme(R.style.Matisse_Dracula) //选择主题 默认是蓝色主题,Matisse_Dracula为黑色主题
.countable(true) //是否显示数字
.capture(true) //是否可以拍照
.captureStrategy(//参数1 true表示拍照存储在共有目录,false表示存储在私有目录;参数2与 AndroidManifest中authorities值相同,用于适配7.0系统 必须设置
new CaptureStrategy(true, "com.zhihu.matisse.sample.fileprovider"))
.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()) //选择图片加载引擎
.forResult(REQUEST_CODE_CHOOSE); //设置requestcode,开启Matisse主页面
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHOOSE && resultCode == RESULT_OK) {
mAdapter.setData(Matisse.obtainResult(data), Matisse.obtainPathResult(data));
}
}
要注意的就是如果想集成拍照功能,除了设置capture(true) 以外 还必须需要设置captureStrategy。并在
AndroidManifest中注册FileProvider,用于适配7.0系统拍照。更多代码细节请参看官方demo,这里就不列出了。下面开始就对源码进行一些分析。
源码结构
其中被框起来的部分是需要着重关注的主线流程。
开启Matisse之旅
首先回归基本用法 从Matisse.from()开始。进入该方法就来到了Matisse入口类,来看一下:
public final class Matisse {
private final WeakReference mContext;
private final WeakReference 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);
}
public static Matisse from(Activity activity) {
return new Matisse(activity);
}
public static Matisse from(Fragment fragment) {
return new Matisse(fragment);
}
public static List obtainResult(Intent data) {
return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION);
}
public static List obtainPathResult(Intent data) {
return data.getStringArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION_PATH);
}
public SelectionCreator choose(Set mimeTypes) {
return this.choose(mimeTypes, true);
}
public SelectionCreator choose(Set mimeTypes, boolean mediaTypeExclusive) {
return new SelectionCreator(this, mimeTypes, mediaTypeExclusive);
}
@Nullable
Activity getActivity() {
return mContext.get();
}
@Nullable
Fragment getFragment() {
return mFragment != null ? mFragment.get() : null;
}
}
接着调用 choose(Set
public enum MimeType {
// ============== images ==============
JPEG("image/jpeg", new HashSet() {
{
add("jpg");
add("jpeg");
}
}),
PNG("image/png", new HashSet() {
{
add("png");
}
}),
GIF("image/gif", new HashSet() {
{
add("gif");
}
}),
BMP("image/x-ms-bmp", new HashSet() {
{
add("bmp");
}
}),
WEBP("image/webp", new HashSet() {
{
add("webp");
}
}),
// ============== videos ==============
MPEG("video/mpeg", new HashSet() {
{
add("mpeg");
add("mpg");
}
}),
MP4("video/mp4", new HashSet() {
{
add("mp4");
add("m4v");
}
}),
QUICKTIME("video/quicktime", new HashSet() {
{
add("mov");
}
}),
THREEGPP("video/3gpp", new HashSet() {
{
add("3gp");
add("3gpp");
}
}),
THREEGPP2("video/3gpp2", new HashSet() {
{
add("3g2");
add("3gpp2");
}
}),
MKV("video/x-matroska", new HashSet() {
{
add("mkv");
}
}),
WEBM("video/webm", new HashSet() {
{
add("webm");
}
}),
TS("video/mp2ts", new HashSet() {
{
add("ts");
}
}),
AVI("video/avi", new HashSet() {
{
add("avi");
}
});
private final String mMimeTypeName;
private final Set mExtensions;
MimeType(String mimeTypeName, Set extensions) {
mMimeTypeName = mimeTypeName;
mExtensions = extensions;
}
//添加所有格式
public static Set ofAll() {
return EnumSet.allOf(MimeType.class);
}
public static Set of(MimeType type, MimeType... rest) {
return EnumSet.of(type, rest);
}
//添加所有图片格式
public static Set ofImage() {
return EnumSet.of(JPEG, PNG, GIF, BMP, WEBP);
}
//添加所有视频格式
public static Set ofVideo() {
return EnumSet.of(MPEG, MP4, QUICKTIME, THREEGPP, THREEGPP2, MKV, WEBM, TS, AVI);
}
@Override
public String toString() {
return mMimeTypeName;
}
/**
* 检查资源类型是否在选择范围内
* @param resolver
* @param uri
* @return
*/
public boolean checkType(ContentResolver resolver, Uri uri) {
MimeTypeMap map = MimeTypeMap.getSingleton();
if (uri == null) {
return false;
}
String type = map.getExtensionFromMimeType(resolver.getType(uri));
for (String extension : mExtensions) {
if (extension.equals(type)) {
return true;
}
String path = PhotoMetadataUtils.getPath(resolver, uri);
if (path != null && path.toLowerCase(Locale.US).endsWith(extension)) {
return true;
}
}
return false;
}
这是个枚举类,枚举了所有的图片和视频格式。我们调用的MimeType.ofAll()就是将所有的资源格式添加到了EnumSet集合中,MimeType.ofImage() 是只添加图片格式,MimeType.ofVideo() 是只添加视频格式。EnumSet是专门用来存储枚举类的数据结构,以便后续的遍历操作。枚举的写法让代码结构非常清晰,使用EnumSet存储也非常方便。性能的话暂时就不过多考虑了~。
还是关注choose方法 ,返回给我们了SelectionCreator对象,SelectionCreator就是我们喜闻乐见的构造者了~主要参数设置都是通过它了。
public final class SelectionCreator {
private final Matisse mMatisse;
private final SelectionSpec mSelectionSpec;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
@IntDef({
SCREEN_ORIENTATION_UNSPECIFIED,
SCREEN_ORIENTATION_LANDSCAPE,
SCREEN_ORIENTATION_PORTRAIT,
SCREEN_ORIENTATION_USER,
SCREEN_ORIENTATION_BEHIND,
SCREEN_ORIENTATION_SENSOR,
SCREEN_ORIENTATION_NOSENSOR,
SCREEN_ORIENTATION_SENSOR_LANDSCAPE,
SCREEN_ORIENTATION_SENSOR_PORTRAIT,
SCREEN_ORIENTATION_REVERSE_LANDSCAPE,
SCREEN_ORIENTATION_REVERSE_PORTRAIT,
SCREEN_ORIENTATION_FULL_SENSOR,
SCREEN_ORIENTATION_USER_LANDSCAPE,
SCREEN_ORIENTATION_USER_PORTRAIT,
SCREEN_ORIENTATION_FULL_USER,
SCREEN_ORIENTATION_LOCKED
})
@Retention(RetentionPolicy.SOURCE)
@interface ScreenOrientation {
}
SelectionCreator(Matisse matisse, @NonNull Set 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 SelectionCreator theme(@StyleRes int themeId) {
mSelectionSpec.themeId = themeId;
return this;
}
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;
}
public SelectionCreator addFilter(@NonNull Filter filter) {
if (mSelectionSpec.filters == null) {
mSelectionSpec.filters = new ArrayList<>();
}
if (filter == null) throw new IllegalArgumentException("filter cannot be null");
mSelectionSpec.filters.add(filter);
return this;
}
public SelectionCreator capture(boolean enable) {
mSelectionSpec.capture = enable;
return this;
}
public SelectionCreator captureStrategy(CaptureStrategy captureStrategy) {
mSelectionSpec.captureStrategy = captureStrategy;
return this;
}
public SelectionCreator restrictOrientation(@ScreenOrientation int orientation) {
mSelectionSpec.orientation = orientation;
return this;
}
public SelectionCreator spanCount(int spanCount) {
if (spanCount < 1) throw new IllegalArgumentException("spanCount cannot be less than 1");
mSelectionSpec.spanCount = spanCount;
return this;
}
public SelectionCreator gridExpectedSize(int size) {
mSelectionSpec.gridExpectedSize = size;
return this;
}
public SelectionCreator thumbnailScale(float scale) {
if (scale <= 0f || scale > 1f)
throw new IllegalArgumentException("Thumbnail scale must be between (0.0, 1.0]");
mSelectionSpec.thumbnailScale = scale;
return this;
}
public SelectionCreator imageEngine(ImageEngine imageEngine) {
mSelectionSpec.imageEngine = imageEngine;
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);
}
}
}
构造方法中,首先拿到入口类对象matisse,拿到了SelectionSpec的一个单例,向单例中存储了刚才添加的Set集合,和默认的屏幕方向策略SCREEN_ORIENTATION_UNSPECIFIED(不限定屏幕方向)。然后我们其他的设置也通过构造者模式存储在SelectionSpec的单例中。其中屏幕方向策略的设置,使用了编译时注解代替了枚举,限定了我们传参的范围。
SelectionSpec主要代码
public final class SelectionSpec {
public Set mimeTypeSet;
public boolean mediaTypeExclusive;
public boolean showSingleMediaType;
@StyleRes
public int themeId;
public int orientation;
public boolean countable;
public int maxSelectable;
public List filters;
public boolean capture;
public CaptureStrategy captureStrategy;
public int spanCount;
public int gridExpectedSize;
public float thumbnailScale;
public ImageEngine imageEngine;
private SelectionSpec() {
}
public static SelectionSpec getInstance() {
return InstanceHolder.INSTANCE;
}
public static SelectionSpec getCleanInstance() {
SelectionSpec selectionSpec = getInstance();
selectionSpec.reset();
return selectionSpec;
}
private void reset() {
mimeTypeSet = null;
mediaTypeExclusive = true;
showSingleMediaType = false;
themeId = R.style.Matisse_Zhihu;
orientation = 0;
countable = false;
maxSelectable = 1;
filters = null;
capture = false;
captureStrategy = null;
spanCount = 3;
gridExpectedSize = 0;
thumbnailScale = 0.5f;
imageEngine = new GlideEngine();
}
}
我们设置的所有属性就存储在这里。
前期准备工作完成,随着一句forResult(REQUEST_CODE_CHOOSE),就进入了Matisse主页面了。
public class MatisseActivity extends AppCompatActivity implements
AlbumCollection.AlbumCallbacks, AdapterView.OnItemSelectedListener,
MediaSelectionFragment.SelectionProvider, View.OnClickListener,
AlbumMediaAdapter.CheckStateListener, AlbumMediaAdapter.OnMediaClickListener,
AlbumMediaAdapter.OnPhotoCapture {
主页面MatisseActivity 做的事情比较多,实现了不少的回调接口。但现在暂时不必关心,我们就从oncreate方法开始
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// programmatically set theme before super.onCreate()
mSpec = SelectionSpec.getInstance();
//设置主题
setTheme(mSpec.themeId);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_matisse);
//设置屏幕方向
if (mSpec.needOrientationRestriction()) {
setRequestedOrientation(mSpec.orientation);
}
//如果设置了可拍照 需要传递设置的captureStrategy参数
if (mSpec.capture) {
mMediaStoreCompat = new MediaStoreCompat(this);
if (mSpec.captureStrategy == null)
throw new RuntimeException("Don't forget to set CaptureStrategy.");
mMediaStoreCompat.setCaptureStrategy(mSpec.captureStrategy);
}
//设置toolbar及相册标题样式
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayHomeAsUpEnabled(true);
Drawable navigationIcon = toolbar.getNavigationIcon();
TypedArray ta = getTheme().obtainStyledAttributes(new int[]{R.attr.album_element_color});
int color = ta.getColor(0, 0);
ta.recycle();
navigationIcon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
mButtonPreview = (TextView) findViewById(R.id.button_preview);
mButtonApply = (TextView) findViewById(R.id.button_apply);
mButtonPreview.setOnClickListener(this);
mButtonApply.setOnClickListener(this);
mContainer = findViewById(R.id.container);
mEmptyView = findViewById(R.id.empty_view);
//================ 主要业务流程 ================
mSelectedCollection.onCreate(savedInstanceState);
updateBottomToolbar();
mAlbumsAdapter = new AlbumsAdapter(this, null, false);
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();
}
重点关注oncreate的后面主要业务流程部分 :
首先执行了mSelectedCollection.onCreate(savedInstanceState); mSelectedCollection是SelectedItemCollection类的对象,这是比较重要的一个类。由于资源是可以多选的并且有序的,这个类功能就是管理选中项的集合。在主页以及预览页面,多处涉及到对资源的选择和取消操作时,把这部分逻辑分离出一个单独的管理类是十分有必要的。来看一下mSelectedCollection的onreate方法:
public void onCreate(Bundle bundle) {
if (bundle == null) {
mItems = new LinkedHashSet<>();
} else {
List- saved = bundle.getParcelableArrayList(STATE_SELECTION);
mItems = new LinkedHashSet<>(saved);
mCollectionType = bundle.getInt(STATE_COLLECTION_TYPE, COLLECTION_UNDEFINED);
}
}
初始情况下创建一个LinkedHashSet集合,用来存储选中项,如果遇到activity销毁重建的情况则从savedInstanceState中获取已经保存过的集合。
然后实例了AlbumsAdapter ,AlbumsSpinner两个类一个是加载相册的适配器,一个用于相册列表展示(注意这里是相册,不是图像和视频的),代码不多,先贴上来,细节可以先不关注,明确类的作用即可:
public class AlbumsAdapter extends CursorAdapter {
private final Drawable mPlaceholder;
public AlbumsAdapter(Context context, Cursor c, boolean autoRequery) {
super(context, c, autoRequery);
TypedArray ta = context.getTheme().obtainStyledAttributes(
new int[]{R.attr.album_thumbnail_placeholder});
mPlaceholder = ta.getDrawable(0);
ta.recycle();
}
public AlbumsAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
TypedArray ta = context.getTheme().obtainStyledAttributes(
new int[]{R.attr.album_thumbnail_placeholder});
mPlaceholder = ta.getDrawable(0);
ta.recycle();
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return LayoutInflater.from(context).inflate(R.layout.album_list_item, parent, false);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
Album album = Album.valueOf(cursor);
((TextView) view.findViewById(R.id.album_name)).setText(album.getDisplayName(context));
((TextView) view.findViewById(R.id.album_media_count)).setText(String.valueOf(album.getCount()));
// do not need to load animated Gif
SelectionSpec.getInstance().imageEngine.loadThumbnail(context, context.getResources().getDimensionPixelSize(R
.dimen.media_grid_size), mPlaceholder,
(ImageView) view.findViewById(R.id.album_cover), Uri.fromFile(new File(album.getCoverPath())));
}
}
public class AlbumsSpinner {
private static final int MAX_SHOWN_COUNT = 6;
private CursorAdapter mAdapter;
private TextView mSelected;
private ListPopupWindow mListPopupWindow;
private AdapterView.OnItemSelectedListener mOnItemSelectedListener;
public AlbumsSpinner(@NonNull Context context) {
mListPopupWindow = new ListPopupWindow(context, null, R.attr.listPopupWindowStyle);
mListPopupWindow.setModal(true);
float density = context.getResources().getDisplayMetrics().density;
mListPopupWindow.setContentWidth((int) (216 * density));
mListPopupWindow.setHorizontalOffset((int) (16 * density));
mListPopupWindow.setVerticalOffset((int) (-48 * density));
mListPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
AlbumsSpinner.this.onItemSelected(parent.getContext(), position);
if (mOnItemSelectedListener != null) {
mOnItemSelectedListener.onItemSelected(parent, view, position, id);
}
}
});
}
public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
}
public void setSelection(Context context, int position) {
mListPopupWindow.setSelection(position);
onItemSelected(context, position);
}
private void onItemSelected(Context context, int position) {
mListPopupWindow.dismiss();
Cursor cursor = mAdapter.getCursor();
cursor.moveToPosition(position);
Album album = Album.valueOf(cursor);
String displayName = album.getDisplayName(context);
if (mSelected.getVisibility() == View.VISIBLE) {
mSelected.setText(displayName);
} else {
if (Platform.hasICS()) {
mSelected.setAlpha(0.0f);
mSelected.setVisibility(View.VISIBLE);
mSelected.setText(displayName);
mSelected.animate().alpha(1.0f).setDuration(context.getResources().getInteger(
android.R.integer.config_longAnimTime)).start();
} else {
mSelected.setVisibility(View.VISIBLE);
mSelected.setText(displayName);
}
}
}
public void setAdapter(CursorAdapter adapter) {
mListPopupWindow.setAdapter(adapter);
mAdapter = adapter;
}
public void setSelectedTextView(TextView textView) {
mSelected = textView;
// tint dropdown arrow icon
Drawable[] drawables = mSelected.getCompoundDrawables();
Drawable right = drawables[2];
TypedArray ta = mSelected.getContext().getTheme().obtainStyledAttributes(
new int[]{R.attr.album_element_color});
int color = ta.getColor(0, 0);
ta.recycle();
//使用设置的主题颜色对目标Drawable(这里是一个小箭头)进行SRC_IN模式合成 达到改变Drawable颜色的效果
right.setColorFilter(color, PorterDuff.Mode.SRC_IN);
mSelected.setVisibility(View.GONE);
mSelected.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int itemHeight = v.getResources().getDimensionPixelSize(R.dimen.album_item_height);
mListPopupWindow.setHeight(
mAdapter.getCount() > MAX_SHOWN_COUNT ? itemHeight * MAX_SHOWN_COUNT
: itemHeight * mAdapter.getCount());
mListPopupWindow.show();
}
});
//设置textView向下拖拽可下拉ListPopupWindow
mSelected.setOnTouchListener(mListPopupWindow.createDragToOpenListener(mSelected));
}
/**
* 设置锚点view
* @param view
*/
public void setPopupAnchorView(View view) {
mListPopupWindow.setAnchorView(view);
}
}
AlbumsSpinner 内部使用了ListPopupWindow (PopupWindow+ListView的组合),接受CursorAdapter对象 ,而AlbumsAdapter 正是继承了CursorAdapter 。CursorAdapter 需要接受的Cursor对象,就需要我们接下来提供。
还是oncreate方法,来到mAlbumCollection.onCreate(this, this)这一句;看看这个类AlbumCollection:
public class AlbumCollection implements LoaderManager.LoaderCallbacks {
private static final int LOADER_ID = 1;
private static final String STATE_CURRENT_SELECTION = "state_current_selection";
private WeakReference mContext;
private LoaderManager mLoaderManager;
private AlbumCallbacks mCallbacks;
private int mCurrentSelection;
@Override
public Loader onCreateLoader(int id, Bundle args) {
Context context = mContext.get();
if (context == null) {
return null;
}
return AlbumLoader.newInstance(context);
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumLoad(data);
}
@Override
public void onLoaderReset(Loader loader) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumReset();
}
public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) {
mContext = new WeakReference(activity);
mLoaderManager = activity.getSupportLoaderManager();
mCallbacks = callbacks;
}
public void onRestoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
}
mCurrentSelection = savedInstanceState.getInt(STATE_CURRENT_SELECTION);
}
public void onSaveInstanceState(Bundle outState) {
outState.putInt(STATE_CURRENT_SELECTION, mCurrentSelection);
}
public void onDestroy() {
mLoaderManager.destroyLoader(LOADER_ID);
mCallbacks = null;
}
public void loadAlbums() {
mLoaderManager.initLoader(LOADER_ID, null, this);
}
public int getCurrentSelection() {
return mCurrentSelection;
}
public void setStateCurrentSelection(int currentSelection) {
mCurrentSelection = currentSelection;
}
public interface AlbumCallbacks {
void onAlbumLoad(Cursor cursor);
void onAlbumReset();
}
}
AlbumCollection是我们加载相册的LoaderManager,实现了 LoaderManager.LoaderCallbacks
public class AlbumLoader extends CursorLoader {
public static final String COLUMN_COUNT = "count";
private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external"); //外部存储卡uri
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, //id
"bucket_id", //文件夹id
"bucket_display_name", //文件夹名称(用来做相册名称)
MediaStore.MediaColumns.DATA, //资源路径(用来做相册封面)
"COUNT(*) AS " + COLUMN_COUNT}; // 配合GROUP BY分组查询文件夹资源内数量
//MediaStore.Files.FileColumns.MEDIA_TYPE 资源类型
//MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE 图片类型
//MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO 视频类型
//MediaStore.MediaColumns.SIZE 资源大小
// GROUP BY (bucket_id 根据bucket_id分组查询,统计数量
// === params for showSingleMediaType: false 全部类型 ===
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),
};
// =============================================
// === params for showSingleMediaType: true 只查一种类型 ===
private static final String SELECTION_FOR_SINGLE_MEDIA_TYPE =
MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ " AND " + MediaStore.MediaColumns.SIZE + ">0"
+ ") GROUP BY (bucket_id";
private static String[] getSelectionArgsForSingleMediaType(int mediaType) {
return new String[]{String.valueOf(mediaType)};
}
// =============================================
//按时间降序
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) {
String selection;
String[] selectionArgs;
//判断只查询图片或者视频其中一种类型还是两种都查询,构造不同的查询条件
if (SelectionSpec.getInstance().onlyShowImages()) {
selection = SELECTION_FOR_SINGLE_MEDIA_TYPE;
selectionArgs = getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
} else if (SelectionSpec.getInstance().onlyShowVideos()) {
selection = SELECTION_FOR_SINGLE_MEDIA_TYPE;
selectionArgs = getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
} else {
selection = SELECTION;
selectionArgs = SELECTION_ARGS;
}
return new AlbumLoader(context, selection, selectionArgs);
}
/**
* 重写了loadInBackground方法是为了在查询出来的结果的上加一行数据 这行数据是由列名(ALL),
* 所有的资源的总数以及一个资源地址,其他列用Album.ALBUM_ID_ALL(无用数据)占位。目的是用来
* 展现在popwindow中的第一个数据:全部
* @return
*/
@Override
public Cursor loadInBackground() {
Cursor albums = super.loadInBackground();
//创建一个cursor
MatrixCursor allAlbum = new MatrixCursor(COLUMNS);
int totalCount = 0;
String allAlbumCoverPath = "";
if (albums != null) {
//获得全部资源总数
while (albums.moveToNext()) {
totalCount += albums.getInt(albums.getColumnIndex(COLUMN_COUNT));
}
//拿到第一个相册封面资源的地址
if (albums.moveToFirst()) {
allAlbumCoverPath = albums.getString(albums.getColumnIndex(MediaStore.MediaColumns.DATA));
}
}
//给新创建的cursor添加数据
allAlbum.addRow(new String[]{Album.ALBUM_ID_ALL, Album.ALBUM_ID_ALL, Album.ALBUM_NAME_ALL, allAlbumCoverPath,
String.valueOf(totalCount)});
//返回垂直拼接的cursor
return new MergeCursor(new Cursor[]{allAlbum, albums});
}
@Override
public void onContentChanged() {
// FIXME a dirty way to fix loading multiple times
}
这个类作用十分明显:查询相册。查询相册就是查询包含图片(视频)的文件夹。
它继承了CursorLoader ,需提供构造方法, 在构造方法中调用父类构造方法执行查询
private AlbumLoader(Context context, String selection, String[] selectionArgs) {
super(context, QUERY_URI, PROJECTION, selection, selectionArgs, BUCKET_ORDER_BY);
}
CursorLoader 内部是通过 ContentResolver 执行查询, 即我们只需要提供ContentResolver执行查询所需参数:
QUERY_URI:uri
PROJECTION:查询字段名
selection:where约束条件
selectionArgs:where中占位符的值
BUCKET_ORDER_BY:排序方式;
其余工作全部交由CursorLoader 执行即可。
构造方法是由入口方法newInstance调用的,来到newInstance方法,这里首先判断一下用户设置的是查询类型,只查图片,只查视频还是两种都查。判断方法:
public boolean onlyShowImages() {
return showSingleMediaType && MimeType.ofImage().containsAll(mimeTypeSet);
}
public boolean onlyShowVideos() {
return showSingleMediaType && MimeType.ofVideo().containsAll(mimeTypeSet);
}
判断一下showSingleMediaType 参数是否为true ,即是否只显示一种类型 ,同时比对EmunSet集合中是否已添加这种类型。由于代码中先判断了SelectionSpec.getInstance().onlyShowImages(),所以当showSingleMediaType 设置为ture但EmunSet既添加了image又添加了Video类型时,优先展示image类型。
确定要查询的类型后,为此就构造出了不同的selection,selectionArgs。最后调用构造方法执行查询。
关于构造方法中所需的其他参数的构建,代码中已给出注释,不再详细说明。
按理说这样已经完成了查询,可以在onLoadFinished中拿到携带相册信息的cursor了。但是发现AlbumLoader 又重写了CursorLoader的 loadInBackground方法,而loadInBackground就是CursorLoader执行查询并返回结果的方法。为什么这样呢?那就看看里面干了什么。关注loadInBackground方法:这里首先上来调用了super.loadInBackground()获取了查询结果的cursor。然后创建了一个MatrixCursor并传入了一个String数组COLUMNS :
private static final String[] COLUMNS = {
MediaStore.Files.FileColumns._ID,
"bucket_id",
"bucket_display_name",
MediaStore.MediaColumns.DATA,
COLUMN_COUNT};
干啥用的呢?查了资料得知原来MatrixCursor是一种允许我们自己来构建的一种cursor。这里传入了与查询条件完全相同的数组,就构造出了一个与查询结果cursor相同结构的cursor。之后又遍历了结果集cursor,累加得到所有相册内资源的总数,又拿到第一个相册封面资源的地址,把这两个数据通过MatrixCursor .addRow方法向cursor中的对应列添加了一行数据,其余的参数以Album.ALBUM_ID_ALL(无实际意义)进行填充。这样就诞生了一个新的cursor,所包含的数据就是它!
最终借助MergeCursor将这两个cursor垂直拼接成一个新的cursor返回给了我们。这样了我们拿到的cursor就多了一个全部相册啦!
回到我们的AlbumCollection类的onLoadFinished中就拿到我们查询并加工好的数据了。并通过回调传递出去。
@Override
public void onLoadFinished(Loader loader, Cursor data) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumLoad(data);
}
这样相册数据获取完成 ,LoaderManger+CursorLoader的加载流程告一段落。
MatisseActivity 中实现回调方法,调用AlbumsAdapter .swapCursor( cursor)更新相册列表,前面说过AlbumsAdapter 继承了CursorAdapter
@Override
public void onAlbumLoad(final Cursor cursor) {
//更新相册列表
mAlbumsAdapter.swapCursor(cursor);
// 后面代码暂时隐藏,下篇再看~~~~.
}
先到这里,休息一下吧~