知乎 Matisse 源码解析,带你探究高效图片选择库的秘密

2,055 阅读14分钟

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

目录

  • 基本介绍
  • 整体的设计和实现流程
  • 资源文件夹的加载和展示
  • 主页图片墙的实现
  • 预览界面的实现
  • 总结

一、基本介绍


Matisse 是「知乎」开源的一款十分精美的本地图像和视频选择库。

Matisse

Matisse 的代码写的相当的简洁、规范,很有学习的价值。

讲一下 Matisse 的一些优点:

  • 在 Activity 或 Fragment 都可以轻松的调用

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

  • 支持不同的样式,包括两种内置主题和自定义主题

  • 可以自定义文件的过滤规则

可以看到 Matisse 的可拓展性是非常强的,不仅可以自定义我们需要的主题,而且还可以按照需求来过滤出我们想要的文件,除此之外,Matisse 采用了建造者模式,使得我们可以通过链式调用的方式,配置各种各样的属性,使我们的图片选择更加灵活。

二、整体的设计和实现流程


在介绍 Matisse 的工作流程之前,我们先来看看几个比较重要的类,有助于我们后面的理解

类名 功能
Matisse 通过外部传入的 Activity 或 Fragment,以弱引用的形式进行保存,同时通过 from() 方法返回 SelectionCreator 进行各个参数的配置
SelectionCreator 通过建造者模式,链式配置我们需要的各种属性
MatisseActivity Matisse 首页的 Activity,将图片和视频进行展示

我们先从 Matisse 的使用入手,看看 Matisse 的工作流程。

Matisse.from(MainActivity.this)
        .choose(MimeType.allOf()) // 1、获取 SelectionCreator
        .countable(true)
        .maxSelectable(9)
        .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
        .gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))
        .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
        .thumbnailScale(0.85f)
        .imageEngine(new GlideEngine()) // 2、配置各种各样的参数
        .forResult(REQUEST_CODE_CHOOSE); // 3、打开 MatisseActivity

上面的使用代码,我们以 Activity 为例,可以分成三部分来看

  • 将外部传入的 Activity 以弱引用的形式进行保存,然后调用 choose() 获取 SelectionCreator

  • 通过链式调用的方式,配置 SelectionCreator 的各种属性,如可选择的数量、缩略图的大小、加载图片的引擎等

  • 使用从第一步中传入的 Activity 调用 startActivityForResult(),并从外部传入请求码,以便到时候返回所选择图片的 List

具体的流程图如下:

Matisse 流程图

以上便是 Matisse 的工作流程,接下来详细的分析下相关的类。有一点要先说明一下,我下面贴出的所有类中的源码并不是完整的代码,而是将源码中与性能、兼容性、扩展性有关的代码剔除后的「核心代码」。

Matisse

public final class Matisse {

    private final WeakReference<Activity> mContext;
    private final WeakReference<Fragment> mFragment;

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

    /**
     *  在打开 MatisseActivity 的 Activity 或 Fragment 中获取用户选择的媒体 Uri 列表
     */
    public static List<Uri> obtainResult(Intent data) {
        return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION);
    }

    public SelectionCreator choose(Set<MimeType> mimeTypes, boolean mediaTypeExclusive) {
        return new SelectionCreator(this, mimeTypes, mediaTypeExclusive);
    }

}

这个类的代码还是很简单的,将外部传入的 Activity 或 Fragment,用弱引用的形式保存,防止内存泄露。然后通过 choose() 方法返回 SelectionCreator 用于之后参数的配置。等到图片选择完成后,我们可以在 Fragment 或 Activity 中的 onActivityResult() 中通过 obtainResult() 获取我们所选择媒体的 Uri 列表。

SelectionCreator

public final class SelectionCreator {
    private final Matisse mMatisse;
    private final SelectionSpec mSelectionSpec;

    SelectionCreator(Matisse matisse, @NonNull Set<MimeType> mimeTypes) {
        mMatisse = matisse;
        mSelectionSpec = SelectionSpec.getCleanInstance();
        mSelectionSpec.mimeTypeSet = mimeTypes;
    }

    public SelectionCreator theme(@StyleRes int themeId) {
        mSelectionSpec.themeId = themeId;
        return this;
    }

