Matisse 是一款知乎開源的設(shè)計精美的圖片選擇器,這里是源碼地址。先放兩張官方圖來看一下。
這么俏皮可愛的界面怎么讓人不喜愛呢?下面就來了解一下它吧。
基本用法:
Matisse.from(SampleActivity.this)
.choose(MimeType.ofAll(), false) //參數(shù)1 顯示資源類型 參數(shù)2 是否可以同時選擇不同的資源類型 true表示不可以 false表示可以
// .theme(R.style.Matisse_Dracula) //選擇主題 默認(rèn)是藍(lán)色主題,Matisse_Dracula為黑色主題
.countable(true) //是否顯示數(shù)字
.capture(true) //是否可以拍照
.captureStrategy(//參數(shù)1 true表示拍照存儲在共有目錄,false表示存儲在私有目錄;參數(shù)2與 AndroidManifest中authorities值相同,用于適配7.0系統(tǒng) 必須設(shè)置
new CaptureStrategy(true, "com.zhihu.matisse.sample.fileprovider"))
.maxSelectable(9) //最大選擇資源數(shù)量
.addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K)) //添加自定義過濾器
.gridExpectedSize( getResources().getDimensionPixelSize(R.dimen.grid_expected_size)) //設(shè)置列寬
.restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) //設(shè)置屏幕方向
.thumbnailScale(0.85f) //圖片縮放比例
.imageEngine(new GlideEngine()) //選擇圖片加載引擎
.forResult(REQUEST_CODE_CHOOSE); //設(shè)置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));
}
}
要注意的就是如果想集成拍照功能,除了設(shè)置capture(true) 以外 還必須需要設(shè)置captureStrategy。并在
AndroidManifest中注冊FileProvider,用于適配7.0系統(tǒng)拍照。更多代碼細(xì)節(jié)請參看官方demo,這里就不列出了。下面開始就對源碼進(jìn)行一些分析。
源碼結(jié)構(gòu)
其中被框起來的部分是需要著重關(guān)注的主線流程。
開啟Matisse之旅
首先回歸基本用法 從Matisse.from()開始。進(jìn)入該方法就來到了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);
}
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);
}
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;
}
}
接著調(diào)用 choose(Set<MimeType> mimeTypes, boolean mediaTypeExclusive)方法 參數(shù)1指定要顯示資源類型(圖片,視頻), 參數(shù)2 是否可以同時選擇不同的資源類型 true表示不可以 false表示可以。看一下MimeType是個啥
public enum MimeType {
// ============== images ==============
JPEG("image/jpeg", new HashSet<String>() {
{
add("jpg");
add("jpeg");
}
}),
PNG("image/png", new HashSet<String>() {
{
add("png");
}
}),
GIF("image/gif", new HashSet<String>() {
{
add("gif");
}
}),
BMP("image/x-ms-bmp", new HashSet<String>() {
{
add("bmp");
}
}),
WEBP("image/webp", new HashSet<String>() {
{
add("webp");
}
}),
// ============== videos ==============
MPEG("video/mpeg", new HashSet<String>() {
{
add("mpeg");
add("mpg");
}
}),
MP4("video/mp4", new HashSet<String>() {
{
add("mp4");
add("m4v");
}
}),
QUICKTIME("video/quicktime", new HashSet<String>() {
{
add("mov");
}
}),
THREEGPP("video/3gpp", new HashSet<String>() {
{
add("3gp");
add("3gpp");
}
}),
THREEGPP2("video/3gpp2", new HashSet<String>() {
{
add("3g2");
add("3gpp2");
}
}),
MKV("video/x-matroska", new HashSet<String>() {
{
add("mkv");
}
}),
WEBM("video/webm", new HashSet<String>() {
{
add("webm");
}
}),
TS("video/mp2ts", new HashSet<String>() {
{
add("ts");
}
}),
AVI("video/avi", new HashSet<String>() {
{
add("avi");
}
});
private final String mMimeTypeName;
private final Set<String> mExtensions;
MimeType(String mimeTypeName, Set<String> extensions) {
mMimeTypeName = mimeTypeName;
mExtensions = extensions;
}
//添加所有格式
public static Set<MimeType> ofAll() {
return EnumSet.allOf(MimeType.class);
}
public static Set<MimeType> of(MimeType type, MimeType... rest) {
return EnumSet.of(type, rest);
}
//添加所有圖片格式
public static Set<MimeType> ofImage() {
return EnumSet.of(JPEG, PNG, GIF, BMP, WEBP);
}
//添加所有視頻格式
public static Set<MimeType> ofVideo() {
return EnumSet.of(MPEG, MP4, QUICKTIME, THREEGPP, THREEGPP2, MKV, WEBM, TS, AVI);
}
@Override
public String toString() {
return mMimeTypeName;
}
/**
* 檢查資源類型是否在選擇范圍內(nèi)
* @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;
}
這是個枚舉類,枚舉了所有的圖片和視頻格式。我們調(diào)用的MimeType.ofAll()就是將所有的資源格式添加到了EnumSet集合中,MimeType.ofImage() 是只添加圖片格式,MimeType.ofVideo() 是只添加視頻格式。EnumSet是專門用來存儲枚舉類的數(shù)據(jù)結(jié)構(gòu),以便后續(xù)的遍歷操作。枚舉的寫法讓代碼結(jié)構(gòu)非常清晰,使用EnumSet存儲也非常方便。性能的話暫時就不過多考慮了~。
還是關(guān)注choose方法 ,返回給我們了SelectionCreator對象,SelectionCreator就是我們喜聞樂見的構(gòu)造者了~主要參數(shù)設(shè)置都是通過它了。
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<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 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);
}
}
}
構(gòu)造方法中,首先拿到入口類對象matisse,拿到了SelectionSpec的一個單例,向單例中存儲了剛才添加的Set集合,和默認(rèn)的屏幕方向策略SCREEN_ORIENTATION_UNSPECIFIED(不限定屏幕方向)。然后我們其他的設(shè)置也通過構(gòu)造者模式存儲在SelectionSpec的單例中。其中屏幕方向策略的設(shè)置,使用了編譯時注解代替了枚舉,限定了我們傳參的范圍。
SelectionSpec主要代碼
public final class SelectionSpec {
public Set<MimeType> mimeTypeSet;
public boolean mediaTypeExclusive;
public boolean showSingleMediaType;
@StyleRes
public int themeId;
public int orientation;
public boolean countable;
public int maxSelectable;
public List<Filter> 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();
}
}
我們設(shè)置的所有屬性就存儲在這里。
前期準(zhǔn)備工作完成,隨著一句forResult(REQUEST_CODE_CHOOSE),就進(jìn)入了Matisse主頁面了。
public class MatisseActivity extends AppCompatActivity implements
AlbumCollection.AlbumCallbacks, AdapterView.OnItemSelectedListener,
MediaSelectionFragment.SelectionProvider, View.OnClickListener,
AlbumMediaAdapter.CheckStateListener, AlbumMediaAdapter.OnMediaClickListener,
AlbumMediaAdapter.OnPhotoCapture {
主頁面MatisseActivity 做的事情比較多,實現(xiàn)了不少的回調(diào)接口。但現(xiàn)在暫時不必關(guān)心,我們就從oncreate方法開始
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// programmatically set theme before super.onCreate()
mSpec = SelectionSpec.getInstance();
//設(shè)置主題
setTheme(mSpec.themeId);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_matisse);
//設(shè)置屏幕方向
if (mSpec.needOrientationRestriction()) {
setRequestedOrientation(mSpec.orientation);
}
//如果設(shè)置了可拍照 需要傳遞設(shè)置的captureStrategy參數(shù)
if (mSpec.capture) {
mMediaStoreCompat = new MediaStoreCompat(this);
if (mSpec.captureStrategy == null)
throw new RuntimeException("Don't forget to set CaptureStrategy.");
mMediaStoreCompat.setCaptureStrategy(mSpec.captureStrategy);
}
//設(shè)置toolbar及相冊標(biāo)題樣式
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);
//================ 主要業(yè)務(wù)流程 ================
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();
}
重點關(guān)注oncreate的后面主要業(yè)務(wù)流程部分 :
首先執(zhí)行了mSelectedCollection.onCreate(savedInstanceState); mSelectedCollection是SelectedItemCollection類的對象,這是比較重要的一個類。由于資源是可以多選的并且有序的,這個類功能就是管理選中項的集合。在主頁以及預(yù)覽頁面,多處涉及到對資源的選擇和取消操作時,把這部分邏輯分離出一個單獨的管理類是十分有必要的。來看一下mSelectedCollection的onreate方法:
public void onCreate(Bundle bundle) {
if (bundle == null) {
mItems = new LinkedHashSet<>();
} else {
List<Item> saved = bundle.getParcelableArrayList(STATE_SELECTION);
mItems = new LinkedHashSet<>(saved);
mCollectionType = bundle.getInt(STATE_COLLECTION_TYPE, COLLECTION_UNDEFINED);
}
}
初始情況下創(chuàng)建一個LinkedHashSet集合,用來存儲選中項,如果遇到activity銷毀重建的情況則從savedInstanceState中獲取已經(jīng)保存過的集合。
然后實例了AlbumsAdapter ,AlbumsSpinner兩個類一個是加載相冊的適配器,一個用于相冊列表展示(注意這里是相冊,不是圖像和視頻的),代碼不多,先貼上來,細(xì)節(jié)可以先不關(guān)注,明確類的作用即可:
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();
//使用設(shè)置的主題顏色對目標(biāo)Drawable(這里是一個小箭頭)進(jìn)行SRC_IN模式合成 達(dá)到改變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();
}
});
//設(shè)置textView向下拖拽可下拉ListPopupWindow
mSelected.setOnTouchListener(mListPopupWindow.createDragToOpenListener(mSelected));
}
/**
* 設(shè)置錨點view
* @param view
*/
public void setPopupAnchorView(View view) {
mListPopupWindow.setAnchorView(view);
}
}
AlbumsSpinner 內(nèi)部使用了ListPopupWindow (PopupWindow+ListView的組合),接受CursorAdapter對象 ,而AlbumsAdapter 正是繼承了CursorAdapter 。CursorAdapter 需要接受的Cursor對象,就需要我們接下來提供。
還是oncreate方法,來到mAlbumCollection.onCreate(this, this)這一句;看看這個類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();
if (context == null) {
return null;
}
return AlbumLoader.newInstance(context);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumLoad(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumReset();
}
public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) {
mContext = new WeakReference<Context>(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,實現(xiàn)了 LoaderManager.LoaderCallbacks<T>接口,這里指定T為Cursor,表示我們要獲取一個cursor。不了解LoaderManager的朋友看看這篇,就可以繼續(xù)往下走了。mAlbumCollection.onCreate方法中得到了LoaderManager對象 ,接著又調(diào)用了mAlbumCollection.loadAlbums(),其中調(diào)用了initLoader,這樣就回調(diào)到LoaderManager.LoaderCallbacks的實現(xiàn)方法onCreateLoader中,而回調(diào)方法onLoadFinished(Loader<Cursor> loader, Cursor data)第二個參數(shù)即為onCreateLoader 的返回值。所以我們要去關(guān)心一下這個返回值的來源:AlbumLoader.newInstance(context);那就看看AlbumLoader這個類吧。
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分組查詢文件夾資源內(nèi)數(shù)量
//MediaStore.Files.FileColumns.MEDIA_TYPE 資源類型
//MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE 圖片類型
//MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO 視頻類型
//MediaStore.MediaColumns.SIZE 資源大小
// GROUP BY (bucket_id 根據(jù)bucket_id分組查詢,統(tǒng)計數(shù)量
// === 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;
//判斷只查詢圖片或者視頻其中一種類型還是兩種都查詢,構(gòu)造不同的查詢條件
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方法是為了在查詢出來的結(jié)果的上加一行數(shù)據(jù) 這行數(shù)據(jù)是由列名(ALL),
* 所有的資源的總數(shù)以及一個資源地址,其他列用Album.ALBUM_ID_ALL(無用數(shù)據(jù))占位。目的是用來
* 展現(xiàn)在popwindow中的第一個數(shù)據(jù):全部
* @return
*/
@Override
public Cursor loadInBackground() {
Cursor albums = super.loadInBackground();
//創(chuàng)建一個cursor
MatrixCursor allAlbum = new MatrixCursor(COLUMNS);
int totalCount = 0;
String allAlbumCoverPath = "";
if (albums != null) {
//獲得全部資源總數(shù)
while (albums.moveToNext()) {
totalCount += albums.getInt(albums.getColumnIndex(COLUMN_COUNT));
}
//拿到第一個相冊封面資源的地址
if (albums.moveToFirst()) {
allAlbumCoverPath = albums.getString(albums.getColumnIndex(MediaStore.MediaColumns.DATA));
}
}
//給新創(chuàng)建的cursor添加數(shù)據(jù)
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 ,需提供構(gòu)造方法, 在構(gòu)造方法中調(diào)用父類構(gòu)造方法執(zhí)行查詢
private AlbumLoader(Context context, String selection, String[] selectionArgs) {
super(context, QUERY_URI, PROJECTION, selection, selectionArgs, BUCKET_ORDER_BY);
}
CursorLoader 內(nèi)部是通過 ContentResolver 執(zhí)行查詢, 即我們只需要提供ContentResolver執(zhí)行查詢所需參數(shù):
QUERY_URI:uri
PROJECTION:查詢字段名
selection:where約束條件
selectionArgs:where中占位符的值
BUCKET_ORDER_BY:排序方式;
其余工作全部交由CursorLoader 執(zhí)行即可。
構(gòu)造方法是由入口方法newInstance調(diào)用的,來到newInstance方法,這里首先判斷一下用戶設(shè)置的是查詢類型,只查圖片,只查視頻還是兩種都查。判斷方法:
public boolean onlyShowImages() {
return showSingleMediaType && MimeType.ofImage().containsAll(mimeTypeSet);
}
public boolean onlyShowVideos() {
return showSingleMediaType && MimeType.ofVideo().containsAll(mimeTypeSet);
}
判斷一下showSingleMediaType 參數(shù)是否為true ,即是否只顯示一種類型 ,同時比對EmunSet集合中是否已添加這種類型。由于代碼中先判斷了SelectionSpec.getInstance().onlyShowImages(),所以當(dāng)showSingleMediaType 設(shè)置為ture但EmunSet既添加了image又添加了Video類型時,優(yōu)先展示image類型。
確定要查詢的類型后,為此就構(gòu)造出了不同的selection,selectionArgs。最后調(diào)用構(gòu)造方法執(zhí)行查詢。
關(guān)于構(gòu)造方法中所需的其他參數(shù)的構(gòu)建,代碼中已給出注釋,不再詳細(xì)說明。
按理說這樣已經(jīng)完成了查詢,可以在onLoadFinished中拿到攜帶相冊信息的cursor了。但是發(fā)現(xiàn)AlbumLoader 又重寫了CursorLoader的 loadInBackground方法,而loadInBackground就是CursorLoader執(zhí)行查詢并返回結(jié)果的方法。為什么這樣呢?那就看看里面干了什么。關(guān)注loadInBackground方法:這里首先上來調(diào)用了super.loadInBackground()獲取了查詢結(jié)果的cursor。然后創(chuàng)建了一個MatrixCursor并傳入了一個String數(shù)組COLUMNS :
private static final String[] COLUMNS = {
MediaStore.Files.FileColumns._ID,
"bucket_id",
"bucket_display_name",
MediaStore.MediaColumns.DATA,
COLUMN_COUNT};
干啥用的呢?查了資料得知原來MatrixCursor是一種允許我們自己來構(gòu)建的一種cursor。這里傳入了與查詢條件完全相同的數(shù)組,就構(gòu)造出了一個與查詢結(jié)果cursor相同結(jié)構(gòu)的cursor。之后又遍歷了結(jié)果集cursor,累加得到所有相冊內(nèi)資源的總數(shù),又拿到第一個相冊封面資源的地址,把這兩個數(shù)據(jù)通過MatrixCursor .addRow方法向cursor中的對應(yīng)列添加了一行數(shù)據(jù),其余的參數(shù)以Album.ALBUM_ID_ALL(無實際意義)進(jìn)行填充。這樣就誕生了一個新的cursor,所包含的數(shù)據(jù)就是它!
最終借助MergeCursor將這兩個cursor垂直拼接成一個新的cursor返回給了我們。這樣了我們拿到的cursor就多了一個全部相冊啦!
回到我們的AlbumCollection類的onLoadFinished中就拿到我們查詢并加工好的數(shù)據(jù)了。并通過回調(diào)傳遞出去。
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumLoad(data);
}
這樣相冊數(shù)據(jù)獲取完成 ,LoaderManger+CursorLoader的加載流程告一段落。
MatisseActivity 中實現(xiàn)回調(diào)方法,調(diào)用AlbumsAdapter .swapCursor( cursor)更新相冊列表,前面說過AlbumsAdapter 繼承了CursorAdapter
@Override
public void onAlbumLoad(final Cursor cursor) {
//更新相冊列表
mAlbumsAdapter.swapCursor(cursor);
// 后面代碼暫時隱藏,下篇再看~~~~.
}
先到這里,休息一下吧~