知乎開源matisse精讀:學(xué)習(xí)如何打造精美實用的圖片選擇器(一)

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)

源碼結(jié)構(gòu).png
源碼結(jié)構(gòu).png

其中被框起來的部分是需要著重關(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ù)就是它!


.png

最終借助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);
        // 后面代碼暫時隱藏,下篇再看~~~~.
   }

先到這里,休息一下吧~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容