    public SelectionCreator maxSelectable(int maxSelectable) {
        mSelectionSpec.maxSelectable = maxSelectable;
        return this;
    }
    // 其余方法都类似上面这两个,这里面就不贴出来了

    public void forResult(int requestCode) {
        Activity activity = mMatisse.getActivity();
        Intent intent = new Intent(activity, MatisseActivity.class);
        Fragment fragment = mMatisse.getFragment();
        if (fragment != null) {
            fragment.startActivityForResult(intent, requestCode);
        } else {
            activity.startActivityForResult(intent, requestCode);
        }
    }

}

可以看到 SelectionCreator 内部保存了 Matisse 的实例,用于获取外部调用的 Activity 或 Fragment,以及一个 SelectionSpec 类的实例,这个类封装了图片加载类中常见的参数,使得 SelectionCreator 的代码更加简洁。SelectionCreator 内部使用了建造者模式,让我们能够进行链式调用,配置各种各样的属性。最后 forResult() 里面其实就是跳转到 MatisseActivity,然后通过外部传入的 requestCode 将用户选择的媒体 Uri 列表返回给相应的 Activity 或 Fragment.

三、资源文件夹的加载和展示


Matisse 中所展示的资源都是用 Loader 机制进行加载的,Loader 机制是 Android 3.0 之后官方推荐的加载 ContentProvider 中资源的最佳方式,不仅能极大地提高我们资源加载的速度,而且还能让我们的代码变得更加的简洁。对于 Loader 机制不熟悉的同学,可以先看下这篇文章 Android Loader 机制,让你的数据加载更加高效

先附上此项操作的流程图:

继承了 Cursor 的 AlbumLoader,作为资源的加载器,通过配置与资源相关的一些参数,从而加载资源。AlbumCollection 实现了 LoaderManager.LoaderCallbacks 接口,将 AlbumLoader 作为加载器,其内部定义了 AlbumCallbacks 接口,在加载资源完成后,将包含数据的 Cursor 回调给外部调用的 MatisseActivity,然后在 MatisseActivity 中进行资源文件夹的展示。

AlbumsLoader

public class AlbumLoader extends CursorLoader {

    // content://media/external/file
    private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external");

    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,
            "bucket_id",
            "bucket_display_name",
            MediaStore.MediaColumns.DATA,
            "COUNT(*) AS " + COLUMN_COUNT};

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

    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) {
        return new AlbumLoader(context, SELECTION, SELECTION_ARGS);
    }

    @Override
    public Cursor loadInBackground() {
       return super.loadInBackground();
    }
}

因为在 Matisse 只需要获取到手机中的图片和视频资源,所以直接将必要的参数配置在 AlbumLoader 中,然后提供 newInstance() 方法给外部调用,获取 AlbumLoader 的实例。

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();
        return AlbumLoader.newInstance(context);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        Context context = mContext.get();
        mCallbacks.onAlbumLoad(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        Context context = mContext.get();
        mCallbacks.onAlbumReset();
    }

    public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) {
        mContext = new WeakReference<Context>(activity);
        mLoaderManager = activity.getSupportLoaderManager();
        mCallbacks = callbacks;
    }

    public void loadAlbums() {
        mLoaderManager.initLoader(LOADER_ID, null, this);
    }

    public interface AlbumCallbacks {
        void onAlbumLoad(Cursor cursor);

        void onAlbumReset();
    }
}

Matisse 为了降低代码的耦合度,将一些客户端与 LoaderManager 交互的一些操作封装在 AlbumCollection 中。在 onCreate() 中,传入 Activity 用于获取 LoaderManager,加载资源完成后,在 onLoadFinished() 方法中,通过 AlbumCallbacks 的 onAlbumLoad(Cursor cursor) 方法将「包含数据的 Cursor」返回给外部调用的 MatisseActivity.

AlbumsSpinner

AlbumsSpinner 将 MatisseActivity 左上角的一组控件进行了封装,主要包括显示文件夹名称的 TextView 以及显示文件夹列表的 ListPopupWindow,相当于把一个相对完整的功能抽取出来,把逻辑操作写在里面,在 Activity 中当做一种控件来用,有点类似自定义 View.

public class AlbumsSpinner {

