源码学习之 Matisse

前言

作为一名一年多的 Android 开发者,面对源码阅读一直是踯躅不前。毫不夸张的说内心一直是拒绝的,担心自己没能力去读懂那该死的源码。在和朋友的交流中,我向他请教了很多关于源码阅读的技巧。提及最多的就是 搞清楚每个类的职责类与类之间的设计最终明白源码的执行过程

重要的是通过阅读源码,针对不会的知识点进行查漏补缺,这个过程就像是打怪升级,逐渐的提高自己的技术。在朋友不断的鼓励下,终于迈出了源码阅读第一步,希望在今后的工作学习中,能把这种学习方式坚持下去。好了,现在进入今天的主题 Matisse

简介

Matisse 是知乎开源的一款,针对 Android 本地图像和视频的选择器。它主要有以下优点:

  • 方便在 Activity 和 Fragment 中使用

  • 支持各种格式的图片和视频

  • 应用不同的主题,包括内置的两种主题和自定义主题‘

  • 使用不同的图片加载器

  • 自定义文件的过滤规则

  • 方便拓展

针对 方便拓展基佬RxImagePicker 完美的诠释了这一优点。在 RxImagePicker 中,Matisse 被抽出来放入了 RxImagePicker_Support,成为了 UI 层的基础组件。

Matisse 的基本使用

参考 Matisse 的 SampleActivity 中的例子,我们来一步步探索 Matisse 的实现过程。

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);
  • 调用 from() 获取 Matisse 的一个实例。

  • 调用 choose() 方法获取 SelectionCreator,进行一些列的参数配置。如主题、最大选择数量。参数的保存是由全局单例 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);
        }
    
        /**
         * 获得选择的数据
         */
        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 对象,进行参数配置。

    • 获得用户选择的数据。

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

    这个类的主要职责是:

    • 通过链式调用,进行参数配置。

    • 开启 MatisseActivity

  2. SelectionSpec

    该类主要定义了一系列可配置的参数

    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);
        
        // 资源文件夹的数据源
        mAlbumCollection.onCreate(this, this);
        mAlbumCollection.onRestoreInstanceState(savedInstanceState);
        mAlbumCollection.loadAlbums(); // 加载资源文件夹

        updateBottomToolbar();
    }
}        

Matisse 进行数据的获取是通过 Loader 机制完成的。Loader 是官方在 3.0 之后推荐的加载 ContentProvider 资源的最佳使用方式。方便我们在 Activity 和 Fragment 异步加载数据,在这里安利一篇关于 Loader 的文章。

  1. AlbumSpinner

    AlbumSpinner 是对资源文件夹的 ListPopubWindow 和显示文件夹名称的 TextView 进行了一层封装。最终把点击事件的处理,通过接口的方式暴露给 MatisseActivity,展示该资源文件夹下所有的图片。

    /**
     * 每次点击 ListPopuWindow 都会触发
     *
     * @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 提供数据。

  3. AlbumCollection

    调用 loadAlbums(),去查询资源文件夹的数据,具体的查询过程是交给 AlbumLoader 执行。

    最后将查询的结果通过 AlbumCallbacks 接口回调给 MatisseActivity,为 AlbumAdapter 提供数据并默认显示第一个文件夹下的图片。

     /**
     * 资源文件夹,数据查询完成的回调
     *
     * @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);
            }
        });
    }
    

现在资源文件夹的数据已经获取到了,照片墙的数据又是如何获得的哪?答案就在 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();
        }
    }

可以看到照片墙的实现是通过 MediaSelectionFragment 进行展示的。

MediaSelectionFragment

显示照片墙的 Fragment,布局中只有一个 RecyclerView。

在初始化 MediaSelectionFragment 的时候,传入了一个 Album 对象,这个对象的作用又是做什么的呢?让我们继续追踪 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 是否选中的监听事件
        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); // 加载媒体数据
    }

原来 Album 对象最终传递到了 AlbumMediaCollection 中。

  1. AlbumMediaCollection

    和查询资源文件夹的套路一样,都是通过 Loader 机制完成。

    根据传入的 Album 对象,获取该文件夹的 Id 作为查询参数,通过 AlbumMediaLoader 去查询数据。

    将最终查询的结果,通过 AlbumMediaCallbacks 接口暴露给 MediaSelectionFragment 。

  2. AlbumMediaAdapter

    为 RecyclerView 提供数据。以下是绑定数据的代码。

    @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); // 绑定数据
            mediaViewHolder.mMediaGrid.setOnMediaGridClickListener(this); // 设置条目的点击事件
            setCheckStatus(item, mediaViewHolder.mMediaGrid);
        }
    }
    

    在这个类中,还声明了以下 3 个接口。具体的实现有 MatisseActivity 完成。

    
    // 修改底部工具栏的状态,更改已选择条目的集合数据
     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)

    进行数据具体的绑定工作。并将点击事件通过接口暴露给 AlbumMediaAdapter

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

通过这么多接口的回调,减少了每个类的代码,使得每个类的逻辑更加清晰。这样的设计的确值得学习和借鉴。

至此照片墙的部分了解的差不多了,让我们去看下预览界面的设计。

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 ,携带的参数有 该图片对应的文件夹对象。在其内部通过 Loader 机制去加载预览图片的数据:

@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); // 加载数据

        Item item = getIntent().getParcelableExtra(EXTRA_ITEM); // 点击的图片对象
        if (mSpec.countable) {
            mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
        } else {
            mCheckView.setChecked(mSelectedCollection.isSelected(item));
        }
        updateSize(item);
    }

通过方式 2 进入SelectedPreviewActivity,直接把携带的数据,作为数据源进行展示

@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 这方面的知识,也非常值得我们学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容