源碼學習之 Matisse

前言

作為一名一年多的 Android 開發(fā)者,面對源碼閱讀一直是躑躅不前。毫不夸張的說內(nèi)心一直是拒絕的,擔心自己沒能力去讀懂那該死的源碼。在和朋友的交流中,我向他請教了很多關于源碼閱讀的技巧。提及最多的就是 搞清楚每個類的職責類與類之間的設計,最終明白源碼的執(zhí)行過程。

重要的是通過閱讀源碼,針對不會的知識點進行查漏補缺,這個過程就像是打怪升級,逐漸的提高自己的技術。在朋友不斷的鼓勵下,終于邁出了源碼閱讀第一步,希望在今后的工作學習中,能把這種學習方式堅持下去。好了,現(xiàn)在進入今天的主題 Matisse

簡介

Matisse 是知乎開源的一款,針對 Android 本地圖像和視頻的選擇器。它主要有以下優(yōu)點:

  • 方便在 Activity 和 Fragment 中使用

  • 支持各種格式的圖片和視頻

  • 應用不同的主題,包括內(nèi)置的兩種主題和自定義主題‘

  • 使用不同的圖片加載器

  • 自定義文件的過濾規(guī)則

  • 方便拓展

針對 方便拓展,基佬RxImagePicker 完美的詮釋了這一優(yōu)點。在 RxImagePicker 中,Matisse 被抽出來放入了 RxImagePicker_Support,成為了 UI 層的基礎組件。

Matisse 的基本使用

參考 Matisse 的 SampleActivity 中的例子,我們來一步步探索 Matisse 的實現(xiàn)過程。

Matisse.from(SampleActivity.this)
            .choose(MimeType.ofImage()) 
            .theme(R.style.Matisse_Dracula)
            .countable(true)
            .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
            .maxSelectable(9)
            .originalEnable(true)
            .maxOriginalSize(10)
            .imageEngine(new Glide4Engine())
            .forResult(REQUEST_CODE_CHOOSE);
  • 調(diào)用 from() 獲取 Matisse 的一個實例。

  • 調(diào)用 choose() 方法獲取 SelectionCreator,進行一些列的參數(shù)配置。如主題、最大選擇數(shù)量。參數(shù)的保存是由全局單例 SelectionSpec 完成的。

  • 通過 forResult() 去打開 MatisseActivity。

接下來我們看下每個類的設計

  1. 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);
        }
    
        /**
         * 獲取 Matisse 對象
         */
        public static Matisse from(Activity activity) {
            return new Matisse(activity);
        }
    
    
        public static Matisse from(Fragment fragment) {
            return new Matisse(fragment);
        }
    
        /**
         * 獲得選擇的數(shù)據(jù)
         */
        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);
        }
    
        /**
         * 獲取 SelectionCreator 對象
         */
        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;
        }
    
    }
    

    可以看到,這個類的代碼還是很少的。這個類的主要職責是:

    • 獲取上下文對象,以弱引用的方式進行保存。

    • 獲取 SelectionCreator 對象,進行參數(shù)配置。

    • 獲得用戶選擇的數(shù)據(jù)。

  1. SelectionCreator,這是一個配置類,所以省略大部分相同的代碼:

    public final class SelectionCreator {
        private final Matisse mMatisse;
        private final SelectionSpec mSelectionSpec;
    
      
        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 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);
            }
        }
    }
    

    這個類的主要職責是:

    • 通過鏈式調(diào)用,進行參數(shù)配置。

    • 開啟 MatisseActivity。

  2. SelectionSpec

    該類主要定義了一系列可配置的參數(shù)

    public final class SelectionSpec {
    
        public List<Item> selectItems;
        public Set<MimeType> mimeTypeSet;
        public boolean mediaTypeExclusive;
        public boolean showSingleMediaType;
        @StyleRes
        public int themeId;
        public int orientation;
        public boolean countable;
        public int maxSelectable;
        public int maxImageSelectable;
        public int maxVideoSelectable;
        public List<Filter> filters;
        public boolean capture;
        public CaptureStrategy captureStrategy;
        public int spanCount;
        public int gridExpectedSize;
        public float thumbnailScale;
        public ImageEngine imageEngine;
        public boolean hasInited;
        public OnSelectedListener onSelectedListener;
        public boolean originalable;
        public int originalMaxSize;
        public OnCheckedListener onCheckedListener;
    
        private SelectionSpec() {
        }
    
        public static SelectionSpec getInstance() {
            return 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();
        }
    }
    