    private static final int MAX_SHOWN_COUNT = 6;
    private CursorAdapter mAdapter;
    private TextView mSelected;
    private ListPopupWindow mListPopupWindow;
    private AdapterView.OnItemSelectedListener mOnItemSelectedListener;

在 AlbumCollection 中返回的 Cursor,作为 AlbumsSpinner 的数据源,然后通过 AlbumsAdapter 将资源文件夹显示出来。当选中文件夹的时候,将所点击的文件夹的 position 回调给 MatisseActivity 中的 onItemSelected() 方法。

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        mAlbumCollection.setStateCurrentSelection(position);
        mAlbumsAdapter.getCursor().moveToPosition(position);
        // Album 是文件夹的实体类,封装了文件夹的名字、封面图片等信息
        Album album = Album.valueOf(mAlbumsAdapter.getCursor());
        onAlbumSelected(album);
    }

通过 AlbumsSpinner 回调出来的 position 拿到对应的文件夹的信息,然后将当前的界面进行刷新,使当前界面显示所选择的文件夹的图片。

    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);
            // MediaSelectionFragment 中包含一个 RecyclerView,用于显示文件夹中所有的图片
            Fragment fragment = MediaSelectionFragment.newInstance(album);
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
                    .commitAllowingStateLoss();
        }
    }

四、主页图片墙的实现


主页的照片墙可以说是 Matisse 中最有意思的模块了,而且学习价值也是最高的。图片墙的数据源同样是通过 Loader 机制来进行加载的,实现思路也跟上一节讲的「资源文件夹的加载和展示」差不多,这里简单讲一下就好。

主页的照片墙会通过我们选择不同的资源文件夹而展示不同的图片,所以我们在选择资源文件夹的时候,便将资源文件夹的 id,传给对应的 Loader,让它对相应的资源文件进行加载。

Matisse 把图片和音频的信息封装成了实体类,并实现了 Parcelable 接口,让其序列化,通过外部传入的 Cursor,拿到对应的 Uri、媒体类型、文件大小,如果是视频的话,就获取视频播放的时长。

/**
 * 图片或音频的实体类
 */
public class Item implements Parcelable {

    public final long id;
    public final String mimeType;
    public final Uri uri;
    public final long size;
    public final long duration; // only for video, in ms

    private Item(long id, String mimeType, long size, long duration) {
        this.id = id;
        this.mimeType = mimeType;
        Uri contentUri;
        if (isImage()) {
            contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        } else if (isVideo()) {
            contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
        } else {
            // 如果不是图片也不是音频就直接当文件存储
            contentUri = MediaStore.Files.getContentUri("external");
        }
        this.uri = ContentUris.withAppendedId(contentUri, id);
        this.size = size;
        this.duration = duration;
    }

    public static Item valueOf(Cursor cursor) {
        return new Item(cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)),
                cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)),
                cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns.SIZE)),
                cursor.getLong(cursor.getColumnIndex("duration")));
    }

}

图片墙是直接用一个 RecyclerView 进行展示的,Item 是一个继承了 SquareFrameLayout(正方形的 FrameLayout) 的自定义控件,主要包含三个部分

  • 右上角的 CheckView

  • 显示图片的 ImageView

  • 显示视频时长的 TextView

CheckView 就是右上角那个白色的小圆圈,可以理解为是一个自定义的 CheckBox,或者说是一个比较好看的复选框。我在前文中说 Matisse 的学习价值比较高,一个很重要的原因就是 Matisse 中有很多的自定义 View,能够让我们学习图片选择库的同时,学习自定义 View 的一些好的思路和做法。

那我们就来看看 CheckView 究竟是怎样实现的。

首先,CheckView 重写了 onMeasure() 方法,将宽和高都定为 48,而且为了屏幕适配性,将 48dp 乘以 density,将 dp 单位转换为像素单位。

    private static final int SIZE = 48; // dp

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int sizeSpec = MeasureSpec.makeMeasureSpec((int) (SIZE * mDensity), MeasureSpec.EXACTLY);
        super.onMeasure(sizeSpec, sizeSpec);
    }

接下来就看重头戏的 onDraw() 方法了

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 1、画出外在和内在的阴影
        initShadowPaint();
        canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                (STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint);

        // 2、画出白色的空心圆
        canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                STROKE_RADIUS * mDensity, mStrokePaint);

        // 3、画出圆里面的内容
        if (mCountable) {
                initBackgroundPaint();
                canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                        BG_RADIUS * mDensity, mBackgroundPaint);
                initTextPaint();
                String text = String.valueOf(mCheckedNum);
                int baseX = (int) (canvas.getWidth() - mTextPaint.measureText(text)) / 2;
                int baseY = (int) (canvas.getHeight() - mTextPaint.descent() - mTextPaint.ascent()) / 2;
                canvas.drawText(text, baseX, baseY, mTextPaint);
        } else {
            if (mChecked) {
                initBackgroundPaint();
                canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                        BG_RADIUS * mDensity, mBackgroundPaint);

                mCheckDrawable.setBounds(getCheckRect());
                mCheckDrawable.draw(canvas);
            }
        }
    }

onDraw() 方法主要分为三个部分

  • 画出空心圆内外的阴影 不得不说,Matisse 的细节处理真的做得特别好,为了图片选择库看起来更加美观,在空心圆的内外增加了一层辐射渐变的阴影

  • 画出白色的空心圆 这个真没什么好讲的

  • 描绘出里面的内容 通过我们外部配置的 mCountable 参数,来决定 CheckView 的显示方式,如果 mCountable 的值为 true 的话,便在内部描绘一层主题颜色的背景,以及代表所选择图片数量的数字,如果 mCount 的值为 false 的话,那么便描绘背景以及填入一个白色的 ✓

这部分主要是有关 Paint 的知识,以及数学方面的计算,如果对于 Paint 不是很熟悉的读者,可以看看这篇文章 HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解,顺便安利一波,凯哥的 HenCoder 教程,写得是真的好,强烈建议去好好看看。

看完了 CheckView 的实现逻辑,我们接着来看看图片墙的 Item 布局「MediaGrid」的实现逻辑,MediaGrid 是一个继承了 SquareFrameLayout(正方形的 FrameLayout)的自定义控件,可以理解为是一个拓展了复选功能(CheckView)和显示视频时长(TextView)功能的 ImageView.

我们从 MediaGrid 在 Adapter 中的使用入手,进一步看看 MediaGrid 的代码实现

mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo(
        getImageResize(mediaViewHolder.mMediaGrid.getContext()),
        mPlaceholder,
        mSelectionSpec.countable,
        holder
        ));
       mediaViewHolder.mMediaGrid.bindMedia(item);

可以看到 MediaGrid 的使用主要分两步

  • 初始化图片的公有属性(MediaGrid.preBindMedia(new MediaGrid.PreBindInfo()))

  • 将图片对应的信息进行绑定(MediaGrid.bindMedia(Item) )

PreBindInfo 是 MediaGrid 的一个静态内部类,封装了一些图片的一些公用的属性

    public static class PreBindInfo {
        int mResize; // 图片的大小
        Drawable mPlaceholder; // ImageView 的占位符
        boolean mCheckViewCountable; // √ 的图标
        RecyclerView.ViewHolder mViewHolder; // 对应的 ViewHolder

        public PreBindInfo(int resize, Drawable placeholder, boolean checkViewCountable,
                           RecyclerView.ViewHolder viewHolder) {
            mResize = resize;
            mPlaceholder = placeholder;
            mCheckViewCountable = checkViewCountable;
            mViewHolder = viewHolder;
        }
    }

Item 在上文已经介绍了,是图片或音频的实体类。第二步便是将一个包含图片信息的 Item 传给 MediaGrid,然后进行相应信息的设置。

MediaGrid 中自定义了回调的接口

    public interface OnMediaGridClickListener {

        void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder);

        void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
    }

当用户点击图片的时候,将点击事件回调到 Adapter,再回调到 MediaSelectionFragment,再回调到 MatisseActivity,然后打开图片的大图预览界面,你没看错,真的回调了三层,我也是一脸蒙蔽。一遇到这种情况,我就觉得 EventBus 还是挺好用的。

当点击右上角的 CheckView 的时候,便将点击事件回调到 Adapter 中,然后根据 countable 的值,来进行相应的设置(显示数字或者显示 √),然后再将对应的 Item 信息保存在 SelectedItemCollection(Item 的容器) 中。