到此,我們應進入 MatisseActivity 中一看究竟,讓我們思考下展示的圖片是如何獲取的,帶著這樣的疑問向 MatisseActivity 邁進吧。

MatisseActivity

public class MatisseActivity extends AppCompatActivity implements
        AlbumCollection.AlbumCallbacks,
        AdapterView.OnItemSelectedListener,
        MediaSelectionFragment.SelectionProvider,
        View.OnClickListener,
        AlbumMediaAdapter.CheckStateListener,
        AlbumMediaAdapter.OnMediaClickListener,
        AlbumMediaAdapter.OnPhotoCapture {
        
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            
        ...... 省略部分代碼


        // 拍照功能
        if (mSpec.capture) {
            mMediaStoreCompat = new MediaStoreCompat(this);
            if (mSpec.captureStrategy == null)
                throw new RuntimeException("Don't forget to set CaptureStrategy.");
            mMediaStoreCompat.setCaptureStrategy(mSpec.captureStrategy);
        }

        
        // 資源文件夾的適配器
        mAlbumsAdapter = new AlbumsAdapter(this, null, false);
        
        // 資源文件夾的 Spinner
        mAlbumsSpinner = new AlbumsSpinner(this);
        mAlbumsSpinner.setOnItemSelectedListener(this);
        mAlbumsSpinner.setSelectedTextView((TextView) findViewById(R.id.selected_album));
        mAlbumsSpinner.setPopupAnchorView(findViewById(R.id.toolbar));
        mAlbumsSpinner.setAdapter(mAlbumsAdapter);
        
        // 資源文件夾的數(shù)據(jù)源
        mAlbumCollection.onCreate(this, this);
        mAlbumCollection.onRestoreInstanceState(savedInstanceState);
        mAlbumCollection.loadAlbums(); // 加載資源文件夾

        updateBottomToolbar();
    }
}        

Matisse 進行數(shù)據(jù)的獲取是通過 Loader 機制完成的。Loader 是官方在 3.0 之后推薦的加載 ContentProvider 資源的最佳使用方式。方便我們在 Activity 和 Fragment 異步加載數(shù)據(jù),在這里安利一篇關于 Loader 的文章。

  1. AlbumSpinner

    AlbumSpinner 是對資源文件夾的 ListPopubWindow 和顯示文件夾名稱的 TextView 進行了一層封裝。最終把點擊事件的處理,通過接口的方式暴露給 MatisseActivity,展示該資源文件夾下所有的圖片。

    /**
     * 每次點擊 ListPopuWindow 都會觸發(fā)
     *
     * @param parent
     * @param view
     * @param position
     * @param id
     */
    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        mAlbumCollection.setStateCurrentSelection(position);
        mAlbumsAdapter.getCursor().moveToPosition(position);
        Album album = Album.valueOf(mAlbumsAdapter.getCursor());
        if (album.isAll() && SelectionSpec.getInstance().capture) {
            album.addCaptureCount();
        }
        onAlbumSelected(album);
    }
    
  2. AlbumAdapter

    繼承自 CursorAdapter ,為 ListPopubWindow 提供數(shù)據(jù)。

  3. AlbumCollection

    調(diào)用 loadAlbums(),去查詢資源文件夾的數(shù)據(jù),具體的查詢過程是交給 AlbumLoader 執(zhí)行。

    最后將查詢的結果通過 AlbumCallbacks 接口回調(diào)給 MatisseActivity,為 AlbumAdapter 提供數(shù)據(jù)并默認顯示第一個文件夾下的圖片。

     /**
     * 資源文件夾,數(shù)據(jù)查詢完成的回調(diào)
     *
     * @param cursor
     */
    @Override
    public void onAlbumLoad(final Cursor cursor) {
        mAlbumsAdapter.swapCursor(cursor);
        // select default album.
        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(new Runnable() {
    
            @Override
            public void run() {
                cursor.moveToPosition(mAlbumCollection.getCurrentSelection());
                mAlbumsSpinner.setSelection(MatisseActivity.this,
                        mAlbumCollection.getCurrentSelection());
                Album album = Album.valueOf(cursor);
                if (album.isAll() && SelectionSpec.getInstance().capture) {
                    album.addCaptureCount();
                }
                onAlbumSelected(album);
            }
        });
    }
    