五、预览界面的实现


打开预览界面有两种方法

  • 点击首页的某个图片

  • 选择图片之后,点击首页左下角的预览(Preview)按钮

这两种方法打开的界面看起来似乎是一样的,但实际上他们两个的实现逻辑很不一样,因此用了两个不同的 Activity.

点击首页的某张图片之后,会跳转到一个包含 ViewPager 的界面,因为对应资源文件夹中可能会有很多的图片,这时候如果将包含该文件夹中所有的图片直接传给预览界面的 Activity,这是非常不实际的。比较好的实现方式便是将「包含对应文件夹的信息的 Album」传给界面,然后再用 Loader 机制进行加载。

选择首页图片后,点击左下角的预览按钮,跳转到预览界面,因为我们选择的图片一般都比较少,所以这时候直接将「包含所有选择图片信息的 List」传给预览界面就行了。

虽然,两个 Activity 的实现逻辑不太一样,但由于都是预览界面,所以有很多相同的地方。因此,Matisse 便实现了一个 BasePreviewActivity,减少代码的冗余程度。

BasePreviewActivity 的布局主要由三部分组成

  • 右上角的 CheckView

  • 自定义的 ViewPager

  • 底部栏(包括预览(Preview)和使用按钮(Apply))

主要的代码逻辑也基本上是围绕这三个部分进行展开的。

当点击 CheckView 的时候,根据该图片是否已经被选择以及图片的类型,对 CheckView 进行相应的设置以及更新底部栏。

        mCheckView.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Item item = mAdapter.getMediaItem(mPager.getCurrentItem());
                // 如果当前的图片已经被选择
                if (mSelectedCollection.isSelected(item)) {
                    mSelectedCollection.remove(item);
                    if (mSpec.countable) {
                        mCheckView.setCheckedNum(CheckView.UNCHECKED);
                    } else {
                        mCheckView.setChecked(false);
                    }
                } else {
                    // 判断能否添加该图片
                    if (assertAddSelection(item)) {
                        mSelectedCollection.add(item);
                        if (mSpec.countable) {
                            mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
                        } else {
                            mCheckView.setChecked(true);
                        }
                    }
                }
                // 更新底部栏
                updateApplyButton();
            }
        });

当用户对 ViewPager 进行左右滑动的时候,根据当前的 position 拿到对应的 Item 信息,然后对 CheckView 进行相应的设置以及切换图片。

    @Override
    public void onPageSelected(int position) {
        PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter();
        if (mPreviousPos != -1 && mPreviousPos != position) {
            ((PreviewItemFragment) adapter.instantiateItem(mPager, mPreviousPos)).resetView();
            // 获取对应的 Item 
            Item item = adapter.getMediaItem(position);
            if (mSpec.countable) {
                int checkedNum = mSelectedCollection.checkedNumOf(item);
                mCheckView.setCheckedNum(checkedNum);
                if (checkedNum > 0) {
                    mCheckView.setEnabled(true);
                } else {
                    mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
                }
            } else {
                boolean checked = mSelectedCollection.isSelected(item);
                mCheckView.setChecked(checked);
                if (checked) {
                    mCheckView.setEnabled(true);
                } else {
                    mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
                }
            }
            updateSize(item);
        }
        mPreviousPos = position;
    }

以上便是 BasePreviewActivity 的实现逻辑,至于它的子类 AlbumPreviewActivity(包含所有图片的预览界面)和 SelectedPreviewActivity(所选择图片的预览界面)就很简单了,大家自己看下源码就能明白了。

总结


Matisse 应该是我第一个完整啃下来的开源项目了,从一开始被 MatisseActivity 实现的一堆接口吓蒙。到后来的一步一步抽丝剥茧,从各个功能点入手,慢慢的理解了其中的代码设计以及实现思路,看完整个项目之后,对于 Matisse 的架构设计和代码质量深感佩服。

在阅读比较大型的开源项目的时候,由于这个项目你是完全陌生的,而且代码量通常都比较大,这时如果在阅读源码的时候,深陷代码细节的话,很容易让我们陷入到思维黑洞里面。如果我们从功能点入手,一步一步分析功能点是如何实现的,分析主体的逻辑,这样阅读起来就会更加轻松,也更加有成效。


猜你喜欢