現(xiàn)在資源文件夾的數(shù)據(jù)已經(jīng)獲取到了,照片墻的數(shù)據(jù)又是如何獲得的哪?答案就在 onAlbumSelected() 中。

private void onAlbumSelected(Album album) {
        if (album.isAll() && album.isEmpty()) {
            mContainer.setVisibility(View.GONE);
            mEmptyView.setVisibility(View.VISIBLE);
        } else {
            mContainer.setVisibility(View.VISIBLE);
            mEmptyView.setVisibility(View.GONE);
            Fragment fragment = MediaSelectionFragment.newInstance(album);
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
                    .commitAllowingStateLoss();
        }
    }

可以看到照片墻的實現(xiàn)是通過 MediaSelectionFragment 進行展示的。

MediaSelectionFragment

顯示照片墻的 Fragment,布局中只有一個 RecyclerView。

在初始化 MediaSelectionFragment 的時候,傳入了一個 Album 對象,這個對象的作用又是做什么的呢?讓我們繼續(xù)追蹤 MediaSelectionFragment 中的代碼。

 @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Album album = getArguments().getParcelable(EXTRA_ALBUM);

        mAdapter = new AlbumMediaAdapter(getContext(),
                mSelectionProvider.provideSelectedItemCollection(), mRecyclerView);
        mAdapter.registerCheckStateListener(this); // 注冊 CheckView 是否選中的監(jiān)聽事件
        mAdapter.registerOnMediaClickListener(this); // 注冊圖片的點擊事件
        mRecyclerView.setHasFixedSize(true);

        int spanCount;
        SelectionSpec selectionSpec = SelectionSpec.getInstance();
        if (selectionSpec.gridExpectedSize > 0) {
            spanCount = UIUtils.spanCount(getContext(), selectionSpec.gridExpectedSize);
        } else {
            spanCount = selectionSpec.spanCount;
        }
        mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), spanCount));

        int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
        mRecyclerView.addItemDecoration(new MediaGridInset(spanCount, spacing, false));
        mRecyclerView.setAdapter(mAdapter);
        mAlbumMediaCollection.onCreate(getActivity(), this);
        mAlbumMediaCollection.load(album, selectionSpec.capture); // 加載媒體數(shù)據(jù)
    }

原來 Album 對象最終傳遞到了 AlbumMediaCollection 中。

  1. AlbumMediaCollection

    和查詢資源文件夾的套路一樣,都是通過 Loader 機制完成。

    根據(jù)傳入的 Album 對象,獲取該文件夾的 Id 作為查詢參數(shù),通過 AlbumMediaLoader 去查詢數(shù)據(jù)。

    將最終查詢的結果,通過 AlbumMediaCallbacks 接口暴露給 MediaSelectionFragment 。

  2. AlbumMediaAdapter

    為 RecyclerView 提供數(shù)據(jù)。以下是綁定數(shù)據(jù)的代碼。

    @Override
    protected void onBindViewHolder(final RecyclerView.ViewHolder holder, Cursor cursor) {
        if (holder instanceof CaptureViewHolder) {
            CaptureViewHolder captureViewHolder = (CaptureViewHolder) holder;
            Drawable[] drawables = captureViewHolder.mHint.getCompoundDrawables();
            TypedArray ta = holder.itemView.getContext().getTheme().obtainStyledAttributes(
                    new int[]{R.attr.capture_textColor});
            int color = ta.getColor(0, 0);
            ta.recycle();
    
            for (int i = 0; i < drawables.length; i++) {
                Drawable drawable = drawables[i];
                if (drawable != null) {
                    final Drawable.ConstantState state = drawable.getConstantState();
                    if (state == null) {
                        continue;
                    }
    
                    Drawable newDrawable = state.newDrawable().mutate();
                    newDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
                    newDrawable.setBounds(drawable.getBounds());
                    drawables[i] = newDrawable;
                }
            }
            captureViewHolder.mHint.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3]);
        } else if (holder instanceof MediaViewHolder) {
            MediaViewHolder mediaViewHolder = (MediaViewHolder) holder;
    
            final Item item = Item.valueOf(cursor);
            mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo(
                    getImageResize(mediaViewHolder.mMediaGrid.getContext()),
                    mPlaceholder,
                    mSelectionSpec.countable,
                    holder
            ));
            mediaViewHolder.mMediaGrid.bindMedia(item); // 綁定數(shù)據(jù)
            mediaViewHolder.mMediaGrid.setOnMediaGridClickListener(this); // 設置條目的點擊事件
            setCheckStatus(item, mediaViewHolder.mMediaGrid);
        }
    }
    

    在這個類中,還聲明了以下 3 個接口。具體的實現(xiàn)有 MatisseActivity 完成。

    
    // 修改底部工具欄的狀態(tài),更改已選擇條目的集合數(shù)據(jù)
     public interface CheckStateListener {
        void onUpdate();
    }
    
    // 處理點擊條目的跳轉
    public interface OnMediaClickListener {
        void onMediaClick(Album album, Item item, int adapterPosition);
    }
    
    // 相機按鈕的點擊事件
    public interface OnPhotoCapture {
        void capture();
    }
    
  3. MediaGrid

    每個條目顯示的View。自定義的 ViewGroup(包含 ImageView,CheckView,顯示視頻時長的 TextView,是否是 Gif 標志的 ImageView)

    進行數(shù)據(jù)具體的綁定工作。并將點擊事件通過接口暴露給 AlbumMediaAdapter。

    public interface OnMediaGridClickListener {
    
        // ImageView 的點擊事件
        void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder);
        
        // CheckView 的點擊事件
        void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
    }
    

通過這么多接口的回調(diào),減少了每個類的代碼,使得每個類的邏輯更加清晰。這樣的設計的確值得學習和借鑒。

至此照片墻的部分了解的差不多了,讓我們?nèi)タ聪骂A覽界面的設計。

BasePreviewActivity

打開預覽界面有兩種方式:

  1. 點擊圖片,直接進入的預覽界面 AlbumPreviewActivity

     // 點擊圖片
        @Override
        public void onMediaClick(Album album, Item item, int adapterPosition) {
            Intent intent = new Intent(this, AlbumPreviewActivity.class);
            intent.putExtra(AlbumPreviewActivity.EXTRA_ALBUM, album); // 該圖片對應的文件夾對象
            intent.putExtra(AlbumPreviewActivity.EXTRA_ITEM, item);
            intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); 
            intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
            startActivityForResult(intent, REQUEST_CODE_PREVIEW);
        }
    
  2. 點擊 CheckView,選中照片,再點擊底部的預覽按鈕 SelectedPreviewActivity

    @Override
    public void onClick(View v) {
        // 點擊預覽按鈕
        if (v.getId() == R.id.button_preview) {
            Intent intent = new Intent(this, SelectedPreviewActivity.class);
            intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle());
            intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
            startActivityForResult(intent, REQUEST_CODE_PREVIEW);
    
        }
    }    
    

通過方式 1 進入的 AlbumPreviewActivity ,攜帶的參數(shù)有 該圖片對應的文件夾對象。在其內(nèi)部通過 Loader 機制去加載預覽圖片的數(shù)據(jù):

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!SelectionSpec.getInstance().hasInited) {
            setResult(RESULT_CANCELED);
            finish();
            return;
        }
        mCollection.onCreate(this, this);
        // 點擊的圖片,所屬的文件夾對象
        Album album = getIntent().getParcelableExtra(EXTRA_ALBUM);
        mCollection.load(album); // 加載數(shù)據(jù)

        Item item = getIntent().getParcelableExtra(EXTRA_ITEM); // 點擊的圖片對象
        if (mSpec.countable) {
            mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
        } else {
            mCheckView.setChecked(mSelectedCollection.isSelected(item));
        }
        updateSize(item);
    }

通過方式 2 進入SelectedPreviewActivity,直接把攜帶的數(shù)據(jù),作為數(shù)據(jù)源進行展示

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!SelectionSpec.getInstance().hasInited) {
            setResult(RESULT_CANCELED);
            finish();
            return;
        }
        
        Bundle bundle = getIntent().getBundleExtra(EXTRA_DEFAULT_BUNDLE);
        List<Item> selected = bundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION);
        mAdapter.addAll(selected);
        mAdapter.notifyDataSetChanged();
        if (mSpec.countable) {
            mCheckView.setCheckedNum(1);
        } else {
            mCheckView.setChecked(true);
        }
        mPreviousPos = 0;
        updateSize(selected.get(0));
    }

由于同樣都是預覽界面,代碼的重復性比較多,便抽取了一個 BasePreviewActivity。該主要是由 ViewPager 和 Fragment 組成。這部分的代碼就比較容易閱讀了。

總結

在 Matisse 中,有許多關于自定義 View 的知識,如自定義的 CheckViewCheckRadioView。關于自定義 View 這方面的知識,也非常值得我們學習。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容